Formation Android

TP 3: L’Internet

Avant de commencer

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:

Accéder à l’internet

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

Ajout des dépendances

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

Retrofit

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

User

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
)

UserWebService

interface UserWebService {
  @GET("/sync/v9/user/")
  suspend fun fetchUser(): Response<User>
}
object Api {
  val userWebService : UserWebService by lazy {
    retrofit.create(UserWebService::class.java)
  }
}

Affichage

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

TaskListFragment

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

TasksListViewModel

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

“Collecter” le Flow

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

Compléter TasksWebService

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>

Suppression, Ajout, É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
  }
}