Consigli per l'architettura Android

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

Le best practice riportate di seguito sono raggruppate per argomento. Ognuno di questi ha una priorità che riflette la raccomandazione del team. L'elenco di priorità è il seguente:

  • Consigliato vivamente:ti consigliamo di implementare questa pratica, a meno che non entri in conflitto fondamentale con il tuo approccio.
  • Consigliato:questa pratica potrebbe migliorare la tua app.
  • Facoltativo:questa pratica può migliorare la tua app in determinate circostanze.

Architettura a più livelli

La nostra architettura a livelli consigliata favorisce la separazione dei problemi. Determina l'interfaccia utente da modelli dei dati, è conforme al principio della singola fonte attendibile e segue i principi del flusso di dati unidirezionali. Di seguito sono riportate alcune best practice per l'architettura a più 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.
  • Dovresti creare repository anche se contengono solo un'unica origine dati.
  • Nelle app di piccole dimensioni, puoi scegliere di inserire i tipi di livelli 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.
  • Nelle app di piccole dimensioni, puoi scegliere di inserire i tipi di livelli dati in un pacchetto o modulo ui.
Scopri altre best practice per il livello UI qui.
Il livello dati deve esporre i dati dell'applicazione utilizzando un repository.

I componenti del livello dell'interfaccia utente, come gli elementi componibili, le attività o i ViewModel, non devono interagire direttamente con un'origine dati. Ecco alcuni esempi di origini dati:

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

Scopri altre best practice sulle coroutine qui.

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

Livello UI

Il ruolo del livello UI è visualizzare i dati dell'applicazione sullo schermo e fungere da punto di interazione principale con l'utente. Ecco alcune best practice per il livello UI:

Consiglio Descrizione
Segui Unidirectional Data Flow (UDF). Segui i principi del flusso di dati unidirezionale (UDF), in cui i ViewModel espongono lo stato dell'interfaccia utente utilizzando il pattern di osservazione e ricevono azioni dall'interfaccia utente tramite chiamate di metodo.
Utilizza i ViewModel AAC se i relativi 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'interfaccia utente all'interfaccia utente (Compose o Android Views).

Consulta altre best practice per ViewModel qui.

Scopri i vantaggi dei ViewModel qui.

Utilizza la raccolta dello stato dell'interfaccia utente che tiene conto del ciclo di vita. Raccogli lo stato dell'interfaccia utente utilizzando il builder di coroutine che tiene conto del ciclo di vita appropriato: repeatOnLifecycle nel sistema View e collectAsStateWithLifecycle in Jetpack Compose.

Scopri di più su repeatOnLifecycle.

Scopri di più su collectAsStateWithLifecycle.

Non inviare eventi dal ViewModel alla UI. Elabora immediatamente l'evento nel ViewModel e provoca un aggiornamento dello stato con il risultato della gestione dell'evento. Scopri di più sugli eventi UI qui.
Utilizza un'applicazione con una singola attività. Utilizza Navigation Fragments o Navigation Compose per spostarti tra le schermate e creare un link diretto alla tua app se ha più di una schermata.
Utilizza Jetpack Compose. Usa Jetpack Compose per creare nuove app per smartphone, tablet, pieghevoli e Wear OS.

Il seguente snippet illustra come raccogliere lo stato dell'interfaccia utente in modo consapevole del ciclo di vita:

Visualizzazioni

class MyFragment : Fragment() {

    private val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Process item
                }
            }
        }
    }
}

Scrivi

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

ViewModel

I ViewModel sono responsabili di fornire lo stato dell'interfaccia utente e l'accesso al livello di dati. Di seguito sono riportate alcune best practice per i ViewModel:

Consiglio Descrizione
I ViewModel devono essere indipendenti dal ciclo di vita di Android. I ViewModel non devono contenere un riferimento a nessun tipo relativo al ciclo di vita. Non passare Activity, Fragment, Context o Resources come dipendenza. Se qualcosa richiede un Context nel ViewModel, devi valutare 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 dell'applicazione,
  • Funzioni suspend per eseguire azioni utilizzando viewModelScope.
Utilizza ViewModels a livello di schermo.

Non utilizzare i ViewModel in componenti dell'interfaccia utente riutilizzabili. Dovresti utilizzare i ViewModel in:

  • Componenti composibili a livello di schermata,
  • Attività/frammenti nelle visualizzazioni,
  • Destinazioni o grafici quando utilizzi Jetpack Navigation.
Utilizza le classi di contenitori di stato semplici nei componenti dell'interfaccia utente riutilizzabili. Utilizza le classi di contenitori di stato semplici per gestire la complessità nei componenti dell'interfaccia utente riutilizzabili. In questo modo, lo stato può essere sollevato e controllato esternamente.
Non utilizzare AndroidViewModel. Utilizza il corso ViewModel, non AndroidViewModel. La classe Application non deve essere utilizzata in ViewModel. Sposta invece la dipendenza nell'interfaccia utente o nel livello di dati.
Esponi uno stato dell'interfaccia utente. I ViewModel devono esporre i dati all'interfaccia utente tramite una singola proprietà denominata uiState. Se l'interfaccia utente mostra più dati non correlati, la VM può esporre più proprietà di stato dell'interfaccia utente.
  • Devi impostare uiState su StateFlow.
  • Devi creare uiState utilizzando l'operatore stateIn con il criterio WhileSubscribed(5000) (esempio) se i dati provengono da uno stream di dati di altri livelli della gerarchia.
  • Per casi più semplici in cui non sono presenti flussi di dati provenienti dal livello dati, è accettabile utilizzare un elemento MutableStateFlow esposto come StateFlow immutabile (esempio).
  • Puoi scegliere di avere ${Screen}UiState come classe di dati che può contenere dati, errori e indicatori di caricamento. Questa classe potrebbe anche essere una classe sigillata se i diversi stati sono esclusivi.

Il seguente snippet illustra come esporre lo stato dell'interfaccia utente 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

Di seguito sono riportate alcune best practice per l'utilizzo del ciclo di vita di Android:

Consiglio Descrizione
Non eseguire l'override dei metodi di ciclo di vita in Activities o Fragment. Non eseguire l'override di metodi di ciclo di vita come onResume in Activities o Fragments. Utilizza invece LifecycleObserver. Se l'app deve eseguire un'operazione quando il ciclo di vita raggiunge un determinato Lifecycle.State, utilizza l'API repeatOnLifecycle.

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

Visualizzazioni

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onResume(owner: LifecycleOwner) {
                // ...
            }
            override fun onPause(owner: LifecycleOwner) {
                // ...
            }
        }
    }
}

Scrivi

@Composable
fun MyApp() {

    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner, ...) {
        val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                // ...
            }
        }

        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
        }
    }
}

Gestire le dipendenze

Esistono diverse best practice da seguire per gestire le dipendenze tra i componenti:

Consiglio Descrizione
Utilizza l'iniezione di dipendenze. Utilizza le best practice di iniezione di dipendenze, in particolare l'iniezione del costruttore, se possibile.
Limita l'ambito a un componente quando necessario. Assegna l'ambito a un container delle dipendenze quando il tipo contiene dati modificabili che devono essere condivisi o quando il tipo è costoso da inizializzare ed è ampiamente utilizzato nell'app.
Usa Hilt. Utilizza Hilt o l'inserimento manuale delle dipendenze nelle app semplici. Utilizza Hilt se il tuo progetto è abbastanza complesso. Ad esempio, se hai:
  • Integrazione di più schermate con ViewModel
  • Utilizzo di WorkManager: integrazione
  • Utilizzo avanzato di Navigation, ad esempio l'integrazione di ViewModel con ambito limitato al grafico di navigazione.

Test

Di seguito sono riportate alcune best practice per i test:

Consiglio Descrizione
Sai cosa testare.

A meno che il progetto non sia approssimativamente semplice come un'app Hello World, devi testarlo, almeno con:

  • Esegui test di unità sui ViewModel, inclusi i flussi.
  • Entità del livello dati del test di unità. ovvero repository e origini dati.
  • Test della navigazione dell'interfaccia utente utili come test di regressione in CI.
Preferisci i falsi alle simulazioni. Scopri di più nella documentazione sull'utilizzo del test Doppio in Android.
Testa StateFlows. Durante il test di StateFlow:

Per ulteriori informazioni, consulta la guida Cosa testare in Android DAC.

Modelli

Quando sviluppi modelli nelle tue app, devi osservare queste best practice:

Consiglio Descrizione
Crea un modello per livello nelle app complesse.

Nelle app complesse, crea nuovi modelli in livelli o componenti diversi quando necessario. 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 dell'interfaccia utente.
  • ViewModel può includere modelli di livello dati 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
I metodi devono essere una frase di verbi. Ad esempio, makePayment().
Denominazione delle proprietà.
Facoltativo
Le proprietà devono essere una frase nominale. Ad esempio, inProgressTopicSelection.
Assegnare un nome agli stream di dati.
Facoltativo
Quando una classe espone uno stream di Flow, LiveData o qualsiasi altro stream, la convenzione di denominazione è get{model}Stream(). Ad esempio, getAuthorStream(): Flow<Author> Se la funzione restituisce un elenco di modelli, il nome del modello deve essere al plurale: getAuthorsStream(): Flow<List<Author>>
Implementazioni delle interfacce di denominazione.
Facoltativo
I nomi per le implementazioni delle interfacce devono essere significativi. Avere 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. Le implementazioni false devono essere precedute dal prefisso Fake, come in FakeAuthorsRepository.