Recomendaciones para la arquitectura de Android

En esta página, se presentan varias recomendaciones y prácticas recomendadas para la arquitectura. Adopta estas funciones para mejorar la calidad, la solidez y la escalabilidad de tu app. También facilitan el mantenimiento y la prueba de tu app.

Las siguientes prácticas recomendadas se agrupan por tema. Cada una tiene una prioridad que refleja cuán recomendable es según el equipo. La lista de prioridades es la siguiente:

  • Muy recomendada: Deberías implementar esta práctica, a menos que entre en conflicto con tu estrategia.
  • Recomendada: Es probable que esta práctica mejore tu app.
  • Opcional: Esta práctica puede mejorar tu app en determinadas circunstancias.

Arquitectura en capas

En la arquitectura en capas que recomendamos, se prefiere la separación de problemas. Controla la IU a partir de modelos de datos, cumple con el principio de fuente de confianza única y sigue los principios del flujo unidireccional de datos. Estas son algunas prácticas recomendadas para la arquitectura en capas:

Recomendación Descripción
Usa una capa de datos claramente definida. La capa de datos expone los datos de la aplicación al resto de la app y contiene la gran mayoría de su lógica empresarial.
  • Debes crear repositorios incluso si contienen una sola fuente de datos.
  • En las apps pequeñas, puedes colocar tipos de capas de datos en un módulo o paquete data.
Usa una capa de la IU claramente definida. La capa de la IU muestra los datos de la aplicación en la pantalla y sirve como punto principal de la interacción con el usuario.
  • En las apps pequeñas, puedes colocar tipos de capas de datos en un módulo o paquete ui.
Aquí encontrarás más prácticas recomendadas sobre la capa de la IU.
La capa de datos debería exponer los datos de la aplicación mediante un repositorio.

Los componentes de la capa de la IU, como los elementos componibles, las actividades o los ViewModels, no deben interactuar de forma directa con una fuente de datos. Estos son algunos ejemplos de fuentes de datos:

  • Bases de datos, DataStore, SharedPreferences, APIs de Firebase
  • Proveedores de ubicación GPS
  • Proveedores de datos Bluetooth
  • Proveedor de estado de conectividad de red
Usa corrutinas y flujos. Usa corrutinas y flujos para establecer la comunicación entre capas.

Aquí encontrarás más prácticas recomendadas sobre corrutinas.

Usa una capa de dominio. Usa una capa de dominio, casos de uso, si necesitas reutilizar la lógica empresarial que interactúa con la capa de datos en varios ViewModels o si deseas simplificar la complejidad de la lógica empresarial de un ViewModel en particular.

Capa de la IU

La función de la capa de la IU es mostrar los datos de la aplicación en la pantalla y servir como punto principal de interacción con el usuario. Estas son algunas prácticas recomendadas para la capa de la IU:

Recomendación Descripción
Sigue el Flujo unidireccional de datos. Sigue los principios del flujo unidireccional de datos, en el que los ViewModels exponen el estado de la IU usando el patrón del observador y reciben acciones de la IU a través de llamadas de método.
Usa AAC de ViewModels si sus beneficios se aplican a tu app. Usa AAC de ViewModels para controlar la lógica empresarial y recuperar datos de la aplicación a fin de exponer el estado de la IU a la IU (Compose o vistas de Android).

Consulta más prácticas recomendadas para ViewModel aquí.

Consulta los beneficios de ViewModels aquí.

Usa la recopilación de estado de la IU optimizada para ciclos de vida. Recopila el estado de la IU con el compilador de corrutinas optimizado para ciclos de vida: repeatOnLifecycle en el sistema de vistas y collectAsStateWithLifecycle en Jetpack Compose.

Obtén más información sobre repeatOnLifecycle.

Obtén más información sobre collectAsStateWithLifecycle.

No envíes eventos del ViewModel a la IU. Procesa el evento inmediatamente en ViewModel y genera una actualización de estado con el resultado del control del evento. Obtén más información sobre los eventos de IU aquí.
Usa una aplicación de una sola actividad. Usa Navigation Fragments o Navigation Compose para navegar entre pantallas y establece un vínculo directo a tu app si esta tiene más de una.
Usa Jetpack Compose. Usa Jetpack Compose para compilar apps nuevas para teléfonos, tablets, plegables y dispositivos Wear OS.

En el siguiente fragmento, se describe cómo recopilar el estado de la IU de manera optimizada para los ciclos de vida:

Vistas

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
                }
            }
        }
    }
}

Compose

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

ViewModel

Los ViewModels son responsables de proporcionar el estado de la IU y el acceso a la capa de datos. Estas son algunas prácticas recomendadas para ViewModels:

Recomendación Descripción
Los ViewModels deben ser independientes del ciclo de vida de Android. Los ViewModels no deben contener referencias a ningún tipo relacionado con el ciclo de vida. No pases Activity, Fragment, Context ni Resources como dependencia. Si algo necesita un Context en el ViewModel, debes evaluar con atención si está en la capa correcta.
Usa corrutinas y flujos.

ViewModel interactúa con los datos o las capas de dominio mediante lo siguiente:

  • Flujos de Kotlin para recibir datos de aplicaciones
  • Funciones suspend para realizar acciones con viewModelScope
Usa ViewModels a nivel de la pantalla.

No uses ViewModels en piezas de IU reutilizables. Debes usar ViewModels en lo siguiente:

  • Elementos componibles a nivel de pantalla
  • Actividades o fragmentos en vistas
  • Destinos o gráficos cuando se usa Jetpack Navigation
Usa clases contenedoras de estados sin formato en componentes de IU reutilizables. Usa clases contenedoras de estados sin formato para controlar la complejidad en componentes de IU reutilizables. Cuando haces esto, el estado se puede elevar y controlar de forma externa.
No uses AndroidViewModel. Usa la clase ViewModel, no AndroidViewModel. No se debe usar la clase Application en ViewModel. En su lugar, mueve la dependencia a la IU o a la capa de datos.
Expón un estado de IU. Los ViewModels deben exponer datos a la IU a través de una única propiedad llamada uiState. Si la IU muestra varios datos no relacionados, la VM puede exponer varias propiedades del estado de la IU.
  • Debes hacer que uiState sea StateFlow.
  • Debes crear el uiState mediante el operador stateIn con la política WhileSubscribed(5000) (ejemplo) si los datos vienen como una transmisión de datos de otras capas de la jerarquía.
  • Para los casos más simples sin transmisiones de datos provenientes de la capa de datos, resulta aceptable usar un MutableStateFlow expuesto como StateFlow inmutable (ejemplo).
  • Puedes optar por que ${Screen}UiState sea una clase de datos que pueda contener datos, indicadores de carga y errores. Esta clase también podría ser sellada si los diferentes estados son exclusivos.

En el siguiente fragmento, se describe cómo exponer el estado de la IU desde 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 de vida

Las siguientes son algunas recomendaciones para trabajar con el ciclo de vida de Android:

Recomendación Descripción
No anules los métodos de ciclo de vida de objetos Activity ni Fragment. No anules métodos del ciclo de vida, como onResume, en actividades o fragmentos. En su lugar, usa LifecycleObserver. Si la app necesita realizar un trabajo cuando el ciclo de vida alcanza un cierto Lifecycle.State, usa la API de repeatOnLifecycle.

En el siguiente fragmento, se describe cómo realizar operaciones en función de un determinado estado de ciclo de vida:

Vistas

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) {
                // ...
            }
        }
    }
}

Compose

@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)
        }
    }
}

Control de dependencias

Hay varias prácticas recomendadas que debes tener en cuenta cuando administras dependencias entre componentes:

Recomendación Descripción
Usa la inserción de dependencias. Usa las prácticas recomendadas de inserción de dependencias, en especial, la inserción del constructor, cuando sea posible.
Define el alcance para un componente cuando sea necesario. Define el alcance para un contenedor de dependencias cuando el tipo contenga datos mutables que se deban compartir o resulte costoso inicializar el tipo y se use mucho en la app.
Usa Hilt. Usa Hilt o la inserción manual de dependencias en apps simples. Usa Hilt si tu proyecto es lo suficientemente complejo. Por ejemplo, si tienes lo siguiente:
  • Varias pantallas con ViewModels: integración
  • Uso de WorkManager: integración
  • Uso avanzado de Navigation, como los ViewModels con alcance para el gráfico de navegación: integración

Pruebas

Estas son algunas recomendaciones para las pruebas:

Recomendación Descripción
Comprende qué probar.

A menos que el proyecto sea tan simple como la app de Hello World, deberías realizar pruebas, como mínimo, con lo siguiente:

  • ViewModels de prueba de unidades, incluidos los flujos
  • Entidades de capa de datos de prueba de unidades (es decir, repositorios y fuentes de datos)
  • Pruebas de navegación de la IU que son útiles como pruebas de regresión en CI
Opta por simulaciones en lugar de muestras. Obtén más información en la documentación para usar dobles de prueba en Android.
Prueba StateFlows. Cuando pruebes StateFlow, haz lo siguiente:

Para obtener más información, consulta la guía Qué probar en Android DAC.

Modelos

Debes tener en cuenta las siguientes prácticas recomendadas a la hora de desarrollar modelos en tus apps:

Recomendación Descripción
Crea un modelo por capa en apps complejas.

En apps complejas, crea modelos nuevos en diferentes capas o componentes cuando hacerlo tenga sentido. Considera los siguientes ejemplos:

  • Una fuente de datos remota puede asignar el modelo que recibe a través de la red a una clase más simple con solo los datos que necesita la app.
  • Los repositorios pueden asignar modelos DAO a clases de datos más simples con solo la información que necesita la capa de la IU.
  • ViewModel puede incluir modelos de capas de datos en las clases UiState.

Convenciones de nombres

A la hora de nombrar tu base de código, debes tener en cuenta las siguientes prácticas recomendadas:

Recomendación Descripción
Nombra métodos.
Opcional
Los métodos deberían constar de una frase verbal. Por ejemplo, makePayment().
Nombra propiedades.
Opcional
Las propiedades deberían constar de una frase nominal. Por ejemplo, inProgressTopicSelection.
Nombra flujos de datos.
Opcional
Cuando una clase expone una transmisión de flujo, de LiveData o cualquier otra, la convención de nombres es get{model}Stream(). Por ejemplo, getAuthorStream(): Flow<Author>. Si la función muestra una lista de modelos, el nombre del modelo debería estar en plural: getAuthorsStream(): Flow<List<Author>>.
Nombra las implementaciones de interfaces.
Opcional
Los nombres de las implementaciones de interfaces deberían ser significativos. Usa Default como prefijo si no se encuentra un nombre mejor. Por ejemplo, para una interfaz NewsRepository, puedes tener un OfflineFirstNewsRepository o InMemoryNewsRepository. Si no encuentras ningún nombre apropiado, usa DefaultNewsRepository. Las implementaciones simuladas deben contener el prefijo Fake, como en FakeAuthorsRepository.