5 consejos para estructurar el código en Jetpack Compose
Antonio Leiva

Escribir la interfaz con código es genial, pero puede ser que pronto se nos vaya de las manos.

Aunque la forma de escribir el código de Jetpack Compose es muy natural y relativamente directa, sí que es verdad que en pro de la flexibilidad, también hay algunos componentes que ocupan bastante código, y pueden hacer que este crezca indiscriminadamente.

Por ejemplo, solo para escribir una AppBar con dos acciones, hemos necesitado esto:

TopAppBar(
    title = { Text(stringResource(id = R.string.app_name)) },
    actions = {
        IconButton(onClick = { /*TODO*/ }) {
            Icon(
                imageVector = Icons.Default.Search,
                contentDescription = null
            )
        }
        IconButton(onClick = { /*TODO*/ }) {
            Icon(
                imageVector = Icons.Default.Share,
                contentDescription = null
            )
        }
    }
)

Aunque ya hemos extraído algunos componentes, la verdad es que aún queda un poco por hacer.

Así que en este artículo te voy a dar algunos consejos para que el código de Compose no se te vaya de las manos.

¡Atención! Si quieres acceder más rápido a todo el contenido en vídeo, organizado, con contenido extra, soporte y muchas más sorpresas, puedes apuntarte gratis a mi formación Compose Expert y ver gratis el primer módulo de más de 3 horas con todo lo necesario para empezar.

1. Crea un Composable con la base de tu App

Hay una serie de configuraciones que, si quieres que previews reflejen correctamente, vas a tener que repetir cada vez.

Lo ideal es extraer un Composable que defina esa configuración, para luego poder reutilizarlo.

En nuestro caso, el código a extraer es:

MyMoviesTheme {
    // A surface container using the 'background' color from the theme
    Surface(color = MaterialTheme.colors.background) {
    ...
}

Para ello, nos podemos crear un componente MyMoviesApp que contenga esa configuración. Lo que nos gustaría es tener algo de este estilo:

MyMoviesApp {
    ...
}

Lo que necesitamos es una función con este nombre, que a su vez acepte un Composable como parámetro.

Para ello, basta con anotar una lambda que devuelva Unit (es obligatorio, ya que los Composables no devuelven nada, solo emiten UI) con la anotación @Composable:

@Composable
fun MyMoviesApp(content: @Composable () -> Unit) {
    MyMoviesTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            content()
        }
    }
}

Con esta función hacemos un wrapper del tema y la Surface y emitimos el content que recibimos como argumento.

Ahora en la Activity podemos hacer:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        MyMoviesApp {
           ...
        }
    }
}

Además de usarlo para la App completa, también podemos reaprovecharlo para las previews, de tal forma que la configuración también se aplique a estas:

@Preview
@Composable
fun MediaListItemPreview() {
    MyMoviesApp {
        val mediaItem = MediaItem(1, "Item 1", "", MediaItem.Type.VIDEO)
        MediaListItem(mediaItem = mediaItem)
    }
}

2. Divide el Composable en otros más pequeños

Haz tantos y tan pequeños como sea necesario para que el código sea más fácil de entender.

Los Composables deberían explicar bien lo que hacen y tener nombres semánticos, en vez de tener un montón de elementos visuales seguidos que nos dificultan la lectura.

Por ejemplo, de aquí:

Scaffold(
    topBar = {
        TopAppBar(
            title = { Text(stringResource(id = R.string.app_name)) },
            actions = {
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(
                        imageVector = Icons.Default.Search,
                        contentDescription = null
                    )
                }
                IconButton(onClick = { /*TODO*/ }) {
                    Icon(
                        imageVector = Icons.Default.Share,
                        contentDescription = null
                    )
                }
            }
        )
    }
) { padding ->
    MediaList(Modifier.padding(padding))
}

Podríamos extraer perfectamente la TopAppBar:

Scaffold(
    topBar = { MainAppBar() }
) { padding ->
    MediaList(Modifier.padding(padding))
}

Mucho mejor, ¿verdad?

Incluso dentro de la barra, podemos simplificar las acciones:

@Composable
fun MainAppBar() {
    TopAppBar(
        title = { Text(stringResource(id = R.string.app_name)) },
        actions = {
            AppBarAction(
                imageVector = Icons.Default.Search,
                onClick = { /* TODO */ }
            )
            AppBarAction(
                imageVector = Icons.Default.Share,
                onClick = { /* TODO */ }
            )
        }
    )
}

@Composable
private fun AppBarAction(
    imageVector: ImageVector,
    onClick: () -> Unit
) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = imageVector,
            contentDescription = null
        )
    }
}

Por supuesto, no dejes los Composables todos en el mismo archivo, crea uno por cada componente nuevo.

3. Crea Composables que definan una pantalla

Ahora mismo solo tenemos una pantalla, así que podemos tirarlo todo en el setContent { } del MainActivity y olvidarnos.

Pero esto no va a ser así cuando tengamos que pintar más pantallas.

Lo ideal es extraer esto a funciones propias:

@Composable
fun MainScreen() {
    MyMoviesApp {
        Scaffold(
            topBar = { MainAppBar() }
        ) { padding ->
            MediaList(Modifier.padding(padding))
        }

    }
}

Y la Activity se queda tan sencilla como esto:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

4. Estructura los paquetes de UI por pantallas

De esa forma el código te quedará mucho más ordenado, y puedes crear tantos ficheros de Composables como te sean necesarios.

Mira cómo he estructurado yo este proyecto:

Dentro del paquete ui (que ya existe, se creó al crear el proyecto desde cero con Compose) he añadido el paquete screens, y dentro, un paquete por cada pantalla. Lo he dejado ya preparado para cuando cree la pantalla de detalle.

Además, dentro del paquete main he creado un archivo para la propia pantalla (MainScreen), otro para la barra superior (MainAppBar) y otro para la lista (MediaList). No es la única forma válida de dividirlo, pero es la que por el momento he creído la adecuada.

Dentro de cada archivo, puedes también tener las previews relacionadas con los componentes de ese archivo.

También he creado un paquete model, aunque seguramente este evolucionaría en el futuro si la App fuera más grande, pero para este ejemplo sobra.

Finalmente tenemos MyMoviesApp en el root de ui, ya que solo va a existir uno de estos por proyecto, y MainActivity en el root de app por lo mismo.

Ya hablaremos de navegación en el próximo artículo y te mostraré opciones, pero si sigues lo que te voy a enseñar, solo vas a tener una Activity.

5. Extrae las dimensiones

Como es tan sencillo, es muy tentador dejar las dimensiones hardcodeadas gracias a la propiedad de extensión dp.

Pero esto nos va a ser un problema si trabajamos con pantallas de múltiples tamaños (lo que en Android es casi inevitable), ya que lo que en un dispositivo pequeño queda bien, en uno más grande va a ser insuficiente.

Para eso necesitamos un sistema que, en función del tamaño de la pantalla, nos devuelva un valor u otro. Nos podemos crear el nuestro propio basado en CompositionLocal siguiendo la idea de lo que se hace con los temas.

Pero la realidad es que ya tenemos en el propio sistema de Android y que funciona muy bien: los resources.

Además, acceder a ellos desde Compose es muy sencillo, así que vamos a hacerlo. Con estas dimensiones:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="cell_min_width">150dp</dimen>
    <dimen name="cell_thumb_height">200dp</dimen>
    <dimen name="cell_play_icon_size">92dp</dimen>
    
    <dimen name="padding_xsmall">2dp</dimen>
    <dimen name="padding_medium">16dp</dimen>
</resources>

Podemos eliminar las dimensiones hardcodeadas. Por ejemplo en el LazyVerticalGrid:

LazyVerticalGrid(
    cells = GridCells.Adaptive(dimensionResource(R.dimen.cell_min_width)),
    contentPadding = PaddingValues(dimensionResource(R.dimen.padding_xsmall)),
    modifier = modifier
) {
    items(getMedia()) {
        MediaListItem(
            mediaItem = it,
            modifier = Modifier.padding(dimensionResource(R.dimen.padding_xsmall))
        )
    }
}

Un código más ordenado es más reutilizable

Al final, una de las grandes ventajas de poder escribir la UI en código Kotlin es que podemos utilizar toda la potencia del lenguaje para ayudarnos con ello.

Con los XMLs estábamos mucho más limitados.

Es por eso que nos podemos crear componentes genéricos que nos permitan reutilizar al máximo nuestro código.

Ya lo tenemos todo listo para poder empezar a navegar entre pantallas, así que si no quieres perderte nada, apúntate a mi formación Compose Expert, donde tendrás acceso gratuito a un primer módulo donde aprenderás todas las bases del trabajo con Jetpack Compose, y podrás ver el siguiente vídeo sobre navigation.

Apúntate ahora a Compose Expert y accede a más de 3h de contenido gratuito, soporte, extras y mucho más totalmente gratis.

Quizá también te interese…

Navegación en Jetpack Compose con Navigation Compose

Navegación en Jetpack Compose con Navigation Compose

Jetpack Compose es un cambio de paradigma enorme en muchos aspectos. Cambia la forma de pensar en casi todos los puntos involucrados en el desarrollo de una App Android. Y la navegación no iba a ser menos. ¿Cómo se navega en Jetpack Compose? ¿Qué opciones tenemos?...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de Antonio Leiva Gordillo, con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *