Skip to content

REST services

Changes for REST

Be sure to watch the videos on the previous page before looking at these code changes.

Here you can see the changes made from our movie example so far to include the REST support. I've added comments to some sections to explain things that might not be obvious.

A few notes:

RestServer module

I added a restserver module. Normally this wouldn't be included with your application code, but it's here to keep things together and simple. This module uses Jersey to create a simple REST server.

MovieRestRepository

I added a MovieRestRepository that uses MovieApiService (using Retrofit as a REST client). Note that we need a coroutine scope here. We want to use the viewModelScope so the coroutines stay alive as long as the view model, but we end up with a chicken-and-egg problem:

  • We can't create the view model without a repository
  • We can't create the repository without the viewModelScope

There are a few ways to handle this. One approach would be to allow the view model to set the coroutine scope in the passed-in repository. That would keep things pretty much the same as they are with delegation and passing-in of the repository.

An alternate approach is to pass in a factory to create the view model instance. I chose to demonstrate this approach as it ensures the repository must have a scope, and has the side effect that you must delegate from the view model to the repository explicitly (rather than using MovieRepository by repository) as we did before.

See the comments in the MovieViewModel for more details.

Code Changes

CHANGED: /app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
//<!--             android:launchMode="singleInstance" -->
    <!-- requires internet permission to talk with server -->    <uses-permission android:name="android.permission.INTERNET" />    <!-- we're cheating here and using "usesClearTextTraffic"         instead of setting up SSL. I didn't want to get you bogged         down in SSL details here -->    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"        android:theme="@style/Theme.MovieUi1"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleInstance"
            android:theme="@style/Theme.MovieUi1">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <data android:scheme="https" />
                <data android:scheme="http" />
                <category android:name="android.intent.category.BROWSABLE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <receiver android:name=".glance.MovieAppWidgetReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/movie_app_widget_info" />
        </receiver>
    </application>

</manifest>
CHANGED: /app/src/main/java/com/androidbyexample/movie/MovieViewModel.kt
package com.androidbyexample.movie

import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.glance.appwidget.updateAll
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.androidbyexample.movie.glance.MovieAppWidget
import com.androidbyexample.movie.repository.ActorDto
//import com.androidbyexample.movie.repository.MovieDatabaseRepositoryimport com.androidbyexample.movie.repository.MovieDto
import com.androidbyexample.movie.repository.MovieRepository
import com.androidbyexample.movie.repository.MovieRestRepositoryimport com.androidbyexample.movie.repository.RatingDto
import com.androidbyexample.movie.screens.MovieList
import com.androidbyexample.movie.screens.Screen
import kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch

@OptIn(FlowPreview::class)
class MovieViewModel(
    application: Application,
//  private val repository: MovieRepository,//): AndroidViewModel(application), MovieRepository by repository {    // The rest repository needs a coroutine scope    // We want to use the viewModelScope, but that's not    //   available until the view model has been created    // The view model couldn't be created without a repository    // To fix this, instead of passing a repository instance,    //   we pass a factory function to create one, so we can    //   pass our viewModelScope into it    repositoryFactory: (CoroutineScope) -> MovieRepository): AndroidViewModel(application) {    // Here we create the repository instance by passing the viewModelScope    // This creates a problem - because we cannot pass the repository    //   as a parameter to the view model, we cannot implement the    //   MovieRepository interface and delegate to it using "by"    // This means we need to explicitly define any functions we    //   need to delegate to the repository.    private val repository = repositoryFactory(viewModelScope)    private val _selectedIdsFlow = MutableStateFlow<Set<String>>(emptySet())
    val selectedIdsFlow: Flow<Set<String>> = _selectedIdsFlow.asStateFlow()

    fun clearSelectedIds() {
        _selectedIdsFlow.value = emptySet()
    }
    fun toggleSelection(id: String) {
        if (id in _selectedIdsFlow.value) {
            _selectedIdsFlow.value -= id
        } else {
            _selectedIdsFlow.value += id
        }
    }

    private var screenStack = listOf<Screen>(MovieList)
        set(value) {
            field = value
            clearSelectedIds()
            currentScreen = value.lastOrNull()
        }

    // NOTE: We're keep this as a Compose State for comparison.
    //       You can use Compose state to expose anything from the view model,
    //       but our example will be using Flow from now on to demonstrate how
    //       the view model can be used without Compose, perhaps for other
    //       platforms such as iOS, desktop, web or command line
    var currentScreen by mutableStateOf<Screen?>(MovieList)
        private set

    fun pushScreen(screen: Screen) {
        screenStack = screenStack + screen
    }
    fun popScreen() {
        screenStack = screenStack.dropLast(1)
    }

    fun setScreen(screen: Screen) {
        screenStack = listOf(screen)
    }

    fun setScreens(vararg screens: Screen) {
        screenStack = screens.toList()
    }

    // explicitly delegate the flows to the repository    val moviesFlow = repository.moviesFlow    val actorsFlow = repository.actorsFlow    val ratingsFlow = repository.ratingsFlow    fun update(movie: MovieDto) {
        movieUpdateFlow.value = movie
    }
    fun update(actor: ActorDto) {
        actorUpdateFlow.value = actor
    }
    fun update(rating: RatingDto) {
        ratingUpdateFlow.value = rating
    }

    // using a debounced flow as a person-update queue
    private val movieUpdateFlow = MutableStateFlow<MovieDto?>(null)
    private val actorUpdateFlow = MutableStateFlow<ActorDto?>(null)
    private val ratingUpdateFlow = MutableStateFlow<RatingDto?>(null)
    init {
        viewModelScope.launch {
            repository.moviesFlow.collectLatest {
                MovieAppWidget().updateAll(getApplication())
            }
        }
        viewModelScope.launch {
            movieUpdateFlow.debounce(500).collect { movie ->
                movie?.let { repository.upsert(it) }
            }
        }
        viewModelScope.launch {
            actorUpdateFlow.debounce(500).collect { actor ->
                actor?.let { repository.upsert(it) }
            }
        }
        viewModelScope.launch {
            ratingUpdateFlow.debounce(500).collect { rating ->
                rating?.let { repository.upsert(it) }
            }
        }
    }

    // explicitly delegate these functions to the repository    suspend fun getMovieWithCast(id: String) = repository.getMovieWithCast(id)    suspend fun getMovie(id: String) = repository.getMovie(id)    suspend fun getActorWithFilmography(id: String) = repository.getActorWithFilmography(id)    fun getRatingWithMoviesFlow(id: String) = repository.getRatingWithMoviesFlow(id)    suspend fun deleteActorsById(ids: Set<String>) {        repository.deleteActorsById(ids)    }    suspend fun deleteMoviesById(ids: Set<String>) {        repository.deleteMoviesById(ids)    }    suspend fun deleteRatingsById(ids: Set<String>) {        repository.deleteRatingsById(ids)    }    suspend fun resetDatabase() {        repository.resetDatabase()    }    fun deleteSelectedMovies() {
        viewModelScope.launch {
            deleteMoviesById(_selectedIdsFlow.value)
            _selectedIdsFlow.value = emptySet()
        }
    }
    fun deleteSelectedActors() {
        viewModelScope.launch {
            deleteActorsById(_selectedIdsFlow.value)
            _selectedIdsFlow.value = emptySet()
        }
    }
    fun deleteSelectedRatings() {
        viewModelScope.launch {
            deleteRatingsById(_selectedIdsFlow.value)
            _selectedIdsFlow.value = emptySet()
        }
    }

    companion object {
        val Factory: ViewModelProvider.Factory = object: ViewModelProvider.Factory {
            @Suppress("UNCHECKED_CAST")
            override fun <T : ViewModel> create(
                modelClass: Class<T>,
                extras: CreationExtras
            ): T {
                // Get the Application object from extras
                val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
//              return MovieViewModel(//                  application,//                  MovieDatabaseRepository.create(application)//              ) as T                return MovieViewModel(application) { scope ->                    MovieRestRepository(scope)                } as T            }
        }
    }
}
CHANGED: /build.gradle.kts
// Top-level build file where you can add configuration options common to all sub-projects/modules.
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    alias(libs.plugins.androidApplication) apply false
    alias(libs.plugins.kotlinAndroid) apply false
    alias(libs.plugins.androidLibrary) apply false
    alias(libs.plugins.ksp) apply false
    alias(libs.plugins.org.jetbrains.kotlin.jvm) apply false}
true // Needed to make the Suppress annotation work for the plugins block
CHANGED: /gradle/libs.versions.toml
[versions]
agp = "8.2.0-beta06"
kotlin = "1.9.10"
core-ktx = "1.12.0"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
lifecycle-runtime-ktx = "2.6.2"
activity-compose = "1.8.0"
lifecycle-compose = "2.6.2"
compose-bom = "2023.10.00"
appcompat = "1.6.1"
material = "1.10.0"
room = "2.5.2"
ksp = "1.9.10-1.0.13"
icons-extended = "1.6.0-alpha07"
glance = "1.0.0"
org-jetbrains-kotlin-jvm = "1.9.10"jersey="3.0.2"activation="2.0.1"retrofit = "2.9.0"
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-compose" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
ui = { group = "androidx.compose.ui", name = "ui" }
ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
material3 = { group = "androidx.compose.material3", name = "material3" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
icons-extended = { group = "androidx.compose.material", name = "material-icons-extended-android", version.ref = "icons-extended"}
glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }
glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" }

jersey-grizzly2 = { group = "org.glassfish.jersey.containers", name = "jersey-container-grizzly2-http", version.ref = "jersey" }jersey-jetty = { group = "org.glassfish.jersey.containers", name = "jersey-container-jetty-http", version.ref = "jersey" }jersey-servlet = { group = "org.glassfish.jersey.containers", name = "jersey-container-servlet-core", version.ref = "jersey" }jersey-jackson = { group = "org.glassfish.jersey.media", name = "jersey-media-json-jackson", version.ref = "jersey" }jersey-server = { group = "org.glassfish.jersey.core", name = "jersey-server", version.ref = "jersey" }jersey-hk2 = { group = "org.glassfish.jersey.inject", name = "jersey-hk2", version.ref = "jersey" }activation = { group = "jakarta.activation", name = "jakarta.activation-api", version.ref = "activation" }retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }retrofit-json = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }## Bundles allow you to create common groupings of dependencies[bundles]retrofit = [ "retrofit", "retrofit-json" ]server = [    "jersey-grizzly2",    "jersey-jetty",    "jersey-servlet",    "jersey-jackson",    "jersey-server",    "jersey-hk2",    "activation"][plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "org-jetbrains-kotlin-jvm" }
CHANGED: /repository/build.gradle.kts
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.kotlinAndroid)
}

android {
    namespace = "com.androidbyexample.movie.repository"
    compileSdk = 34

    defaultConfig {
        minSdk = 24

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    implementation(project(":data"))

    // example of using a version catalog bundle to pull in multiple deps    implementation(libs.bundles.retrofit)    implementation(libs.core.ktx)
    implementation(libs.appcompat)
    implementation(libs.material)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.test.ext.junit)
    androidTestImplementation(libs.espresso.core)
}
ADDED: /repository/src/main/java/com/androidbyexample/movie/repository/MovieApiService.kt
package com.androidbyexample.movie.repositoryimport retrofit2.Responseimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport retrofit2.http.Bodyimport retrofit2.http.DELETEimport retrofit2.http.GETimport retrofit2.http.POSTimport retrofit2.http.PUTimport retrofit2.http.Pathconst val BASE_URL = "http://10.0.2.2:8080" // host computer for emulator// NOTE: In a real app, you might allow the user to set this via settingsinterface MovieApiService {    @GET("movie")    suspend fun getMovies(): Response<List<MovieDto>>    @GET("actor")    suspend fun getActors(): Response<List<ActorDto>>    @GET("rating")    suspend fun getRatings(): Response<List<RatingDto>>    @GET("rating/{id}/movies")    suspend fun getRatingWithMovies(@Path("id") id: String): Response<RatingWithMoviesDto?>    @GET("movie/{id}/cast")    suspend fun getMovieWithCast(@Path("id") id: String): Response<MovieWithCastDto?>    @GET("actor/{id}/filmography")    suspend fun getActorWithFilmography(@Path("id") id: String): Response<ActorWithFilmographyDto?>    @GET("movie/{id}")    suspend fun getMovie(@Path("id") id: String): Response<MovieDto?>    @POST("movie")    suspend fun createMovie(@Body movie: MovieDto): Response<MovieDto>    @POST("actor")    suspend fun createActor(@Body actor: ActorDto): Response<ActorDto>    @POST("rating")    suspend fun createRating(@Body rating: RatingDto): Response<RatingDto>    @PUT("movie/{id}")    suspend fun updateMovie(        @Path("id") id: String,        @Body movie: MovieDto    ): Response<Int> // number updated    @PUT("actor/{id}")    suspend fun updateActor(        @Path("id") id: String,        @Body actor: ActorDto    ): Response<Int> // number updated    @PUT("rating/{id}")    suspend fun updateRating(        @Path("id") id: String,        @Body rating: RatingDto    ): Response<Int> // number updated    @DELETE("rating/{id}")    suspend fun deleteRating(        @Path("id") id: String,    ): Response<Int> // number deleted    @DELETE("movie/{id}")    suspend fun deleteMovie(        @Path("id") id: String,    ): Response<Int> // number deleted    @DELETE("actor/{id}")    suspend fun deleteActor(        @Path("id") id: String,    ): Response<Int> // number deleted    @GET("reset")    suspend fun reset(): Response<Int>    companion object {        fun create() =            Retrofit.Builder()                .addConverterFactory(GsonConverterFactory.create())                .baseUrl(BASE_URL)                .build()                .create(MovieApiService::class.java)    }}
CHANGED: /repository/src/main/java/com/androidbyexample/movie/repository/MovieRepository.kt
package com.androidbyexample.movie.repository

import kotlinx.coroutines.flow.Flow

interface MovieRepository {
    val ratingsFlow: Flow<List<RatingDto>>
    val moviesFlow: Flow<List<MovieDto>>
    val actorsFlow: Flow<List<ActorDto>>

//  fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMoviesDto>    fun getRatingWithMoviesFlow(id: String): Flow<RatingWithMoviesDto?>
    suspend fun getMovie(id: String): MovieDto
    suspend fun getRatingWithMovies(id: String): RatingWithMoviesDto
    suspend fun getMovieWithCast(id: String): MovieWithCastDto
    suspend fun getActorWithFilmography(id: String): ActorWithFilmographyDto

    suspend fun insert(movie: MovieDto)
    suspend fun insert(actor: ActorDto)
    suspend fun insert(rating: RatingDto)

    suspend fun upsert(movie: MovieDto)
    suspend fun upsert(actor: ActorDto)
    suspend fun upsert(rating: RatingDto)

    suspend fun deleteMoviesById(ids: Set<String>)
    suspend fun deleteActorsById(ids: Set<String>)
    suspend fun deleteRatingsById(ids: Set<String>)

    suspend fun resetDatabase()
}
ADDED: /repository/src/main/java/com/androidbyexample/movie/repository/MovieRestRepository.kt
package com.androidbyexample.movie.repositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.launchimport kotlinx.coroutines.withContextimport retrofit2.Responseclass MovieRestRepository(    val coroutineScope: CoroutineScope,): MovieRepository {    // I abstracted this a bit more from the ListFlowManager in the video    // It now works for lists and single items    inner class FlowManager<T>(        private val defaultValue: T,        private val fetcher: suspend () -> Response<T>    ) {        private val _flow = MutableStateFlow(defaultValue)        init {            fetch()        }        val flow: Flow<T>            get() = _flow        fun fetch() =            coroutineScope.launch(Dispatchers.IO) {                _flow.value = fetcher().takeIf { it.isSuccessful }?.body() ?: defaultValue            }    }    private val movieApiService = MovieApiService.create()    private val moviesFlowManager = FlowManager(emptyList()) { movieApiService.getMovies() }    private val actorsFlowManager = FlowManager(emptyList()) { movieApiService.getActors() }    private val ratingsFlowManager = FlowManager(emptyList()) { movieApiService.getRatings() }    override val ratingsFlow: Flow<List<RatingDto>> = ratingsFlowManager.flow    override val moviesFlow: Flow<List<MovieDto>> = moviesFlowManager.flow    override val actorsFlow: Flow<List<ActorDto>> = actorsFlowManager.flow    // NOTE: The following assume that only one of each is active at a time    //       For our app, that should be the case (the edit screens don't allow    //       any deeper navigation), but if you wanted to be more general, you    //       could set up a WeakHashMap where the key is the flow and the value    //       is the manager (which also has a weak reference to the Flow).    //       Weak references allow the object they point to to be garbage collected    //       when there are no remaining strong references to it, and they'll be removed    //       from the map when no longer referenced. You would then walk through all the    //       values in the map and call their fetch() functions.    private var ratingWithMoviesFlowManager: FlowManager<RatingWithMoviesDto?>? = null    // not implementing the ActorWithFilmographyDto and MovieWithCastDto versions as we    //   don't use them in the app right now    override fun getRatingWithMoviesFlow(id: String) =        FlowManager(null) {            movieApiService.getRatingWithMovies(id)        }.let {            ratingWithMoviesFlowManager = it            it.flow        }    private suspend fun <T> getOrError(        id: String,        fetch: suspend MovieApiService.(String) -> Response<T?>    ): T = withContext(Dispatchers.IO) {        movieApiService.fetch(id).takeIf { it.isSuccessful }?.body()            ?: throw RuntimeException("$id not found")    }    override suspend fun getRatingWithMovies(id: String) =        getOrError(id) { getRatingWithMovies(it) }    override suspend fun getMovieWithCast(id: String) =        getOrError(id) { getMovieWithCast(it) }    override suspend fun getActorWithFilmography(id: String) =        getOrError(id) { getActorWithFilmography(it) }    override suspend fun getMovie(id: String) =        getOrError(id) { getMovie(it) }    override suspend fun insert(movie: MovieDto) {        movieApiService.createMovie(movie)        moviesFlowManager.fetch()        ratingWithMoviesFlowManager?.fetch()    }    override suspend fun insert(actor: ActorDto) {        movieApiService.createActor(actor)        actorsFlowManager.fetch()    }    override suspend fun insert(rating: RatingDto) {        movieApiService.createRating(rating)        ratingsFlowManager.fetch()    }    override suspend fun upsert(movie: MovieDto) {        movieApiService.updateMovie(movie.id, movie)        moviesFlowManager.fetch()        ratingWithMoviesFlowManager?.fetch()    }    override suspend fun upsert(actor: ActorDto) {        movieApiService.updateActor(actor.id, actor)        actorsFlowManager.fetch()    }    override suspend fun upsert(rating: RatingDto) {        movieApiService.updateRating(rating.id, rating)        ratingsFlowManager.fetch()    }    private suspend fun deleteById(        ids: Set<String>,        delete: suspend MovieApiService.(String) -> Unit,        vararg flowManagersToFetch: FlowManager<*>?,    ) {        ids.forEach { id ->            movieApiService.delete(id)        }        flowManagersToFetch.forEach { it?.fetch() }    }    override suspend fun deleteMoviesById(ids: Set<String>) {        deleteById(ids, { deleteMovie(it) }, moviesFlowManager, ratingWithMoviesFlowManager)    }    override suspend fun deleteActorsById(ids: Set<String>) {        deleteById(ids, { deleteActor(it) }, actorsFlowManager)    }    override suspend fun deleteRatingsById(ids: Set<String>) {        deleteById(ids, { deleteRating(it) }, ratingsFlowManager, moviesFlowManager)    }    override suspend fun resetDatabase() {        movieApiService.reset()        actorsFlowManager.fetch()        moviesFlowManager.fetch()        ratingsFlowManager.fetch()    }}
ADDED: /restserver/build.gradle.kts
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixedplugins {    application    alias(libs.plugins.org.jetbrains.kotlin.jvm)}kotlin {    jvmToolchain(17)}dependencies {    // example of using a version catalog bundle to pull in multiple deps    implementation(libs.bundles.server)}application {    mainClass.set("com.androidbyexample.movie.restserver.RunServerKt")}
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/Actor.kt
package com.androidbyexample.movie.restserverimport com.fasterxml.jackson.annotation.JsonPropertyimport java.util.UUIDimport javax.xml.bind.annotation.XmlRootElement@XmlRootElementdata class Actor(    @JsonProperty("id") var id: String = UUID.randomUUID().toString(),    @JsonProperty("name") var name: String)@XmlRootElementdata class ActorWithFilmography(    @JsonProperty("actor") val actor: Actor,    @JsonProperty("filmography") val filmography: List<RoleWithMovie>,)@XmlRootElementdata class RoleWithMovie(    @JsonProperty("movie") val movie: Movie,    @JsonProperty("character") val character: String,    @JsonProperty("orderInCredits") val orderInCredits: Int,)
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/Movie.kt
package com.androidbyexample.movie.restserverimport com.fasterxml.jackson.annotation.JsonPropertyimport java.util.UUIDimport javax.xml.bind.annotation.XmlRootElement@XmlRootElementdata class Movie(    @JsonProperty("id") val id: String = UUID.randomUUID().toString(),    @JsonProperty("title") val title: String,    @JsonProperty("description") val description: String,    @JsonProperty("ratingId") val ratingId: String,)@XmlRootElementdata class MovieWithCast(    @JsonProperty("movie") val movie: Movie,    @JsonProperty("cast") val cast: List<RoleWithActor>,)@XmlRootElementdata class RoleWithActor(    @JsonProperty("actor") val actor: Actor,    @JsonProperty("character") val character: String,    @JsonProperty("orderInCredits") val orderInCredits: Int,)
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/Rating.kt
package com.androidbyexample.movie.restserverimport com.fasterxml.jackson.annotation.JsonPropertyimport java.util.UUIDimport javax.xml.bind.annotation.XmlRootElement@XmlRootElementdata class Rating(    @JsonProperty("id") val id: String = UUID.randomUUID().toString(),    @JsonProperty("name") val name: String,    @JsonProperty("description") val description: String)@XmlRootElementdata class RatingWithMovies(    @JsonProperty("rating") var rating: Rating,    @JsonProperty("movies") var movies: List<Movie>,)
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/RestController.kt
package com.androidbyexample.movie.restserverimport jakarta.ws.rs.Consumesimport jakarta.ws.rs.DELETEimport jakarta.ws.rs.GETimport jakarta.ws.rs.POSTimport jakarta.ws.rs.PUTimport jakarta.ws.rs.Pathimport jakarta.ws.rs.PathParamimport jakarta.ws.rs.Producesimport jakarta.ws.rs.core.Contextimport jakarta.ws.rs.core.MediaTypeimport jakarta.ws.rs.core.Responseimport jakarta.ws.rs.core.UriInfo// NOTE: If Room were cross platform I would have directly used//       our data module to store things in a database in this//       server. Unfortunately, it's Android-only, so I'm//       implementing an in-memory set of maps to track the//       data. Kinda gross, but developing a real server isn't//       important for the class. The important thing is how we//       can communicate with the serverprivate fun <T> response(status: Response.Status, entity: T) =    Response.status(status).entity(entity).build()private fun <T> ok(entity: T) =    response(Response.Status.OK, entity)private fun <T> notFound(entity: T) =    response(Response.Status.NOT_FOUND, entity)private fun <T> created(entity: T) =    response(Response.Status.CREATED, entity)private val moviesByIdIndex = mutableMapOf<String, Movie>()private val actorsByIdIndex = mutableMapOf<String, Actor>()private val ratingsByIdIndex = mutableMapOf<String, Rating>()private val rolesByMovieIdIndex = mutableMapOf<String, MutableList<Role>>()private val rolesByActorIdIndex = mutableMapOf<String, MutableList<Role>>()private val moviesByRatingIdIndex = mutableMapOf<String, MutableList<Movie>>()private val notFoundRating = Rating("-", "NOT FOUND", "NOT FOUND")private val notFoundMovie = Movie("-", "NOT FOUND", "NOT FOUND", "--")private val notFoundMovieWithRoles = MovieWithCast(notFoundMovie, emptyList())private val notFoundActor = Actor("-", "NOT FOUND")private val notFoundActorWithRoles = ActorWithFilmography(notFoundActor, emptyList())private val notFoundRatingWithMovies = RatingWithMovies(notFoundRating, emptyList())@Path("/")class RestController {    @GET    @Path("rating")    @Produces(MediaType.APPLICATION_JSON)    fun getRatings(): Response = ok(ratingsByIdIndex.values.sortedBy { it.id })    @GET    @Path("movie")    @Produces(MediaType.APPLICATION_JSON)    fun getMovies(): Response = ok(moviesByIdIndex.values.sortedBy { it.title })    @GET    @Path("actor")    @Produces(MediaType.APPLICATION_JSON)    fun getActors(): Response = ok(actorsByIdIndex.values.sortedBy { it.name })    @GET    @Path("rating/{id}/movies")    @Produces(MediaType.APPLICATION_JSON)    fun getRatingWithMovies(@PathParam("id") id: String): RatingWithMovies =        ratingsByIdIndex[id]?.let { rating ->            val movies = moviesByRatingIdIndex[id] ?: emptyList()            RatingWithMovies(rating, movies)        } ?: notFoundRatingWithMovies    @GET    @Path("movie/{id}/cast")    @Produces(MediaType.APPLICATION_JSON)    fun getMovieWithRoles(@PathParam("id") id: String): MovieWithCast =        moviesByIdIndex[id]?.let { movie ->            val roles =                rolesByMovieIdIndex[id]                    ?.map { role ->                        RoleWithActor(                            actor = actorsByIdIndex[role.actorId] ?: throw IllegalStateException(),                            character = role.character,                            orderInCredits = role.orderInCredits,                        )                    }                    ?: emptyList()            MovieWithCast(movie, roles)        } ?: notFoundMovieWithRoles    @GET    @Path("actor/{id}/filmography")    @Produces(MediaType.APPLICATION_JSON)    fun getActorWithRoles(@PathParam("id") id: String): ActorWithFilmography =        actorsByIdIndex[id]?.let { actor ->            val roles =                rolesByActorIdIndex[id]                    ?.map { role ->                        RoleWithMovie(                            movie = moviesByIdIndex[role.movieId] ?: throw IllegalStateException(),                            character = role.character,                            orderInCredits = role.orderInCredits,                        )                    }                    ?: emptyList()            ActorWithFilmography(actor, roles)        } ?: notFoundActorWithRoles    @PUT    @Path("movie/{id}")    @Produces(MediaType.TEXT_PLAIN)    fun updateMovie(@PathParam("id") id: String, movie: Movie): Response {        moviesByIdIndex[id] = movie        moviesByRatingIdIndex.getOrPut(movie.ratingId) { mutableListOf() }.add(movie)        return ok(1)    }    @PUT    @Path("actor/{id}")    @Produces(MediaType.TEXT_PLAIN)    fun updateActor(@PathParam("id") id: String, actor: Actor): Response {        actorsByIdIndex[id] = actor        return ok(1)    }    @PUT    @Path("rating/{id}")    @Produces(MediaType.TEXT_PLAIN)    fun updateRating(@PathParam("id") id: String, rating: Rating): Response {        ratingsByIdIndex[id] = rating        return ok(1)    }    private operator fun MutableMap<String, MutableMap<String, Role>>.set(id1: String, id2: String, role: Role) {        val roles = this[id1] ?: mutableMapOf<String, Role>().apply { this@set[id1] = this }        roles[id2] = role    }    private operator fun MutableMap<String, MutableMap<String, Role>>.get(id1: String, id2: String) =        this[id1]?.get(id2)    @POST    @Path("rating/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun createRating(@Context uriInfo: UriInfo, rating: Rating): Response {        ratingsByIdIndex[rating.id] = rating        return created(rating)    }    @POST    @Path("movie/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun createMovie(@Context uriInfo: UriInfo, movie: Movie): Response {        moviesByIdIndex[movie.id] = movie        return created(movie)    }    @POST    @Path("actor/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun createActor(@Context uriInfo: UriInfo, actor: Actor): Response {        actorsByIdIndex[actor.id] = actor        return created(actor)    }    @DELETE    @Path("movie/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun deleteMovie(@PathParam("id") id: String): Response =        if (moviesByIdIndex[id] == null) {            notFound(0)        } else {            moviesByIdIndex.remove(id)            rolesByMovieIdIndex.remove(id) // remove all roles            rolesByActorIdIndex.values.forEach { roles ->                roles.removeIf { it.movieId == id }            }            ok(1)        }    @DELETE    @Path("actor/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun deleteActor(@PathParam("id") id: String): Response =        if (actorsByIdIndex[id] == null) {            notFound(0)        } else {            actorsByIdIndex.remove(id)            rolesByActorIdIndex.remove(id) // remove all roles            rolesByMovieIdIndex.values.forEach { roles -> // remove filmography for those movies                roles.removeIf { it.actorId == id }            }            ok(1)        }    @DELETE    @Path("rating/{id}")    @Consumes(MediaType.APPLICATION_JSON)    @Produces(MediaType.TEXT_PLAIN)    fun deleteRating(@PathParam("id") id: String): Response =        if (ratingsByIdIndex[id] == null) {            notFound(0)        } else {            ratingsByIdIndex.remove(id)            // delete associated movies            moviesByRatingIdIndex[id]?.forEach {                moviesByIdIndex.remove(it.id)            }            moviesByRatingIdIndex.remove(id)            ok(1)        }    private fun insertMovies(vararg newMovies: Movie) {        newMovies.forEach { movie ->            moviesByIdIndex[movie.id] = movie            moviesByRatingIdIndex.getOrCreate(movie.ratingId).add(movie)        }    }    private fun insertActors(vararg newActors: Actor) {        newActors.forEach { actor ->            actorsByIdIndex[actor.id] = actor        }    }    private fun insertRatings(vararg newRatings: Rating) {        newRatings.forEach { rating ->            ratingsByIdIndex[rating.id] = rating        }    }    private fun insertRoles(vararg newRoles: Role) {        newRoles.forEach { role ->            rolesByActorIdIndex.getOrCreate(role.actorId).add(role)            rolesByMovieIdIndex.getOrCreate(role.movieId).add(role)        }    }    private fun <K, V> MutableMap<K, MutableList<V>>.getOrCreate(key: K) =        this[key] ?: mutableListOf<V>().apply {            this@getOrCreate[key] = this        }    @GET    @Path("reset")    @Produces(MediaType.TEXT_PLAIN)    fun resetDatabase(): Response {        ratingsByIdIndex.clear()        moviesByIdIndex.clear()        actorsByIdIndex.clear()        rolesByActorIdIndex.clear()        rolesByMovieIdIndex.clear()        insertRatings(            Rating(id = "r0", name = "Not Rated", description = "Not yet rated"),            Rating(id = "r1", name = "G", description = "General Audiences"),            Rating(id = "r2", name = "PG", description = "Parental Guidance Suggested"),            Rating(id = "r3", name = "PG-13", description = "Unsuitable for those under 13"),            Rating(id = "r4", name = "R", description = "Restricted - 17 and older"),        )        insertMovies(            Movie("m1", "The Transporter", "Jason Statham kicks a guy in the face", "r3"),            Movie("m2", "Transporter 2", "Jason Statham kicks a bunch of guys in the face", "r4"),            Movie("m3", "Hobbs and Shaw", "Cars, Explosions and Stuff", "r3"),            Movie("m4", "Jumanji", "The Rock smolders", "r3"),        )        insertActors(            Actor("a1", "Jason Statham"),            Actor("a2", "The Rock"),            Actor("a3", "Shu Qi"),            Actor("a4", "Amber Valletta"),            Actor("a5", "Kevin Hart"),        )        insertRoles(            Role("m1", "a1", "Frank Martin", 1),            Role("m1", "a3", "Lai", 2),            Role("m2", "a1", "Frank Martin", 1),            Role("m2", "a4", "Audrey Billings", 2),            Role("m3", "a2", "Hobbs", 1),            Role("m3", "a1", "Shaw", 2),            Role("m4", "a2", "Spencer", 1),            Role("m4", "a5", "Fridge", 2),        )        return ok(1)    }}
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/Role.kt
package com.androidbyexample.movie.restserverimport com.androidbyexample.movie.restserver.Actorimport com.androidbyexample.movie.restserver.Movieimport com.fasterxml.jackson.annotation.JsonPropertyimport javax.xml.bind.annotation.XmlRootElement@XmlRootElementclass Role(    @JsonProperty("movieId") var movieId: String,    @JsonProperty("actorId") var actorId: String,    @JsonProperty("character") var character: String,    @JsonProperty("orderInCredits") var orderInCredits: Int)@XmlRootElementclass ExpandedRole(    @JsonProperty("movie") var movie: Movie,    @JsonProperty("actor") var actor: Actor,    @JsonProperty("character") var character: String,    @JsonProperty("orderInCredits") var orderInCredits: Int)
ADDED: /restserver/src/main/java/com/androidbyexample/movie/restserver/RunServer.kt
package com.androidbyexample.movie.restserverimport org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactoryimport org.glassfish.jersey.server.ResourceConfigimport java.net.URIimport com.fasterxml.jackson.databind.ObjectMapperimport com.fasterxml.jackson.databind.SerializationFeatureimport jakarta.ws.rs.ext.ContextResolverimport jakarta.ws.rs.ext.Provider// from https://mkyong.com/webservices/jax-rs/json-example-with-jersey-jackson/// MIT License////  Copyright (c) 2020 Mkyong.com//// Permission is hereby granted, free of charge, to any person obtaining a copy// of this software and associated documentation files (the “Software”), to deal// in the Software without restriction, including without limitation the rights// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell// copies of the Software, and to permit persons to whom the Software is// furnished to do so, subject to the following conditions://// The above copyright notice and this permission notice shall be included in all// copies or substantial portions of the Software.//// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE// SOFTWARE.@Providerclass CustomJacksonMapperProvider : ContextResolver<ObjectMapper> {    private val mapper = ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)    override fun getContext(type: Class<*>?) = mapper}fun main() {    GrizzlyHttpServerFactory        .createHttpServer(            URI.create("http://localhost:8080"),            ResourceConfig().apply {                register(RestController::class.java)                register(CustomJacksonMapperProvider::class.java)            }        ).start()}
CHANGED: /settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "MovieRest"
include(":app")
include(":data")
include(":repository")
include(":restserver")