Ricevere notifiche per onPause e onResume utilizzando React-Native su Android

Potrebbe capitarvi di utilizzare React-Native su piattaforma Android e di dover necessariamente gestire la vostra App in situazioni limite come, per esempio, l’uscita momentanea (onPause) e la riattivazione successiva alla chiusura (onResume).

Pur non presentando alcun metodo nativo (JS) che consenta di accedere al controllo dello stato dell’applicazione, React-Native offre tutti gli strumenti necessari per poter gestire tale casistica senza troppi problemi.

La soluzione è un modulo nativo personalizzato che si aggancia agli eventi del dispositivo e funge da mediatore tra il contesto Javascript e il processo dell’applicazione nativa consentendo la ricezione di notifiche rispetto al cambiamento di stato dell’app (messa in pausa o ripresa).

Per prima cosa dobbiamo realizzare una classe Java che lavori come modulo React-Native:

public class DeviceEventsModule 
  extends ReactContextBaseJavaModule implements LifecycleEventListener {

  @Override 
  public void onHostResume() {
    sendEvent("onResume");
  }

  @Override
  public void onHostPause() {
    sendEvent("onPause");
  }

  @Override
  public void onHostDestroy() {
    sendEvent("onDestroy");
  }

  private void sendEvent(String eventName) {
    // ... TO BE ... 
  }
}

L’oggetto realizzato estende la classe base ReactContextBaseJavaModule e implementa l’interfaccia LifecycleEventListener. L’utilizzo dell’interfaccia LifecycleEventListener richiede l’implementazione dei metodi onHostResume, onHostPause e onHostDestroy. Questi sono metodi invocati in corrispondenza degli eventi sopracitati. In ognuno di questi eventi ho aggiunto la chiamata ad un ulteriore metodo, sendEvent, di cui vi ripropongo di seguito l’implementazione:

private void sendEvent(String eventName) {
  ReactApplicationContext appContext = getReactApplicationContext();
  appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit(eventName, null);
}

Questo non fa altro che attivare una comunicazione con l’oggetto react preposto all’inoltro delle notifiche al thread JS e, quando invocato, invia una notifica dell’evento specificato eventName.

Ora è necessario rendere disponibile questo modulo all’interno della nostra applicazione, sotto forma di modulo JS, questo lo si fa realizzando un ReactPackage:

public class AppPackage implements ReactPackage {
  @Override 
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    return Arrays.<NativeModule>asList(new DeviceEventsModule(reactContext));
  }
}

Successivamente è opportuno registrare il pacchetto all’avvio dell’applicazione, all’interno dell’oggetto MainApplication, andando a modificare il metodo getPackages:

@Override
protected List<ReactPackage> getPackages() {
  return Arrays.asList(
    new MainReactPackage(),
    new AppPackage()
  );
}

Terminato lo sviluppo nativo, possiamo finalmente integrate, all’interno della nostra app, la gestione degli eventi di sospensione e ripresa dell’applicazione come mostrato in questo esempio:

import { 
  NativeModules
} from 'react-native';
let DeviceEvents = NativeModules.DeviceEvents;

class App extends React.Component {
  componentDidMount() {
    this.onPauseListener = DeviceEvents.addListener('onPause', () => {
      console.warn('Suspending app...');
      // La tua logica di sospensione qui!
    });
    this.onResumeListener = DeviceEvents.addListener('onResume', () => {
      console.warn('Resuming app...');
      // La tua logica di ripresa qui!
    });
  }
  componentWillUnmount() {
    // E' importante stoppare i listener quando il componente
    // è distrutto da React.
    this.onPauseListener.remove();
    this.onResumeListener.remove();
  }
}

L’esempio riportato inizializza i Listener nella fase di creazione del componente e li distrugge nella fase di distruzione dello stesso. Nel momento in cui l’app va in pausa o viene ripresa successivamente alla sospensione, riceviamo notifica di questo cambio di stato attraverso il nostro modulo personalizzato.

Questa guida si basa sulla documentazione tecnica disponibile ai seguenti link:

Mappare una risposta JSON con nomi differenti su Android

Supponiamo di voler convertire la seguente risposta JSON in un oggetto Java le cui proprietà hanno un nome differente da quello specificato nella risposta HTTP (situazione tipica nei progetti in cui lavoro):

{ "nome": "Roberto", "cognome": "Conte Rosito", "età": "16" }

Utilizzando Jackson è possibile mappare la risposta in questo modo:

class Person {

  @JsonProperty("nome")
  private String name;

  @JsonProperty("cognome")
  private String surname;

  @JsonProperty("età")
  private String age;

  // ... Getter & Setter ... 
}

Apriamo il file build.gradle e aggiungiamo i riferimenti necessari:

dependencies {
  // ... Other deps ...
  compile (
    [group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.4.1'],
    [group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.4.1'],
    [group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.4.1']
  )
}

Successivamente, all’interno del nostro progetto, possiamo convertire la risposta in un oggetto di tipo Person (avendo precedentemente realizzato la classe) utilizzando queste istruzioni:

  String jsonResponse = ... ;
  ObjectMapper objectMapper = new ObjectMapper();
  Person personObject = objectMapper.readValue(jsonResponse, Person.class);

Ogni campo dell’oggetto marcato con l’annotazione @JsonProperty sarà processato secondo le direttive specificate all’interno dello stesso, in questo caso appunto il nome da utilizzare per la mappatura che dovrà essere diverso da quello utilizzato per dichiarare il campo all’interno del codice Java.

Filtrare una ListView in Android

Forse è l’operazione più comune con cui abbiamo a che fare quando dobbiamo gestire una ListView nelle nostre applicazioni Android. Farlo non è poi cosi complicato, anzi, lo stesso sistema operativo ci facilità il compito fornendo tutti gli strumenti necessari per poter realizzare tale funzionalità.

In questa guida vedremo come è possibile applicare un filtro su una lista. Per farlo ho pensato di non utilizzare il classico esempio, array di stringhe, ma di gestire una lista di oggetti complessi (niente di cosi complicato).

Supponiamo di voler caricare e filtrare un elenco di “Contatti”, ognuno dei quali è cosi composto:

class Contact {
  private String name;
  private String surname;
  private String value;
  // ... Getter & Setter ...

  public boolean containsText(String keyword) {
    if (keyword == null || keyword.isEmpty()) {
      return true;
    }
    if (name.toLowerCase().contains(keyword)) {
      return true;
    }
    if (surname.toLowerCase().contains(keyword)) {
      return true;
    }
    if (value.contains(keyword)) {
      return true;
    }
    return false;
  }
}

Nel codice riportato è presente un metodo, containsText, che ci tornerà utile nei passaggi successivi per l’applicazione dei filtri. Per prima cosa dobbiamo realizzare un ArrayAdapter che ci consenta di gestire i dati da presentare all’interno della lista:

class ContactListViewAdapter extends ArrayAdapter<Contact> {
  private List mOriginalData;
  private List mFilteredData;
  public ContactListViewAdapter(Context context, int resource, List list) {
    super(context, resource, list);
    mOriginalData = list;
    mFilteredData = list;
  }
  
  @Override
  public int getCount() {
    return mFilteredData.size();
  }
  
  @Override
  public Contact getItem(int position) {
    return mFilteredData.get(position);
  }

  @Override
  public long getItemId(int position) {
    return position;
  } 
  
  @NonNull
  @Override
  public View getView(int position, final View convertView, ViewGroup parent) {
    // ... Render ...
  }
}

Nell’esempio riportato è stato effettuato l’override di alcuni metodi poiché, come è possibile notare dallo stesso codice, l’adapter realizzato riporta due List di contatti, la prima è la lista originale dei dati, mOriginalData, la seconda rappresenta quella filtrata, appunto mFilteredData. L’adapter prenderà come riferimento sempre l’elenco filtrato per caricare i contatti all’interno della ListView.

Cosi com’è ora, la lista non consente l’effettiva applicazione del filtro, ci manca ancora un passaggio, cioè la realizzazione del Filter, l’oggetto che a regime ci consentirà di applicare un criterio di ricerca e aggiornare l’elenco dei risultati. Per farlo dobbiamo modificare la classe ContactListViewAdapter aggiungendo un ulteriore dettaglio:

class ContactListViewAdapter implements ArrayAdapter extends Filterable {
  private class ItemFilter extends Filter {
    @Override
    protected Filter.FilterResults performFiltering(CharSequence constraint) {
      String filterString = constraint != null ? constraint.toString() : "";
      FilterResults results = new FilterResults();
      final List list = originalData;
      int count = list.size();
      final ArrayList<Contact> outputList = new ArrayList<>(count);
      Contact c;
      for (int i = 0; i < count; i++) {
        c = list.get(i);
        if (c.containsText(filterString)) {
          outputList.add(filterableEBook);
        }
      }

      results.values = outputList;
      results.count = outputList.size();

      return results;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
      filteredData = (ArrayList<EBook>)results.values;
      notifyDataSetChanged();
    }

 }   
 
 // Aggiungo un nuovo campo nell'adapter:
 private ItemFilter mItemFilter = new ItemFilter();

 // Eseguo l'override del metodo preposto per l'applicazione dei filtri:
 @Override
 public Filter getFilter() {
   return mItemFilter;
 }
 // Proprietà precedentemente mostrate ... 
}

In pratica ciò che abbiamo fatto ora è implementare l’interfaccia Filterable che, successivamente, ci consente di applicare un filtro sulla nostra lista con estrema facilità. Utilizzando il metodo containsText presente nell’oggetto Contact, verifichiamo il match tra chiave di ricerca e contatto stesso per capire se deve o meno essere mostrato all’interno dei risultati.

Di seguito un esempio di codice con cui poter testare il risultato:

List<Contact> data= new ArrayList<>();
contactList.add(new Contact() {{
  setName("Roberto");
  setSurname("Conte Rosito");
  setValue("roberto.conterosito@gmail.com");
}});
contactList.add(new Contact() {{
  setName("Mario");
  setSurname("Rossi");
  setValue("mario.rossi@gmail.com");
}});
contactList.add(new Contact() {{
  setName("Mark");
  setSurname("Zuck");
  setValue("mark@facebook.com");
}});
mListViewAdapter = new ContactListViewAdapter(getBaseContext(), R.layout.item, data);
mListView = (ListView)findViewById(R.id.list);
mListView.setAdapter(mListViewAdapter);
postDelayed(new Runnable() {
  @Override
  public void run() {
    // Dopio 10 secondi vedremo comparire i soli contatti 
    // che hanno indirizzo Gmail!
    mListView.getFilter().filter("@gmail.com");
  }
}, 10 * 1000);

L’esempio riportato carica la lista e, dopo un tempo minimo di attesa (per simulare un esempio), applica un filtro che mostra i soli contatti che rispettano il criterio di ricerca specificato.