Movies UI - Lists
Add Actors and Ratings
To flesh out the app more, let's add in Actors and Ratings.
Navigation to List Screens
We need some way to navigate between the lists. We'll do this by adding a navigation bar at the
bottom of the ListScaffold. This contains buttons that act list tabs.

But what if the screen is larger? Instead of tabs at the bottom, we can also have tabs on the side.
To accomplish this, we wrap our overall Scaffold with a NavigationSuiteScaffold that manages
the buttons. (If we always wanted bottom buttons, we could define them in the bottomBar of the
Scaffold)
We add the navigation suite to our version catalog:
show in full file gradle/libs.versions.toml
[versions]
// ...
[libraries]
// ...
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
androidx-compose-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
// ...
[plugins]
// ...
Note
The compose navigation suite is included in the Compose Bill-Of-Materials (BOM), so we don't
need a version specification. Because our build script (app/build.gradle.kts) includes the
Compose BOM, we can just reference the library.
show in full file app/build.gradle.kts
// ...
dependencies {
// ...
implementation(libs.androidx.material3.adaptive.navigation3)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
// ...
}
Each tab is represented as a "navigation suite item" that has an icon, label, and selection
management. To make this simpler to set up, let's tweak our screen data to include a ListScreen
class that contains the icon and label ids.
show in full file app/src/main/java/com/androidbyexample/movies/screens/Screens.kt
// ...
sealed interface Screen: Parcelable
sealed class ListScreen(
@DrawableRes val iconId: Int,
@StringRes val labelId: Int,
): Screen
@Parcelize
//data object MovieList: Screen
data object MovieList: ListScreen(
iconId = R.drawable.movie_24,
labelId = R.string.movies,
)
@Parcelize
data object RatingList: ListScreen(
iconId = R.drawable.star_24,
labelId = R.string.ratings,
)
@Parcelize
data object ActorList: ListScreen(
iconId = R.drawable.person_24,
labelId = R.string.actors,
)
@Parcelize
// ...
In our view model, we'll track which list screen is currently selected
show in full file app/src/main/java/com/androidbyexample/movies/MovieViewModel.kt
// ...
class MovieViewModel(
// ...
): ViewModel(), MovieRepository by repository {
val currentListScreenFlow = savedStateHandle.getMutableStateFlow<ListScreen>(
key = "current_list_screen",
initialValue = MovieList
)
fun goToListScreen(screen: ListScreen) {
currentListScreenFlow.value = screen
backStackFlow.value = listOf(screen)
}
val backStackFlow = savedStateHandle.getMutableStateFlow<List<Screen>>(
// ...
}
Then we add support in Ui() to wrap everything with a navigation scaffold
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
// ...
) {
// ...
val backStack by viewModel.backStackFlow.collectAsStateWithLifecycle(listOf(MovieList))
val currentListScreen by viewModel.currentListScreenFlow.collectAsStateWithLifecycle(MovieList)
val listScreens = remember { listOf(RatingList, MovieList, ActorList) }
NavigationSuiteScaffold(
navigationSuiteItems = {
listScreens.forEach { target ->
item(
icon = {
Icon(
painter = painterResource(target.iconId),
contentDescription = stringResource(target.labelId)
)
},
label = { Text(stringResource(target.labelId)) },
selected = currentListScreen == target,
onClick = {
viewModel.goToListScreen(target)
}
)
}
}
) {
NavDisplay(
backStack = backStack,
onBack = viewModel::popScreen,
sceneStrategies = listOf(listDetailStrategy),
entryProvider = entryProvider {
entry<MovieList>(
// metadata = ListDetailSceneStrategy.listPane()
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_movie_to_view) }
)
) {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
MovieListUi(
movies = movies,
onMovieClicked = { movieId ->
viewModel.pushScreen(MovieDisplay(movieId))
},
onDeleteSelectedMovies = { ids ->
viewModel.deleteSelectedMovies(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<MovieDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
MovieDisplayUi(
id = key.id,
fetchMovie = viewModel::getMovieWithCast,
// onActorClicked = {},
onActorClicked = { viewModel.pushScreen(ActorDisplay(it)) },
onDeleteSelectedActors = { },
// onResetDatabase = viewModel::doResetDatabase,
)
}
entry<ActorList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_an_actor_to_view) }
)
) {
val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
ActorListUi(
actors = actors,
onActorClicked = { actorId ->
viewModel.pushScreen(ActorDisplay(actorId))
},
onDeleteSelectedActors = { ids ->
viewModel.deleteSelectedActors(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<ActorDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
ActorDisplayUi(
id = key.id,
fetchActor = viewModel::getActorWithFilmography,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onDeleteSelectedMovies = { },
)
}
entry< RatingList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_rating_to_view) }
)
) {
val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
RatingListUi(
ratings = ratings,
onRatingClicked = { ratingId ->
viewModel.pushScreen(RatingDisplay(ratingId))
},
onDeleteSelectedRatings = { ids ->
viewModel.deleteSelectedRatings(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<RatingDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
RatingDisplayUi(
id = key.id,
fetchRating = viewModel::getRatingWithMovies,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onDeleteSelectedMovies = { },
)
}
}
)
}
}
This creates a nice Ui that allows us to navigate starting from movies, actors or ratings. The screen tabs could be moved into a common base scaffold for all screens to allow instant jumping to a list from any screen (I'll leave that as an "exercise for the interested reader").
Actor and Rating Screens
Now we add the new screens. These new screens are similar to the existing screens
show in full file app/src/main/java/com/androidbyexample/movies/screens/ActorListUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.ActorDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorListUi(
actors: List<ActorDto>,
modifier: Modifier = Modifier,
onActorClicked: (String) -> Unit,
onDeleteSelectedActors: (Set<String>) -> Unit,
onResetDatabase: () -> Unit,
) {
ListScaffold(
title = stringResource(R.string.actors),
items = actors,
onItemClicked = onActorClicked,
onDeleteSelectedItems = onDeleteSelectedActors,
onResetDatabase = onResetDatabase,
itemIconId = R.drawable.person_24,
itemContentDescriptionId = R.string.actor,
modifier = modifier
) { actor ->
Display(text = actor.name)
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/RatingListUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.RatingDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingListUi(
ratings: List<RatingDto>,
modifier: Modifier = Modifier,
onRatingClicked: (String) -> Unit,
onDeleteSelectedRatings: (Set<String>) -> Unit,
onResetDatabase: () -> Unit,
) {
ListScaffold(
title = stringResource(R.string.ratings),
items = ratings,
onItemClicked = onRatingClicked,
onDeleteSelectedItems = onDeleteSelectedRatings,
onResetDatabase = onResetDatabase,
itemIconId = R.drawable.star_24,
itemContentDescriptionId = R.string.rating,
modifier = modifier
) { rating ->
Display(text = rating.name)
}
}
Next we create the display screens for actor and rating
show in full file app/src/main/java/com/androidbyexample/movies/screens/ActorDisplayUi.kt
// ...
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorDisplayUi(
id: String,
fetchActor: suspend (String) -> ActorWithFilmographyDto,
onMovieClicked: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) }
LaunchedEffect(key1 = id) {
withContext(Dispatchers.IO) {
actorWithFilmography = fetchActor(id)
}
}
actorWithFilmography?.let { actorWithFilmography ->
ListScaffold(
title = actorWithFilmography.actor.name,
items = actorWithFilmography.filmography,
onItemClicked = onMovieClicked,
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.title)
Display(text = actorWithFilmography.actor.name)
Label(
textId = R.string.movies_starring,
actorWithFilmography.actor.name
)
},
cardContent = { role ->
Display(
text = stringResource(
R.string.cast_entry,
role.character,
role.movie.title,
)
)
},
)
}
}
show in full file app/src/main/java/com/androidbyexample/movies/screens/RatingDisplayUi.kt
// ...
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingDisplayUi(
id: String,
fetchRating: suspend (String) -> RatingWithMoviesDto,
onMovieClicked: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) }
LaunchedEffect(key1 = id) {
withContext(Dispatchers.IO) {
ratingWithMovies = fetchRating(id)
}
}
ratingWithMovies?.let { ratingWithMovies ->
ListScaffold(
title = ratingWithMovies.rating.name,
items = ratingWithMovies.movies,
onItemClicked = onMovieClicked,
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.name)
Display(text = ratingWithMovies.rating.name)
Label(textId = R.string.description)
Display(text = ratingWithMovies.rating.description)
Label(
textId = R.string.movies_rated,
ratingWithMovies.rating.name
)
},
cardContent = { movie ->
Display(
text = movie.title
)
},
)
}
}
These require a tweak to Label, as we're using a text resource with filled-in arguments.
show in full file app/src/main/java/com/androidbyexample/movies/helper/Label.kt
// ...
@Composable
fun Label(
@StringRes textId: Int,
vararg formatArgs: Any,
modifier: Modifier = Modifier,
) {
Text(
// text = stringResource(id = textId),
text = stringResource(id = textId, formatArgs = formatArgs),
style = MaterialTheme.typography.titleMedium,
modifier = modifier
.padding(8.dp)
.fillMaxWidth()
)
}
Define new user-facing strings
show in full file app/src/main/res/values/strings.xml
<resources>
<string name="app_name">movies</string>
<string name="movies">Movies</string>
<string name="actors">Actors</string>
<string name="ratings">Ratings</string>
<string name="actor">Actor</string>
<string name="rating">Rating</string>
<string name="loading">Loading…</string>
<string name="name">Name</string>
<string name="movies_starring">Movies Starring %1$s</string>
<string name="movies_rated">Movies Rated %1$s</string>
<string name="title">Title</string>
<string name="description">Description</string>
// ...
</resources>
Fix the onActorClicked parameter being passed to MovieDisplayUi to push the screen
for the clicked RoleWithActorDto, and add the new screens to the Ui
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
// ...
) {
// ...
NavigationSuiteScaffold(
// ...
) {
NavDisplay(
// ...
entryProvider = entryProvider {
// ...
entry<MovieDisplay>(
// ...
) { key ->
MovieDisplayUi(
id = key.id,
fetchMovie = viewModel::getMovieWithCast,
// onActorClicked = {},
onActorClicked = { viewModel.pushScreen(ActorDisplay(it)) },
onDeleteSelectedActors = { },
// ...
)
}
entry<ActorList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_an_actor_to_view) }
)
) {
val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
ActorListUi(
actors = actors,
onActorClicked = { actorId ->
viewModel.pushScreen(ActorDisplay(actorId))
},
onDeleteSelectedActors = { ids ->
viewModel.deleteSelectedActors(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<ActorDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
ActorDisplayUi(
id = key.id,
fetchActor = viewModel::getActorWithFilmography,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onDeleteSelectedMovies = { },
)
}
entry< RatingList>(
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_rating_to_view) }
)
) {
val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(
initialValue = emptyList()
)
RatingListUi(
ratings = ratings,
onRatingClicked = { ratingId ->
viewModel.pushScreen(RatingDisplay(ratingId))
},
onDeleteSelectedRatings = { ids ->
viewModel.deleteSelectedRatings(ids)
},
onResetDatabase = viewModel::doResetDatabase,
)
}
entry<RatingDisplay>(
metadata = ListDetailSceneStrategy.detailPane()
) { key ->
RatingDisplayUi(
id = key.id,
fetchRating = viewModel::getRatingWithMovies,
onMovieClicked = { viewModel.pushScreen(MovieDisplay(it)) },
onDeleteSelectedMovies = { },
)
}
}
)
}
}
When we run this version of the application, we can now drill deeper into the data. Pick a movie, then pick an actor, then pick movies starring that actor and so forth. Navigating back pops each screen off the stack, returning to the previous screen.
Finally, a quick tweak to animate list changes (such as when items are added or deleted from a list)
show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
// ...
) {
// ...
Scaffold(
// ...
) { innerPadding ->
LazyColumn(
// ...
) {
// ...
items(
// ...
) { item ->
// ...
Card(
// ...
modifier = Modifier
.padding(8.dp)
.animateItem()
.combinedClickable(
// NOTE - use targetId for navigation, id for selections
// ...
)
) {
// ...
}
}
}
}
}

There is one little glitch when we try rotating the screen. A nasty gap between the panes:

Note that this wouldn't be visible if we used a single Scaffold around the entire screen, rather
that defining it at the list or display screen level. But because we want different identifiers
(and later, actions) above each section, we see the gap. Depending on what you're trying to
accomplish, you may want the gap, but here it looks wrong.
To fix this, we need to tweak how we create our list/detail strategy:
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
viewModel: MovieViewModel,
) {
// val listDetailStrategy = rememberListDetailSceneStrategy<Screen>()
val windowAdaptiveInfo = currentWindowAdaptiveInfoV2()
val directive = remember(windowAdaptiveInfo) {
calculatePaneScaffoldDirective(windowAdaptiveInfo)
.copy(horizontalPartitionSpacerSize = 0.dp)
}
val listDetailStrategy = rememberListDetailSceneStrategy<Screen>(directive = directive)
val backStack by viewModel.backStackFlow.collectAsStateWithLifecycle(listOf(MovieList))
// ...
}

But what about that duplicate "refresh" icon? For this app, let's set it up so that it only
appears on a list screen. We can do this by making onResetDatabase nullable in our
ListScaffold and only show the button if it's non-null:
show in full file app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
// ...
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
// ...
onItemClicked: (String) -> Unit,
onDeleteSelectedItems: (Set<String>) -> Unit,
// onResetDatabase: () -> Unit,
onResetDatabase: (() -> Unit)?,
@DrawableRes itemIconId: Int,
@StringRes itemContentDescriptionId: Int,
// ...
) {
// ...
Scaffold(
topBar = {
// ...
if (selectedIds.isEmpty()) {
TopAppBar(
// ...
},
actions = {
onResetDatabase?.let {
IconButton(onClick = onResetDatabase) {
Icon(
painter = painterResource(R.drawable.refresh_24),
contentDescription = stringResource(R.string.reset_database),
)
}
}
}
)
} else {
// ...
}
},
// ...
) { innerPadding ->
// ...
}
}
and then passing null from the display screens (and we can remove their onResetDatabase
parameters)
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
// ...
) {
// ...
movieWithCast?.let { movieWithCast ->
ListScaffold(
// ...
onItemClicked = onActorClicked,
onDeleteSelectedItems = onDeleteSelectedActors,
// onResetDatabase = onResetDatabase,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
// ...
)
} ?: run {
// ...
}
}
And it looks better

But there is one more thing that looks off. If we start with a wide screen, we only see the movie list on the left.

Fortunately, we can provide a placeholder for the detail pane:
show in full file app/src/main/java/com/androidbyexample/movies/screens/DetailPlaceholder.kt
// ...
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailPlaceholder(
@StringRes messageId: Int,
) {
Scaffold(
topBar = {
val colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onSecondary
)
TopAppBar(
colors = colors,
title = {},
)
}
) { innerPadding ->
Box(
modifier =
Modifier
.padding(innerPadding)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(stringResource(id = messageId))
}
}
}
and use it when setting up our list navigation
show in full file app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
// ...
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
// ...
) {
// ...
NavigationSuiteScaffold(
// ...
) {
NavDisplay(
// ...
entryProvider = entryProvider {
entry<MovieList>(
// metadata = ListDetailSceneStrategy.listPane()
metadata = ListDetailSceneStrategy.listPane(
detailPlaceholder = { DetailPlaceholder(R.string.select_a_movie_to_view) }
)
) {
val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
// ...
}
// ...
}
)
}
}
Unfortunately, with the way our displays load, they start blank while loading their data, causing a flash in the detail area. We can fix this by using the placeholder in an Elvis clause:
show in full file app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
// ...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
// ...
) {
// ...
movieWithCast?.let { movieWithCast ->
// ...
},
)
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
s
All code changes
CHANGED: app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
id("kotlin-parcelize")
}
android {
namespace = "com.androidbyexample.movies"
compileSdk {
version = release((libs.versions.compileSdk.get().toInt()))
}
defaultConfig {
applicationId = "com.androidbyexample.movies"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
optimization {
enable = false
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.valueOf(libs.versions.javaVersion.get())
targetCompatibility = JavaVersion.valueOf(libs.versions.javaVersion.get())
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":repository"))
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.material3.adaptive.navigation3)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
testImplementation(libs.junit)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
}
CHANGED: app/src/main/java/com/androidbyexample/movies/MovieViewModel.kt
package com.androidbyexample.movies
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.androidbyexample.movies.repository.MovieDatabaseRepository
import com.androidbyexample.movies.repository.MovieRepository
import com.androidbyexample.movies.screens.ListScreen
import com.androidbyexample.movies.screens.MovieList
import com.androidbyexample.movies.screens.Screen
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MovieViewModel(
private val savedStateHandle: SavedStateHandle,
private val repository: MovieRepository,
): ViewModel(), MovieRepository by repository {
val currentListScreenFlow = savedStateHandle.getMutableStateFlow<ListScreen>(
key = "current_list_screen",
initialValue = MovieList
)
fun goToListScreen(screen: ListScreen) {
currentListScreenFlow.value = screen
backStackFlow.value = listOf(screen)
}
val backStackFlow = savedStateHandle.getMutableStateFlow<List<Screen>>(
key = "back_stack",
initialValue = listOf(MovieList)
)
fun pushScreen(screen: Screen) {
backStackFlow.value += screen
}
fun popScreen() {
backStackFlow.value = backStackFlow.value.dropLast(1)
}
fun doResetDatabase() {
viewModelScope.launch(Dispatchers.IO) {
repository.resetDatabase()
}
}
fun deleteSelectedMovies(ids: Set<String>) {
viewModelScope.launch {
deleteMoviesById(ids)
}
}
fun deleteSelectedActors(ids: Set<String>) {
viewModelScope.launch {
deleteActorsById(ids)
}
}
fun deleteSelectedRatings(ids: Set<String>) {
viewModelScope.launch {
deleteRatingsById(ids)
}
}
companion object {
val Factory = viewModelFactory {
initializer {
val application =
checkNotNull(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
val savedStateHandle = this.createSavedStateHandle()
MovieViewModel(
savedStateHandle = savedStateHandle,
repository = MovieDatabaseRepository.create(application)
)
}
}
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/helper/Label.kt
package com.androidbyexample.movies.helper
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@Composable
fun Label(
@StringRes textId: Int,
vararg formatArgs: Any,
modifier: Modifier = Modifier,
) {
Text(
// text = stringResource(id = textId),
text = stringResource(id = textId, formatArgs = formatArgs),
style = MaterialTheme.typography.titleMedium,
modifier = modifier
.padding(8.dp)
.fillMaxWidth()
)
}
CHANGED: app/src/main/java/com/androidbyexample/movies/helper/ListScaffold.kt
package com.androidbyexample.movies.helper
import androidx.activity.compose.BackHandler
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.movies.R
import com.androidbyexample.movies.repository.HasId
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
title: String,
items: List<T>,
onItemClicked: (String) -> Unit,
onDeleteSelectedItems: (Set<String>) -> Unit,
// onResetDatabase: () -> Unit,
onResetDatabase: (() -> Unit)?,
@DrawableRes itemIconId: Int,
@StringRes itemContentDescriptionId: Int,
modifier: Modifier = Modifier,
fixedContentAbove: @Composable () -> Unit = {},
cardContent: @Composable (T) -> Unit,
) {
val selectedIds = rememberSaveable { mutableStateSetOf<String>()}
fun onSelectionToggle(id: String) {
if (id in selectedIds) {
selectedIds -= id
} else {
selectedIds += id
}
}
fun clearSelectedIds() {
selectedIds.clear()
}
if (selectedIds.isNotEmpty()) {
BackHandler {
clearSelectedIds()
}
}
Scaffold(
topBar = {
val colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onSecondary
)
if (selectedIds.isEmpty()) {
TopAppBar(
colors = colors,
title = {
Text(text = title)
},
actions = {
}
)
} else {
TopAppBar(
colors = colors,
navigationIcon = {
Icon(
painter = painterResource(R.drawable.arrow_back_24),
contentDescription = stringResource(R.string.clear_selections),
modifier = Modifier.clickable(onClick = ::clearSelectedIds),
)
},
title = {
Text(
text = selectedIds.size.toString(),
modifier = Modifier.padding(8.dp)
)
},
actions = {
IconButton(
onClick = {
onDeleteSelectedItems(selectedIds.toSet())
// NOTE - this is a mutable set, and we need to be sure we just
// pass a read-only copy out of here. If we don't, the
// caller could modify the contents AND we set up a race
// condition between the clear() call below and the read
// of the data in the caller.
selectedIds.clear()
}
) {
Icon(
painter = painterResource(R.drawable.delete_24),
contentDescription = stringResource(R.string.delete_selected_items)
)
}
},
)
}
},
modifier = modifier,
) { innerPadding ->
LazyColumn(
modifier = Modifier.padding(innerPadding)
) {
item {
fixedContentAbove()
}
items(
items = items,
key = { it.id },
) { item ->
val containerColor =
if (item.id in selectedIds) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surface
}
val contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor)
Card(
elevation = CardDefaults.cardElevation(
defaultElevation = 8.dp,
),
colors = CardDefaults.cardColors(
containerColor = containerColor,
contentColor = contentColor,
),
modifier = Modifier
.padding(8.dp)
.animateItem()
.combinedClickable(
// NOTE - use targetId for navigation, id for selections
onClick = {
if (selectedIds.isEmpty()) {
onItemClicked(item.targetId)
} else {
onSelectionToggle(item.id)
}
},
onLongClick = {
onSelectionToggle(item.id)
},
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(8.dp),
) {
Icon(
painter = painterResource(itemIconId),
contentDescription = stringResource(itemContentDescriptionId),
modifier = Modifier.clickable {
onSelectionToggle(item.id)
}
)
cardContent(item)
}
}
}
}
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/ActorDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.ActorWithFilmographyDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorDisplayUi(
id: String,
fetchActor: suspend (String) -> ActorWithFilmographyDto,
onMovieClicked: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) }
LaunchedEffect(key1 = id) {
withContext(Dispatchers.IO) {
actorWithFilmography = fetchActor(id)
}
}
actorWithFilmography?.let { actorWithFilmography ->
ListScaffold(
title = actorWithFilmography.actor.name,
items = actorWithFilmography.filmography,
onItemClicked = onMovieClicked,
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.title)
Display(text = actorWithFilmography.actor.name)
Label(
textId = R.string.movies_starring,
actorWithFilmography.actor.name
)
},
cardContent = { role ->
Display(
text = stringResource(
R.string.cast_entry,
role.character,
role.movie.title,
)
)
},
)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/ActorListUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.ActorDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorListUi(
actors: List<ActorDto>,
modifier: Modifier = Modifier,
onActorClicked: (String) -> Unit,
onDeleteSelectedActors: (Set<String>) -> Unit,
onResetDatabase: () -> Unit,
) {
ListScaffold(
title = stringResource(R.string.actors),
items = actors,
onItemClicked = onActorClicked,
onDeleteSelectedItems = onDeleteSelectedActors,
onResetDatabase = onResetDatabase,
itemIconId = R.drawable.person_24,
itemContentDescriptionId = R.string.actor,
modifier = modifier
) { actor ->
Display(text = actor.name)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/DetailPlaceholder.kt
package com.androidbyexample.movies.screens
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailPlaceholder(
@StringRes messageId: Int,
) {
Scaffold(
topBar = {
val colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onSecondary
)
TopAppBar(
colors = colors,
title = {},
)
}
) { innerPadding ->
Box(
modifier =
Modifier
.padding(innerPadding)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(stringResource(id = messageId))
}
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/MovieDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.MovieWithCastDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDisplayUi(
id: String,
fetchMovie: suspend (String) -> MovieWithCastDto,
onActorClicked: (String) -> Unit,
onDeleteSelectedActors: (Set<String>) -> Unit,
// onResetDatabase: () -> Unit,
modifier: Modifier = Modifier,
) {
var movieWithCast by remember { mutableStateOf<MovieWithCastDto?>(null) }
LaunchedEffect(key1 = id) {
withContext(Dispatchers.IO) {
movieWithCast = fetchMovie(id)
}
}
movieWithCast?.let { movieWithCast ->
ListScaffold(
title = movieWithCast.movie.title,
items = movieWithCast.cast,
onItemClicked = onActorClicked,
onDeleteSelectedItems = onDeleteSelectedActors,
// onResetDatabase = onResetDatabase,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.title)
Display(text = movieWithCast.movie.title)
Label(textId = R.string.description)
Display(text = movieWithCast.movie.description)
Label(textId = R.string.cast)
},
cardContent = { role ->
Display(
text = stringResource(
R.string.cast_entry,
role.character,
role.actor.name,
)
)
},
)
} ?: run {
DetailPlaceholder(messageId = R.string.loading)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/RatingDisplayUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.Label
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.RatingWithMoviesDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingDisplayUi(
id: String,
fetchRating: suspend (String) -> RatingWithMoviesDto,
onMovieClicked: (String) -> Unit,
onDeleteSelectedMovies: (Set<String>) -> Unit,
modifier: Modifier = Modifier,
) {
var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) }
LaunchedEffect(key1 = id) {
withContext(Dispatchers.IO) {
ratingWithMovies = fetchRating(id)
}
}
ratingWithMovies?.let { ratingWithMovies ->
ListScaffold(
title = ratingWithMovies.rating.name,
items = ratingWithMovies.movies,
onItemClicked = onMovieClicked,
onDeleteSelectedItems = onDeleteSelectedMovies,
onResetDatabase = null,
itemIconId = R.drawable.movie_24,
itemContentDescriptionId = R.string.movie,
modifier = modifier,
fixedContentAbove = {
Label(textId = R.string.name)
Display(text = ratingWithMovies.rating.name)
Label(textId = R.string.description)
Display(text = ratingWithMovies.rating.description)
Label(
textId = R.string.movies_rated,
ratingWithMovies.rating.name
)
},
cardContent = { movie ->
Display(
text = movie.title
)
},
)
}
}
ADDED: app/src/main/java/com/androidbyexample/movies/screens/RatingListUi.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.androidbyexample.movies.R
import com.androidbyexample.movies.helper.Display
import com.androidbyexample.movies.helper.ListScaffold
import com.androidbyexample.movies.repository.RatingDto
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingListUi(
ratings: List<RatingDto>,
modifier: Modifier = Modifier,
onRatingClicked: (String) -> Unit,
onDeleteSelectedRatings: (Set<String>) -> Unit,
onResetDatabase: () -> Unit,
) {
ListScaffold(
title = stringResource(R.string.ratings),
items = ratings,
onItemClicked = onRatingClicked,
onDeleteSelectedItems = onDeleteSelectedRatings,
onResetDatabase = onResetDatabase,
itemIconId = R.drawable.star_24,
itemContentDescriptionId = R.string.rating,
modifier = modifier
) { rating ->
Display(text = rating.name)
}
}
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Screens.kt
package com.androidbyexample.movies.screens
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.androidbyexample.movies.R
import kotlinx.parcelize.Parcelize
sealed interface Screen: Parcelable
sealed class ListScreen(
@DrawableRes val iconId: Int,
@StringRes val labelId: Int,
): Screen
@Parcelize
//data object MovieList: Screen
data object MovieList: ListScreen(
iconId = R.drawable.movie_24,
labelId = R.string.movies,
)
@Parcelize
data object RatingList: ListScreen(
iconId = R.drawable.star_24,
labelId = R.string.ratings,
)
@Parcelize
data object ActorList: ListScreen(
iconId = R.drawable.person_24,
labelId = R.string.actors,
)
@Parcelize
data class MovieDisplay(val id: String): Screen
@Parcelize
data class ActorDisplay(val id: String): Screen
@Parcelize
data class RatingDisplay(val id: String): Screen
CHANGED: app/src/main/java/com/androidbyexample/movies/screens/Ui.kt
package com.androidbyexample.movies.screens
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import com.androidbyexample.movies.MovieViewModel
import com.androidbyexample.movies.R
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun Ui(
viewModel: MovieViewModel,
) {
// val listDetailStrategy = rememberListDetailSceneStrategy<Screen>()
val windowAdaptiveInfo = currentWindowAdaptiveInfoV2()
val directive = remember(windowAdaptiveInfo) {
calculatePaneScaffoldDirective(windowAdaptiveInfo)
.copy(horizontalPartitionSpacerSize = 0.dp)
}
val listDetailStrategy = rememberListDetailSceneStrategy<Screen>(directive = directive)
val backStack by viewModel.backStackFlow.collectAsStateWithLifecycle(listOf(MovieList))
}
ADDED: app/src/main/res/drawable/person_24.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M367,433q-47,-47 -47,-113t47,-113q47,-47 113,-47t113,47q47,47 47,113t-47,113q-47,47 -113,47t-113,-47ZM160,800v-112q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v112L160,800ZM240,720h480v-32q0,-11 -5.5,-20T700,654q-54,-27 -109,-40.5T480,600q-56,0 -111,13.5T260,654q-9,5 -14.5,14t-5.5,20v32ZM536.5,376.5Q560,353 560,320t-23.5,-56.5Q513,240 480,240t-56.5,23.5Q400,287 400,320t23.5,56.5Q447,400 480,400t56.5,-23.5ZM480,320ZM480,720Z"
android:fillColor="#e3e3e3"/>
</vector>
ADDED: app/src/main/res/drawable/star_24.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m354,673 l126,-76 126,77 -33,-144 111,-96 -146,-13 -58,-136 -58,135 -146,13 111,97 -33,143ZM233,840l65,-281L80,370l288,-25 112,-265 112,265 288,25 -218,189 65,281 -247,-149 -247,149ZM480,490Z"
android:fillColor="#e3e3e3"/>
</vector>
CHANGED: app/src/main/res/values/strings.xml
<resources>
<string name="app_name">movies</string>
<string name="movies">Movies</string>
<string name="actors">Actors</string>
<string name="ratings">Ratings</string>
<string name="actor">Actor</string>
<string name="rating">Rating</string>
<string name="loading">Loading…</string>
<string name="name">Name</string>
<string name="movies_starring">Movies Starring %1$s</string>
<string name="movies_rated">Movies Rated %1$s</string>
<string name="title">Title</string>
<string name="description">Description</string>
<string name="movie">Movie</string>
<string name="reset_database">Reset database</string>
<string name="cast">Cast</string>
<string name="cast_entry">%1$s: %2$s</string>
<string name="clear_selections">Clear Selections</string>
<string name="delete_selected_items">Delete selected items</string>
<string name="select_a_movie_to_view">Select a movie to view</string>
<string name="select_an_actor_to_view">Select an actor to view</string>
<string name="select_a_rating_to_view">Select a rating to view</string>
</resources>
CHANGED: gradle/libs.versions.toml
[versions]
agp = "9.2.1"
coreKtx = "1.19.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.11.0"
activityCompose = "1.13.0"
kotlin = "2.4.0"
composeBom = "2026.06.01"
navigation = "1.1.4"
material3AdaptiveNav3 = "1.3.0-rc01"
appcompat = "1.7.1"
material = "1.14.0"
room = "2.8.4"
ksp = "2.3.9"
compileSdk = "37"
targetSdk = "37"
minSdk = "24"
javaVersion = "VERSION_11"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation" }
androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" }
androidx-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" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }