Unificando el detalle de los libros: nuevo + ver

Tenemos una actividad de detalle del libro, nuestra intención es poder utilizarla tanto para 
  • crear un libro nuevo
  • visualizar los datos de un libro existente
  • y a futuro, editar los datos de un libro

Modificaciones en el controller

Para eso tenemos que incorporar un parámetro que nos diga si es editable el libro, de tipo booleano. En LibroListActivity, vamos a mapear la selección de un libro con su visualización (por lo tanto no vamos a permitir la edición):

override def onItemSelected(Libro libro) {

       val detailIntent = new Intent(this, typeof(LibroDetailActivity))

       ...

       detailIntent.putExtra(LibroDetailFragment.EDITABLE, false)

       startActivity(detailIntent)


En el LibroDetailFragment agregamos la variable que permite saber si podemos editar el libro o no y debemos modificar TextViews por EditText que son editables o readOnly:


public
static final String EDITABLE = "editable"

private boolean editable

override onCreate(Bundle savedInstanceState) {

    ...

    editable = arguments.getBoolean(EDITABLE)

Modificaciones en la vista

En res/layout/fragment_libro_detail.xml:

   
<TableRow

    ...

    android:background="#000">

  <EditText

      android:id="@+id/txtTitulo"

      android:layout_width="wrap_content"

      android:layout_height="wrap_content"

      android:padding="5dip"

      android:inputType="textCapWords"

      android:textColor="#C0C0C0"

      android:textAppearance="?android:attr/textAppearanceMediumInverse" />

  </TableRow>

 

<TableRow

    ...>

 

 <EditText

    android:id="@+id/txtAutor"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:inputType="textCapWords"

        android:padding="5dip"

        />

</TableRow>


En lugar de bindear propiedades de sólo lectura, accedemos a la referencia de cada control para asignar manualmente la propiedad focusable:

LibroDetailFragment:

override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

       val rootView = inflater.inflate(R.layout.fragment_libro_detail,  

container, false)

       val txtTitulo = rootView.findViewById(R.id.txtTitulo) as EditText

       val txtAutor = rootView.findViewById(R.id.txtAutor) as EditText

       txtTitulo.focusable = editable

       txtAutor.focusable = editable

       if (libro != null) {

             txtTitulo.text = libro.titulo

             txtAutor.text = libro.autor

       }

       rootView

}


Menú nuevo libro

Para agregar un nuevo libro generamos un nuevo menú que se va a desplegar en la lista de libros, al presionar el botón Menú:




Esto requiere que definamos el menú en res/menu: 

menu_libros_list.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:id="@+id/nuevoLibro" android:title="@string/nuevoLibro"></item>

</menu>


En strings.xml tenemos que agregar la descripción del ítem:

<string name="nuevoLibro">Nuevo</string>


Y tenemos que redefinir el método que declara los menúes en LibroListActivity:

override
def onCreateOptionsMenu(Menu menu) {

menuInflater.inflate(R.menu.menu_libros_list, menu)

true

}


(por default el comportamiento no tiene ningún menú)

También decimos qué hacer cuando el usuario seleccione esa opción. En LibroListActivity:

override
def onOptionsItemSelected(MenuItem item) {

    switch (item.itemId) {

        case R.id.nuevoLibro: nuevoLibro()

    }

    true

}


El método nuevoLibro() pasa como parámetros: un libro nuevo y editable true.

Binding bidireccional en el detalle del libro

Revisemos un poco cómo quedó el LibroDetailFragment. Definimos algunas variables: 
  • el libro que hace de nuestro modelo, 
  • el flags editable que discrimina actualización vs. visualización

private Libro libro

private boolean editable


En el onCreate recibimos esos parámetros y los asignamos a las variables de arriba. ¿Cómo determinamos si el libro es nuevo o lo estamos modificando? Por el id: si es nulo se trata de un alta.


En el onCreateView tenemos que bindear el libro con cada control visual, y además establecer visibilidad del botón Guardar (sólo si editable = true),y las propiedades de los edit text como readonly (si actualiza = false, según la propiedad focusable). Además asociamos la acción de guardar con un método guardar:

override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

val rootView = inflater.inflate(R.layout.fragment_libro_detail,  container, false)

val txTitulo =  rootView.findViewById(R.id.txtTitulo) as EditText

val txAutor = rootView.findViewById(R.id.txtAutor) as EditText

rootView.findViewById(R.id.btnGuardar).onClickListener = [ View v | guardar(v) ] as View.OnClickListener

txTitulo.focusable = editable

txAutor.focusable = editable

val btnGuardar = rootView.findViewById(R.id.btnGuardar) as Button

if (editable) {

btnGuardar.visibility = View.VISIBLE

} else {

btnGuardar.visibility = View.INVISIBLE

}

if (libro != null) {

txTitulo.text = libro.titulo

txAutor.text = libro.autor

}

rootView

}     


¿Qué hacemos en el guardar? 
  • en el alta pedimos al home que agregue el libro
  • en la modificación primero eliminamos el libro y lo volvemos a agregar (podríamos modificar la interfaz del home para tener un método específico update(), lo dejamos para más adelante)


def
void guardar(View view) {

libro.titulo = txtTitulo.text.toString

libro.autor = txtAutor.text.toString

val homeLibros = MemoryBasedHomeLibros.instance

if (libro.id != null) {

homeLibros.removeLibro(posicion)

}

homeLibros.addLibro(libro)

activity.finish()

}     


La variable activity referencia al LibroDetailActivity. Al enviar el mensaje finish() cerramos la actividad de detalle y volvemos al list. 
Los métodos txtTitulo y txtAutor nos permiten recuperar la referencia a los controles:

def txtTitulo() {

view.findViewById(R.id.txtTitulo) as EditText

}


Corremos el ejemplo y al crear un nuevo libro nos aparece un mensaje de error:

02-12 06:48:54.754: E/AndroidRuntime(976): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(16908298, class android.widget.ListView) with Adapter(class android.widget.ArrayAdapter)]

02-12 06:48:54.754: E/AndroidRuntime(976): at android.widget.ListView.layoutChildren(ListView.java:1545)

...


El origen del error tiene que ver con el ciclo de vida de las actividades:
  1. Visualizamos la lista de libros, nuestro modelo es una lista con n libros.
  2. Al presionar el botón nuevo, pasa a un segundo plano la actividad Master (que muestra la lista de libros) y se activa la actividad Detail
  3. Ingresamos los datos del nuevo libro y presionamos Guardar. Esto agrega el libro en el home, lo que produce que la lista original de libros se modifique (pasa a haber n+1 libros)
  4. Al volver a primer plano la actividad que muestra la lista de libros, detecta que su modelo se modificó y no es más válido
Para corregir esto tenemos que 
  1. entender cómo funciona el ciclo de vida de una actividad
  2. avisar cuando la actividad vuelva que la lista pudo haberse modificado, notificando al adapter que está escuchando cambios en la lista original de libros:
LibroListFragment

override def onResume() {

super.onResume()

adapterLibros.notifyDataSetChanged()

}


Diagrama de clases de la solución general