Creación de una aplicación de aprendizaje de idiomas con Compose – Parte 1
Este es el primer artículo de una nueva serie en la que comparto mi viaje en la creación de una aplicación de aprendizaje de idiomas con Jetpack Compose. Te mostraré mi progreso diario, cómo ha evolucionado la aplicación con el tiempo, por qué tomé ciertas decisiones, etc. También comparto las cosas nuevas que aprendo en el camino que pueden resultarle útiles. Cuando empecé a aprender a programar, uno de mis primeros proyectos fue un sitio web donde repetía palabras que estaba aprendiendo en otros idiomas. Avance rápido unos años y ahora necesito algo similar. Sin embargo, esta vez lo crearé con Jetpack Compose y trataré de lanzarlo en Play Store. Si es un ingeniero junior, esta serie lo ayudará al mostrarle cómo pasé de la nada a una aplicación terminada para publicarla en Play Store. Si es un ingeniero senior, con suerte esta serie le enseñará algunas cosas nuevas sobre Jetpack Compose y tal vez incluso le haga reconsiderar la forma en que resuelve algunos problemas. Llamo a esta aplicación Lingua, es «idioma» en latín. Se supone que es una combinación de Duolingo y Anki y algunas cosas que creo que serán útiles. Uno de mis objetivos para 2023 es aprender italiano y también necesito dedicar parte de mi tiempo a crear de forma activa en lugar de aprender cosas nuevas de forma pasiva. He leído mucho sobre Compose, ahora es el momento de poner en práctica este conocimiento (luego me di cuenta de que en realidad sabía muy poco sobre Compose). Si se me ocurre una versión que se ve bien y creo que tiene sentido, la publicaré en Play Store. Este es el primer borrador que hice en Figma; está lejos de la versión final, pero me guía en la dirección correcta.Primer borrador el 01/09/2023 Estoy creando esta aplicación de forma iterativa, así que, como verá, las primeras versiones de la aplicación se verán bastante feas, pero está bien, el objetivo es hacer que algo funcione primero. Puedo volver a él más tarde y mejorar el diseño. Dividiré mis actualizaciones en días para que tengas una idea de cuánto tiempo llevó crear esto. Dedico aproximadamente 1 hora al día a Mis tareas. Uso Notion. Para el diseño utilizo Figma. Uso Kotlin y Jetpack Compose. Mencionaré las otras bibliotecas cuando las use. A continuación se muestra mi primer borrador de la pantalla de inicio. Tendré algún tipo de sección de progreso que muestre cuánto ha aprendido el usuario en un día, semana y mes determinados. También quiero agregar algún tipo de función de huelga para motivar a las personas a seguir aprendiendo. Los datos son muy valiosos para mí, por lo que también incluyo gráficos detallados sobre cosas que pueden ser útiles para el usuario. En lugar de simplemente tener el «ejercicio» habitual que tienen la mayoría de las aplicaciones, también me gustaría crear un ejercicio personalizado que te permita elegir lo que quieres practicar. Y finalmente, quiero mostrar la lista de cursos/mazos a los que está suscrito el usuario. Llamaré a estas cartas «mazos» de ahora en adelante, pero el nombre puede cambiar en el futuro.Diseño de pantalla de inicio original Mis pantallas se dividen en ruta y pantalla. En el ejemplo anterior, crearía HomeRoute y HomeScreen. La ruta es solo lo que se agrega a la carta de navegación, la pantalla es el contenido visual real. Aquí tenemos el NavHost principal de la aplicación. NavHost (navController = navController, startDestination = Routes.Home.route) { composable(Routes.Home) { HomeRoute(navController) } } Y aquí la HomeRoute que define la pantalla de inicio más adelante. Para las rutas, acabo de definir una clase sellada con mis rutas, por ahora esto se adapta a mis necesidades, pero probablemente también cambie esto en el futuro. Rutas de clase selladas (ruta val: Cadena) { objeto Inicio: Rutas («/») } Tenga en cuenta que esta es mi primera aplicación importante que usa Compose, por lo que estoy obligado a cometer errores y aprender en el camino. Continué creando los otros componentes, pero por ahora solo tengo valores codificados para ellos. Una cosa que debo mencionar es cómo logré recortar la imagen de la bandera. Estoy usando Coil, no pude recortar la imagen directamente, así que la envolví con una superficie y definí una esquina redondeada solo para la esquina del extremo inferior, ya que la esquina de inicio superior ya está recortada de la tarjeta. También configuré la relación de aspecto en 16/9, pero no estoy 100% satisfecho con el resultado. , // TODO: Agregar marcador de posición contentScale = ContentScale.FillBounds, modifier = Modifier.width(36.dp).aspectRatio(16 / 9f)) } Al final del Día 1 había creado la pantalla que puede ver a continuación, es aún no es funcional, pero es un buen paso en la dirección que quiero seguir.Progreso hasta el final del día 1 El día 2 acabo de hacer la pantalla de la biblioteca, por ahora es bastante fácil. Solo enumera los mazos disponibles. En el futuro agregaré más opciones de clasificación para que sea más fácil para el usuario encontrar lo que busca. También tiene un botón que permite al usuario crear un nuevo mazo.Diseño de biblioteca original Esto era lo que había logrado al final del Día 2, nada especial. Solo un LazyColumn y un FloatingActionButton. De hecho, también creé ViewModel para esta pantalla, por lo que más tarde solo necesito conectarlo a una fuente de datos y se enumerarán los datos correctos.Progreso hasta el final del día 2El día 3 comencé a trabajar en la pantalla de creación de mazos. Básicamente, necesito al menos una baraja de cartas para poder desarrollar el resto de la aplicación, así que voy por ese camino primero. Como puede ver a continuación, esta pantalla no es tan fácil como las demás. Primero puedes nombrar tu mazo, construiré esto primero. Después de eso, puede cambiarlo a público/privado, lo guardaré para más adelante. Todo será público por ahora. Verá una bandera en la parte superior derecha, esa es una característica interesante, pero tampoco la implementaré ahora. En el medio tenemos la parte más importante de esta pantalla, las cartas que se juntan para formar una baraja. Por ahora, solo habrá un tipo de tarjeta, y eso es solo entrada <-> Producción. Por ejemplo, la palabra en inglés Bee Ape está en italiano, entonces sería algo así como Bee <-> mono Esto me permite hacer algunas cosas:
- Muestra la palabra «Bee» y el usuario tiene que escribir «ape».
- Muestra la palabra «simio» y el usuario tiene que escribir «abeja».
- Si hay más palabras, puedo mostrar algo como «Ape» y el usuario tiene que seleccionar una palabra de una lista como («Apple», «Bee, «Pie»).
Esta es básicamente una versión simple de Duolingo. Agregaré soporte para sonido e imagen más adelante, pero por ahora solo es una complejidad adicional. Finalmente tenemos un botón para agregar nuevos mapas y un botón de guardar para guardar todo.Al hacer mi primer diseño de mazo me encontré con algunos problemas, p. B. ¿Cómo paso la identificación del mazo a esta pantalla? Uso la misma pantalla para agregar y editar mazos, por lo que la ID tenía que ser un parámetro opcional. Resolví esto creando un nuevo objeto en Rutas y agregando los métodos createRoute y parse. Object EditDeck: Rutas(«/mazo/{id}/editar») { fun createRoute(mazoId: ¿Cadena?) = «/mazo/$mazoId /editar» fun parse(paquete: ¿Paquete?): ¿Cadena? = bundle?.getString(«id»)?.takeIf { it != «null» }}Y en mis rutas tengo el nuevo route.composable(Routes.EditDeck) { val deckId = Routes.EditDeck.parse(it. argumentos) EditDeckRoute(navController = navController, deckId = deckId)} Luego, cuando se crea mi pantalla, uso un LaunchedEffect para cargar la plataforma en ViewModel. @Composable fun EditDeckRoute(navController: NavController, deckId: String?, viewModel: EditDeckViewModel = hiltViewModel ()) { LaunchedEffect(deckId ?: «none») { viewModel.loadDeck(deckId) } EditDeckScreen(…) }El código en LaunchedEffect se ejecutará nuevamente cuando cambie la clave, en este caso cuando cambie la identificación de la plataforma y eso es exactamente lo que quiero. Al final del día 3, solo había construido una pequeña parte de esta pantalla.Progreso al final del Día 3 El Día 4 dejé de trabajar en la pantalla para crear mazos y cambié a la pantalla para crear cartas. Los mazos y las cartas son la base de esta aplicación, todo lo demás en la aplicación girará en torno a ellos. Por el momento solo estoy desarrollando el tipo «Text» o lo que solía llamar Input <-> llamada salida. Contendrá un campo para entrada y un campo para salida (necesito pensar en mejores nombres para estas cosas jajaja). Más tarde también quiero agregar un tipo de «Información» que funciona de manera un poco diferente, pero eso es para más adelante.Create Initial Card Draft Esta pantalla es bastante similar a la anterior, tengo mi EditDeckCardRoute y EditDeckCardScreen. También tenemos 2 entradas de texto y un FAB para almacenar el mapa. Una diferencia aquí es que para crear una tarjeta necesito la identificación del mazo y para cambiar la tarjeta necesito la identificación de la tarjeta. Para resolver esto creé 2 rutas: EditCard y AddCard.composable(Routes.AddCard) { val deckId = Routes.AddCard.parse(it.arguments) EditCardRoute(navController, deckId = deckId)} composable(Routes.EditCard) { val cardId = Routes.EditCard.parse(it.arguments) EditCardRoute(navController, cardId = cardId) } La persona que llama decide a qué ruta llamar en función de lo que quiere hacer, pero en el lado de la implementación me decidí cuando se crea ViewModel para usar la misma ruta, llama a una función para crear un nuevo mapa oa una para cargar un mapa existente. fun load(deckId: String?, cardId: String?) { when { deckId == null && cardId == null -> { Logger.e(«deckId y cardId son nulos») // navegar hacia arriba } deckId != null – > createNewCard(deckId) cardId != null -> loadCard(cardId) } }Así es como se ve esta pantalla al final del día 4. Pantalla bastante simple, pero ahora al menos puedo crear tarjetas simples y agregarlas a un cubierta.Progreso al final del día 4