implémenter un écran affichant une liste de tâches et permettre de créer des nouvelles tâches.
Vous allez créer un unique projet que vous mettrez à jour au fur à mesure des TPs:
Empty VIEWS Activity
(⚠️ pas Empty Activity
SVP ⚠️)TodoNicolasAlexandre
(⚠️ pas TP1
SVP ⚠️)com.nicoalex.todo
(ce sera la racine de tous vos packages et sert d'identifiant unique d'application)📁 Les fichiers source Java ou Kotlin sont rangés en "packages":
package com.nicoalex.todo.nomdupackage
com/nicoalex/todo/nomdupackage
Parcourez les différents fichiers de config, notamment les plus importants:
app/build.gradle.kts
: contient la configuration de module principal (app
), notamment les versions compatibles, son propre numéro de version, etc et surtout les différentes dépendances../build.gradle.kts
: contient moins de choses, en général des plugins, mais concerne tout le projetlibs.versions.toml
: un catalogue de dépendances, de plugins et de versions, qui est utilisé par les fichiers précédents. Vérifiez que vous utilisez les dernières versions disponible, surtout pour kotlin
.app/src/main/AndroidManifest.xml
: contient les info de packaging de l'app comme les activités existantes, le nom de l'app, l'icône, etc.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
TaskListFragment.kt
qui contiendra la classe TaskListFragment
:class TaskListFragment : Fragment() {
//...
}
fragment_task_list.xml
dans res/layout
TaskListFragment
, overrider la méthode onCreateView(...)
: commencez à taper onCrea...
et utilisez l'auto-completion de l'IDE pour vous aider (vous pouvez supprimer la ligne super.onCreateView(...)
)rootView
à afficher: créez la à l'aide de votre nouveau layout comme ceci:val rootView = inflater.inflate(R.layout.fragment_task_list, container, false)
String
locale, ajoutez la en tant que propriété de votre classe TaskListFragment
:private var taskList = listOf("Task 1", "Task 2", "Task 3")
<resources>
<string name="app_name">Affirmations</string>
<string name="task1">#1 Faire les courses</string>
<string name="task2">#2 Faire la vaisselle</string>
<string name="task3">#3 Faire le ménage</string>
</resources>
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_tasklist"
android:layout_width="match_parent"
android:layout_height="match_parent" />
TaskListAdapter.kt
, créez 2 nouvelles classes: TaskListAdapter
et TaskViewHolder
:// 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
}
}
}
TaskListFragment
, créez une instance de votre nouvelle classe TaskListAdapter
en propriété de votre fragment (comme taskList
):private val adapter = TaskListAdapter()
onCreateView
:adapter.currentList = taskList
TaskListFragment
, placez une balise RecyclerView
(vous pouvez taper < Recyc...
et vous aider de l'auto-complétion ou bien utilisez le mode visuel)layoutManager
qui lui dit de s'afficher comme une liste (verticale par défaut):app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
id
: soit en mode visuel soit en mode code, en vous aidant de l'auto-complétion android:id="@+id/....
TaskListFragment
, overridez onViewCreated
pour y récupérez une référence à la RecyclerView
du layout en utilisant findViewById
:val recyclerView = view.findViewById<RecyclerView>(R.id.id_de_votre_recycler_view)
recyclerView
a une propriété adapter
qui doit être connectée à l'adapter que vous avez créé (elle est nulle par défaut)item_task.xml
qui servira à afficher chaque cellule de la liste avec comme racine un LinearLayout
contenant pour l'instant une seule TextView
en enfant:<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)
getItemCount
doit renvoyer la taille de la liste de tâche à afficheronCreateViewHolder
doit retourner un nouveau TaskViewHolder
en générant un itemView
, à partir du layout item_task.xml
:val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_task, parent, false)
onBindViewHolder
doit insérer la donnée dans la cellule (TaskViewHolder
) en fonction de sa position
dans la liste en utilisant la méthode bind()
que vous avez créée dans TaskViewHolder
(elle ne fait rien pour l'instant)bind()
qui doit récupérer une référence à la TextView
dans item_task.xml
et y insérer le texte récupéré en argument (pour être plus propre, déplacez cette référence en tant que propriété de votre TaskViewHolder
)data class Task
avec 3 attributs: un id, un titre et une descriptionTaskListFragment
, remplacer la liste taskList
parprivate var 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")
)
data class
à la place de simples String
TextView
)fragment_task_list.xml
en ConstraintLayout
(si ce n'est pas déjà fait) en faisant un clic droit dessus en mode designResource Manager
à gauche, cliquez sur le +
en haut à gauche puis Vector Asset
puis double cliquez sur l'image du logo android et trouvez une icône +
(en tapant add
) puis finish
pour ajouter une icône à vos resourceFloating Action Button
(FAB) en bas à droite de ce layout et utilisez l'icône crééeUtilisez .setOnClickListener {}
sur le bouton d'ajout 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"))
Utiliser le ViewBinding
([documentation](https://developer.android.com/topic/li braries/view-binding) / slides) dans TaskListFragment
:
inflate
pour récupérer une instance de type XxxBinding
findViewByIds
par des calls direct du genre binding.myViewId
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.