Fundamentos de Android: comunicación entre actividades

Fundamentos de Android: comunicación entre actividades

Hasta ahora hemos visto aplicaciones sencillas con una sola actividad pero lo más común es que una aplicación tenga varias actividades, y además que enviemos datos desde una a otra actividad. Vamos a verlo con un ejemplo.

Abrimos Android Studio y creamos un nuevo proyecto llamado 'Control Acceso' desde una actividad vacía (Empty Activity):



Adaptamos el diseño de la actividad para que se muestre algo así:



Para ello hemos utilizado elementos que ya conocemos, como LinearLayout (vertical y horizontal) como contenedores para organizar el resto de elementos, elementos de texto (TextView y EditText) y un botón (que todavía no hace nada). El panel del árbol de componentes se ve así:



También hemos utilizado recursos de texto (values/strings.xml), de colores (values/colors.xml), de dimensiones (values/dimens.xml) y de estilos (values/styles.xml). Ten en cuenta que cuando creamos estilos, Android Studio nos pregunta en qué ámbito los vamos a usar, ya sea solo para ese archivo de diseño o para todo el proyecto; como después nos interesará aplicarlo también en otra actividad, hemos seleccionado que tengan alcance para todo el proyecto. Concretamente el archivo layout/activity_main.xml es éste:
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

    <LinearLayout
            android:id="@+id/linearLayout"
            style="@style/estilo_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <TextView
                android:id="@+id/user_txt"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/margen_txt_end1"
                android:text="@string/usuario" />

        <EditText
                android:id="@+id/user_input"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/usuario"
                android:inputType="textPersonName" />
    </LinearLayout>

    <LinearLayout
            android:id="@+id/linearLayout2"
            style="@style/estilo_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/pass_txt"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/margen_txt_end2"
                android:text="@string/contraseña" />

        <EditText
                android:id="@+id/pass_input"
                style="@style/estilo_texto"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/contraseña"
                android:inputType="textPassword" />
    </LinearLayout>

    <Button
            android:id="@+id/entrar_btn"
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/btn_entrar" />

</LinearLayout>

A partir de este diseño vamos a pretender que cuando el usuario introduzca un nombre de usuario y una contraseña pueda acceder a otra actividad, y desde esa nueva actividad pueda 'cerrar sesión' y volver a la primera actividad.

Para crear esta segunda actividad, desde el panel Project (vista Android) New -> Activity -> Empty Activity como se ve en la imagen:



Y le asignamos un nombre a la nueva actividad:



Ahora ya tenemos, además del archivo de la actividad app/src/main/java/com/android/controlacceso/LoginCheck.kt, su correspondiente archivo de diseño layout/activity_login_check.xml. Adaptamos el diseño de éste último para que se vea así:



Y el archivo layout/activity_login_check.xml es:
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        style="@style/estilo_linear"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".LoginCheck">

    <TextView
            android:id="@+id/acceso_txt"
            style="@style/estilo_texto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/login_check"
            android:textAlignment="center" />

    <TextView
            android:id="@+id/login_txt"
            style="@style/estilo_texto"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/bienvenido"
            android:textAlignment="center" />

    <Button
            android:id="@+id/button_cerrar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/margen_linea"
            android:text="@string/cerrar" />
</LinearLayout>

Ahora que ya tenemos dos actividades, en la actividad principal tenemos que implementar la funcionalidad para activar la segunda ventana. Para ello, como vimos en la entrada Fundamentos de Android: Interacción del usuario, utilizamos la llamada a setOnClickListener para manejar el evento de clic sobre el botón:
entrar_btn.setOnClickListener { }
Y como bloque de este controlador creamos un objeto de la clase Intent con dos parámetros: la referencia a esta clase (this) y la referencia a la otra actividad (LoginCheck::class.java). Y después llamamos al método startActivity pasándole ese objeto de la clase Intent:
entrar_btn.setOnClickListener {
    val intento1 = Intent(this, LoginCheck::class.java)
    startActivity(intento1)
}

Quedando el archivo MainActivity.kt así:
package com.android.controlacceso

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        entrar_btn.setOnClickListener {
            val intento1 = Intent(this, LoginCheck::class.java)
            startActivity(intento1)
        }
    }
}

Ejecutamos en un emulador de Android y comprobamos que al pulsar el botón 'ENTRAR' de la actividad principal aparece la segunda ventana. Repetimos el mismo proceso en la segunda actividad para que al pulsar el botón 'CERRAR SESIÓN' volvamos a la actividad anterior, pero aunque podríamos pensar en este código:
button_cerrar.setOnClickListener {
    val intento2 = Intent(this, MainActivity::class.java)
    startActivity(intento2)
}
Y aunque parece funcionar, resulta mejor así:
button_cerrar.setOnClickListener {
    finish()
}
El método finish() tiene por objetivo liberar el espacio de memoria de esta actividad y pedir que se muestre la actividad anterior. El archivo LoginCheck.kt queda así:
package com.android.controlacceso

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_login_check.*

class LoginCheck : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login_check)

        button_cerrar.setOnClickListener {
            finish()
        }
    }
}

Pero la aplicación todavía no es funcional. En este momento le faltan, al menos, tres cosas importantes a la aplicación:
  1. Comprobar que los campos de usuario y contraseña no están vacíos (en próximas entradas perfecionaremos la aplicación permitiendo el registro de usuarios y verificando los datos de acceso).
  2. Pasar el nombre de usuario a la segunda actividad.
  3. Después de cerrar sesión y volver a la primera actividad, limpiar los datos introducidos (medida elemental de seguridad para que después de cerrar sesión no permanezcan los datos en la primera actividad y alguién pueda acceder simplemente pulsando el botón de ENTRAR).

Vamos con ellas. Para la primera tarea vamos a utilizar lo visto en la entrada Programa para verificar si un String está vacío o es nulo, de esta manera:
package com.android.controlacceso

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        entrar_btn.setOnClickListener {
            if (!esNuloVacio(user_input.text.toString()) && !esNuloVacio(pass_input.text.toString())) {
                val intento1 = Intent(this, LoginCheck::class.java)
                startActivity(intento1)
            } else {
                Toast.makeText(
                    this, "ERROR: Usuario y contraseña requeridos.",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    private fun esNuloVacio(str: String?): Boolean {
        return if (str != null && !str.trim().isEmpty()) false else true
        // más elegante así:
        // return !(str != null && !str.trim().isEmpty())
    }
}

Primer objetivo conseguido: si cualquiera de los campos de usuario o contraseña están vacíos (o solo contienen espacios en blanco) aparece un mensaje de advertencia del error y no pasa a la actividad de acceso.

Segunda tarea: pasar el nombre del usuario a la segunda actividad. Esto lo podemos hacer llamando al método putExtra de la clase Intent con dos parámetros de tipo String: el nombre del dato y el valor de ese dato:
val intento1 = Intent(this, LoginCheck::class.java)
intento1.putExtra("usuario", user_input.text.toString())
startActivity(intento1)

Y para recuperar ese valor desde la otra actividad definimos una variable de tipo Bundle que obtiene el String a partir del nombre del dato que envió la primera actividad:
val bundle = intent.extras
val user = bundle?.getString("usuario")
Otra opción es:
val user = intent.getStringExtra("usuario")
Y después utilizamos el valor obtenido para mostrarlo en la actividad:
login_txt.text = getString(R.string.bienvenido, user)
Para poder utilizar de esta manera el TextView 'login_txt' hemos creado el recurso de texto 'bienvenido' en values/strings.xml de esta manera:
<string name="bienvenido">Bienvenido %1$s</string>
Esta solución utiliza el formato %1$s y lo sustituye por el dato obtenido y guardado en la variable user. Los especificadores de este formato se basan en la sintaxis %[argument_index$]format_specifier donde argument_index indica el orden del argumento especificado en la lista de argumentos y format_specifier indica cómo debe formatearse según el tipo de dato (s: String, d: Int, f: Float...). Como el primer argumento es opcional y solo usamos uno, en nuestro caso podemos escribir el recurso de texto así:
<string name="bienvenido">Bienvenido %s</string>

Objetivo conseguido: ahora cuando se entra a la segunda actividad ésta recupera el nombre de usuario que se ha enviado desde la primera actividad y lo muestra en el mensaje de bienvenida. En posteriores entradas veremos otras maneras para enviar varios datos a la vez con Bundle.

Siguiente tarea: limpiar datos al cerrar sesión. Para ello, después de obtener el valor de los campos, los vaciamos:
val intento1 = Intent(this, LoginCheck::class.java)
intento1.putExtra("usuario", user_input.text.toString())
user_input.setText("")
pass_input.setText("")
startActivity(intento1)



A continuación los códigos de ambas actividades:
package com.android.controlacceso

import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        entrar_btn.setOnClickListener {
            if (!esNuloVacio(user_input.text.toString()) && !esNuloVacio(pass_input.text.toString())) {
                val intento1 = Intent(this, LoginCheck::class.java)
                intento1.putExtra("usuario", user_input.text.toString())
                user_input.setText("")
                pass_input.setText("")
                startActivity(intento1)
            } else {
                Toast.makeText(
                    this, "ERROR: Usuario y contraseña requeridos.",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }
    }

    private fun esNuloVacio(str: String?): Boolean {
        return !(str != null && !str.trim().isEmpty())
    }
}
package com.android.controlacceso

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_login_check.*

class LoginCheck : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login_check)

        val bundle = intent.extras
        val user = bundle?.getString("usuario")

        login_txt.text = getString(R.string.bienvenido, user)

        button_cerrar.setOnClickListener {
            finish()
        }
    }
}

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos