Creación de una aplicación de aprendizaje de idiomas con Compose – Parte 4 | de Victor Brandalise | abril 2023

Bienvenido a la Parte 4 de la serie Cree una aplicación de aprendizaje de idiomas con Compose. En esta serie comparto mi progreso en la creación de una aplicación de aprendizaje de idiomas. En esta serie, comparto mi progreso, las opciones de diseño y cualquier nuevo conocimiento que obtenga en el camino. Hoy cubriré los días 13-16. Si no ha leído los primeros tres artículos, puede encontrarlos aquí: Si desea saber exactamente qué cambios de código hice, puede hacer clic en [Code Diff] enlace al lado del encabezado del día y vea todos los cambios para ese día. Lleva mucho tiempo escribir estos artículos, de ahora en adelante continuaré describiendo todo lo que hice pero solo explicaré algunas cosas. Si quieres entender cómo hice algo que no expliqué, puedes mirar el [Code Diff].Empecé el día 13 modificando la carta del mazo para que, cuando la presiones, navegue para practicar con el mazo adecuado. También agregué íconos a la carta del mazo para indicar cuántas cartas necesitas revisar (permanecer) y cuántas cartas ya has aprendido (correcto).No es un gran cambio, pero sí información bastante útil para el usuario.Progreso hasta el final del día 13. El día 14 eliminé todos los mazos falsos que tenía e hice 2 con palabras reales: «Los 20 sustantivos italianos más populares» y «Palabras para los turistas italianos». Habrá cientos de mazos en el futuro, pero por ahora estos 2 al menos harán que el ejercicio parezca más real. También agregué código para mostrar un brindis cuando el usuario presiona una carta de mazo que no tiene cartas para revisar. Para hacer esto, agregué una nueva acción a mi interfaz MyDeckListAction.sealed MyDeckListAction {…object ShowNoCardsToReviewInfo : MyDeckListAction} . Luego, en el método que navega a la práctica, verifico si hay palabras que van a review.viewModelScope.launch {val action =if (model.cardsToReview <= 0) MyDeckListAction.ShowNoCardsToReviewInfoelse MyDeckListAction.NavigateToPractice(model.deckId)_action. emit(action)} También eliminé el botón Practicar de la pantalla de inicio, no lo implementaré en un futuro cercano, por lo que no tiene ningún valor estar allí.Progreso hasta el final del día 14 Comencé el día 15 implementando una de las clases más importantes de este proyecto, la clase que verifica si una respuesta es correcta. Necesito saber un poco más que si la respuesta es correcta o incorrecta. Si es correcto, también necesito saber si es una coincidencia exacta. Para hacer esto, creé una nueva clase que representa los posibles resultados del caso de uso. Interfaz sellada CheckPracticeAnswerResponse {clase de datos correcta (val isExactAnswer: Boolean) : CheckPracticeAnswerResponseobject Wrong : CheckPracticeAnswerResponsefun isCorrect() = esto es correcto} Después de eso, definí una lista de caracteres que quiero ignorar al verificar la respuesta. Eso significa «¿cómo?» y «cómo» se tratan como si fueran la misma cosa. valor privado charsToIgnore = Regex(«[?!,.;\»‘]») Ahora llegamos al código que verifica la respuesta. Comienzo por normalizar la respuesta escrita y las posibles respuestas (salidas), esto simplemente elimina los caracteres para ignorar. Luego comparo la respuesta escrita normalizada con las posibles respuestas normalizadas Respuesta si si cualquiera de ellos coincide, el usuario ingresó la respuesta correcta, de lo contrario es la respuesta incorrecta. )if (normalizedAnswer.isBlank())return Wrongif (normalizedOutputs.any { it == normalizedAnswer })return Correct (isExactAnswer = card.outputs.any { it == answer })return Wrong}private fun String.normalize() = trim().replace(charsToIgnore, «»)private fun List.normalize() = map { it.normalize() }Esta no es la versión final de esta clase todavía, pero es lo suficientemente buena por ahora Debido a que esta clase tiene una lógica que es un poco más complicada que el resto de la aplicación, yo He decidido escribir algunas pruebas unitarias para ello. En ese caso, creo que valdrá la pena mi tiempo. Aquí hay una prueba de ejemplo: private val card1 = card(input = «Potrebbe aiutarmi, per favore?»,outputs = listOf(«¿Podría ayudarme, por favor?»),)@Testfun answerWithoutQuestionMarkIsCorrect() {val answer = «Podría por favor ayúdenme»val result = useCase.checkAnswer(card1, answer)assertEquals(Correct(isExactAnswer = true), result)} También modifiqué mi PracticeViewModel para mostrar la respuesta en la pregunta para que me fuera más fácil esta función era ser probado Lo último que hice fue agregar un contenedor en la parte inferior de la pantalla. Aparece cuando escribe algo que no es exactamente la respuesta correcta.

  • Es un cuadro rojo si te equivocas en la respuesta.
  • Es un cuadro verde si su respuesta es bastante cercana a la respuesta real.

La transición predeterminada para AnimatedVisibility expande/contrae el contenedor vertical y horizontalmente, pero solo quiero que se expanda/contraiga verticalmente, así que tuve que cambiar la transición Entrar/Salir @Composableprivate fun InfoBox(state: PracticeState) {AnimatedVisibility(visible = state.infoText != null,enter = fadeIn() + expandIn(initialSize = {IntSize(it.width, 0) }), exit = ShrinkOut(targetSize = {IntSize(it.width, 0) }) + fadeOut( ) ) { val bgColor = state.infoBackgroundColorRes ?: return@AnimatedVisibilityBox(modifier = Modifier.fillMaxWidth().background(colorResource(id = bgColor)).padding(horizontal = 12.dp, vertical = 20.dp)) {. . .. }}}Después de 15 días finalmente tenemos la primera versión «utilizable» de la aplicación.Progreso hasta el final del día 15 Si el usuario ingresa la respuesta incorrecta o algo que no es exactamente la respuesta, mostraré un cuadro de respuesta correcta a continuación. Cuando eso sucede, no quiero que la entrada esté habilitada, por lo que necesito deshabilitarla de alguna manera. También me gustaría poder usar la tecla ENTER en mi teclado para pasar a la siguiente pregunta, principalmente usaré esto en un emulador, por lo que es una función útil para mí. En realidad, no deshabilito el campo, solo ignoro los nuevos valores si no quiero que obtengan nuevos caracteres. returnstate.answer = answer}Para continuar con la siguiente pregunta cuando se presiona la tecla ENTER, tuve que agregar el modificador onKeyEvent a la respuesta input.TextField(…modifier = Modifier.onKeyEvent { onKeyEvent(it.nativeKeyEvent) } )If el evento clave es diferente de ENTER. Simplemente lo ignoro, de lo contrario, emitiré Unit a un canal. agregó que el canal es para evitar presionar la tecla Intro varias veces en un corto período de tiempo. Si echa un vistazo a continuación, elimino el evento de la tecla Intro en 50 ms para evitar que esto suceda. Originalmente no tenía este canal, pero después de algunas pruebas descubrí que el evento de la tecla ENTER estaba causando algunos problemas y esta era la razón. valor privado enterEventChannel = MutableSharedFlow(extraBufferCapacity = 1,onBufferOverflow = BufferOverflow.DROP_OLDEST)viewModelScope. start { enterEventChannel.debounce(KEY_INPUT_DEBOUNCE_DELAY).collect { onContinue() }} Finalmente, en el método onContinue, cargo la siguiente pregunta o verifico la respuesta según el estado en el que se encuentra la pantalla. fun onContinue() { if (state .infoText != null) {loadNextQuestion()} else {checkAnswer()}}La pantalla del ejercicio se volvió un poco compleja y necesito refactorizarla, pero eso es para más adelante. La pantalla de inicio parecía demasiado simple, así que decidí agregar un color de fondo aleatorio a las cartas de mi mazo. La lógica es muy simple, por lo que los colores pueden repetirse, lo cual no es una idea, pero por ahora está bien. Hasta ahora, el algoritmo de verificación solo verifica si las palabras son iguales, ignorando algunos caracteres. Ese día volví a modificar el algoritmo para tener en cuenta las similitudes. Por ejemplo, si la respuesta es «tastiera» y el usuario escribe «tastier» o «astiera», quiero que esas palabras se consideren correctas. Ciertamente hay mejores formas de hacer esto, pero una forma más sencilla es usar la distancia de Levenshtein. Es un algoritmo que «mide la diferencia entre dos secuencias». No es un algoritmo complejo, este artículo debería brindarle una buena comprensión. No implementé el algoritmo yo mismo, encontré una versión de Java y la convertí a Kotlin. Lo puse en la clase WordDistanceCalculator. class CheckPracticeAnswerUseCaseImpl @Inject constructor(private val wordDistanceCalculator: WordDistanceCalculator) Para cada salida, calculo la distancia. Luego multiplico la longitud de la palabra por un umbral que defino, que actualmente es 0.2. Esto significa que la diferencia no puede exceder el 20% de la longitud de la palabra, con una palabra de 5 dígitos puede ser de 1 carácter. Si la distancia es menor o igual al umbral, devuelvo que la respuesta es correcta, pero configuro isExactAnswer en falso para saber que el usuario ingresó algo similar pero no la respuesta exacta.normalizedOutputs.forEach { salida ->val distancia = WordDistanceCalculator. computar(respuesta normalizada, salida)val umbral = (salida.longitud * WORD_DIFF_THRESHOLD).toInt()if (distancia <= umbral) return Correct(isExactAnswer = false)} Este algoritmo no es perfecto, pero no espero usándolo más para cambiar en un futuro cercano, este es el algoritmo que tenía en mente al describir cómo funcionaría la práctica.Progreso hasta el final del día 16. Este es también el día que desearía haber parado. No estaba seguro de cómo proceder con el proyecto, así que parecía que no tenía motivación para seguir trabajando en él. Esto es algo bastante común para mí, ha sucedido varias veces y generalmente es lo que mata mis proyectos. Hace unos meses estaba leyendo The Unpleasant Essentials de Josh Pigford, en el artículo dice:

No disfrutarás de todo en lo que trabajes. De hecho, muchos de los elementos centrales de un proyecto serán las partes más difíciles que evitará como la peste. Estos «cimientos torpes» son los responsables de la muerte de una infinidad de proyectos. Son las cosas que posponemos hasta el infinito.

Cuando leí esto por primera vez, me quedé asombrado. Esto me ha pasado varias veces pero nunca he entendido por qué. Continúa proporcionando la solución:

La clave para superar estos rasgos desagradables no es la fuerza de voluntad, esta planeando. Significa decidir «aquí es donde quieres que termine este proyecto» y luego trabajar hacia atrás paso a paso para descubrir qué se debe hacer. Piensan: “Aquí están los pasos para llegar a donde quiero estar.” El objetivo no es evitar la incomodidad, es reducir la fatiga de la decisión. Tomas todas las decisiones importantes y luego simplemente sigues el plan.

Lo que me faltaba era «aquí es donde quiero que termine este proyecto». Aunque esbocé una visión al principio del proyecto, dejé de prestar atención y me perdí. Los siguientes pasos no estaban claros. Con estas actualizaciones llegamos al final de la Parte 4.Si tiene algún comentario o sugerencia, por favor póngase en contacto conmigo Gorjeo.Estén atentos para las próximas actualizaciones.Foto de William Warby en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada.