Cómo compilar una app para Android con objetos View

1. Antes de comenzar

Introducción

Hasta ahora, aprendiste todo sobre la compilación de apps para Android con Compose, lo cual es algo bueno. Compose es una herramienta muy potente que puede simplificar el proceso de desarrollo, pero las apps para Android no siempre se compilaron con IUs declarativas. Compose es una herramienta muy reciente en la historia de las apps para Android. Originalmente, las IUs de Android se compilaban con objetos View. Por lo tanto, es muy probable que te encuentres con ellos a medida que avanzas en tu camino como desarrollador de Android. En este codelab, aprenderás los conceptos básicos sobre la compilación de apps para Android antes de Compose: con XML, objetos View, y fragmentos y vinculación de vistas.

Requisitos previos:

  • Haber completado el trabajo de curso de Aspectos básicos de Android con Compose hasta la unidad 7

Requisitos

  • Una computadora con acceso a Internet y Android Studio instalado
  • Un dispositivo o emulador
  • El código de partida de la app de Juice Tracker

Qué compilarás

En este codelab, completarás la app de Juice Tracker. Esta app te permite hacer un seguimiento de los jugos más populares con la compilación de una lista de elementos detallados. Agregarás y modificarás fragmentos y XML para completar la IU y el código de partida. Específicamente, compilarás el formulario de entrada para crear un nuevo jugo, incluidas la IU y cualquier lógica o navegación asociadas. El resultado es una app con una lista vacía a la que puedes agregar tus propios jugos.

d6dc43171ae62047.png 87b2ca7b49e814cb.png 2d630489477e216e.png

2. Obtén el código de partida

  1. En Android Studio, abre la carpeta basic-android-kotlin-compose-training-juice-tracker.
  2. Abre el código de la app de Juice Tracker en Android Studio.

3. Crea un diseño

Cuando compilas una app con Views, creas la IU dentro de un elemento Layout. Por lo general, los diseños se declaran con archivos de diseño XML que se encuentran en el directorio de recursos, en res > layout. Estos contienen los componentes de la IU, que se conocen como View. La sintaxis XML consiste en etiquetas, elementos y atributos. Para obtener más información de la sintaxis XML, consulta el codelab Cómo crear diseños XML para Android.

En esta sección, compilarás un diseño XML para el diálogo de entrada "Type of juice" (Tipo de jugo) que se ve en la imagen.

87b2ca7b49e814cb.png

  1. Crea un nuevo archivo de recursos de diseño en el directorio main > res > layout llamado fragment_entry_dialog.

Se abrió el panel contextual del panel del proyecto de Android Studio con la opción para crear un archivo de recursos de diseño.

6adb279d6e74ab13.png

El diseño del elemento fragment_entry_dialog.xml contiene los componentes de la IU que la app mostrará al usuario.

Ten en cuenta que el elemento raíz es un ConstraintLayout. Este tipo de diseño es un ViewGroup que te permite posicionar y ajustar el tamaño de los objetos View de manera flexible dentro de las restricciones. Un ViewGroup es un tipo de View que contiene otras View, llamadas Views secundarias. En los siguientes pasos, se aborda este tema con más detalle, pero puedes obtener más información sobre ConstraintLayout en Cómo compilar una IU responsiva con ConstraintLayout.

  1. Después de crear el archivo, define el espacio de nombres de la app en ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Agrega los siguientes lineamientos a ConstraintLayout.

fragment_entry_dialog.xml

<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_left"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_middle"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   app:layout_constraintGuide_percent="0.5" />
<androidx.constraintlayout.widget.Guideline
   android:id="@+id/guideline_top"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:orientation="horizontal"
   app:layout_constraintGuide_begin="16dp" />

Estos elementos Guideline funcionarán como relleno para otros objetos View. Los lineamientos limitan el texto del encabezado "Type of juice" (tipo de jugo).

  1. Crea un elemento TextView. Este TextView representa el título del fragmento de detalles.

110cad4ae809e600.png

  1. Establece TextView en id de header_title.
  2. Establece layout_width en 0dp. En última instancia, las restricciones del diseño definen el ancho de este elemento TextView. Por lo tanto, definir un ancho solo agrega cálculos innecesarios durante el diseño de la IU; definir un ancho de 0dp evita los cálculos adicionales.
  3. Establece el atributo TextView text en @string/juice_type.
  4. Establece la textAppearance en @style/TextAppearance.MaterialComponents.Headline5.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />

Por último, define las restricciones. A diferencia de los Guideline, que usan dimensiones como restricciones, los lineamientos en sí limitan estos elementos TextView. Para lograr este resultado, puedes hacer referencia al ID de Guideline con el que deseas restringir la vista.

  1. Restringe la parte superior del encabezado a la parte inferior de guideline_top.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top" />
  1. Restringe el final al inicio de guideline_middle y el principio al inicio de guideline_left para completar el posicionamiento de TextView. Ten en cuenta que la manera de restringir una vista determinada depende por completo de cómo quieres que se vea la IU.

fragment_entry_dialog.xml

<TextView
   android:id="@+id/header_title"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:text="@string/juice_type"
   android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
   app:layout_constraintTop_toBottomOf="@+id/guideline_top"
   app:layout_constraintEnd_toStartOf="@+id/guideline_middle"
   app:layout_constraintStart_toStartOf="@+id/guideline_left" />

Intenta compilar el resto de la IU con estas capturas de pantalla como modelo. Puedes encontrar el archivo fragment_entry_dialog.xml completo en la solución.

4. Crea un fragmento con objetos View

En Compose, puedes compilar diseños de forma declarativa con Kotlin o Java. Puedes navegar a diferentes elementos componibles, por lo general, dentro de la misma actividad, para acceder a diferentes "pantallas". Cuando compilas una app con objetos View, el concepto de "pantalla" componible se reemplaza por un fragmento que aloja el diseño XML.

En esta sección, crearás un Fragment para alojar el diseño de fragment_entry_dialog y proporcionar datos a la IU.

  1. En el paquete juicetracker, crea una nueva clase llamada EntryDialogFragment.
  2. Haz que EntryDialogFragment extienda el BottomSheetDialogFragment.

EntryDialogFragment.kt

import com.google.android.material.bottomsheet.BottomSheetDialogFragment

class EntryDialogFragment : BottomSheetDialogFragment() {
}

El DialogFragment es un Fragment que muestra un diálogo flotante. El BottomSheetDialogFragment hereda de la clase DialogFragment, pero muestra una hoja con el ancho de la pantalla fijado a su parte inferior. Este enfoque coincide con el diseño que se mostró anteriormente.

  1. Vuelve a compilar el proyecto. Esto causará que los archivos de vinculación de vista basados en el diseño fragment_entry_dialog se generen automáticamente. Las vinculaciones de vista te permiten acceder a objetos View declarados en formato XML, además de interactuar con ellos. Para obtener más información al respecto, consulta la documentación sobre la vinculación de vistas.
  2. En la clase EntryDialogFragment, implementa la función onCreateView(). Como su nombre lo indica, esta función crea el elemento View para el Fragment.

EntryDialogFragment.kt

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

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

La función onCreateView() muestra una View, pero, por el momento, no es una View útil.

  1. Muestra la View que se genera con el aumento de la FragmentEntryDialogViewBinding en lugar de mostrar super.onCreateView().

EntryDialogFragment.kt

import com.example.juicetracker.databinding.FragmentEntryDialogBinding

override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   return FragmentEntryDialogBinding.inflate(inflater, container, false).root
}
  1. Crea una instancia de EntryViewModel fuera de la función onCreateView(), pero dentro de la clase EntryDialogFragment.
  2. Implementa la función onViewCreated().

Después de aumentar la vinculación de vista, puedes acceder a las View del diseño y modificarlas. Se llama al método onViewCreated() después de onCreateView() en el ciclo de vida. El método onViewCreated() es el lugar recomendado para acceder a las View y modificarlas dentro del diseño.

  1. Llama al método bind() en FragmentEntryDialogBinding para crear una instancia de vinculación de vista.

En este punto, tu código debería verse como el siguiente ejemplo:

EntryDialogFragment.kt

import androidx.fragment.app.viewModels
import com.example.juicetracker.ui.AppViewModelProvider
import com.example.juicetracker.ui.EntryViewModel

class EntryDialogFragment : BottomSheetDialogFragment() {

   private val entryViewModel by viewModels<EntryViewModel> { AppViewModelProvider.Factory }

   override fun onCreateView(
       inflater: LayoutInflater,
       container: ViewGroup?,
       savedInstanceState: Bundle?
   ): View {
       return FragmentEntryDialogBinding.inflate(inflater, container, false).root
   }

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
    }
}

Puedes acceder a los objetos View desde la vinculación y configurarlos. Por ejemplo, puedes establecer un elemento TextView con el método setText().

binding.name.setText("Apple juice")

En la IU del diálogo de entrada, el usuario puede crear un nuevo elemento, pero también puedes usarla para modificar un elemento existente. Por lo tanto, el fragmento debe recuperar un elemento en el que se hizo clic. El componente de Navigation facilita la navegación al EntryDialogFragment y la recuperación de un elemento en el que se hizo clic.

La EntryDialogFragment aún no está completa, pero no te preocupes. Por ahora, pasa a la siguiente sección para obtener más información para usar el componente de Navigation en una app con View.

5. Modifica el componente de Navigation

En esta sección, usarás el componente de navegación para iniciar el diálogo de entrada y recuperar un elemento, si corresponde.

Compose ofrece la oportunidad de renderizar diferentes elementos componibles con solo llamarlos. Sin embargo, los fragmentos funcionan de manera diferente. El componente de Navigation coordina los "destinos" del fragmento, lo que proporciona una manera fácil de desplazarse entre diferentes fragmentos y los objetos View que contienen.

Usa el componente de Navigation para coordinar la navegación a tu EntryDialogFragment.

  1. Abre el archivo nav_graph.xml y asegúrate de que esté seleccionada la pestaña Design. 783cb5d7ff0ba127.png
  2. Haz clic en el ícono 93401bf098936c15.png para agregar un destino nuevo.

d5410c90e408b973.png

  1. Selecciona el destino EntryDialogFragment. Esta acción declara el entryDialogFragment en el gráfico de navegación para que las acciones de navegación puedan acceder a él.

418feed425072ea4.png

Debes iniciar el EntryDialogFragment desde TrackerFragment. Por lo tanto, una acción de navegación debe realizar esta tarea.

  1. Arrastra el cursor sobre el trackerFragment. Selecciona el punto gris y arrastra la línea al entryDialogFragment. 85decb6fcddec713.png
  2. La vista de diseño de nav_graph te permite declarar argumentos para un destino. Para ello, selecciona el destino y haz clic en el ícono a0d73140a20e4348.png junto al menú desplegable Arguments. Usa esta función para agregar un argumento itemId de tipo Long a entryDialogFragment; el valor predeterminado debe ser 0L.

555cf791f64f62b8.png

840105bd52f300f7.png

Ten en cuenta que el TrackerFragment contiene una lista de elementos Juice. Si haces clic en uno de ellos, se iniciará el EntryDialogFragment.

  1. Vuelve a compilar el proyecto. Ahora se puede acceder al argumento itemId desde el EntryDialogFragment.

6. Completa el fragmento

Con los datos de los argumentos de navegación, completa el diálogo de entrada.

  1. Recupera los navArgs() en el método onViewCreated() del EntryDialogFragment.
  2. Recupera el itemId de los navArgs().
  3. Implementa un saveButton para guardar el jugo nuevo o modificado con elViewModel.

Recuerda que el valor predeterminado del color de la IU del diálogo de entrada es rojo. Por ahora, pasa esto como un marcador de posición.

Pasa el ID de elemento de los argumentos cuando llames a saveJuice().

EntryDialogFragment.kt

import androidx.navigation.fragment.navArgs
import com.example.juicetracker.data.JuiceColor

class EntryDialogFragment : BottomSheetDialogFragment() {

   //...
   var selectedColor: JuiceColor = JuiceColor.Red

   override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        val juiceId = args.itemId

        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
        }
    }
}
  1. Después de guardar los datos, descarta el diálogo con el método dismiss().

EntryDialogFragment.kt

class EntryDialogFragment : BottomSheetDialogFragment() {

    //...
    var selectedColor: JuiceColor = JuiceColor.Red
    //...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = FragmentEntryDialogBinding.bind(view)
        val args: EntryDialogFragmentArgs by navArgs()
        binding.saveButton.setOnClickListener {
           entryViewModel.saveJuice(
               juiceId,
               binding.name.text.toString(),
               binding.description.text.toString(),
               selectedColor.name,
               binding.ratingBar.rating.toInt()
           )
           dismiss()
        }
    }
}

Ten en cuenta que el código anterior no completa el EntryDialogFragment. Aún debes implementar una serie de elementos, como la propagación de los campos con datos de Juice existentes (si corresponde), la selección de un color de colorSpinner y la implementación de cancelButton, entre otras tareas. Sin embargo, este código no es exclusivo de los elementos Fragment, y puedes implementarlo por tu cuenta. Intenta implementar el resto de la funcionalidad. Como último recurso, puedes consultar el código de la solución de este codelab.

7. Inicia el diálogo de entrada

La última tarea es usar el componente de Navigation para iniciar el diálogo de entrada. El diálogo de entrada debe iniciarse siempre que el usuario haga clic en el botón de acción flotante (BAF). También debe iniciarse y pasar el ID correspondiente cuando el usuario hace clic en un elemento.

  1. En el onClickListener() del BAF, llama a navigate() en el controlador de navegación.

TrackerFragment.kt

import androidx.navigation.findNavController

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
   )
}

//...
  1. En la función de navegación, pasa la acción para navegar del rastreador al diálogo de entrada.

TrackerFragment.kt

//...

binding.fab.setOnClickListener { fabView ->
   fabView.findNavController().navigate(
TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment()
   )
}

//...
  1. Repite esta acción en el cuerpo de lambda para el método onEdit() en el JuiceListAdapter, pero esta vez, pasa el id de Juice.

TrackerFragment.kt

//...

onEdit = { drink ->
   findNavController().navigate(
       TrackerFragmentDirections.actionTrackerFragmentToEntryDialogFragment(drink.id)
   )
},

//...

8. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar estos comandos de git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git
$ cd basic-android-kotlin-compose-training-juice-tracker
$ git checkout views

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.