Consigli per l'architettura Android

Questa pagina presenta diverse best practice e consigli sull'architettura. Adottali per migliorare la qualità, la robustezza e la scalabilità della tua app. Inoltre, semplificano la manutenzione e il test dell'app.

Le best practice riportate di seguito sono raggruppate per argomento. Ognuna ha una priorità che riflette la forza del consiglio. L'elenco delle priorità è il seguente:

  • Vivamente consigliato: implementa questa pratica a meno che non sia in contrasto fondamentale con il tuo approccio.
  • Consigliato: è probabile che questa pratica migliori la tua app.
  • Facoltativo: questa pratica può migliorare la tua app in determinate circostanze.

Architettura a livelli

L'architettura a livelli consigliata favorisce la separazione delle responsabilità. Gestisce l'interfaccia utente dai modelli di dati, rispetta il principio della singola fonte attendibile, e segue i principi del flusso di dati unidirezionale. Di seguito sono riportate alcune best practice per l'architettura a livelli:

Suggerimento Descrizione
Utilizza un livello dati chiaramente definito data layer. Il livello dati espone i dati dell'applicazione al resto dell'app e contiene la maggior parte della logica di business dell'app.
  • Crea repository anche se contengono una sola origine dati.
  • Nelle app di piccole dimensioni, puoi scegliere di inserire i tipi di livello dati in un pacchetto o modulo data.
Utilizza un livello UI chiaramente definito. Il livello UI mostra i dati dell'applicazione sullo schermo e funge da punto principale di interazione dell'utente. Jetpack Compose è il toolkit moderno consigliato per la creazione dell'interfaccia utente della tua app.
  • Nelle app di piccole dimensioni, puoi scegliere di inserire i tipi di livello dati in un pacchetto o modulo ui.
Per saperne di più sulle best practice del livello UI, consulta Livello UI.
Espone i dati dell'applicazione dal livello dati utilizzando un repository.

Assicurati che i componenti del livello UI, come i componibili o i ViewModel, non interagiscano direttamente con un'origine dati. Ecco alcuni esempi di origini dati:

  • Database, DataStore, SharedPreferences, API Firebase.
  • Fornitori di posizioni GPS.
  • Fornitori di dati Bluetooth.
  • Fornitori di stato della connettività di rete.
Utilizza coroutine e flussi. Utilizza coroutine e flussi per comunicare tra i livelli.

Per saperne di più sulle best practice delle coroutine, consulta Best practice per le coroutine in Android.

Utilizza un livello di dominio. Utilizza un livello di dominio con casi d'uso se devi riutilizzare la logica di business che interagisce con il livello dati in più ViewModel o se vuoi semplificare la complessità della logica di business di un particolare ViewModel.

Livello UI

Il ruolo del livello UI è quello di mostrare i dati dell'applicazione sullo schermo e fungere da punto principale di interazione dell'utente. Di seguito sono riportate alcune best practice per il livello UI:

Suggerimento Descrizione
Segui il flusso di dati unidirezionale (UDF). Segui i principi del flusso di dati unidirezionale (UDF), in cui i ViewModel espongono lo stato dell'UI utilizzando il pattern Observer e ricevono azioni dall'UI tramite chiamate di metodi.
Utilizza i ViewModel AAC se i loro vantaggi si applicano alla tua app. Utilizza i ViewModel AAC per gestire la logica di business e recuperare i dati dell'applicazione per esporre lo stato dell'UI all'UI.

Per saperne di più sulle best practice di ViewModel, consulta Suggerimenti sull'architettura.

Per saperne di più sui vantaggi dei ViewModel, consulta Il ViewModel come contenitore dello stato della logica di business.

Utilizza la raccolta dello stato dell'UI con riconoscimento del ciclo di vita. Raccogli lo stato dell'UI dall'UI utilizzando il builder di coroutine con riconoscimento del ciclo di vita appropriato, collectAsStateWithLifecycle.

Scopri di più su collectAsStateWithLifecycle.

Non inviare eventi dal ViewModel all'UI. Elabora immediatamente l'evento nel ViewModel e causa un aggiornamento dello stato con il risultato della gestione dell'evento. Per saperne di più sugli eventi dell'UI, consulta Gestire gli eventi di ViewModel.
Utilizza un'applicazione a singola attività. Utilizza Navigation 3 per spostarti tra le schermate e creare deep link alla tua app se ne ha più di una.
Utilizza Jetpack Compose. Utilizza Jetpack Compose per creare nuove app per smartphone, tablet, dispositivi pieghevoli e Wear OS.

Il seguente snippet illustra come raccogliere lo stato dell'UI in modo da riconoscere il ciclo di vita:

  @Composable
  fun MyScreen(
      viewModel: MyViewModel = viewModel()
  ) {
      val uiState by viewModel.uiState.collectAsStateWithLifecycle()
  }

ViewModel

I ViewModel sono responsabili della fornitura dello stato dell'UI e dell'accesso al livello dati. Di seguito sono riportate alcune best practice per i ViewModel:

Suggerimento Descrizione
Mantieni i ViewModel indipendenti dal ciclo di vita di Android. Nei ViewModel, non conservare un riferimento a nessun tipo correlato al ciclo di vita. Non passare Activity, Context o Resources come dipendenza. Se qualcosa richiede un Context nel ViewModel, valuta attentamente se si trova nel livello corretto.
Utilizza coroutine e flussi.

Il ViewModel interagisce con i livelli di dati o di dominio utilizzando quanto segue:

  • Flussi Kotlin per la ricezione dei dati dell'applicazione
  • Funzioni suspend per eseguire azioni utilizzando viewModelScope
Utilizza i ViewModel a livello di schermata.

Non utilizzare i ViewModel in parti riutilizzabili dell'UI. Dovresti utilizzare i ViewModel in:

  • Componibili a livello di schermata
  • Destinazioni o grafici quando utilizzi Jetpack Navigation

Per i componibili più complessi o quelli con un comportamento dinamico basato sullo stato, utilizza rememberViewModelStoreOwner() per definire l'ambito di un ViewModel direttamente nel sito di chiamata del componibile.

Utilizza classi di contenitori di stato semplici nei componenti UI riutilizzabili. Utilizza classi di contenitori di stato semplici per gestire la complessità nei componenti UI riutilizzabili. In questo modo, lo stato può essere innalzato e controllato esternamente.
Non utilizzare AndroidViewModel. Utilizza la ViewModel classe, non AndroidViewModel. Non utilizzare la classe Application nel ViewModel. Sposta invece la dipendenza nell'UI o nel livello dati.
Espone uno stato dell'UI. Fai in modo che i ViewModel espongano i dati all'UI tramite una singola proprietà denominata uiState. Se l'UI mostra più dati non correlati, la VM può esporre più proprietà dello stato dell'UI.
  • Imposta uiState come StateFlow.
  • Crea il uiState utilizzando l'operatore stateIn con la policy WhileSubscribed(5000) se i dati vengono forniti come stream da altri livelli della gerarchia. (Vedi questo esempio di codice.)
  • Per i casi più semplici senza stream di dati provenienti dal livello dati, è accettabile utilizzare un MutableStateFlow esposto come StateFlow immutabile.
  • Puoi scegliere di avere ${Screen}UiState come classe di dati che può contenere dati, errori e indicatori di caricamento. Questa classe può anche essere una classe sealed se i diversi stati sono esclusivi.

Il seguente snippet illustra come esporre lo stato dell'UI da un ViewModel:

@HiltViewModel
class BookmarksViewModel @Inject constructor(
    newsRepository: NewsRepository
) : ViewModel() {

    val feedState: StateFlow<NewsFeedUiState> =
        newsRepository
            .getNewsResourcesStream()
            .mapToFeedState(savedNewsResourcesState)
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = NewsFeedUiState.Loading
            )

    // ...
}

Ciclo di vita

Segui le best practice per lavorare con il ciclo di vita di Activity:

Suggerimento Descrizione
Utilizza gli effetti con riconoscimento del ciclo di vita nei componibili anziché sostituire i callback del ciclo di vita di Activity.

Non sostituire i metodi del ciclo di vita di Activity, come onResume, per eseguire attività correlate all'UI. Utilizza invece LifecycleEffects di Compose o gli ambiti di coroutine con riconoscimento del ciclo di vita:

Il seguente snippet illustra come eseguire le operazioni in base a un determinato stato del ciclo di vita:

  @Composable
  fun LocationChangedEffect(
    locationManager: LocationManager,
    onLocationChanged: (Location) -> Unit
  ) {
    val currentOnLocationChanged by rememberUpdatedState(onLocationChanged)

    LifecycleStartEffect(locationManager) {
        val listener = LocationListener { newLocation ->
            currentOnLocationChanged(newLocation)
        }

        try {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                1000L,
                1f,
                listener,
            )
        } catch (e: SecurityException) {
            // TODO: Handle missing permissions
        }

        onStopOrDispose {
            locationManager.removeUpdates(listener)
        }
    }
  }

Gestire le dipendenze

Segui le best practice quando gestisci le dipendenze tra i componenti:

Suggerimento Descrizione
Utilizza l'iniezione delle dipendenze. Utilizza le best practice per l'iniezione delle dipendenze, principalmente l'iniezione del costruttore, quando possibile.
Definisci l'ambito di un componente quando necessario. Definisci l'ambito di un contenitore di dipendenze quando il tipo contiene dati modificabili che devono essere condivisi o il tipo è costoso da inizializzare ed è ampiamente utilizzato nell'app.
Utilizza Hilt. Utilizza Hilt o l'iniezione manuale delle dipendenze nelle app semplici. Utilizza Hilt se il tuo progetto è abbastanza complesso, ad esempio se include uno dei seguenti elementi:
  • Più schermate con ViewModel
  • Utilizza WorkManager
  • Ha ViewModel con ambito definito nello stack di navigazione

Test

Di seguito sono riportate alcune best practice per i test:

Suggerimento Descrizione
Scopri cosa testare.

A meno che il progetto non sia semplice come un'app "Hello World", esegui il test. Come minimo, includi quanto segue:

  • Test delle unità per i ViewModel, inclusi i flussi
  • Test delle unità per le entità del livello dati, ovvero repository e origini dati
  • Test di navigazione dell'UI utili come test di regressione in CI
Preferisci i fake ai mock. Per saperne di più sull'utilizzo dei fake, consulta Utilizzare i test double in Android.
Testa i StateFlow. Quando testi StateFlow, procedi nel seguente modo:

Per saperne di più, consulta Cosa testare in Android e Testa il layout di Compose.

Modelli

Segui queste best practice quando sviluppi modelli nelle tue app:

Suggerimento Descrizione
Crea un modello per livello nelle app complesse.

Nelle app complesse, crea nuovi modelli in diversi livelli o componenti quando ha senso. Considera i seguenti esempi:

  • Un'origine dati remota può mappare il modello che riceve tramite la rete a una classe più semplice con solo i dati di cui l'app ha bisogno.
  • I repository possono mappare i modelli DAO a classi di dati più semplici con solo le informazioni di cui ha bisogno il livello UI.
  • ViewModel può includere modelli di livello dati nelle classi UiState.

Convenzioni di denominazione

Quando assegni un nome al codebase, devi tenere presente le seguenti best practice:

Suggerimento Descrizione
Assegnazione di nomi ai metodi.
Facoltativo
Utilizza frasi verbali per assegnare un nome ai metodi, ad esempio makePayment().
Assegnazione di nomi alle proprietà.
Facoltativo
Utilizza frasi nominali per assegnare un nome alle proprietà, ad esempio inProgressTopicSelection.
Assegnazione di nomi agli stream di dati.
Facoltativo
Quando una classe espone uno stream di flussi o qualsiasi altro stream, la convenzione di denominazione è get{model}Stream, ad esempio getAuthorStream(): Flow<Author>. Se la funzione restituisce un elenco di modelli, utilizza il nome del modello al plurale: getAuthorsStream(): Flow<List<Author>>.
Assegnazione di nomi alle implementazioni delle interfacce.
Facoltativo
Utilizza nomi significativi per le implementazioni delle interfacce. Utilizza Default come prefisso se non riesci a trovare un nome migliore. Ad esempio, per un'interfaccia NewsRepository, potresti avere un OfflineFirstNewsRepository o un InMemoryNewsRepository. Se non riesci a trovare un nome appropriato, utilizza DefaultNewsRepository. Aggiungi il prefisso Fake alle implementazioni fake, ad esempio FakeAuthorsRepository.

Risorse aggiuntive

Per saperne di più sull'architettura di Android, consulta le seguenti risorse aggiuntive:

Documentazione

Visualizzazioni dei contenuti