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

logo androidEn el tutorial Diseño Android: Cuadros de diálogo de selección vimos cómo crear un cuadro de diálogo de selección encapsulado en un fragment y también echamos un vistazo rápido a las características 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 haremos lo propio con un pequeño formulario. En concreto, implementaremos el siguiente formulario con dos campos de texto.

AppCompatDialogFragment

Proyecto de ejemplo

El proyecto de ejemplo de este tutorial utilizará Material Design y será compatible con Android 4.4 (Api 19) y superior. Para conseguirlo nos apoyaremos en las librerías de compatibilidad, concretamente en el módulo appcompat. Para el formulario usaremos el widget TextInputLayout del mismo modo que en el tutorial Diseño Android: EditText con Material Design y TextInputLayout, lo que requiere utilizar el módulo design. El fichero build.gradle queda tal que así.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    buildToolsVersion '27.0.3'

    defaultConfig {
        applicationId "com.danielme.dialogfragmentform"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

     allprojects {
        repositories {
            jcenter()
            maven {
                url "https://maven.google.com"
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support:design:27.1.1'
}

Se configurará los estilos y una toolbar siguiendo los pasos descritos en el tutorial Diseño Android: ActionBar con Toolbar. Tendremos una única pantalla (MainActivity) donde mostraremos dos campos de texto cuyo contenido estableceremos con el formulario del cuadro de diálogo, así como un botón para abrir dicho formulario.

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 nuestros diálogos tengan un estilo Material Design gracias a appcompat. En este fragment, sobrescribimos el método onCreateDialog el cual devolverá el Dialog que mostrará el fragment. Este diálogo lo creamos de forma típica, esto es, utilizando el builder de AlertDialog. 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">

        <android.support.design.widget.TextInputLayout
            android:id="@+id/textInputLayoutFirstName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/vertical_separator"
            android:hint="@string/firstname"
            app:counterEnabled="true"
            app:counterMaxLength="20">

            <android.support.design.widget.TextInputEditText
                android:id="@+id/textInputFirstName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textEmailAddress"
                android:maxLength="20"
                android:maxLines="1"/>

        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/lastname"
            app:counterEnabled="true"
            app:counterMaxLength="30">

            <android.support.design.widget.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"/>

        </android.support.design.widget.TextInputLayout>
    </LinearLayout>

</ScrollView>

Tendremos que “inflar” este layout, configurar los elementos del mismo que así lo requieran, e incluirlo en el AlertDialog con el método setView. También podemos utilizar la versión del setView que recibe directamente el id del layout. Estas acciones podemos codificarlas del siguiente modo:

 View content = LayoutInflater.from(getContext()).inflate(R.layout.fragment_form, null);
 
 setupContent(content);
 
 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
 builder.setView(content);
...

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) {
    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(new TextView.OnEditorActionListener() {
      @Override
      public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
        if (actionId == EditorInfo.IME_ACTION_DONE) {
          returnValues();
          dismiss();
          return true;
        }
        return false;
      }
    });
  }

También necesitamos recibir los valores iniciales de los campos del formulario (estos 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 patrón “constructor estático” (un fragment sólo debe tener como constructor el constructor vacío).

  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. Ambas opciones ya se describen en el tutorial Diseño Android: Cuadros de diálogo de selección, así que aquí simplemente implementaré la primera estrategia definiendo una interfaz y haciendo que sea implementada por la activity.

public interface FormDialogListener {

  void update(String firstname, String lastname);
}

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

  private FormDialogListener listener;

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    listener = (FormDialogListener) context;
    try {
      listener = (FormDialogListener) context;
    } catch (ClassCastException e) {
      throw new ClassCastException(context.toString()
          + " must implement FormDialogListener");
    }
  }

Ahora unimos todas piezas para construir nuestro fragment.

package com.danielme.dialogfragmentform;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;


public class FormFragment extends AppCompatDialogFragment {

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

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

  private EditText textInputFirstName;
  private EditText textInputLastName;
  private FormDialogListener listener;

  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;
  }

  @Override
  public void onAttach(Context context) {
    super.onAttach(context);
    listener = (FormDialogListener) context;
    try {
      listener = (FormDialogListener) context;
    } catch (ClassCastException e) {
      throw new ClassCastException(context.toString() + " must implement FormDialogListener");
    }
  }

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

    setupContent(content);

    AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
    builder.setView(content)
        .setNegativeButton(getString(R.string.cancel), null)
        .setCancelable(true)
        .setTitle(R.string.edit)
        .setPositiveButton(getString(R.string.save), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialogInterface, int i) {
            returnValues();
          }
        });

    AlertDialog alertDialog = builder.create();
    //asegura que se muestre el teclado
    alertDialog.getWindow().setSoftInputMode(
        WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
    return alertDialog;
  }

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

  private void setupContent(View content) {
    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(new TextView.OnEditorActionListener() {
      @Override
      public boolean onEditorAction(TextView textView, int actionId, KeyEvent 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.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.TextView;

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) {
    FormFragment form = FormFragment.newInstance(textViewFirstName.getText().toString(),
        textViewLastName.getText().toString());
    form.show(getSupportFragmentManager(), FormFragment.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), new DialogInterface.OnClickListener() {
          @Override
          public void onClick(DialogInterface dialogInterface, int 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;
  }

Sin embargo, al estar nuestro formulario dentro de un AlertDialog tenemos un problema: al pulsare el botón, 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á los mensajes con los errores. Para evitar este cierre automático recurro a la siguiente estrategia:

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

    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 )

w

Conectando a %s

A %d blogueros les gusta esto: