Les APIs qui nous allons utiliser exigent 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 (celui du module):

// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11")

// KotlinX Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")

// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

// Lifecycle
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.0"
}

Après tout cela vous pouvez cliquer sur "Sync Now" pour que l'IDE télécharge les dépendances, etc.

En cas de soucis à ce moment là, vérifiez que Android Studio est à jour ("Check for updates")

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
    }

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

@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 "!!"
val user = Api.userWebService.fetchUser().body()!!
lifecycleScope.launch {
  mySuspendMethod()
}
userTextView.text = user.name

➡️ 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 TasksListViewModel, avec une liste de tâches Observable grâce au MutableStateFlow:

class TasksListViewModel : ViewModel() {
  private val webService = Api.tasksWebService

  public val tasksStateFlow = MutableStateFlow<List<Task>>(emptyList())

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

Dans TaskListFragment, à l'aide du squelette de code plus bas:

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

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