L'API de Todoist nécessite qu'une personne soit connectée, pour commencer nous allons simuler cela en passant directement un token dans les headers de nos requêtes HTTP:
Paramètres > Intégrations > Clé API et copiez la quelque partcurl (ou Postman, httpie, ...)Afin de communiquer avec le réseau internet (wifi, ethernet ou mobile), il faut ajouter la permission dans le fichier AndroidManifest, juste au dessus de la balise application
<uses-permission android:name="android.permission.INTERNET" />
Dans le fichier app/build.gradle.kts (celui du module):
dependencies {}, ajouter les dépendances qui vous manquent// Retrofit
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.3.2")
// KotlinX Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0"
}
version.ref = "kotlin"dataobject Api (ses membres et méthodes seront donc static):object Api {
private const val TOKEN = "COPIEZ_VOTRE_CLE_API_ICI"
private val retrofit by lazy {
// client HTTP
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor { chain ->
// intercepteur qui ajoute le `header` d'authentification avec votre token:
val newRequest = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $TOKEN")
.build()
chain.proceed(newRequest)
}
.build()
// transforme le JSON en objets kotlin et inversement
val jsonSerializer = Json {
ignoreUnknownKeys = true
coerceInputValues = true
encodeDefaults = true
}
// instance retrofit pour implémenter les webServices:
Retrofit.Builder()
.baseUrl("https://api.todoist.com/")
.client(okHttpClient)
.addConverterFactory(jsonSerializer.asConverterFactory("application/json".toMediaType()))
.build()
}
}
Extrait d'un json renvoyé par la route /sync/v9/user/:
{
"email": "example@domain.com",
"full_name": "john doe",
"avatar_medium": "https://blablabla/image.jpg"
}
Créer la data class User correspondante:
@Serializable
data class User(
@SerialName("email")
val email: String,
@SerialName("full_name")
val name: String,
@SerialName("avatar_medium")
val avatar: String? = null
)
UserWebService pour requêter les infos de l'utilisateur (importez Response avec alt + enter et choisissez la version retrofit):interface UserWebService {
@GET("/sync/v9/user/")
suspend fun fetchUser(): Response<User>
}
object Api {
val userWebService : UserWebService by lazy {
retrofit.create(UserWebService::class.java)
}
}
Text pour voir les résultats de l'API:// Ici on ne va pas gérer les cas d'erreur donc on force le crash avec "!!"
Api.userWebService.fetchUser().body()!!
Text➡️ Lancez l'app et vérifiez que vos infos s'affichent !
Il est temps de récupérer les tâches depuis le serveur !
Créer un nouveau service TaskWebService:
interface TasksWebService {
@GET("/rest/v2/tasks/")
suspend fun fetchTasks(): Response<List<Task>>
}
TasksWebService dans l'objet ApiExtrait d'un json renvoyé par la route /rest/v2/tasks/:
[
{
"content": "title",
"description": "description",
"id": "123456789"
}
]
Task pour la rendre "serializable" par KotlinX Serialization (inspirez vous de User)Créer la classe TaskListViewModel, avec une liste de tâches Observable grâce au MutableStateFlow:
class TaskListViewModel : ViewModel() {
private val webService = Api.tasksWebService
public val tasksStateFlow = MutableStateFlow<List<Task>>(emptyList())
init {
refresh()
}
fun refresh() {
viewModelScope.launch {
val response = webService.fetchTasks() // Call HTTP (opération longue)
if (!response.isSuccessful) { // à cette ligne, on a reçu la réponse de l'API
Log.e("Network", "Error: ${response.message()}")
return@launch
}
val fetchedTasks = response.body()!!
tasksStateFlow.value = fetchedTasks // on modifie le flow, ce qui déclenche ses observers
}
}
// à compléter plus tard:
fun add(task: Task) {}
fun edit(task: Task) {}
fun remove(task: Task) {}
}
ListScreen, ajouter en a une argument une instance de TaskListViewModelNavDisplay, ajoutez des entryDecorators qui vont permettre aux ViewModels de correspondre aux NavEntry et créez un ViewModel à passer à ListScreen avec viewModel { }NavDisplay(
//...
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
// ...
entryProvider = entryProvider {
entry<ListNavScreen> {
ListScreen(
viewModel = viewModel { TaskListViewModel(it.task) }
// ...
)
}
// ...
}
)
fun ListScreen(
modifier: Modifier = Modifier,
viewModel: TaskListViewModel,
) {
val state by viewModel.tasksStateFlow.collectAsStateWithLifecycle()
}
Modifier TasksWebService et ajoutez y les routes manquantes:
@POST("/rest/v2/tasks/")
suspend fun create(@Body task: Task): Response<Task>
@POST("/rest/v2/tasks/{id}")
suspend fun update(@Body task: Task, @Path("id") id: String = task.id): Response<Task>
// Complétez avec les méthodes précédentes, la doc de l'API, et celle de Retrofit:
@...
suspend fun delete(@... id: String): Response<Unit>
refresh() pour ajouter toutes les autres actions avec le serveur dans le VM, par ex pour l'édition:fun update(task: Task) {
viewModelScope.launch {
val response = ... // TODO: appel réseau
if (!response.isSuccessful) {
Log.e("Network", "Error: ${response.raw()}")
return@launch
}
val updatedTask = response.body()!!
val updatedList = tasksStateFlow.value.map {
if (it.id == updatedTask.id) updatedTask else it
}
tasksStateFlow.value = updatedList
}
}
taskList locale dans l'écran de la Liste et vérifier que vous avez bien tout remplacé par des appels au VM (et donc au serveur), il ne doit rester plus qu'un seul endroit où vous mettez à jour l'adapter: dans le .collect { }On va ajouter un état d'erreur avec une sealed class Dans TaskListViewModel, ajoutez la classe suivante:
sealed class TaskListState {
data object Loading : TaskListState()
data class Success(val list: List<Task>) : TaskListState()
data class Error(val message: String) : TaskListState()
}
tasksStateFlow pour qu'il contienne un TaskListState et pas une liste directement, avec Loading comme valeur initialepublic val tasksStateFlow = MutableStateFlow<TaskListState>(TaskListState.Loading)
ListScreen, utilisez un when pour afficher les différents états:when (val currentState = state.value) {
is TaskListState.Loading -> CircularProgressIndicator()
is TaskListState.Error -> Text(text = currentState.message)
is TaskListState.Success -> { /* votre liste précédente */ }
Ajoutez une "checkbox" qui marque votre tâche comme terminée avec toute la logique, le call API, etc
Toute la logique étant extraite dans le ViewModel, elle peut être partagée avec votre ancien écran listant les tâches puisque qu'il fait la même chose.
Adaptez donc tout ça dans TaskListFragment en utilisant le même ViewModel, indices pour commencer:
private val viewModel: TaskListViewModel by viewModels()
// Dans onResume()
viewModel.refresh() // on demande de rafraîchir les données sans attendre le retour directement
// Dans onViewCreated()
lifecycleScope.launch { // on lance une coroutine car `collect` est `suspend`
viewModel.tasksStateFlow.collect { newList ->
// cette lambda est exécutée à chaque fois que la liste est mise à jour dans le VM
// -> ici, on met à jour la liste dans l'adapter
}
}
Vous pouvez également ajouter la fonctionnalité "checkbox"