Consigli per l'architettura Android

Questa pagina presenta diverse best practice e consigli per l'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. A ognuno è assegnata una priorità che riflette l'importanza del consiglio. L'elenco delle priorità è il seguente:

  • Consigliato vivamente:implementa questa pratica a meno che non sia in contrasto con il tuo approccio.
  • Consigliato:questa pratica probabilmente migliorerà la tua app.
  • (Facoltativo):questa pratica può migliorare la tua app in determinate circostanze.

Architettura a livelli

La nostra architettura a livelli consigliata favorisce la separazione delle competenze. Deriva l'interfaccia utente dai modelli di dati, rispetta il principio di un'unica fonte attendibile e segue i principi del flusso di dati unidirezionale. Ecco alcune best practice per l'architettura a livelli:

Suggerimento Descrizione
Utilizza un livello dati chiaramente definito. 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 della UI 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 per il livello UI, consulta la sezione Livello UI.
Esporre i dati dell'applicazione dal livello dati utilizzando un repository.

Assicurati che i componenti nel livello UI, come i composable 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 per le coroutine, consulta Best practice per le coroutine in Android.

Utilizza un livello di dominio. Utilizza un livello di dominio con i 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 è visualizzare i dati dell'applicazione sullo schermo e fungere da punto principale di interazione dell'utente. Ecco 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 della UI utilizzando il pattern Observer e ricevono azioni dalla UI tramite chiamate di metodi.
Utilizza AAC ViewModels se i relativi vantaggi si applicano alla tua app. Utilizza AAC ViewModels 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 Consigli sull'architettura.

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

Utilizza la raccolta dello stato dell'interfaccia utente in base al ciclo di vita. Raccogli lo stato della UI dalla UI utilizzando il builder di coroutine consapevole del ciclo di vita appropriato, collectAsStateWithLifecycle.

Scopri di più su collectAsStateWithLifecycle.

Non inviare eventi dalla ViewModel alla 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 UI, vedi Gestire gli eventi ViewModel.
Utilizza un'applicazione con una sola attività. Utilizza Navigazione 3 per spostarti tra le schermate e creare link diretti alla tua app se ha più di una schermata.
Utilizza Jetpack Compose. Utilizza Jetpack Compose per creare nuove app per smartphone, tablet, dispositivi pieghevoli e Wear OS.

Il seguente snippet mostra come raccogliere lo stato della UI in modo consapevole del ciclo di vita:

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

ViewModel

Le ViewModel sono responsabili della fornitura dello stato dell'UI e dell'accesso al livello dati. Ecco alcune best practice per i ViewModel:

Suggerimento Descrizione
Mantieni i ViewModel indipendenti dal ciclo di vita di Android. Nei ViewModels, non conservare un riferimento a nessun tipo correlato al ciclo di vita. Non trasmettere 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.

ViewModel interagisce con i livelli di dati o di dominio utilizzando:

  • Flussi Kotlin per la ricezione dei dati delle applicazioni
  • Funzioni suspend per eseguire azioni utilizzando viewModelScope
Utilizza ViewModels a livello di schermata.

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

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

Per i composable 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 composable.

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

Lo snippet seguente mostra come esporre lo stato della 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 dell'attività:

Suggerimento Descrizione
Utilizza effetti che tengono conto del ciclo di vita nei composable anziché eseguire l'override dei callback del ciclo di vita di Activity.

Non eseguire l'override dei metodi del ciclo di vita Activity, come onResume, per eseguire attività correlate alla UI. Utilizza invece LifecycleEffects di Compose o gli ambiti delle coroutine in base al ciclo di vita:

Il seguente snippet descrive 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'inserimento delle dipendenze. Utilizza le best practice per l'inserimento delle dipendenze, principalmente l'inserimento del costruttore, quando possibile.
Se necessario, limita l'ambito a un componente. Definisci l'ambito di un contenitore di dipendenze quando il tipo contiene dati modificabili che devono essere condivisi o quando il tipo è costoso da inizializzare ed è ampiamente utilizzato nell'app.
Utilizza Hilt. Utilizza Hilt o l'inserimento delle dipendenze manuale nelle app semplici. Utilizza Hilt se il tuo progetto è sufficientemente complesso, ad esempio se include uno dei seguenti elementi:
  • Più schermate con ViewModels
  • Utilizza WorkManager
  • Contiene ViewModels con ambito nello stack di navigazione

Test

Di seguito sono riportate alcune best practice per i test:

Suggerimento Descrizione
Sapere cosa testare.

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

  • Test delle unità per i ViewModel, inclusi i flussi
  • Test unitari per le entità del data layer, ovvero repository e origini dati
  • Test di navigazione dell'interfaccia utente utili come test di regressione nell'integrazione continua
  • Test delle prestazioni utilizzando Macrobenchmark per misurare l'avvio dell'app e il rendering dell'interfaccia utente
Preferisce i falsi ai mock. Per saperne di più sull'utilizzo di test doppi, consulta Utilizzare test doppi in Android.
Testa StateFlows. Quando esegui il test di StateFlow, procedi nel seguente modo:

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

Prestazioni

Di seguito sono riportate alcune best practice per migliorare il rendimento:

Suggerimento Descrizione
Utilizza R8 per ridurre e ottimizzare la tua app. Attiva R8 in modalità completa nella configurazione di compilazione per rimuovere il codice inutilizzato, offuscare le classi e riscrivere il codice per migliorare le prestazioni di runtime.
Implementa i profili di baseline e i profili di avvio. Genera profili di base e profili di avvio per precompilare i percorsi di codice critici. In questo modo si riduce notevolmente la latenza di avvio (TTID e TTFD) e si riducono al minimo i problemi di jank e ANR.
Testa le prestazioni con Macrobenchmark. Utilizza la libreria Macrobenchmark per testare le prestazioni della tua app e prevenire le regressioni.

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 necessari all'app.
  • I repository possono mappare i modelli DAO a classi di dati più semplici con solo le informazioni necessarie al livello UI.
  • ViewModel può includere modelli del livello dati nelle classi UiState.

Convenzioni di denominazione

Quando assegni un nome al tuo codebase, tieni presente le seguenti best practice:

Suggerimento Descrizione
Metodi di denominazione.
Facoltativo
Utilizza espressioni verbali per denominare i metodi, ad esempio makePayment().
Proprietà di denominazione.
Facoltativo
Utilizza sintagmi nominali per denominare le proprietà, ad esempio inProgressTopicSelection.
Assegnazione di nomi agli stream di dati.
Facoltativo
Quando una classe espone un flusso Flow o qualsiasi altro flusso, 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>>.
Denominazione delle implementazioni delle interfacce.
Facoltativo
Utilizza nomi significativi per le implementazioni delle interfacce. Utilizza Default come prefisso se non è possibile trovare un nome migliore. Ad esempio, per un'interfaccia NewsRepository, potresti avere un OfflineFirstNewsRepository o un InMemoryNewsRepository. Se non riesci a trovare un nome adatto, utilizza DefaultNewsRepository. Aggiungi il prefisso Fake alle implementazioni fittizie, come in FakeAuthorsRepository.

Risorse aggiuntive

Per ulteriori informazioni sull'architettura di Android, consulta le seguenti risorse aggiuntive:

Documentazione

Visualizza contenuti