Ici on va utiliser le système de vue "classique" et afficher une liste d'éléments modifiavle

Vous allez créer un unique projet que vous mettrez à jour au fur à mesure des TPs:

Créez une icône d'application personnalisée avec l'outil intégré Image Asset Studio: ouvrez le Resource Manager à gauche, près du volet projet puis cliquez sur le + en haut à gauche et choisissez Image Asset: ici vous pouvez choisie une couleur de fond, une image: icône système ou personnalisée avec un SVG ou un "clipart" (bibliotheque d'icones en cliquant sur la petite icon android) et générer automatiquement les différentes tailles nécessaires pour les différentes version d'Android.

Vérifiez que l'icône est bien prise en compte dans le AndroidManifest.xml (attribut android:icon de la balise application) et en lançant l'app.

📁 Les fichiers source Java ou Kotlin sont rangés en "packages":

Parcourez les différents fichiers de config, notamment les plus importants:

Créez un nouveau package list à l'intérieur votre package source de base (pas à côté !) :

clic droit sur 'com.nicoalex.todo' > new > package > tapez 'list'

Vous y mettrez tous les fichiers source (Kotlin) concernant la liste de tâches

class TaskListFragment : Fragment() {
    override fun onCreateView(...): View {
       // ici on crée la vue et on la retourne (regardez le type de retour: `View`), on ne fait rien d'autre.
    }

    override fun onViewCreated(...) {
       // ici la vue est créée, on peut récupérer des références aux vues et les manipuler
    }
}
val rootView = inflater.inflate(R.layout.fragment_task_list, container, false)
private val taskList = listOf("Task 1", "Task 2", "Task 3")

Cette activity va servir de conteneur de fragments:

Dans activity_main.xml, remplacez la balise TextView par celle ci et adaptez:

 <androidx.fragment.app.FragmentContainerView
    android:name="com.nicoalex.todo.list.TaskListFragment"
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
// l'IDE va râler ici car on a pas encore implémenté les méthodes nécessaires
class TaskListAdapter : RecyclerView.Adapter<TaskListAdapter.TaskViewHolder>() {

  var currentList: List<String> = emptyList()

  // on utilise `inner` ici afin d'avoir accès aux propriétés de l'adapter directement
  inner class TaskViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    fun bind(taskTitle: String) {
      // on affichera les données ici
    }
  }
}
private val adapter = TaskListAdapter()
adapter.currentList = taskList
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
val recyclerView = view.findViewById<RecyclerView>(R.id.id_de_votre_recycler_view)
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <TextView
      android:id="@+id/task_title"
      android:background="@android:color/holo_blue_bright"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
</LinearLayout>

Dans TaskListAdapter, implémenter toutes les méthodes requises:

Astuce: Pré-remplissez votre adapter en cliquant sur le nom de votre classe (qui doit être pour l'instant soulignée en rouge) et cliquez sur l'ampoule jaune ou tapez Alt + ENTER (sinon, CTRL/CMD + o n'importe où dans la classe)

val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_task, parent, false)
private val taskList = listOf(
   Task(id = "id_1", title = "Task 1", description = "description 1"),
   Task(id = "id_2", title = "Task 2"),
   Task(id = "id_3", title = "Task 3")
)

Retournez dans le code, récupérez une référence à votre nouveau bouton et utilisez .setOnClickListener {} pour ajouter une tâche à votre liste à chaque fois qu'on clique dessus:

// Instanciation d'un objet task avec des données préremplies:
val newTask = Task(id = UUID.randomUUID().toString(), title = "Task ${taskList.size + 1}")
taskList = taskList + newTask

Cette façon de "notifier" manuellement n'est pas idéale, il existe en fait une sous-classe de RecyclerView.Adapter qui permet de gérer cela automatiquement: ListAdapter

Améliorer l'implémentation de TasksListAdapter en héritant de ListAdapter au lieu de RecyclerView.Adapter

Il faudra notamment: créer un DiffUtil.ItemCallback et le passer au constructeur parent, supprimer getItemCount et la propriété currentList car ils sont déjà définis dans ListAdapter

Exemple:

object MyItemsDiffCallback : DiffUtil.ItemCallback<MyItem>() {
   override fun areItemsTheSame(oldItem: MyItem, newItem: MyItem) : Boolean {
      return // comparaison: est-ce la même "entité" ? => même id?
   }

   override fun areContentsTheSame(oldItem: MyItem, newItem: MyItem) : Boolean {
      return // comparaison: est-ce le même "contenu" ? => mêmes valeurs? (avec data class: simple égalité)
   }
}

class ItemListAdapter : ListAdapter<Item, ItemListAdapter.ItemViewHolder>(ItemsDiffCallback) {
   override fun onCreateViewHolder(...)
   override fun onBindViewHolder(...)
}

// Usage is simpler:
val adapter = ItemListAdapter()
recyclerView.adapter = adapter
adapter.submitList(listOf("Item#1", "Item #2"))
private val taskList = List(100) { index ->
    Task(id = "id_$index", title = "Task $index")
}

Utiliser le ViewBinding (documentation / slides) dans TaskListFragment:

Puis faites pareil pour les ViewHolder: c'est un peu plus complexe, il faudra changer le constructeur pour qu'il prenne un val binding: ItemTaskBinding afin d'y avoir accès dans le corps de la classe et passer binding.root au constructeur parent.

Dans le layout de vos item, ajouter un ImageButton qui servira à supprimer la tâche associée. Vous pouvez utiliser par exemple l'icône @android:drawable/ic_menu_delete

Aidez vous des lignes de code plus bas pour réaliser un "Click Listener" à l'aide d'une lambda en suivant ces étapes:

var onClickDelete: (Task) -> Unit = {}
onClickDelete(task)
myAdapter.onClickDelete = { task ->
    // Supprimer la tâche
}
implementation("androidx.fragment:fragment:1.8.9")
implementation("androidx.fragment:fragment-ktx:1.8.9")
parentFragmentManager.commit {
    replace<DetailFragment>(R.id.fragment_container)
    addToBackStack(null)
}
class DetailFragment : Fragment() {
    override fun onViewCreated(...) {
        // ...
        parentFragmentManager.setFragmentResultListener(REQUEST_KEY) { _, bundle ->
            val result = bundle.getString(RESULT_KEY)
            // Utilisez le résultat ici
        }
    }

    companion object { // pour définir des membres "statiques", ici des constantes:
        const val REQUEST_KEY = "request_key"
        const val RESULT_KEY = "result_key"
    }
val newTask = Task(id = UUID.randomUUID().toString(), title = "New Task !")
parentFragmentManager.setFragmentResult(BlankFragment.REQUEST_KEY, Bundle().apply {
  putString(BlankFragment.RESULT_KEY, newTask)
})
parentFragmentManager.popBackStack() // retour au fragment précédent
val task = result.data?.getSerializableExtra("task") as Task?

Ajoutez un argument taskId de type String?, null par défaut (pour garder le cas d'ajout de nouvelle tâche) à DetailFragment pour identifier la tâche à éditer, vous pourrez ensuite faire:

parentFragmentManager.commit {
    replace(R.id.fragmentContainerView, DetailFragment(taskId))
    addToBackStack(null)
}

Inspirez vous de ce que vous avez fait pour le bouton "supprimer" et le bouton "ajouter" pour créer un bouton "éditer" permettant de modifier chaque tâche en ouvrant l'activité DetailFragment pré-remplie avec les informations de la tâche en question.

Pour l'instant on a utilisé des lambda mais une façon plus classique de gérer les clicks d'un item est de définir une interface que l'on implémentera dans le 1er Fragment, mettez à jour votre code pour utiliser cette méthode:

interface TaskListListener {
  fun onClickDelete(task: Task)
  fun onClickEdit(task: Task)
}

class TaskListAdapter(val listener: TaskListListener) : ... {
  // use: listener.onClickDelete(task)
}

class TaskListFragment : Fragment {
  val adapterListener : TaskListListener = object : TaskListListener {
    override fun onClickDelete(task: Task) {...}
    override fun onClickEdit(task: Task) {...}
  }
  val adapter = TaskListAdapter(adapterListener)
}

Que se passe-t-il pour votre liste si vous tournez votre téléphone pour passer en mode paysage ? 🤔