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. Ciascuna ha una priorità che riflette l'importanza del consiglio. L'elenco delle priorità è il seguente:

  • Fortemente consigliato: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 più livelli

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

Consiglio 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 del livello UI, consulta 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 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:

Consiglio 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'interfaccia utente all'interfaccia utente.

Per ulteriori informazioni sulle best practice di ViewModel, consulta Suggerimenti per l'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 dell'interfaccia utente, vedi Gestire gli eventi ViewModel.
Utilizza un'applicazione con una sola attività. Utilizza Navigazione 3 per spostarti tra le schermate e creare deep link 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. Di seguito sono riportate alcune best practice per i ViewModel:

Consiglio 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 quanto segue:

  • 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,
  • Attività/Frammenti nelle visualizzazioni,
  • Destinazioni o grafici quando utilizzi Jetpack Navigation.
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 data layer, è 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à:

Consiglio Descrizione
Utilizza effetti sensibili al ciclo di vita nei composable anziché eseguire l'override dei callback del ciclo di vita 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:

Consiglio Descrizione
Utilizza l'iniezione delle dipendenze. Utilizza le best practice per l'inserimento delle dipendenze, principalmente il constructor injection, quando possibile.
Limita l'ambito a un componente, se necessario. Definisci l'ambito di un contenitore di dipendenze quando il tipo contiene dati modificabili che devono essere condivisi o quando l'inizializzazione del tipo è costosa ed è ampiamente utilizzato nell'app.
Utilizza Hilt. Utilizza Hilt o l'inserimento manuale delle dipendenze 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
  • Ha ViewModels con ambito nello stack di navigazione back

Test

Di seguito sono riportate alcune best practice per i test:

Consiglio 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 in CI
Preferisci i fake ai mock. Per ulteriori informazioni 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.

Modelli

Segui queste best practice quando sviluppi modelli nelle tue app:

Consiglio Descrizione
Crea un modello per livello nelle app complesse.

Nelle app complesse, crea nuovi modelli in livelli o componenti diversi 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 data layer nelle classi UiState.

Convenzioni di denominazione

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

Consiglio 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 uno stream di Flow 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>>.
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