RxJava 2 per le app Android RxBinding e RxLifecycle

RxJava è una delle librerie più popolari per portare la programmazione reattiva alla piattaforma Android, e in questa serie in tre parti ti ho mostrato come iniziare a usare questa libreria nei tuoi progetti Android.

In Iniziare con RxJava 2 per Android, abbiamo esaminato cos'è RxJava e cosa ha da offrire agli sviluppatori Android, prima di creare un'applicazione Hello World che dimostrava i tre componenti principali di RxJava: un Osservabile, un Osservatore, e un abbonamento.

Negli operatori di programmazione reattiva nel tutorial RxJava 2, abbiamo esaminato come eseguire complesse trasformazioni di dati utilizzando operatori e come combinare Operatores e Schedulers per rendere finalmente il multithreading su Android un'esperienza senza dolore.

Abbiamo anche toccato RxAndroid, una libreria appositamente progettata per aiutarti a utilizzare RxJava nei tuoi progetti Android, ma c'è molto altro da esplorare in RxAndroid. Quindi, in questo post, mi concentrerò esclusivamente sulla famiglia di librerie RxAndroid.

Proprio come RxJava, RxAndroid ha subito una massiccia revisione nella sua versione 2. Il team di RxAndroid ha deciso di modulare la libreria, spostando gran parte delle sue funzionalità in moduli aggiuntivi RxAndroid dedicati.

In questo articolo, ti mostrerò come configurare e utilizzare alcuni dei più popolari e potenti moduli RxAndroid, inclusa una libreria che può rendere ascoltatori, gestori e TextWatchers una cosa del passato dandoti la possibilità di gestire qualunque Evento UI Android come Osservabile.

E dal momento che le perdite di memoria causate da abbonamenti incompleti sono il più grande svantaggio dell'utilizzo di RxJava nelle tue app Android, ti mostrerò anche come utilizzare un modulo RxAndroid in grado di gestire il processo di iscrizione per te. Alla fine di questo articolo, saprai come usare RxJava in qualsiasi Attività o Frammento, senza correre il rischio di incontrare qualunque Perdite di memoria relative a RxJava.

Creazione di interfacce utente Android più reattive

Reagire agli eventi dell'interfaccia utente quali tocchi, passaggi e immissione di testo è una parte fondamentale dello sviluppo di praticamente qualsiasi app per Android, ma la gestione degli eventi dell'interfaccia utente Android non è particolarmente semplice.

Generalmente reagisci agli eventi dell'interfaccia utente utilizzando una combinazione di ascoltatori, gestori, TextWatchers, e possibilmente altri componenti a seconda del tipo di interfaccia utente che stai creando. Ognuno di questi componenti richiede di scrivere una quantità significativa di codice boilerplate, e per peggiorare le cose non c'è coerenza nel modo in cui si implementano questi diversi componenti. Ad esempio, gestisci Al clic eventi implementando un OnClickListener:

Button button = (Button) findViewById (R.id.button); button.setOnClickListener (new View.OnClickListener () @Override public void onClick (View v) // Esecuzione di un lavoro //);

Ma questo è completamente diverso dal modo in cui implementeresti TextWatcher:

final EditText name = (EditText) v.findViewById (R.id.name); // Crea un TextWatcher e specifica che questo TextWatcher deve essere chiamato ogni volta che il contenuto di EditText cambia // name.addTextChangedListener (new TextWatcher () @Override public void beforeTextChanged (CharSequence s, int start, int count, int after)  @ Override public void onTextChanged (CharSequence s, int start, int before, int count) // Esegue un lavoro // @Override public void afterTextChanged (Editable s) );

Questa mancanza di coerenza può potenzialmente aggiungere molta complessità al codice. E se hai componenti dell'interfaccia utente che dipendono dall'output di altri componenti dell'interfaccia utente, preparati a rendere le cose ancora più complicate! Anche un semplice caso d'uso, come chiedere all'utente di digitare il proprio nome in un Modifica il testo in modo da poter personalizzare il testo che appare in seguito TextViews-richiede callback annidati, che sono notoriamente difficili da implementare e mantenere. (Alcune persone si riferiscono a callback annidati come "inferno di callback".)

Chiaramente, un approccio standardizzato alla gestione degli eventi dell'interfaccia utente ha il potenziale di semplificare enormemente il codice, e RxBinding è una libreria che si propone di fare proprio questo, fornendo associazioni che ti permettono di convertire qualsiasi Android vista evento in un Osservabile.  

Una volta convertito un evento vista in un Osservabile, emetterà i suoi eventi UI come flussi di dati a cui puoi iscriverti esattamente nello stesso modo in cui ti iscriveresti a qualsiasi altro Osservabile.

Poiché abbiamo già visto come cattureresti un evento click utilizzando lo standard di Android OnClickListener, diamo un'occhiata a come otterresti gli stessi risultati usando RxBinding:

import com.jakewharton.rxbinding.view.RxView; ... Pulsante button = (Button) findViewById (R.id.button); RxView.clicks (button) .subscribe (aVoid -> // Esegui qualche lavoro qui //);

Non solo questo approccio è più conciso, ma è un'implementazione standard che puoi applicare a tutti gli eventi dell'interfaccia utente che si verificano nella tua app. Ad esempio, l'acquisizione di input di testo segue lo stesso schema di cattura degli eventi di clic:

RxTextView.textChanges (editText) .subscribe (charSequence -> // Esegui qualche lavoro qui //);

Un'applicazione di esempio con RxBinding

In questo modo puoi vedere esattamente come RxBinding può semplificare il codice relativo all'interfaccia utente della tua app, creiamo un'app che mostri alcuni di questi binding in azione. Includerò anche un vista questo dipende dall'output di un altro vista, per dimostrare come RxBinding semplifica la creazione di relazioni tra componenti dell'interfaccia utente.

Questa app sarà composta da:

  • UN Pulsante che mostra a Crostini quando toccato.
  • Un Modifica il testo che rileva le modifiche del testo.
  • UN TextView che si aggiorna per visualizzare i contenuti del Modifica il testo.

Impostazione del progetto

Crea un progetto Android Studio con le impostazioni di tua scelta, quindi apri il tuo modulo build.gradle file e aggiungere l'ultima versione della libreria RxBinding come dipendenza del progetto. Nell'interesse di mantenere al minimo il codice boilerplate, userò anche lambdas, quindi ho aggiornato il mio build.gradle file per supportare questa funzionalità di Java 8:

applica plugin: 'com.android.application' android compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig applicationId "com.jessicathornsby.myapplication" minSdkVersion 23 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner. AndroidJUnitRunner "// Abilita la toolchain Jack // jackOptions enabled true buildTypes release minifyEnabled false proguardFiles getDefaultProguardFile ('proguard-android.txt'), 'proguard-rules.pro' // Imposta sourceCompatibility e targetCompatibility su JavaVersion.VERSION_1_8 // compileOptions sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 dependencies compile fileTree (dir: 'libs', include: ['* .jar']) androidTestCompile ('com.android.support.test.espresso : espresso-core: 2.2.2 ', exclude group:' com.android.support ', module:' support-annotations ') // Aggiungi la libreria principale RxBinding // compile' com.jakewharton.rxbinding: rxbinding: 0.4.0 'compilare' com .android.support: appcompat-v7: 25.3.0 '// Non dimenticare di aggiungere le dipendenze RxJava e RxAndroid // compile' io.reactivex.rxjava2: rxandroid: 2.0.1 'compile' io.reactivex.rxjava2: rxjava: 2.0.5 'testCompile' junit: junit: 4.12 ' 

Quando lavori con più librerie RxJava, è possibile che tu possa incontrare a File duplicati copiati in APK META-INF / DIPENDENZE messaggio di errore in fase di compilazione. Se si verifica questo errore, la soluzione alternativa consiste nell'eliminare questi file duplicati aggiungendo quanto segue al livello di modulo build.gradle file:

android packagingOptions // Usa "escludi" per indicare il file (oi file) specifico su cui Android Studio si lamenta // esclude "META-INF / rxjava.properties"

Crea il layout dell'attività principale

Sincronizzare i file Gradle e quindi creare un layout composto da a Pulsante, un Modifica il testo, e a TextView:

  

Codifica i binding di eventi

Ora vediamo come usereste questi RxBinding per catturare i vari eventi UI a cui la nostra applicazione deve reagire. Per cominciare, dichiarare le importazioni e definire il Attività principale classe. 

pacchetto com.jessicathornsby.myapplication; importare android.os.Bundle; import android.support.v7.app.AppCompatActivity; importa android.widget.Button; importare android.widget.EditText; import android.widget.TextView; importare android.widget.Toast; // Importa la classe view.RxView, quindi puoi usare RxView.clicks // import com.jakewharton.rxbinding.view.RxView; // Importa widget.RxTextView in modo da poter utilizzare RxTextView.textChanges // import com.jakewharton.rxbinding.widget.RxTextView; public class MainActivity estende AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); Button button = (Button) findViewById (R.id.button); TextView textView = (TextView) findViewById (R.id.textView); EditText editText = (EditText) findViewById (R.id.editText); // Il codice per i binding va qui // // ... //

Ora puoi iniziare ad aggiungere associazioni per rispondere agli eventi dell'interfaccia utente. Il RxView.clicks il metodo viene utilizzato per associare eventi di clic. Creare un'associazione per visualizzare un brindisi ogni volta che si fa clic sul pulsante:  

 RxView.clicks (button) .subscribe (aVoid -> Toast.makeText (MainActivity.this, "RxView.clicks", Toast.LENGTH_SHORT) .show ();); 

Quindi, usa il RxTextView.textChanges () metodo per reagire a un evento di modifica del testo aggiornando il TextView con il contenuto del nostro Modifica il testo.

 RxTextView.textChanges (editText) .subscribe (charSequence -> textView.setText (charSequence);); 

Quando esegui la tua app, ti ritroverai con una schermata simile alla seguente.

Installa il tuo progetto su uno smartphone o tablet Android fisico o un AVD compatibile, quindi passa un po 'di tempo a interagire con i vari elementi dell'interfaccia utente. La tua app dovrebbe reagire per fare clic su eventi e immissione di testo come normali e tutto senza un listener, TextWatcher o callback in vista!


RxBinding per le viste della libreria di supporto

Mentre la libreria RxBinding di base fornisce collegamenti per tutti gli elementi dell'interfaccia utente che compongono la piattaforma standard di Android, ci sono anche moduli fratelli RxBinding che forniscono collegamenti per le viste che sono incluse come parte delle varie librerie di supporto di Android.

Se hai aggiunto una o più librerie di supporto al tuo progetto, in genere vorrai aggiungere anche il modulo RxBinding corrispondente.

Questi moduli fratelli seguono una semplice convenzione di denominazione che semplifica l'identificazione della corrispondente libreria di supporto Android: ogni modulo di sibling prende semplicemente il nome della libreria di supporto e sostituisce com.android con com.jakewharton.rxbinding2: rxbinding.

  • compila com.jakewharton.rxbinding2: rxbinding-recyclerview-v7: 2.0.0 '
  • compila 'com.jakewharton.rxbinding2: rxbinding-support-v4: 2.0.0'
  • compila 'com.jakewharton.rxbinding2: rxbinding-appcompat-v7: 2.0.0'
  • compila 'com.jakewharton.rxbinding2: rxbinding-design: 2.0.0'
  • compila 'com.jakewharton.rxbinding2: rxbinding-recyclerview-v7: 2.0.0'
  • compila 'com.jakewharton.rxbinding2: rxbinding-leanback-v17: 2.0.0'

Se stai utilizzando Kotlin nei tuoi progetti Android, è disponibile anche una versione di Kotlin per ogni modulo RxBinding. Per accedere alla versione di Kotlin, basta aggiungere -Kotlin al nome della libreria con cui vuoi lavorare, quindi:

compila 'com.jakewharton.rxbinding2: rxbinding-design: 2.0.0'

diventa:

compila 'com.jakewharton.rxbinding2: rxbinding-design-kotlin: 2.0.0'

Una volta convertito a vista evento in un Osservabile, tutti questi eventi vengono emessi come flusso di dati. Come abbiamo già visto, puoi iscriverti a questi stream e quindi eseguire qualsiasi attività per attivare questo particolare evento dell'interfaccia utente, ad esempio la visualizzazione di Crostini o aggiornando a TextView. Tuttavia, puoi anche applicare una qualsiasi delle enormi raccolte di operatori di RxJava a questo flusso osservabile e persino riunire più operatori per eseguire complesse trasformazioni sugli eventi dell'interfaccia utente..

Ci sono troppi operatori da discutere in un singolo articolo (e i documenti ufficiali elencano comunque tutti gli operatori) ma quando si tratta di lavorare con gli eventi dell'interfaccia utente di Android, ci sono alcuni operatori che possono risultare particolarmente utili.

Il antirimbalzo () Operatore

In primo luogo, se sei preoccupato che un utente impaziente tocchi ripetutamente un elemento dell'interfaccia utente, potenzialmente confondendo la tua app, puoi utilizzare il antirimbalzo () operatore per filtrare tutti gli eventi UI che vengono emessi in rapida successione.

Nell'esempio seguente, sto specificando che questo pulsante dovrebbe reagire a un Al clic evento solo se c'è stato almeno un gap di 500 millisecondi dall'evento di clic precedente:

RxView.clicks (button) .debounce (500, TimeUnit.MILLISECONDS) .subscribe (aVoid -> Toast.makeText (MainActivity.this, "RxView.clicks", Toast.LENGTH_SHORT) .show (););

Il pubblicare() Operatore

Puoi anche usare il pubblicare() operatore per collegare più listener alla stessa vista, qualcosa che è stato tradizionalmente difficile implementare in Android.

Il pubblicare() l'operatore converte uno standard Osservabile in un collegabile osservabile. Mentre un osservatore regolare inizia a emettere oggetti non appena il primo osservatore lo sottoscrive, un osservabile collegabile non emetterà alcunché fino a quando non lo istruisci esplicitamente, applicando il Collegare() operatore. Questo ti dà una finestra di opportunità in cui iscrivere più osservatori, senza che l'osservabile inizi a emettere elementi non appena il primo abbonamento abbia luogo.

Dopo aver creato tutti i tuoi abbonamenti, applica semplicemente il Collegare() l'operatore e l'osservabile inizieranno ad emettere i dati a tutti gli osservatori assegnati.  

Evita perdite di memoria che interrompono l'app

Come abbiamo visto in tutta questa serie, RxJava può essere un potente strumento per la creazione di applicazioni Android reattive e interattive, con molto meno codice di quanto in genere è necessario ottenere gli stessi risultati utilizzando Java da solo. Tuttavia, vi è uno svantaggio principale nell'usare RxJava nelle applicazioni Android: il potenziale di perdite di memoria causato da abbonamenti incompleti.

Queste perdite di memoria si verificano quando il sistema Android tenta di distruggere un Attività che contiene una corsa Osservabile. Poiché l'osservabile è in esecuzione, il suo osservatore manterrà ancora un riferimento all'attività e il sistema non sarà in grado di raccogliere tali attività come risultato.

Poiché Android distrugge e ricrea AttivitàOgni volta che la configurazione del dispositivo cambia, la tua app potrebbe creare un duplicato Attività ogni volta l'utente passa dalla modalità verticale a quella orizzontale e ogni volta che apre e chiude la tastiera del dispositivo.

Queste attività si aggirano in background, potenzialmente non ottenendo mai la raccolta di dati inutili. Poiché le attività sono oggetti di grandi dimensioni, questo può portare rapidamente a grave problemi di gestione della memoria, soprattutto dal momento che smartphone e tablet Android hanno una memoria limitata per cominciare. La combinazione di una grande perdita di memoria e di una memoria limitata può causare rapidamente un errore Fuori dalla memoria errore.

Le perdite di memoria RxJava potrebbero avere il potenziale di provocare il caos con le prestazioni dell'applicazione, ma esiste una libreria RxAndroid che consente di utilizzare RxJava nella tua app senza preoccuparsi di perdite di memoria.

La libreria RxLifecycle, sviluppata da Trello, fornisce API di gestione del ciclo di vita che è possibile utilizzare per limitare la durata di un Osservabile al ciclo di vita di un Attività o Frammento. Una volta stabilita questa connessione, RxLifecycle interromperà la sequenza osservabile in risposta agli eventi del ciclo di vita che si verificano nell'attività o frammento assegnato di tale osservabile. Ciò significa che puoi creare un osservabile che termina automaticamente ogni volta che un'attività o un frammento viene distrutto.

Nota che stiamo parlando terminazione una sequenza e non annullamento dell'iscrizione. Sebbene RxLifecycle sia spesso parlato nel contesto della gestione del processo di iscrizione / disiscrizione, tecnicamente non annulla l'iscrizione a un osservatore. Invece, la libreria RxLifecycle termina la sequenza osservabile emettendo il onComplete () o onError () metodo. Quando si annulla l'iscrizione, l'osservatore smette di ricevere notifiche dal suo osservabile, anche se l'osservabile sta ancora emettendo degli elementi. Se si richiede specificamente il comportamento di disiscrizione, allora è qualcosa che è necessario implementare.

Utilizzando RxLifecycle

Per utilizzare RxLifecycle nei progetti Android, apri il tuo modulo build.gradle file e aggiungi l'ultima versione della libreria RxLifeycle, oltre alla libreria Android RxLifecycle:

dependencies ... compile 'com.trello.rxlifecycle2: rxlifecycle: 2.0.1' compile 'com.trello.rxlifecycle2: rxlifecycle-android: 2.0.1'

Quindi, nel Attività o Frammento dove si desidera utilizzare le API di gestione del ciclo di vita della libreria, estendere entrambi RxActivity, RxAppCompatActivity o RxFragment, e aggiungi la dichiarazione di importazione corrispondente, ad esempio:

import com.trello.rxlifecycle2.components.support.RxAppCompatActivity; ... publicAtility MainActivity estende RxAppCompatActivity 

Quando si tratta di legare un Osservabile al ciclo di vita di un Attività o Frammento, è possibile specificare l'evento del ciclo di vita in cui deve terminare l'osservabile oppure lasciare che la libreria RxLifecycle decida quando terminare la sequenza osservabile.

Per impostazione predefinita, RxLifecycle interromperà un osservabile nell'evento del ciclo di vita complementare a quello in cui si è verificata tale iscrizione, quindi se ti iscrivi ad un osservabile durante l'attività dell'utente onCreate () metodo, quindi RxLifecycle terminerà la sequenza osservabile durante quella Attività OnDestroy () metodo. Se ti iscrivi durante un Frammento'S onAttach () metodo, quindi RxLifecycle terminerà questa sequenza nel file onDetach () metodo.

Puoi lasciare questa decisione fino a RxLifecycle, usando RxLifecycleAndroid.bindActivity:

Osservabile myObservable = Observable.range (0, 25); ... @Override public void onResume () super.onResume (); myObservable .compose (RxLifecycleAndroid.bindActivity (lifecycle)) .subscribe (); 

In alternativa, è possibile specificare l'evento del ciclo di vita in cui RxLifecycle deve terminare a Osservabile sequenza, usando RxLifecycle.bindUntilEvent.

Qui, sto specificando che la sequenza osservabile dovrebbe essere terminata in OnDestroy ():

@ Override public void onResume () super.onResume (); myObservable .compose (RxLifecycle.bindUntilEvent (lifecycle, ActivityEvent.DESTROY)) .subscribe ();  

Funzionando con le autorizzazioni Marshmallow Android

La libreria finale che esamineremo è RxPermissions, che è stata progettata per aiutarti a utilizzare RxJava con il nuovo modello di autorizzazioni introdotto in Android 6.0. Questa libreria consente inoltre di emettere una richiesta di autorizzazione e gestire il risultato dell'autorizzazione nello stesso percorso, invece di richiedere l'autorizzazione in un posto e quindi gestirne i risultati separatamente, in Activity.onRequestPermissionsResult ().

Inizia aggiungendo la libreria RxPermissions al tuo build.gradle file:

compila "com.tbruyelle.rxpermissions2: rxpermissions: 0.9.3@aar"

Quindi, crea un'istanza RxPermissions:

RxPermissions rxPermissions = new RxPermissions (this);

Sei quindi pronto per iniziare a creare richieste di autorizzazione tramite la libreria RxPermissions, utilizzando la seguente formula:

rxPermissions.request (Manifest.permission.READ_CONTACTS) .subscribe (concesso -> if (garantito) // L'autorizzazione è stata concessa // else // L'autorizzazione è stata negata //);

Dove la tua richiesta di autorizzazione è cruciale, in quanto c'è sempre una possibilità che l'hosting Attività può essere distrutto e quindi ricreato mentre il dialogo delle autorizzazioni è sullo schermo, solitamente a causa di una modifica della configurazione come l'utente che si sposta tra le modalità verticale e orizzontale. Se ciò si verifica, la tua iscrizione non può essere ricreata, il che significa che non sarai iscritto alla RxPermissions osservabile e non riceverai la risposta dell'utente alla finestra di dialogo della richiesta di autorizzazione. Per garantire che la tua applicazione riceva la risposta dell'utente, sempre invoca la tua richiesta durante una fase di inizializzazione come Activity.onCreate ()Activity.onResume (), o View.onFinishInflate ().

Non è raro che le funzionalità richiedano diverse autorizzazioni. Ad esempio, l'invio di un messaggio SMS di solito richiede che la tua app abbia il INVIARE SMS e READ_CONTACTS permessi. La libreria RxPermissions fornisce un metodo conciso per l'emissione di più richieste di autorizzazioni e quindi unendo le risposte dell'utente in un singolo falso (una o più autorizzazioni sono state respinte) o vero (tutte le autorizzazioni sono state concesse) risposta che è quindi possibile reagire di conseguenza.

RxPermissions.getInstance (this) .request (Manifest.permission.SEND_SMS, Manifest.permission.READ_CONTACTS) .subscribe (concesso -> if (garantito) // Tutte le autorizzazioni sono state concesse // else // Uno o più permessi è stato negato//  );

In genere, si desidera attivare una richiesta di autorizzazione in risposta a un evento dell'interfaccia utente, ad esempio l'utente che tocca una voce di menu o un pulsante, quindi RxPermissions e RxBiding sono due librerie che funzionano particolarmente bene insieme.

Gestire l'evento UI come osservabile e inoltrare la richiesta di autorizzazione tramite RxPermissions consente di eseguire molto lavoro con poche righe di codice:

RxView.clicks (findViewById (R.id.enableBluetooth)). Oggetto (RxPermissions.getInstance (this) .ensure (Manifest.permission.BLUETOOTH_ADMIN)) .subscribe (concesso -> // Il pulsante 'enableBluetooth' è stato cliccato / /);

Conclusione

Dopo aver letto questo articolo, hai qualche idea su come tagliare un sacco di codice boilerplate dalle tue app per Android, utilizzando RxJava per gestire tutti gli eventi dell'interfaccia utente dell'applicazione e inviando le richieste di autorizzazione tramite RxPermissions. Abbiamo anche esaminato come è possibile utilizzare RxJava in qualsiasi Android Attività o Frammento, senza doversi preoccupare delle perdite di memoria che possono essere causate da abbonamenti incompleti.

Abbiamo esplorato alcune delle più popolari e utili librerie RxJava e RxAndroid di questa serie, ma se sei desideroso di vedere cos'altro RxJava ha da offrire agli sviluppatori Android, controlla alcune delle molte altre librerie RxAndroid. Troverai un elenco completo di ulteriori librerie RxAndroid su GitHub.

Nel frattempo, controlla alcuni dei nostri altri post di sviluppo Android qui su Envato Tuts+!

  • Inizia con Retrofit 2 Client HTTP

    Retrofit è un client HTTP sicuro per tipo per Android e Java. Retrofit semplifica la connessione a un servizio Web REST traducendo l'API in Java ...
    Chike Mgbemena
    SDK Android
  • Introduzione alle cose di Android

    Nel dicembre del 2016, Google ha rilasciato una versione aggiornata del sistema operativo Android con un'opzione per gli sviluppatori di utilizzare una versione ridotta per il collegamento ...
    Paul Trebilcox-Ruiz
    androide
  • Come effettuare chiamate e utilizzare SMS nelle app Android

    In questo tutorial, imparerai a conoscere la Telefonia Android e l'API SMS. Imparerai come effettuare una chiamata dalla tua app e come monitorare gli eventi di chiamata telefonica, ...
    Chike Mgbemena
    SDK Android
  • 6 Cosa fare e cosa non fare per una grande esperienza utente Android

    Le app Android più popolari hanno qualcosa in comune: offrono un'esperienza utente eccezionale. In questo post, condividerò alcuni suggerimenti che aiuteranno la tua app ...
    Jessica Thornsby
    SDK Android