Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

8. Networking
Written by Kevin D Moore

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Loading data from the network to show it on the UI is a very common task for apps. In this chapter, you’ll learn how to make network calls, convert the data from those calls to model classes and handle asynchronous operations.

Your goal is to learn how to use networking libraries to save important information on your device.

Getting Started

Open the starter project for this chapter in Android Studio and then run the app.

Notice the two tabs at the bottom — each shows a different screen when you tap it. The “Recipes” tab looks like this:

The “Groceries” tab looks like this:

Once you finish, you can search for recipes, display them in a grid, bookmark the ones you want to keep and show the list of groceries needed for those meals.

Coroutines

Asynchronous programming requires tasks to be executed on different threads. Usually, there’s a “main” thread for UI drawing. To do other tasks without slowing the UI thread, you need a way to execute them without slowing what the user is working on. How can you do two or more things at once? When there was just one processor, the system would have to switch between tasks quickly, giving the appearance of multitasking. Different tasks can run on different cores now that multi-core processors exist. You can have as many threads as you want. But typically, you’ll have just a UI, IO and a default thread or “dispatcher”. For example, if you needed to retrieve some data from the internet, you wouldn’t want your UI to freeze while that data is downloaded. You would use a default dispatcher (which is different than the UI dispatcher) to download the data and then switch to the UI dispatcher to display that data.

public val ViewModel.viewModelScope: CoroutineScope
    // 1
    get() {
        // 2
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        // 3
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
  ...
}
viewModelScope.launch {
  ...
}

Adding the Coroutine Library

To add the coroutine library, open the libs.versions.toml file in the Gradle directory. Under the versions section, add:

kotlinx-coroutines = "1.7.2"
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
implementation(libs.coroutines.android)

Flows

Most Android developers have been using the LiveData library for quite some time. It provides a way of notifying the UI of events and handles the Android lifecycle. The new kid on the block is Flows. Like with LiveData, you’ll create a mutable flow in the ViewModel but only expose a non-mutable version. Here’s an example:

private val _queryState = MutableStateFlow(QueryState())
val queryState = _queryState.asStateFlow()
val uiState by viewModel.uiState.collectAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
    scope.launch {
        viewModel.recipeListState.collect { state ->
            recipeListState.value = state
        }
    }
}

Network Requests

To retrieve information from the internet, you need to make a network request. The easiest way to do that is with a library, and one of the best and most used libraries is Retrofit.

Alternatives

In this chapter, you’ll use Retrofit, but other libraries exist:

Retrofit

Square developed Retrofit. It’s a type-safe HTTP client for Android and Java/Kotlin. Although there are newer libraries, knowing Retrofit will let you work with almost any app.

Adding Retrofit

To add the Retrofit library, open the libs.versions.toml file in the Gradle directory. Under the versions section, add:

retrofit="2.9.0"
# Retrofit
retrofit = {module="com.squareup.retrofit2:retrofit", version.ref="retrofit" }
implementation(libs.retrofit)

Response Parsing

To convert network response data (usually returned as a JSON string), you need an easy way to convert the string into models and vice versa. You’ll use the Moshi library (also created by Square). Moshi allows you to parse JSON into Kotlin classes. There are other parsing libraries (a common one is Gson), but Moshi is newer and more modern.

Adding Moshi

To add the Moshi library, open the libs.versions.toml file in the Gradle directory.

moshi="1.15.0"
moshi-kotlin = {module="com.squareup.moshi:moshi-kotlin", version.ref="moshi" }
retrofit-moshi-converter = {module="com.squareup.retrofit2:converter-moshi", version.ref="retrofit" }
implementation(libs.moshi.kotlin)
implementation(libs.retrofit.moshi.converter)

Signing Up With the Recipe API

For your remote content, you’ll use the Spoonacular Recipe API. Open this link in your browser: https://spoonacular.com/food-api.

Using Your API Key

For your next step, you need to use your new API Key.

SpoonacularService

To fetch data from the recipe API, you’ll create a Kotlin interface and an object to manage the connection. This Kotlin class file contains your API Key, ID and URL. You’ll also need a model class for the response.

import com.squareup.moshi.Json
data class SearchRecipesResponse(
  val offset: Int,
  val number: Int,
  val totalResults: Int,
  @Json(name = "results")
  val recipes: List<Recipe>
)
import com.kodeco.recipefinder.data.models.RecipeInformationResponse
import com.kodeco.recipefinder.data.models.SearchRecipesResponse
import com.kodeco.recipefinder.viewmodels.PAGE_SIZE
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
const val apiKey = "<Replace with API Key>"
interface SpoonacularService {
   // 1
  @GET("recipes/complexSearch?&apiKey=$apiKey")
 	// 2
  suspend fun queryRecipes(
    @Query("query") query: String,
    @Query("offset") offset: Int,
    @Query("number") number: Int = PAGE_SIZE
  ): SearchRecipesResponse

  // 3
  @GET("recipes/{id}/information?includeNutrition=false&apiKey=$apiKey")
  suspend fun queryRecipe(@Path("id") id: Int): RecipeInformationResponse
}
object RetrofitInstance {
  // 1
  private const val BASE_URL = "https://api.spoonacular.com/"

  // 2
  private fun provideMoshi(): Moshi =
    Moshi
      .Builder()
      .addLast(KotlinJsonAdapterFactory())
      .build()

  // 3
  private val retrofit: Retrofit by lazy {
    Retrofit.Builder()
      .baseUrl(BASE_URL)
      .addConverterFactory(MoshiConverterFactory.create(provideMoshi()))
      .build()
  }

  // 4
  val spoonacularService: SpoonacularService by lazy {
      retrofit.create(SpoonacularService::class.java)
  }
}

Implement the ViewModel

Now that you’ve written the service, you must retrieve the list of recipes matching the query string. Open viewmodels/RecipeViewModel.kt. Find // TODO: Add Service and replace it with:

private val spoonacularService = RetrofitInstance.spoonacularService
import com.kodeco.recipefinder.network.RetrofitInstance
viewModelScope.launch {
  try {
    // 1
    val response = spoonacularService.queryRecipes(query, offset, number)
    // 2
    _recipeListState.value = response.recipes
    // 3
    _queryState.value =
        QueryState(query, offset, number, response.totalResults)
  } catch (e: Exception) {
    Timber.e(e, "Problems getting Recipes")
    // 4
    _recipeListState.value = listOf()
  }
}
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import timber.log.Timber
scope.launch {
  viewModel.recipeListState.collect { state ->
    recipeListState.value = state
  }
}

Run the App

Run the app on a device or emulator by pressing the Play button.

Use Moshi’s Codegen

Currently, Moshi uses reflection to convert JSON to Kotlin classes. But for better performance, you can use Moshi’s codegen support, which can generate adapters at compile-time.

moshi-kotlin = {module="com.squareup.moshi:moshi-kotlin", version.ref="moshi" }
moshi = {module="com.squareup.moshi:moshi", version.ref="moshi" }
moshiCodeGen = {module="com.squareup.moshi:moshi-kotlin-codegen", version.ref="moshi" }
implementation(libs.moshi.kotlin)
implementation(libs.moshi)
ksp (libs.moshiCodeGen)
private fun provideMoshi(): Moshi =
  Moshi
    .Builder()
    .build()
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
@JsonClass(generateAdapter = true)
data class SearchRecipesResponse(
  val offset: Int,
  val number: Int,
  val totalResults: Int,
  @Json(name = "results")
  val recipes: List<Recipe>
)
import com.squareup.moshi.JsonClass

Details Screen

If you were to click a recipe, you would see a blank screen. It’s time to fix that.

// 1
viewModelScope.launch(Dispatchers.Default) {
  try {
    // 2
    val spoonacularRecipe = spoonacularService.queryRecipe(id)
    // 3
    _recipeState.value = spoonacularRecipe
  } catch (e: Exception) {
    Timber.e(e, "Problems getting Recipe for id $id")
    _recipeState.value = null
  }
}
import kotlinx.coroutines.Dispatchers

Key Points

Where to Go From Here?

In this chapter, you learned how to retrieve network data easily.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now