Navegando entre fragmentos

Navegando entre fragmentos

En esta entrada vamos desarrollar una aplicación para ilustrar el uso de fragmentos (en la primera parte) y las rutas de navegación (en una segunda parte) en Android con Kotlin. La aplicación representa un sencillo test de inteligencia cuyo diseño se distribuye en varios componentes principales (estos componentes nos servirán de referencia a la hora de crear los fragmentos):
  1. Pantalla de inicio, que contiene el logo y nombre de la aplicación y un botón para iniciar el test.
  2. Pantalla del test, abierta al pulsar el botón de inicio, en la que se van sucediendo preguntas con las posibles opciones de respuesta.
  3. Pantallas de resultados.
  4. Pantallas de información y acerca de.
  5. Un cajón de navegación que se abre al pulsar sobre un botón de navegación situado en la barra de la aplicación y que contiene un menú con enlaces a las pantallas de información y acerca de.
En esta entrada veremos los tres primeros puntos, dejando el resto para próximas entregas.

Empezamos creando un nuevo proyecto llamado Test CI desde una actividad vacía. Lo configuramos para trabajar con Data Binding (para más detalles, revisar Fundamentos de Android: Data Binding), esto es:
  1. En app/build.gradle (Module: app) habilitamos dataBinding (y ya que estamos habilitamos la compatibilidad con Java 8).
  2. Ajustamos el archivo de diseño con una etiqueta layout como clase raíz (también eliminamos el TextView y cambiamos el ConstraintLayout por un LinearLayout de orientación vertical).
  3. Y en el archivo de la actividad creamos el objeto de enlace.

Hasta ahora los archivos de la actividad y del diseño han quedado así:
// MainActivity.kt
package com.android.testci

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.android.testci.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }
}
<!-- layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

    </LinearLayout>

</layout>

Parte 1: Estructura de fragmentos

Creamos un fragmento llamado TitleFragment y en la ventana New Android Component desactivamos todas las opciones excepto Create Layout XML?. Abrimos layout/fragment_title.xml y lo configuramos para que una etiqueta layout sea el elemento superior que contenga un ConstraintLayout, en cuyo interior creamos una imagen, un par de TextView encadenados y un botón:
<!-- layout/fragment_title.xml -->
?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.android.testci.TitleFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/titleConstraint"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/titleImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:contentDescription="@string/logo"
            android:scaleType="fitCenter"
            app:layout_constraintBottom_toTopOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_mid_mapping" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="@dimen/horizontal_margin"
            android:paddingRight="@dimen/horizontal_margin"
            android:text="@string/test"
            android:textAllCaps="false"
            android:textColor="@color/azul"
            android:textSize="@dimen/titulo"
            android:textStyle="bold"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView2"
            app:layout_constraintEnd_toStartOf="@+id/textView2"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/titleImage" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="32dp"
            android:paddingLeft="@dimen/horizontal_margin"
            android:paddingRight="@dimen/horizontal_margin"
            android:text="@string/ci"
            android:textColor="@color/rosa"
            android:textSize="@dimen/titulo"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@+id/playButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/textView" />

        <Button
            android:id="@+id/playButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:layout_marginBottom="32dp"
            android:paddingStart="@dimen/button_padding"
            android:paddingEnd="@dimen/button_padding"
            android:text="@string/start"
            android:textColor="@color/colorAccent"
            android:textSize="@dimen/button_text_size"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Que en la vista Design del editor de diseño se ve así:



Abrimos el archivo del nuevo fragmento y en la función onCreateView (es uno de los métodos que se llama durante el ciclo de vida de un fragmento) quitamos la instrucción return dejándola así:
override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {

}
Y en su lugar creamos un objeto binding y activamos (inflamos) la vista del fragmento (lo que es equivalente a usar setContentView en una actividad). Para esta activación utilizamos el método DataBindingUtil.inflate sobre el objeto binding del fragmento con cuatro parámetros:
  1. inflater, que es el LayoutInflater utilizado para activar el diseño del binding.
  2. R.layout.fragment_title: el recurso de diseño para activar.
  3. container (parámetro opcional) para el ViewGroup padre.
  4. false, valor que se asigna a attachToParent (adjuntar al padre).

Asignamos al objeto binding el resultado del método DataBindingUtil.inflate y hacemos que la función onCreateView devuelva binding.root, que contiene la vista activada:
class TitleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        // return inflater.inflate(R.layout.fragment_title, container, false)
        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(
            inflater, R.layout.fragment_title, container, false
        )
        return binding.root
    }
}
Ahora añadimos el nuevo fragmento al diseño de la actividad, dentro del LinearLayout:
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <fragment
            android:id="@+id/titleFragment"
            android:name="com.android.testci.TitleFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </LinearLayout>

</layout>

Repitiendo los pasos anteriores, vamos a crear otros fragmentos correspondientes a los distintos componentes que necesitamos en nuestra aplicación: 1) pantalla de inicio, 2) las preguntas del test, dos posibles resultados de 3) acierto y 4) error, y los dos componentes para las 5) instrucciones y la información 6) Acerca de. Aunque de momento no están conectados entre sí, los diseños de estos fragmentos se presentan así:



Además en el fragmento dedicado a presentar las preguntas del test (TestFragment.kt) utilizamos una data class con un parámetro tipo String para contener las preguntas y otro parámetro tipo Lista de String para contener las respuesta posibles.
data class Question(
    val text: String,
    val answers: List<String>
)
Después creamos una lista de objetos de este tipo (Question) que asignan a cada parámetro los valores de cada pregunta y sus respectivas respuestas alternativas:
private val questions: MutableList<Question> = mutableListOf(
    Question(
        text = "¿Cuál de las siguientes palabras no encaja con el resto?",
        answers = listOf("Ordenanza", "Escriba", "Secretario", "Amanuense", "Copista")
    ),
    Question(
        text = "Complete la analogía: ENOJO es a CALMA como ENFURECER a",
        answers = listOf("Apaciguar", "Descansar", "Despertar", "Marear", "Alterar")
    ),
    ...
)
Y dentro de la función onCreateView:
  1. Llamamos a la función randomizeQuestions que aleatoriza el orden de las preguntas y establece la primera pregunta (al final de la entrada se puede ver el código de los principales archivos).
  2. Con setOnClickListener capturamos la acción del botón de respuesta del diseño (binding.submitButton.setOnClickListener), que a su vez captura la opción activada en el RadioGroup (binding.questionRadioGroup.checkedRadioButtonId) para comprobar si la respuesta es correcta y en ese caso avanza a la siguiente pregunta después de comprobar si han realizado todas las preguntas (entoncés se activará el fragmento de victoria), o si la respuesta no es correcta se activará el fragmento de fin del test.

Parte 2: Definir las rutas de navegación

Una vez que hemos construido la estructura de la aplicación, ahora crearemos la posibilidad de navegar (las rutas) entre estos componentes conectando las pantallas con acciones y ofreciendo al usuario una forma de navegar entre las pantallas.

Para ello debemos agregar las dependencias de navegación a los archivos de Gradle. Abrimos el archivo build.gradle a nivel de proyecto, y en la parte superior, junto con las otras variables ext, agregamos una variable para navigationVersion. Si solo tenemos una variable ext, como por ejemplo ext.kotlin_version, la reescribimos en el bloque ext de esta forma:
// ext.kotlin_version = '1.3.21'
ext {
    kotlin_version = '1.3.21'
}
Y añadimos la dependencia de navegación (para encontrar la última versión estable consulta Declaring dependencies):
ext {
    kotlin_version = '1.3.21'
    navigationVersion = '1.0.0-rc02'
}
Ahora abrimos build.gradle a nivel de módulo para agregar las dependencias navigation-fragment-ktx y navigation-ui-ktx:
dependencies {
  ...
  implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
  implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"
  ...
}
En caso de utilizar navigationVersion = '2.1.0-rc01' posiblemente necesites estas dependencias:
dependencies {
  ...
  implementation "androidx.navigation:navigation-fragment-ktx:$$navigationVersion"
  implementation "androidx.navigation:navigation-ui-ktx:$$navigationVersion"
  ...
}
Build -> Rebuild Project y sincronizamos los cambios.

Ahora vamos a agregar un gráfico de navegación desde la carpeta res -> New -> Android Resource File, seleccionamos Navigation como tipo de recurso y le asignamos un nombre:



Esto crea, en la carpera res, la carpeta navigation con el archivo navigation.xml, que en la vista Design solo muestra el mensaje 'Click to add a destination':



También vemos que en el panel Destinations aparece el mensaje 'No NavHostFragments found'. Un fragmento host de navegación (NavHostFragment, de navigation host fragment) actúa como cabeza u organizador de fragmentos en un gráfico de navegación. A medida que el usuario se mueve entre los destinos definidos en el gráfico de navegación, el NavHostFragment intercambia fragmentos según sea necesario.

Para crear el NavHostFragment abrimos el archivo de diseño de la actividad principal (activity_main.xml) y cambiamos el nombre (atributo name) del fragmento (com.android.testci.TitleFragment) por androidx.navigation.fragment.NavHostFragment y también cambiamos la identificación (atributo id) a myNavHostFragment. Le añadimos el atributo app:navGraph con el valor del recurso del gráfico de navegación que hemos creado (@navigation/navigation) y el atributo app:defaultNavHost con el valor true:
<!--
<fragment
    android:id="@+id/titleFragment"
    android:name="com.android.testci.TitleFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
-->

<fragment
    android:id="@+id/myNavHostFragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/navigation" />
Ahora este punto de navegación es el NavHostFragments predeterminado y como tal interceptará el botón Atrás del sistema.

Ahora queda añadir los fragmentos que hemos creado al gráfico de navegación, empezando por los fragmentos de portada (fragment_title.xml) y de test (fragment_test.xml). En el archivo navigation/navigation.xml al pulsar sobre el botón New Destination aparece una lista de fragmentos y actividades, y seleccionamos fragment_title para que sea el inicio de la aplicación.



Repetimos para añadir como nuevo destino el fragmento fragment_test (si la vista previa no está disponible, vamos a la vista Text para asegurarnos que el elemento correspondiente a testFragment incluye el atributo tools:layout="@layout/fragment_test".



Seleccionamos el fragmento del título y pulsamos sobre el punto de conexión del lado derecho, arrastrándolo al fragmento del test, lo que crea una acción que conecta ambos fragmentos. Para ver los atributos de esta acción, seleccionamos la línea o flecha de conexión y comprobamos el valor de su ID (action_titleFragment_to_testFragment) y el atributo Destination (testFragment).

Ahora debemos añadir un controlador de clic al botón EMPEZAR del fragmento de inicio para que al pulsarlo se pueda navegar desde el primero al segundo fragmento (y se ejecute la conexión que acabamos de crear, conduciendo al usuario a la pantalla del test). Abrimos TitleFragment.kt y dentro de onCreateView agregamos el controlador antes de la declaración de devolución (return):
binding.playButton.setOnClickListener { }
Y en su interior escribimos el código para navegar al fragmento del test (requiere import androidx.navigation.findNavController):
binding.playButton.setOnClickListener { view: View ->
    view.findNavController().navigate(R.id.action_titleFragment_to_testFragment)
}
Ejecutamos la aplicación para comprobar que el botón cumple su cometido de llevarnos a la pantalla del test. Además con el botón Atrás del dispositivo volvemos a la primera pantalla.

Ahora vamos a crear rutas de navegación condicionales en función de las respuestas del usuario, lo que activará el fragmento fragment_test_won.xml o el fragmento fragment_test_over.xml en función de si responde correctamente o no a las preguntas.

Para ello, con el botón New Destination agregamos estos dos fragmentos al gráfico de navegación del archivo navigation.xml:



Ahora conectamos el fragmento del test a estos dos fragmentos:



El archivo TestFragment.kt contiene la clase TestFragment que es un fragmento que contiene las preguntas y respuestas del test y también incluye la lógica que determina si el usuario responde correctamente o no, y en cada caso tendrá una acción distinta (distintas rutas de navegación) por parte de la aplicación.
if (answers[answerIndex] == currentQuestion.answers[0]) { }  // si esto es true, la respuesta es correcta
Si la condición anterior devuelve false (la respuesta es incorrecta), se termina el test y la aplicación navega a TestOverFragment.
else {
    view.findNavController().navigate(R.id.action_testFragment_to_testOverFragment)
}
Pero si la respuesta es correcta, debemos comprobar (dentro del bloque if anterior) si se ha completado el test (3 aciertos seguidos) o no:
if (questionIndex < numQuestions) { // si true, continuar con la siguiente pregunta
    ...
} else { // si false, test completado -> navegar a TestWonFragment
    view.findNavController().navigate(R.id.action_testFragment_to_testWonFragment)
}
El sistema Android realiza un seguimiento de los destinos visitados por los usuarios en un dispositivo (cada vez que el usuario va a un nuevo destino, Android lo agrega a una pila posterior de destinos o back stack). Y cuando se presiona sobre el botón Atrás del dispositivo, la aplicación accede al destino situado en la parte superior de esa pila, que por defecto es el último destino visitado. Pero podemos cambiar este funcionamiento para ajustar otro destino. Por ejemplo, en nuestra aplicación estaría bien que desde el fragmento TestOverFragment (fin del test), el botón Atrás sirviera para acceder a la pantalla de inicio de la aplicación (TitleFragment) en lugar de a las preguntas del test (TestFragment) como hace ahora.

Vamos a tomar el control de la pila de destinos visitados. En concreto vamos a eliminar TestFragment de la pila para que cuando el usuario gane o pierda (TestWonFragment o TestOverFragment) y pulse el botón Atrás la aplicación no encuentre esos fragmentos y vuelva al fragmento de portada de la aplicación (TitleFragment).

Para administrar esta pila debemos configurar el comportamiento de "pop" para las acciones que conectan los fragmentos, lo cual admite diversos ajustes en sus atributos:
  • El atributo popUpTo de una acción abre la pila a un destino determinado (los destinos se eliminan de la pila).
  • Si el atributo popUpToInclusive es false o no está configurado, popUpTo elimina los destinos hasta el destino especificado y lo deja en la pila posterior.
  • Si popUpToInclusive se establece como true, el atributo popUpTo elimina todos los destinos hasta el destino dado de la pila posterior.
  • Si popUpToInclusive es true y popUpTo está configurado en la ubicación de inicio de la aplicación, la acción elimina todos los destinos de la aplicación de la pila posterior, por lo que el botón Atrás saca al usuario de la aplicación.

Entonces, desde el gráfico de navegación, seleccionamos la acción (flecha) para navegar desde TestFragment a TestOverFragment, y en el panel de atributos ajustamos el atributo Pop To a testFragment y activamos la casilla Inclusive (true). Podemos combrobar en el archivo xml (vista Text) que se han agregado estos atributos:
app:popUpTo="@+id/testFragment"
app:popUpToInclusive="true"
Esto hace que el componente de navegación elimine los fragmentos de la pila posterior hasta TestFragment (inclusive). Lo mismo lo podríamos heber hecho ajustando el atributo Pop To en titleFragment y desactivando Inclusive (false). Hacemos lo mismo para la acción de navegación desde TestFragment a TestWonFragment.

Ejecutamos la aplicación en el emulador para comprobar que ahora el botón Atrás del dispositivo salta la pantalla de preguntas y llega hasta la pantalla de inicio desde cualquiera de las dos pantallas de resultados.

Ahora vamos a darle funcionalidades a los botones 'Siguiente Test' (TestWonFragment) e '¿Interntarlo otra vez?' (TestOverFragment) para que ambos accedan a las preguntas (TestFragment) y después, si el usuario presiona el botón Atrás no vuelva a TestWonFragment ni a TestOverFragment, sino a TitleFragment.

En el gráfico de navegación añadimos una acción de navegación que conecte TestOverFragment con TestFragment (esta conexión la usará el botón). Y en el atributo Pop To de esta acción ajustamos a titleFragment con Inclusive false (una vez en TestFragment, el botón Atrás irá a TitleFragment).

Repetimos para crear una nueva acción de navegación desde TestWonFragment a TestFragment y la configuramos igual:



En el archivo TestOverFragment.kt, al final del método onCreateView, antes del return, escribimos un controlador de clics al botón:
binding.tryAgainButton.setOnClickListener { view: View ->
    view.findNavController().navigate(R.id.action_testOverFragment_to_testFragment)
}
Repetimos en el archivo TestWonFragment para su botón:
binding.nextTestButton.setOnClickListener { view: View ->
    view.findNavController().navigate(R.id.action_testWonFragment_to_testFragment)
}
Ejecutamos para comprobar que funciona. Hasta aquí esta entrada donde hemos repasado la creación de fragmentos y hemos aprendido a navegar entre ellos. En una próxima entrega seguiremos trabajando con esta aplicación para añadir un botón en la barra de la aplicación que nos ayudará a navegar por la aplicación, y un menú de opciones y un cajón de navegación para ir a los fragmentos Info y About.

A continuación se muestran los archivos más importantes relativos a las actividades, fragmentos y el recurso de navegación de la aplicación:
// MainActivity.kt
package com.android.testci

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.android.testci.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }
}
<!-- layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <fragment
            android:id="@+id/myNavHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/navigation" />

    </LinearLayout>

</layout>
// TitleFragment.kt
package com.android.testci

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.android.testci.databinding.FragmentTitleBinding

class TitleFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val binding = DataBindingUtil.inflate<FragmentTitleBinding>(
            inflater, R.layout.fragment_title, container, false
        )

        binding.playButton.setOnClickListener { view: View ->
            view.findNavController().navigate(R.id.action_titleFragment_to_testFragment)
        }

        return binding.root
    }
}
<!-- layout/fragment_title.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.android.testci.TitleFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/titleConstraint"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/titleImage"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:scaleType="fitCenter"
            app:layout_constraintBottom_toTopOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_mid_mapping" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="@dimen/horizontal_margin"
            android:paddingRight="@dimen/horizontal_margin"
            android:text="@string/test"
            android:textAllCaps="false"
            android:textColor="@color/azul"
            android:textSize="@dimen/titulo"
            android:textStyle="bold"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView2"
            app:layout_constraintEnd_toStartOf="@+id/textView2"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/titleImage" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="32dp"
            android:paddingLeft="@dimen/horizontal_margin"
            android:paddingRight="@dimen/horizontal_margin"
            android:text="@string/ci"
            android:textColor="@color/rosa"
            android:textSize="@dimen/titulo"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@+id/playButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/textView" />

        <Button
            android:id="@+id/playButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:layout_marginBottom="32dp"
            android:paddingStart="@dimen/button_padding"
            android:paddingEnd="@dimen/button_padding"
            android:text="@string/start"
            android:textColor="@color/colorAccent"
            android:textSize="@dimen/button_text_size"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
// TestFragment.kt
package com.android.testci

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.android.testci.databinding.FragmentTestBinding

class TestFragment : Fragment() {
    data class Question(
        val text: String,
        val answers: List<String>
    )

    // Lista de Question (pregunta y lista de respuestas)
    // La primera respuesta es la correcta y después se presentan de manera aleatoria
    private val questions: MutableList<Question> = mutableListOf(
        Question(
            text = "¿Cuál de las siguientes palabras no encaja con el resto?",
            answers = listOf("Ordenanza", "Escriba", "Secretario", "Amanuense", "Copista")
        ),
        Question(
            text = "Complete la analogía: ENOJO es a CALMA como ENFURECER a",
            answers = listOf("Apaciguar", "Descansar", "Despertar", "Marear", "Alterar")
        ),
        Question(
            text = "Al lanzar dos dados ¿cuál es la probabilidad de que las caras orientadas hacia arriba sumen un total de 6 en dos ocasiones consecutivas?\n",
            answers = listOf("25/1296", "8/1296", "10/1296", "16/1296", "36/1296")
        ),
        Question(
            text = "El nieto de mis padres es uno de mis sobrinos y tengo tres sobrinos más que no son hermanos entre sí. Como mínimo ¿cuántos hijos e hijas tienen mis padres? (asumiendo que nadie ha muerto)",
            answers = listOf("Cuatro", "Dos", "Tres", "Cinco", "No hay datos suficientes para saberlo")
        ),
        Question(
            text = "¿Cuántos múltiplos de 4 hay entre 1000 y 2000 inclusives?",
            answers = listOf("251", "300", "500", "204", "164")
        ),
        Question(
            text = "Dibujando sobre un papel un cuadrado y dos triángulos ¿cuántas áreas pueden delimitarse?. Dicho de otro modo, ¿cuál es el número máximo de parcelas que pueden delimitarse en un prado con una cerca de alambre cuadrada y dos triangulares?",
            answers = listOf("19", "7", "11", "15", "24")
        ),
        Question(
            text = "Encuentre el número que completa esta serie lógica: 3, 5, 10, 24, 65, ?",
            answers = listOf("187", "174", "201", "123", "130")
        ),
        Question(
            text = "Complete la analogía: DESDEÑOSO es a DESPECTIVO como ALTIVO es a",
            answers = listOf("Soberbio", "Estrafalario", "Arreglado", "Negativo", "Positivo")
        ),
        Question(
            text = "¿Si el hijo de Marmaduke es el padre de mi hijo, ¿qué parentesco tengo yo con Marmaduke?",
            answers = listOf("Su hijo", "Soy su abuelo", "Su padre", "Su nieto", "Yo soy Marmaduke")
        ),
        Question(
            text = "En una hilera de cuatro casas, los Brown viven al lado de los Smith pero no al lado de los Bruce. Si los Bruce no viven al lado de los Jones, ¿quiénes son los vecinos inmediatos de los Jones?",
            answers = listOf("Los Brown", "Los Smith", "Los Brown y los Smith", "Es imposible averiguarlo", "Ninguno de los anteriores")
        )
    )
    private lateinit var binding: FragmentTestBinding
    lateinit var currentQuestion: Question
    lateinit var answers: MutableList<String>
    private var questionIndex = 0
    private val numQuestions = Math.min((questions.size + 1) / 2, 3)

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        binding = DataBindingUtil.inflate(
            inflater, R.layout.fragment_test, container, false
        )

        // Mezcla las preguntas y determina el índice de la primera pregunta en la lista de preguntas
        randomizeQuestions()

        binding.test = this

        binding.submitButton.setOnClickListener @Suppress("UNUSED_ANONYMOUS_PARAMETER")
        { view: View ->
            val checkedId = binding.questionRadioGroup.checkedRadioButtonId
            if (-1 != checkedId) {  // no hace nada si ninguna opción está activada
                var answerIndex = 0
                when (checkedId) {
                    R.id.secondAnswerRadioButton -> answerIndex = 1
                    R.id.thirdAnswerRadioButton -> answerIndex = 2
                    R.id.fourthAnswerRadioButton -> answerIndex = 3
                    R.id.fiveAnswerRadioButton -> answerIndex = 4
                }
                if (answers[answerIndex] == currentQuestion.answers[0]) {  // si true: respuesta correcta
                    questionIndex++
                    if (questionIndex < numQuestions) {  // si true: siguiente pregunta
                        currentQuestion = questions[questionIndex]  // siguiente pregunta
                        setQuestion()
                        binding.invalidateAll()
                    } else {  // tres respuestas correctas: navegar a testWonFragment
                        view.findNavController().navigate(R.id.action_testFragment_to_testWonFragment)
                    }
                } else {  // error de respuesta: navegar a testOverFragment
                    view.findNavController().navigate(R.id.action_testFragment_to_testOverFragment)
                }
            }
        }
        return binding.root
    }

    private fun randomizeQuestions() {
        questions.shuffle()  // mezcla la lista
        questionIndex = 0
        setQuestion()  // obtiene una pregunta y mezcla sus respuestas
    }

    private fun setQuestion() {
        binding.questionRadioGroup.clearCheck()
        currentQuestion = questions[questionIndex] // selecciona la Question
        answers = currentQuestion.answers.toMutableList()
        answers.shuffle()  // y mezcla sus respuestas
        (activity as AppCompatActivity).supportActionBar?.title =
            getString(R.string.title_android_testci_question, questionIndex + 1, numQuestions)
    }
}
<!-- layout/fragment_test.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.android.navigation.InGame">

    <data>
        <variable
            name="test"
            type="com.android.testci.TestFragment" />
    </data>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/frameLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/questionImage"
                android:layout_width="0dp"
                android:layout_height="@dimen/image_header_height_min"
                android:layout_marginStart="@dimen/horizontal_margin"
                android:layout_marginTop="@dimen/vertical_margin"
                android:layout_marginEnd="@dimen/horizontal_margin"
                android:layout_marginBottom="@dimen/vertical_margin"
                android:scaleType="fitCenter"
                app:layout_constraintBottom_toTopOf="@+id/questionText"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_chainStyle="packed"
                app:srcCompat="@drawable/ic_preguntas" />

            <TextView
                android:id="@+id/questionText"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/question_horizontal_margin"
                android:layout_marginTop="@dimen/vertical_margin"
                android:layout_marginEnd="@dimen/question_horizontal_margin"
                android:fontFamily="sans-serif"
                android:text="@{test.currentQuestion.text}"
                android:textSize="@dimen/question_size"
                android:textStyle="bold"
                android:typeface="normal"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/questionImage"
                tools:text="¿Qué número sigue en la siguiente serie?\n9, 16, 25, 36 ...  " />

            <RadioGroup
                android:id="@+id/questionRadioGroup"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/question_horizontal_margin"
                android:layout_marginTop="@dimen/vertical_margin"
                android:layout_marginEnd="@dimen/question_horizontal_margin"
                android:animateLayoutChanges="true"
                android:orientation="vertical"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/questionText">

                <RadioButton
                    android:id="@+id/firstAnswerRadioButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/question_vertical_margin"
                    android:checked="true"
                    android:text="@{test.answers[0]}"
                    android:textSize="@dimen/answer_text_size"
                    tools:text="42" />

                <RadioButton
                    android:id="@+id/secondAnswerRadioButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/question_vertical_margin"
                    android:text="@{test.answers[1]}"
                    android:textSize="@dimen/answer_text_size"
                    tools:text="49" />

                <RadioButton
                    android:id="@+id/thirdAnswerRadioButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/question_vertical_margin"
                    android:text="@{test.answers[2]}"
                    android:textSize="@dimen/answer_text_size"
                    tools:text="51" />

                <RadioButton
                    android:id="@+id/fourthAnswerRadioButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/question_vertical_margin"
                    android:text="@{test.answers[3]}"
                    android:textSize="@dimen/answer_text_size"
                    tools:text="54" />

                <RadioButton
                    android:id="@+id/fiveAnswerRadioButton"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/question_vertical_margin"
                    android:text="@{test.answers[4]}"
                    android:textSize="@dimen/answer_text_size"
                    tools:text="Ninguna de las anteriores" />

            </RadioGroup>

            <Button
                android:id="@+id/submitButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/question_horizontal_margin"
                android:layout_marginTop="@dimen/vertical_margin"
                android:layout_marginEnd="@dimen/question_horizontal_margin"
                android:text="@string/respoder"
                android:textSize="@dimen/button_text_size"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/questionRadioGroup" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>
</layout>
// TestWonFragment.kt
package com.android.testci

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.android.testci.databinding.FragmentTestWonBinding

class TestWonFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding: FragmentTestWonBinding = DataBindingUtil.inflate(
            inflater, R.layout.fragment_test_won, container, false
        )

        binding.nextTestButton.setOnClickListener { view: View ->
            view.findNavController().navigate(R.id.action_testWonFragment_to_testFragment)
        }

        return binding.root
    }
}
<!-- layout/fragment_test_won.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.android.testci.TestWonFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/testWonConstraintLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/fondoazul">

        <Button
            android:id="@+id/nextTestButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingStart="@dimen/button_padding"
            android:paddingEnd="@dimen/button_padding"
            android:text="@string/next"
            android:textColor="@color/youWinDark"
            android:textSize="@dimen/button_text_size"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/youWinImage" />

        <ImageView
            android:id="@+id/youWinImage"
            android:layout_width="0dp"
            android:layout_height="@dimen/game_over_height"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:layout_marginBottom="@dimen/vertical_margin"
            android:scaleType="fitCenter"
            app:layout_constraintBottom_toTopOf="@+id/nextTestButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_award" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
// TestOverFragment.kt
package com.android.testci

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.android.testci.databinding.FragmentTestOverBinding

class TestOverFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val binding: FragmentTestOverBinding = DataBindingUtil.inflate(
            inflater, R.layout.fragment_test_over, container, false
        )

        binding.tryAgainButton.setOnClickListener { view: View ->
            view.findNavController().navigate(R.id.action_testOverFragment_to_testFragment)
        }

        return binding.root
    }
}
<!-- layout/fragment_test_over.xml -->
<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.android.testci.TestOverFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/gameOverConstraintLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/fondorosa">

        <ImageView
            android:id="@+id/testOverFragment"
            android:layout_width="wrap_content"
            android:layout_height="362dp"
            android:layout_marginStart="@dimen/horizontal_margin"
            android:layout_marginEnd="@dimen/horizontal_margin"
            android:layout_marginBottom="8dp"
            android:scaleType="fitCenter"
            app:layout_constraintBottom_toTopOf="@+id/tryAgainButton"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_chainStyle="packed"
            app:srcCompat="@drawable/ic_assignment" />

        <Button
            android:id="@+id/tryAgainButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="@dimen/vertical_margin"
            android:layout_marginTop="@dimen/vertical_margin"
            android:layout_marginEnd="@dimen/vertical_margin"
            android:layout_marginBottom="8dp"
            android:paddingStart="@dimen/button_padding"
            android:paddingEnd="@dimen/button_padding"
            android:text="@string/try_again"
            android:textColor="?colorAccent"
            android:textSize="@dimen/button_text_size"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/testOverFragment" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<!-- navigation/navigation.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation"
    app:startDestination="@id/titleFragment">

    <fragment
        android:id="@+id/titleFragment"
        android:name="com.android.testci.TitleFragment"
        android:label="fragment_title"
        tools:layout="@layout/fragment_title">
        <action
            android:id="@+id/action_titleFragment_to_testFragment"
            app:destination="@+id/testFragment" />
    </fragment>
    <fragment
        android:id="@+id/testFragment"
        android:name="com.android.testci.TestFragment"
        android:label="TestFragment"
        tools:layout="@layout/fragment_test">
        <action
            android:id="@+id/action_testFragment_to_testOverFragment"
            app:destination="@id/testOverFragment"
            app:popUpTo="@+id/testFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_testFragment_to_testWonFragment"
            app:destination="@id/testWonFragment"
            app:popUpTo="@+id/testFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/testOverFragment"
        android:name="com.android.testci.TestOverFragment"
        android:label="fragment_test_over"
        tools:layout="@layout/fragment_test_over">
        <action
            android:id="@+id/action_testOverFragment_to_testFragment"
            app:destination="@id/testFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
    </fragment>
    <fragment
        android:id="@+id/testWonFragment"
        android:name="com.android.testci.TestWonFragment"
        android:label="fragment_test_won"
        tools:layout="@layout/fragment_test_won">
        <action
            android:id="@+id/action_testWonFragment_to_testFragment"
            app:destination="@id/testFragment"
            app:popUpTo="@+id/titleFragment"
            app:popUpToInclusive="false" />
    </fragment>
</navigation>

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos