Utilitzant ListView a Android

Aquest article segueix del principal Android en aquesta wiki.

  • ListView és un widget obsolet.
  • Es manté per backward compatibility.
  • Widget recomanat actual: Android RecyclerView (però més complicat d'utilitzar).

Referències:

Perquè és complicada una ListView?

Com veurem ara a l'hora d'implementar-ho, la ListView resulta una mica més complicada del què ens esperaríem. Això es deu a la particularitat dels dispositius mòbils de la seva escassa RAM. No podem, doncs, implementar una llista amb gran quantitat d'entrades que poden contenir pesants elements multimèdia com fotos d'alta resolució, i que es carreguin totes a la RAM directament. Ens cal uns tipus de Views que carreguin només les dades que s'estan visualitzant, i que generin nous ítems només quan l'usuari faci el scroll.

Reciclatge d'elements gràfics

Reciclatge d'elements gràfics (2)

recycling-items.jpg

Com podem veure a la imatge, el sistema farà un reciclat dels ítems que ha generat prèviament i que ja no estan visibles, estalviant RAM de forma global.


MVC ampliat i Adapter

listview-adapter.jpg

  • Tota View intenta seguir un paradigma Model - Vista - Controlador.
  • En altres entorns segurament trobaríem una connexió més simple:
    • Model de dades (Ex. ArrayList) connectat directament a la ListView.
    • El codi de Controlador podria estar en altres objectes de l'aplicació (com la Activity) o en una classe derivada de la ListView.
  • En Android, degut a que necessitem la gestió del reciclatge dels ítems gràfics, ens apareix un element extra anomenat Adapter. Aquest és l'element que coneix la View i el nostre Model i que ha de saber com reciclar els ítems per a l'estalvi de RAM.

Layouts

Per a cada element (item) de la ListView es necessita un layout personalitzat. El pots crear tu mateix, o simplement adoptar uns layouts prefabricats per als items. Al arxiu de recursos android.R.layout trobaràs alguns com per exemple android.R.layout.simple_list_item_1 que et pot estalviar feina per als casos més habituals.


Codi taula de rècords

Fixeu-vos en què:

  • Hi ha els 3 elements: records (model) ↔ adapter (ArrayAdapter) ↔ listView
  • L'adapter es pot crear com una classe derivada, o bé un objecte particularitzat. En aquest cas és la 2a opció (objecte particularitzat «inline»).
  • Dintre de adapter.getView es realitza el reciclatge: si ens ve un objecte null, l'inicialitzem amb el LayoutInflater. Si no és null, el reciclem sobreescrivint i modificant les dades.

Kotlin

MainActivity.kt
class MainActivity : AppCompatActivity() {
    // Model: ArrayList de Record (intents=puntuació, nom)
    class Record(var intents: Int, var nom: String)
    var records: ArrayList<Record> = ArrayList<Record>()

    // ArrayAdapter serà l'intermediari amb la ListView
    lateinit var adapter: ArrayAdapter<Record>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        // Afegim alguns exemples
        records.add(Record(33, "Manolo"))
        records.add(Record(12, "Pepe"))
        records.add(Record(42, "Laura"))

        // Inicialitzem l'ArrayAdapter amb el layout pertinent
        adapter = object : ArrayAdapter<Record>(this,R.layout.list_item,records)
        {
             override fun getView(pos: Int, convertView: View?, container: ViewGroup): View {
                // getView ens construeix el layout i hi "pinta" els valors de l'element en la posició pos
                var convertView = convertView
                if (convertView == null) {
                    // inicialitzem l'element la View amb el seu layout
                    convertView = getLayoutInflater().inflate(R.layout.list_item, container, false)
                }
                // pintem imatge
                val bitmap = BitmapFactory.decodeStream( assets.open("ieti_logo.png") )
                convertView.findViewById<ImageView>(R.id.imageView).setImageBitmap( bitmap )
                // "Pintem" valors (quan es refresca)
                convertView.findViewById<TextView>(R.id.nom).text = getItem(pos)?.nom
                convertView.findViewById<TextView>(R.id.intents).text = getItem(pos)?.intents.toString()
                return convertView
            }
        }

        // busquem la ListView i li endollem l'ArrayAdapter
        val lv = findViewById<ListView>(R.id.recordsView)
        lv.setAdapter(adapter)

    }
}

Java

MainActivity.java
public class MainActivity extends AppCompatActivity {

    // Model: Record (intents=puntuació, nom)
    class Record {
        public int intents;
        public String nom;

        public Record(int _intents, String _nom ) {
            intents = _intents;
            nom = _nom;
        }
    }
    // Model = Taula de records: utilitzem ArrayList
    ArrayList<Record> records;

    // ArrayAdapter serà l'intermediari amb la ListView
    ArrayAdapter<Record> adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Inicialitzem model
        records = new ArrayList<Record>();
        // Afegim alguns exemples
        records.add( new Record(33,"Manolo") );
        records.add( new Record(12,"Pepe") );
        records.add( new Record(42,"Laura") );

        // Inicialitzem l'ArrayAdapter amb el layout pertinent
        adapter = new ArrayAdapter<Record>( this, R.layout.list_item, records )
        {
            @Override
            public View getView(int pos, View convertView, ViewGroup container)
            {
                // getView ens construeix el layout i hi "pinta" els valors de l'element en la posició pos
                if( convertView==null ) {
                    // inicialitzem l'element la View amb el seu layout
                    convertView = getLayoutInflater().inflate(R.layout.list_item, container, false);
                }
                // "Pintem" valors (també quan es refresca)
                ((TextView) convertView.findViewById(R.id.nom)).setText(getItem(pos).nom);
                ((TextView) convertView.findViewById(R.id.intents)).setText(Integer.toString(getItem(pos).intents));
                return convertView;
            }

        };

        // busquem la ListView i li endollem el ArrayAdapter
        ListView lv = (ListView) findViewById(R.id.recordsView);
        lv.setAdapter(adapter);

        // botó per afegir entrades a la ListView
        Button b = (Button) findViewById(R.id.button);
        b.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i=0;i<3;i++) {
                    records.add(new Record(100, "Anonymous"));
                }
                // notificar l'adapter dels canvis al model
                adapter.notifyDataSetChanged();
            }
        });
    }
}

Exercicis

Exercici 1

Implementa el codi d'exemple en un nou projecte anomenat «Listilla».

  • Crea nou projecte amb una empty activity.
  • Substitueix el codi a ActivityMain per l'exemple.
  • Arregla el package perquè concordi amb el teu projecte.
  • Afegeix al activity_main.xml una ListView anomenada recordsView.
  • Crea un nou layout amb el nom list_item.xml que serà el placeholder per cada element de la llista. Pots crear-ho amb
    File -> New -> Layout Resource File
    • Transforma el seu layout per defecte a LinearLayout.
    • Afegiex al layout 2 TextView amb IDs nom i intents
  • Afegeix un botó al layout activity_main.xml amb ID = button. Servirà per afegir ítems al ListView i comprovar el scroll del ListView.

Exercici 2

Afegeix un botó Afegir rècord que ens ofereixi un Dialog per entrar nom i rècord.

Exercici 3

Afegeix una imatge als elements de la llista (imatge fixa):

Solució 1:

  • Afegeix una ImageView amnb ID «imageView» al list_item.xml.
  • Arranja el layout perquè quedi com a la imatge anterior aproximadament.
  • Afegeix la carpeta app/src/main/assets al projecte.
  • Afegeix una foto arrossegant-la sobre la vista de projecte d'Android Studio.
  • La podràs fer servir amb el codi de l'exemple:
    val bitmap = BitmapFactory.decodeStream( assets.open("ieti_logo.png") )
    convertView.findViewById<ImageView>(R.id.imageView).setImageBitmap( bitmap )

Solució 2:

  • Ves a la view de projecte de l'Android Studio. Visualitza la carepta
    res -> drawable
  • Importar una imatge arrossegant-la dins de Drawable.
  • Modificar el list_item.xml i afegir-hi una ImageView amb la imatge anterior.
  • Modifica el layout del list_item perquè et quedi com la imatge suggerida adjunta.
    • Pista: pots combinar diversos LinearLayout horitzontals i verticals per aconseguir el resultat desitjat.
  • Afegeix diverses imatges als resources i aleatoritza l'assignació d'imatges a cada element Record.

Exercici 4

Afegeix un botó que ordeni la llista del model, i que refresqui la ListView.