Diseño Android: Formulario en cuadro de diálogo con Fragment

Última actualización: 24/11/2020

logo android

En el tutorial Diseño Android: Cuadros de diálogo de selección vimos cómo crear un cuadro de diálogo de selección de opciones (simple y múltiple) encapsulado en un fragment. También echamos un vistazo rápido a las características que presentan en general de los cuadros de diálogo en Android. En el presente tutorial haremos lo mismo pero en lugar de mostrar un listado de elementos a seleccionar tendremos un pequeño formulario.

AppCompatDialogFragment

Proyecto de ejemplo

El proyecto de ejemplo tiene como target Android 30 y será compatible hasta Android 4.4 (Api 19) y superior. Usaremos algunos widgets gráficos proporcionados por Material Components for Android y AndroidX, librerías que son la evolución de la ya obsoleta librería de compatibilidad. Para el formulario vamos a recurrir el widget TextInputLayout del mismo modo que en el tutorial Diseño Android: EditText con Material Design y TextInputLayout. El fichero build.gradle queda tal que así.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.danielme.dialogfragmentform"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.android.material:material:1.2.1'
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
}

La aplicación tiene una única pantalla, gestionada por la clase MainActivity, con la típica Toolbar y en la que mostraremos dos campos de texto cuyo contenido estableceremos con el formulario del cuadro de diálogo, así como un botón para abrir el mismo.

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

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        style="@style/AppTheme.Toolbar" />

    <TextView
        android:id="@+id/textViewFirstNameLabel"
        style="@style/AppTheme.TextViewLabel"
        android:text="@string/firstname" />

    <TextView
        android:id="@+id/textViewFirstNameContent"
        style="@style/AppTheme.TextViewContent" />

    <TextView
        android:id="@+id/textViewLastNameLabel"
        style="@style/AppTheme.TextViewLabel"
        android:text="@string/lastname" />

    <TextView
        android:id="@+id/textViewLastNameContent"
        style="@style/AppTheme.TextViewContent"/>

    <Button
        android:id="@+id/button"
        style="@style/AppTheme.Margins"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="edit"
        android:text="@string/edit"/>

</LinearLayout>

Cursos aplicaciones móviles

Construyendo el formulario

Tal y como se ha comentado en la introducción, nuestro cuadro de diálogo estará encapsulado dentro de un fragment. Vamos a crearlo especializando la clase AppCompatDialogFragment para que se aplique automáticamente un diseño consecuente con las guías de estilo de Material Design. En este fragment, sobrescribimos el método onCreateDialog el cual devolverá el Dialog que mostrará el fragment. Este diálogo lo creamos de forma “fluida” con el builder MaterialAlertDialogBuilder. Su particularidad es que el contenido será el siguiente layout con nuestro formulario.

<?xml version="1.0" encoding="utf-8"?>

<ScrollView
    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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dialog_margin"
        android:layout_marginRight="@dimen/dialog_margin"
        android:orientation="vertical">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/textInputLayoutFirstName"
            style="@style/AppTheme.TextInputLayoutForm"
            android:hint="@string/firstname"
            app:counterEnabled="true"
            app:counterMaxLength="20">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/textInputFirstName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textEmailAddress"
                android:maxLength="20"
                android:maxLines="1"/>

        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            style="@style/AppTheme.TextInputLayoutForm"
            android:hint="@string/lastname"
            app:counterEnabled="true"
            app:counterMaxLength="30">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/textInputLastName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:imeOptions="actionDone"
                android:inputType="text"
                android:maxLength="30"
                android:maxLines="1"/>

        </com.google.android.material.textfield.TextInputLayout>
    </LinearLayout>

</ScrollView>

Tendremos que “inflar” este layout, configurar los elementos del mismo que así lo requieran, e incluirlo en el Dialog con el método setView. Usaremos un AlertDialog porque ya incluye el título y los botones.

  @NonNull
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState) {
    View content = LayoutInflater.from(getContext()).inflate(R.layout.fragment_form, null);

    setupContent(content);

    AlertDialog alertDialog = new MaterialAlertDialogBuilder(getContext())
            .setView(content)
            .setCancelable(true)
            .setNegativeButton(getString(R.string.cancel), null)
            .setTitle(R.string.edit)
            .setPositiveButton(getString(R.string.save), (dialogInterface, i) -> returnValues())
            .create();
    //asegura que se muestre el teclado con el diálogo completo
    alertDialog.getWindow().setSoftInputMode(
            WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
                    | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
    return alertDialog;
  }

Una vez inflado el layout, procedemos a configurar los EditText con los valores iniciales, asegurando además que el cursor se muestre al final del texto. Asimismo, usaremos lo visto en Tip Android #34: Personalizar el teclado con imeOptions, para que cuando estemos en el segundo campo podamos directamente desde el teclado realizar la acción de guardado de los campos.

 private void setupContent(View content) {
    textInputLayoutFirstName = content.findViewById(R.id.textInputLayoutFirstName);
    textInputFirstName = content.findViewById(R.id.textInputFirstName);
    textInputLastName = content.findViewById(R.id.textInputLastName);
    textInputFirstName.setText(getArguments().getString(ARG_FIRSTNAME));
    textInputFirstName.setSelection(getArguments().getString(ARG_FIRSTNAME).length());
    textInputLastName.setText(getArguments().getString(ARG_LASTNAME));
    textInputLastName.setSelection(getArguments().getString(ARG_LASTNAME).length());

    textInputLastName.setOnEditorActionListener((textView, actionId, keyEvent) -> {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        returnValues();
        dismiss();
        return true;
      }
      return false;
    });
  }

También necesitamos recibir esos valores iniciales de los campos del formulario (y que se muestran en MainActivity), y, en el caso de que el usuario pulse “Guardar”, enviar los nuevos valores de vuelta a MainActivity.

El envío de datos de la Activity al fragment lo haremos mediante el típico “constructor estático” (un fragment sólo puede tener como constructor el constructor vacío a menos que se utilice FragmentFactory).

  public static FormFragment newInstance(String firstName, String lastName) {
    Bundle args = new Bundle();
    args.putString(ARG_FIRSTNAME, firstName);
    args.putString(ARG_LASTNAME, lastName);

    FormFragment frag = new FormFragment();
    frag.setArguments(args);

    return frag;
  }

El envío de los datos a la activity podemos hacerlo invocando un método de la misma o bien mediante eventos, ya sea con LocalBroadcast o con un bus de eventos como EventBus (muy recomendable). Ambas opciones ya se describen en el tutorial Diseño Android: Cuadros de diálogo de selección, así que en esta ocasión iré a lo sencillo y recurriré a la primera estrategia definiendo una interfaz y haciendo que sea implementada por la activity.

public interface FormDialogListener {

  void update(String firstname, String lastname);
}
 
@Override
public void update(String firstName, String lastName) {
  textViewFirstName.setText(firstName);
  textViewLastName.setText(lastName);
}

En el fragment se sobrescribe el método onAttach para recuperar la Activity que muestra el fragment como un objeto de tipo FormDialogListener. Guardamos la Activity en un atributo para posteriormente poder ejecutar su método update.

  private FormDialogListener listener;

  @Override
  public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    if (context instanceof FormDialogListener) {
      listener = (FormDialogListener) context;
    } else {
      throw new IllegalArgumentException("context is not FormDialogListener");
    }
  }

Ahora unimos todas piezas y el fragment queda tal que así.

package com.danielme.dialogfragmentform;

import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;


public class FormDialogFragment extends AppCompatDialogFragment {

  public static final String TAG = FormDialogFragment.class.getSimpleName();

  private static final String ARG_FIRSTNAME = "ARG_FIRSTNAME";
  private static final String ARG_LASTNAME = "ARG_LASTNAME";

  private TextInputLayout textInputLayoutFirstName;
  private EditText textInputFirstName;
  private EditText textInputLastName;
  private FormDialogListener listener;

  public static FormDialogFragment newInstance(String firstName, String lastName) {
    Bundle args = new Bundle();
    args.putString(ARG_FIRSTNAME, firstName);
    args.putString(ARG_LASTNAME, lastName);

    FormDialogFragment frag = new FormDialogFragment();
    frag.setArguments(args);

    return frag;
  }

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    if (context instanceof FormDialogListener) {
      listener = (FormDialogListener) context;
    } else {
      throw new IllegalArgumentException("context is not FormDialogListener");
    }
  }

  @NonNull
  @Override
  public Dialog onCreateDialog(Bundle savedInstanceState) {
    View content = LayoutInflater.from(getContext()).inflate(R.layout.fragment_form, null);

    setupContent(content);

    AlertDialog alertDialog = new MaterialAlertDialogBuilder(getContext())
            .setView(content)
            .setCancelable(true)
            .setNegativeButton(getString(R.string.cancel), null)
            .setTitle(R.string.edit)
            .setPositiveButton(getString(R.string.save), (dialogInterface, i) -> returnValues())
            .create();

     //asegura que se muestre el teclado con el diálogo completo
    alertDialog.getWindow().setSoftInputMode(
            WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
                    | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
    return alertDialog;
  }

  private void returnValues() {
    listener.update(textInputFirstName.getText().toString(),
            textInputLastName.getText().toString());
  }

  private void setupContent(View content) {
    textInputLayoutFirstName = content.findViewById(R.id.textInputLayoutFirstName);
    textInputFirstName = content.findViewById(R.id.textInputFirstName);
    textInputLastName = content.findViewById(R.id.textInputLastName);
    textInputFirstName.setText(getArguments().getString(ARG_FIRSTNAME));
    textInputFirstName.setSelection(getArguments().getString(ARG_FIRSTNAME).length());
    textInputLastName.setText(getArguments().getString(ARG_LASTNAME));
    textInputLastName.setSelection(getArguments().getString(ARG_LASTNAME).length());

    textInputLastName.setOnEditorActionListener((textView, actionId, keyEvent) -> {
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        returnValues();
        dismiss();
        return true;
      }
      return false;
    });
  }

}


Este fragment lo mostramos desde MainActivity creándolo e invocando a su método show ya que se trata de un AppCompatDialogFragment. Obsérvese además que se ha implementado la interfaz FormDialogListener que vimos anteriormente.

package com.danielme.dialogfragmentform;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;

public class MainActivity extends AppCompatActivity implements FormDialogListener{

  private TextView textViewFirstName;
  private TextView textViewLastName;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    setSupportActionBar((Toolbar) findViewById(R.id.toolbar));

    textViewFirstName = findViewById(R.id.textViewFirstNameContent);
    textViewLastName = findViewById(R.id.textViewLastNameContent);
  }

  public void edit(View view) {
    FormDialogFragment form = FormDialogFragment.newInstance(textViewFirstName.getText().toString(),
        textViewLastName.getText().toString());
    form.show(getSupportFragmentManager(), FormDialogFragment.TAG);
  }

  @Override
  public void update(String firstName, String lastName) {
    textViewFirstName.setText(firstName);
    textViewLastName.setText(lastName);
  }

}

Validaciones

Vamos a añadir a nuestro formulario una validación: el campo First Name no puede estar vacío. Para hacerlo recurrimos nuevamente al tutorial correspondiente e implementamos la validación al pulsarse el botón guardar.

.setPositiveButton(getString(R.string.save), (dialogInterface, i) -> if (validate()) returnValues())
  private boolean validate() {
    if (TextUtils.isEmpty(textInputFirstName.getText())) {
      textInputLayoutFirstName.setError(getString(R.string.mandatory));
      textInputLayoutFirstName.setErrorEnabled(true);
      return false;
    }
    return true;
  }

Una pequeña mejora de usabilidad: si se muestra el mensaje de error y el usuario edita el campo, se borra el mensaje.

    textInputFirstName.addTextChangedListener(new TextWatcher() {
      @Override
      public void afterTextChanged(Editable s) {
        if (textInputFirstName.getVisibility() == View.VISIBLE) {
          textInputLayoutFirstName.setError(null);
        }
      }

      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        //nothing here
      }

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
        //nothing here
      }
    });

Sin embargo, al estar nuestro formulario dentro de un AlertDialog tenemos un problema: al pulsare el botón de guardar tras ejecutarse el código de nuestro listener el cuadro de diálogo se cierra de forma automática por lo que si la validación falla el usuario no verá el mensaje de error. Para evitar este cierre automático recurro a la siguiente estrategia:

  1. Al construir el AlertDialog, establecer el listener del botón a null.
     AlertDialog alertDialog = new MaterialAlertDialogBuilder(getContext())
                .setView(content)
                .setCancelable(true)
                .setNegativeButton(getString(R.string.cancel), null)
                .setTitle(R.string.edit)
                .setPositiveButton(getString(R.string.save), null)
                .create();
    
  2. Sobrescribir el método onStart y establecer aquí los listener de los botones, teniendo en cuenta que a partir de ahora el cierre del cuadro de diálogo hay que realizarlo de forma explícita cuando sea necesario invocando al método dismiss del mismo.
      @Override
      public void onStart() {
        super.onStart();
        Button positiveButton = ((AlertDialog) getDialog()).getButton(Dialog.BUTTON_POSITIVE);
        positiveButton.setOnClickListener(view -> {
          if (validate()) {
            returnValues();
            getDialog().dismiss();
          }
        });
      }
    
  3. Tras estos cambios ya hemos terminado nuestro formulario.

    Código de ejemplo

    El proyecto de ejemplo para Android Studio se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

    Master Pyhton, Java, Scala or Ruby

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .