Llegamos a algo imprescindible en el desarrollo de aplicaciones móviles, las listas. Antaño se usaban ListView en android, pero con el tiempo Google sacó las RecyclerView, listas más customizables, potentes y eficientes.

Formato en vídeo con las últimas actualizaciones:

Configurando nuestro proyecto

Lo primero que haremos será crear un nuevo proyecto o podemos usar el del capítulo anterior, pues reutilizaremos código de ahí.

curso android en kotlin

Una vez creado iremos al build.gradle del módulo app e implementaremos una dependencia. Una dependencia es básicamente código externo del proyecto el cual podemos implementar para aprovecharlo, en este caso implementaremos el Recyclerview que por defecto no lo trae.

Así que vamos a la parte de las dependencies{}  y añadimos

implementation 'com.android.support:recyclerview-v7:25.0.0'

Y también añadiremos una librería llamada Picasso que la usaremos para la gestión de imágenes.

implementation 'com.squareup.picasso:picasso:2.5.2'

Nos aparecerá en la parte superior una barra que nos dirá que es necesario sincronizar el gradle, así que le damos. Si todo sale bien ya tendremos esta parte configurada, si te da error de sdk posiblemente es que tengas varias versiones diferentes (CompileSDKVersion, buildToolsVersion…) todas a 25. Debería quedar así.

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        applicationId "com.cursokotlin.dataclasskotlin" //Nombre de nuestra app
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.0', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:25.4.0'
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.android.support:recyclerview-v7:25.0.0'
    implementation 'com.squareup.picasso:picasso:2.5.2'
}

Editando nuestra layout y creando una celda

El layout principal será el contenedor del RecyclerView, pero luego para inflarlo tendremos que crear un adapter (hablaremos de él un poco más abajo) y una celda que será la que muestre cada una de las filas de la lista.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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.support.v7.widget.RecyclerView
        android:id="@+id/rvSuperheroList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>

En este ejemplo seguiremos con los objetos superhéroe del capítulo anterior así que la celda que hagamos llevará un ImageView para el logo del superhéroe y tres textos, uno para el nombre real, el nombre de superhéroe y el de la compañía (DC o Marvel). Vamos a res/layout y creamos un archivo llamado item_superhero_list.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="10dp">

    <ImageView
        android:id="@+id/ivAvatar"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginEnd="10dp"
        android:layout_marginRight="10dp"
        android:scaleType="centerCrop" />

    <TextView
        android:id="@+id/tvSuperhero"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textSize="20sp"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/ivAvatar"
        android:layout_toEndOf="@+id/ivAvatar"
        tools:text= "dadawdawdwd" />

    <TextView
        android:id="@+id/tvRealName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"
        android:layout_below="@+id/tvSuperhero"
        android:layout_toRightOf="@+id/ivAvatar"
        android:layout_toEndOf="@+id/ivAvatar"
        tools:text = "wdawd"/>

    <TextView
        android:id="@+id/tvPublisher"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="italic"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        tools:text = "DC"/>
</RelativeLayout>

Si no me equivoco todos los atributos de arriba ya los hemos visto excepto los «tools:text» la función tools lo que hace es ayudarnos a ver algo de un modo más sencillo. Aquí por ejemplo está poniendo un texto para poder ir maquetando mejor la vista, pero cuando compile la aplicación, ese texto no estará, así que podríamos decir que es un molde. Si tenéis alguna otra duda podéis dejar un comentario.

Ahora vamos a crear un modelo superhéroe del cual haremos varias listas para cargar en el recyclerview, como esto ya está hecho en el capítulo anterior solo os voy a dejar la clase aquí para que la copien.

data class Superhero(
        var superhero:String,
        var publisher:String,
        var realName:String,
        var photo:String
)

Creando nuestros objetos superhéroes

Volvemos a MainActivity.kt  y vamos a crear dos funciones, una que configurará nuestro recyclerview con el adapter y otra que generará la lista de objetos superhéroes.

class MainActivity : AppCompatActivity() {

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

    fun setUpRecyclerView(){
        ...
    }

    fun getSuperheros(): MutableList<Superhero>{
        var superheros:MutableList<Superhero> = ArrayList()
        superheros.add(Superhero("Spiderman", "Marvel", "Peter Parker", "https://cursokotlin.com/wp-content/uploads/2017/07/spiderman.jpg"))
        superheros.add(Superhero("Daredevil", "Marvel", "Matthew Michael Murdock", "https://cursokotlin.com/wp-content/uploads/2017/07/daredevil.jpg"))
        superheros.add(Superhero("Wolverine", "Marvel", "James Howlett", "https://cursokotlin.com/wp-content/uploads/2017/07/logan.jpeg"))
        superheros.add(Superhero("Batman", "DC", "Bruce Wayne", "https://cursokotlin.com/wp-content/uploads/2017/07/batman.jpg"))
        superheros.add(Superhero("Thor", "Marvel", "Thor Odinson", "https://cursokotlin.com/wp-content/uploads/2017/07/thor.jpg"))
        superheros.add(Superhero("Flash", "DC", "Jay Garrick", "https://cursokotlin.com/wp-content/uploads/2017/07/flash.png"))
        superheros.add(Superhero("Green Lantern", "DC", "Alan Scott", "https://cursokotlin.com/wp-content/uploads/2017/07/green-lantern.jpg"))
        superheros.add(Superhero("Wonder Woman", "DC", "Princess Diana", "https://cursokotlin.com/wp-content/uploads/2017/07/wonder_woman.jpg"))
        return superheros
    }
}

Aunque la clase no está terminada, quería que vieran el modelo de superhéroe creado, para que ahora entiendan mejor que vamos a hacer en el adapter.

Creando el adapter

Ahora vamos a crear el adapter del Recyclerview. Un adapter es la clase que hace de puente entre la vista (el recyclerview) y los datos. Así que creamos una nueva clase llamada RecyclerAdapter.kt

La clase RecyclerAdapter se encargará de recorrer la lista de superhéroes que le pasaremos más tarde, y llamando a otra clase interna que tendrá, este rellenará los campos.

Vamos a ir viendo método por método, así que hasta el final de la explicación la clase tendrá fallos.

class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    var superheros: MutableList<Superhero>  = ArrayList()
    lateinit var context:Context
    
    ...
}

Lo primero que hacemos es decirle a la clase que tendrá un RecyclerAdapter y un ViewHolder. Después creamos las dos variables que necesitamos para el adapter, una lista de superhéroes (que rellenaremos en la clase principal y se la mandaremos aquí y el contexto de la mainAcitivity para poder trabajar con la librería Picasso.

fun RecyclerAdapter(superheros : MutableList<Superhero>, context: Context){
       this.superheros = superheros
       this.context = context
   }

   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
       val item = superheros.get(position)
       holder.bind(item, context)
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       return ViewHolder(layoutInflater.inflate(R.layout.item_superhero_list, parent, false))
   }

   override fun getItemCount(): Int {
       return superheros.size
   }

Lo primero es un simple constructor (tiene el mismo nombre que la clase y lo único que hará será recibir la lista y el contexto que le pasamos desde la clase principal).

Los siguientes tres métodos tienen delante la palabra reservada override, esto es porque son métodos obligatorios que se implementan de la clase RecyclerView. onBindViewHolder() se encarga de coger cada una de las posiciones de la lista de superhéroes y pasarlas a la clase ViewHolder(todavía no está hecha) para que esta pinte todos los valores.

Después tenemos onCreateViewHolder() que como su nombre indica lo que hará será devolvernos un objeto ViewHolder al cual le pasamos la celda que hemos creado.

Y para finalizar el método getItemCount() nos devuelve el tamaño de la lista, que lo necesita el RecyclerView.

Para finalizar el adapter queda hacer la clase ViewHolder de la que tanto hemos hablado. No es necesaria hacerla dentro del adapter, pero como van tan ligadas la una a la otra creo que es lo mejor.

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val superheroName = view.findViewById(R.id.tvSuperhero) as TextView
        val realName = view.findViewById(R.id.tvRealName) as TextView
        val publisher = view.findViewById(R.id.tvPublisher) as TextView
        val avatar = view.findViewById(R.id.ivAvatar) as ImageView

        fun bind(superhero:Superhero, context: Context){
            superheroName.text = superhero.superhero
            realName.text = superhero.realName
            publisher.text = superhero.publisher
            itemView.setOnClickListener(View.OnClickListener { Toast.makeText(context, superhero.superhero, Toast.LENGTH_SHORT).show() })
            avatar.loadUrl(superhero.photo) 
        }
        fun ImageView.loadUrl(url: String) {
            Picasso.with(context).load(url).into(this)
        }
    }

Lo primero que hace esta clase es hacer referencia a los items de la celda, el view.findViewByID() busca los items a través de la id que le ponemos, y luego añadimos el as X donde X es el tipo del componente (ImageView, TextView…).

Dentro tiene el método bind(superhero:Superhero, context: Context) que dicho método lo llamamos desde el onBindViewHolder() para que rellene los datos. Después de añadir todos los textos que queremos, hacemos 2 cositas más. La primera es que llamamos a itemView que es la vista (celda) que estamos rellenando y le ponemos un setOnClickListener que pintará en pantalla el nombre del superhéroe en el que hemos hecho click. Y para finalizar seleccionamos el ImageView y con el método que hemos creado le pasamos la URL de la imagen. Esta url la usará Picasso para descargarla de internet y pintarla en nuestra lista. Pero para que Picasso pueda hacer eso, debemos ponerle permiso de internet a nuestra app.

Vamos a AndroidManifest.xml y añadimos el permiso después de abrir la etiqueta manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.cursokotlin.dataclasskotlin">

    <uses-permission android:name="android.permission.INTERNET" /> //Pedimos permiso de internet

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Y nuestro adapter completo sería así.

class RecyclerAdapter : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    var superheros: MutableList<Superhero>  = ArrayList()
    lateinit var context:Context

    fun RecyclerAdapter(superheros : MutableList<Superhero>, context: Context){
        this.superheros = superheros
        this.context = context
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = superheros.get(position)
        holder.bind(item, context)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return ViewHolder(layoutInflater.inflate(R.layout.item_superhero_list, parent, false))
    }

    override fun getItemCount(): Int {
        return superheros.size
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val superheroName = view.findViewById(R.id.tvSuperhero) as TextView
        val realName = view.findViewById(R.id.tvRealName) as TextView
        val publisher = view.findViewById(R.id.tvPublisher) as TextView
        val avatar = view.findViewById(R.id.ivAvatar) as ImageView

        fun bind(superhero:Superhero, context: Context){
            superheroName.text = superhero.superhero
            realName.text = superhero.realName
            publisher.text = superhero.publisher
            avatar.loadUrl(superhero.photo)
            itemView.setOnClickListener(View.OnClickListener { Toast.makeText(context, superhero.superhero, Toast.LENGTH_SHORT).show() })
        }
        fun ImageView.loadUrl(url: String) {
            Picasso.with(context).load(url).into(this)
        }
    }
}

Ahora volvemos al MainActivity.kt para finalizar el método que nos queda. Primero crearemos dos variables, la del RecyclerView y la del adapter que acabamos de terminar.

lateinit var mRecyclerView : RecyclerView
val mAdapter : RecyclerAdapter = RecyclerAdapter()

El recyclerview lo tenemos que instanciar en el método setUpRecyclerView() por lo que tenemos que ponerle la propiedad lateinit a la variable, indicándole a Kotlin que la instanciaremos más tarde.

fun setUpRecyclerView(){
    mRecyclerView = findViewById(R.id.rvSuperheroList) as RecyclerView
    mRecyclerView.setHasFixedSize(true)
    mRecyclerView.layoutManager = LinearLayoutManager(this)
    mAdapter.RecyclerAdapter(getSuperheros(), this)
    mRecyclerView.adapter = mAdapter
}

Para finalizar creamos el método, lo primero que hará será buscar en el layout el Recycler y asignarselo a la variable que creamos. El siguiente método se usa para evitar problemas cuando el padre que contiene la lista cambia y puede estropear el recycler.

Luego llamamos a nuestro adapter y le pasamos los campos que necesite, en este caso getSuperheros (que devuelve una lista de superhéroes) y this (el contexto). Y para finalizar se lo asignamos a el recyclerview.

Ahora solo necesitamos compilar para ver el resultado de nuestra aplicación.

curso android en kotlin

Continúa con el curso: Capítulo 16 – Persistencia de datos con Shared Preferences