Das wohl wichtigste und zentralste Element einer App ist in den meisten Fällen die Anzeige einer Liste.
Sei es ein News-Feed, eine Social Media App oder der Highscore eines Spiels - irgendwann stößt man unweigerlich darauf.

In Android gibt es daher hierfür eine besonders performante View zur Anzeige von Listen: die RecyclerView.
Wie der Name schon vermuten lässt, kann sie im Kern Ansichten wiederverwenden. Das ist von elementarer Bedeutung bei größer werdenden Datenmengen.

Mit einem Adapter setzt man ein beliebiges Datenmodell in einzelne Listeneinträge um. Ein eingebauter Listeneinträge-Observer kann dann mit verschiedenen Methoden (notifyItemRangeChanged(), notifyItemAdded(), ...) darauf aufmerksam gemacht werden, dass der Datenbestand im Adapter sich geändert hat und die Listendarstellung verändert sich daraufhin automatisch.

Besonders schön lassen sich Listenveränderungen animieren, wenn man mit einem Diffing Tool, wie einem AsyncListDiffer, Veränderungen im Datenbestand automatisiert erkennen lässt und so z.B. Positionsänderungen nachverfolgen kann.

Einen Anfang hast du schon gemacht, indem du in dem XML-Layout-File der für die Liste angedacht ist mit <RecyclerView> eine vollflächige Liste angelegt und dieser eine Id vergeben hast.

Im nächsten Schritt ist das Layout des Listeneintrags dran. Erstelle ein neues Layout File innerhalb des Ordners, indem auch alle anderen Layout-Files liegen, in denen du gerade gearbeitet hast. Nenne es "item_user.xml"

Befülle die Datei mit folgendem Content:

<?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="wrap_content"
    android:background="?selectableItemBackground"
    android:orientation="vertical">

    <!-- export dimensions like 12 dp to dimens.xml in production app -->
    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:textStyle="bold"
        android:layout_toEndOf="@+id/avatar_image"
        tools:text="A title" />

    <TextView
        android:id="@+id/body"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/title"
        android:layout_toEndOf="@+id/avatar_image"
        android:layout_marginBottom="12dp"
        tools:text="A body" />

    <ImageView
        android:layout_margin="8dp"
        android:id="@+id/avatar_image"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        tools:src="@drawable/ic_launcher_foreground" />

</RelativeLayout>

Wie in der Einleitung erwähnt, passiert die ganze Magie im Adapter. Daher kümmern wir uns erstmal darum, welche Daten wir in einen Listeneintrag verwandeln wollen. Zum Glück müssen wir nicht lange überlegen, da wir ja schon eine User-Klasse haben, die sich für sowas perfekt anbietet.

Wir erstellen ein neues Kotlin File "ListAdapter" im Ordner, indem sich auch die Actitivity und die Fragment Klassen befinden (com.example.helloworld).

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import kotlinx.android.synthetic.main.item_user.view.*

class ListAdapter(
    diffCallback: DiffUtil.ItemCallback<User> = UserDiffCallback()
) : RecyclerView.Adapter<ListAdapter.ListItemViewHolder>() {

    class ListItemViewHolder(root: View) : RecyclerView.ViewHolder(root)

    private var differ: AsyncListDiffer<User>

    init {
        @Suppress("LeakingThis")
        differ = AsyncListDiffer(this, diffCallback)
    }

    fun setItems(newItems: List<User>) {
        differ.submitList(newItems)

    }

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

    override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) {
        val item: User = differ.currentList[holder.adapterPosition]

        holder.itemView.apply {
            title.text = item.loginName
            body.text = item.identifier.toString()

            item.avatarUrl?.let { url ->
                Glide.with(context)
                    .load(url)
                    .apply(RequestOptions.bitmapTransform(RoundedCorners(30)))
                    .transition(DrawableTransitionOptions.withCrossFade())
                    .into(avatar_image)
            }

            setOnClickListener {
                Toast.makeText(context, "Click on: $position", Toast.LENGTH_SHORT).show()
            }
        }
    }

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

    class UserDiffCallback : DiffUtil.ItemCallback<User>() {

        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.identifier == newItem.identifier
        }

        override fun areContentsTheSame(
            oldItem: User,
            newItem: User
        ): Boolean {
            return oldItem.loginName == newItem.loginName
        }
    }
}


Die wichtigste Methode des Adapter ist onBindView(): Hier passiert das eigentliche Umwandeln eines Datenbestands in einen Listeneintrag. Da die Ansicht von der Recyclerview aber nicht mehr gebraucht werden könnte, wenn der User von einem Eintrag weit weg scrollt, kann diese Bindung auch wieder verloren gehen oder irgendwann erneut ausgeführt werden.

Jetzt muss nur noch der Adapter mit der Liste verknüpft werden und ein Befehl sollte die Liste füllen.

Dazu in dem ListFragment hinzufügen:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
     super.onViewCreated(view, savedInstanceState)

     val listAdapter = ListAdapter()

     recycler.apply {
         setHasFixedSize(true)

         layoutManager = LinearLayoutManager(context)

         adapter = listAdapter
     }


     listAdapter.setItems() // set the items here
 }

Das war es fast: Der Compiler sollte jetzt nur noch wegen "no value passed for parameter newItems" meckern.

In dieser Übung befüllen wir den Adapter mit der Methode setItems() nach unseren Wünschen.

Starte die Anwendung im Anschluss und gucke was mit wenigen und vielen Datenpunkten passiert.

Jetzt fehlt nur noch eine echte Datenquelle.

Als nächstes kümmern wir uns um networking und laden Daten aus dem Internet in die Liste.

Mache nun weiter mit den Networking.