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:

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):

// 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"
}
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
)
interface UserWebService {
  @GET("/sync/v9/user/")
  suspend fun fetchUser(): Response<User>
}
object Api {
  val userWebService : UserWebService by lazy {
    retrofit.create(UserWebService::class.java)
  }
}
// Ici on ne va pas gérer les cas d'erreur donc on force le crash avec "!!"
Api.userWebService.fetchUser().body()!!

➡️ 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>>
}

Extrait d'un json renvoyé par la route /rest/v2/tasks/:

[
  {
    "content": "title",
    "description": "description",
    "id": "123456789"
  }
]

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) {}
}
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>
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
  }
}

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()
}
public val tasksStateFlow = MutableStateFlow<TaskListState>(TaskListState.Loading)
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"