Movies UI - Lists

Add Actors and Ratings

To flesh out the app more, let's add in Actors and Ratings.

First, we create the screen data to represent these new screens

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/Screens.kt
// ...
sealed interface Screen
data object MovieList: Screen
data object ActorList: Screen
data object RatingList: Screen
data class MovieDisplay(val id: String): Screen
data class ActorDisplay(val id: String): Screen
data class RatingDisplay(val id: String): Screen

We'll create a common ListScaffold that we can reuse on all "list" screens. (We'll define it in a bit, but need some prep work first).

At the bottom of the list scaffold we'll place three buttons which act like tabs.

We define ScreenSelectButton for these tabs.

show in full file app/src/main/java/com/androidbyexample/compose/movies/components/ScreenSelectButton.kt
// ...
import com.androidbyexample.compose.movies.screens.Screen

@Composable
fun RowScope.ScreenSelectButton(
    currentScreen: Screen,
    targetScreen: Screen,
    imageVector: ImageVector,
    @StringRes labelId: Int,
    onSelectListScreen: (Screen) -> Unit,
) =
    NavigationBarItem(
        selected = currentScreen == targetScreen,
        icon = {
            Icon(
                imageVector = imageVector,
                contentDescription = stringResource(id = labelId)
            )
        },
        label = {
            Text(text = stringResource(id = labelId))
        },
        onClick = {
            onSelectListScreen(targetScreen)
        }
    )

Each tab takes a Screen as its target, and calls onSelectListScreen() passing that target screen when clicked. We use NavigationBarItem, provided by the Material 3 library, for the tab-like styling.

This type of navigation requires a new function in the view model to explicitly set the screen stack to the selected list screen.

show in full file app/src/main/java/com/androidbyexample/compose/movies/MovieViewModel.kt
// ...
class MovieViewModel(
    // ...
): ViewModel(), MovieRepository by repository {
    // ...
    }

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

    fun deleteSelectedMovies() {
        // ...
}

These buttons require some extra parameters to track which is selected (currentScreen) and what to do when they're clicked (onSelectListScreen). We pass these up to all callers up through Ui.

We need to pass the currentScreen and onSelectListScreen to Composables that will use our ListScaffold, which uses the ScreenSelectButtons. We add them to MovieListUi so we can pass them to the ListScaffold.

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
// ...
//@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
    // ...
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onDeleteSelectedMovies: () -> Unit,
    onResetDatabase: () -> Unit,
) {
    // ...
}

When calling MovieListUi in Ui, we'll need to pass values.

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
// ...

@Composable
fun Ui(
    // ...
) {
    // ...
    when (val screen = viewModel.currentScreen) {
        // ...
        MovieList -> {
            // ...
            MovieListUi(
                // ...
                onSelectionToggle = viewModel::toggleSelection,
                onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
                currentScreen = screen,
                onSelectListScreen = viewModel::setScreen,
                onResetDatabase = {
                    scope.launch (Dispatchers.IO) {
                        // ...
            )
        }
        // ...
    }
}

We'll place these buttons in a NavigationBar in the bottomBar slot of the ListScaffold.

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").

Now we create the ListScaffold

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/ListScaffold.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T: HasId> ListScaffold(
    @StringRes titleId: Int,
    items: List<T>,
    onItemClicked: (T) -> Unit,
    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedItems: () -> Unit,
    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onResetDatabase: () -> Unit,
    modifier: Modifier = Modifier,
    itemContent: @Composable ColumnScope.(T) -> Unit,
) {
    Scaffold(
        topBar = {
            if (selectedIds.isEmpty()) {
                TopAppBar(
                    title = { Text(text = stringResource(titleId)) },
                    actions = {
                        IconButton(onClick = onResetDatabase) {
                            Icon(
                                imageVector = Icons.Default.Refresh,
                                contentDescription = stringResource(R.string.reset_database)
                            )
                        }
                    }
                )
            } else {
                TopAppBar(
                    navigationIcon = {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = stringResource(R.string.clear_selections),
                            modifier = Modifier.clickable(onClick = onClearSelections),
                        )
                    },
                    title = { Text(text = selectedIds.size.toString(), modifier = Modifier.padding(8.dp)) },
                    actions = {
                        IconButton(onClick = onDeleteSelectedItems) {
                            Icon(
                                imageVector = Icons.Default.Delete,
                                contentDescription = stringResource(R.string.delete_selected_items)
                            )
                        }
                    },
                )
            }
        },
        bottomBar = {
            NavigationBar {
                ScreenSelectButton(
                    targetScreen = RatingList,
                    imageVector = Icons.Default.Star,
                    labelId = R.string.ratings,
                    currentScreen = currentScreen,
                    onSelectListScreen = onSelectListScreen
                )
                ScreenSelectButton(
                    targetScreen = MovieList,
                    imageVector = Icons.Default.Star,
                    labelId = R.string.movies,
                    currentScreen = currentScreen,
                    onSelectListScreen = onSelectListScreen
                )
                ScreenSelectButton(
                    targetScreen = ActorList,
                    imageVector = Icons.Default.Person,
                    labelId = R.string.actors,
                    currentScreen = currentScreen,
                    onSelectListScreen = onSelectListScreen
                )
            }
        },
        modifier = modifier
    ) { paddingValues ->
        List(
            items = items,
            onItemClicked = onItemClicked,
            selectedIds = selectedIds,
            onSelectionToggle = onSelectionToggle,
            onClearSelections = onClearSelections,
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize(),
            itemContent = itemContent,
        )
    }
}

and use it in MovieListUi

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
// ...
//@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
    // ...
    onResetDatabase: () -> Unit,
) {
//  Scaffold(
//      topBar = {
//          if (selectedIds.isEmpty()) {
//              TopAppBar(
//                  title = { Text(text = stringResource(R.string.movies)) },
//                  actions = {
//                      IconButton(onClick = onResetDatabase) {
//                          Icon(
//                              imageVector = Icons.Default.Refresh,
//                              contentDescription = stringResource(R.string.reset_database)
//                          )
//                      }
//                  }
//              )
//          } else {
//              TopAppBar(
//                  navigationIcon = {
//                      Icon(
//                          imageVector = Icons.AutoMirrored.Filled.ArrowBack,
//                          contentDescription = stringResource(R.string.clear_selections),
//                          modifier = Modifier.clickable(onClick = onClearSelections),
//                      )
//                  },
//                  title = {
//                      Text(
//                          text = selectedIds.size.toString(),
//                          modifier = Modifier.padding(8.dp)
//                      )
//                  },
//                  actions = {
//                      IconButton(onClick = onDeleteSelectedMovies) {
//                          Icon(
//                              imageVector = Icons.Default.Delete,
//                              contentDescription = stringResource(R.string.delete_selected_items)
//                          )
//                      }
//                  },
//              )
//          }
//      },
//      modifier = modifier,
//  ) { innerPadding ->
//      List(
    ListScaffold(
        titleId = R.string.movies,
        items = movies,
        onItemClicked = onMovieClicked,
        selectedIds = selectedIds,
        onSelectionToggle = onSelectionToggle,
        onClearSelections = onClearSelections,
//          modifier = Modifier
//              .padding(innerPadding)
//              .fillMaxSize()
        onDeleteSelectedItems = onDeleteSelectedMovies,
        currentScreen = currentScreen,
        onSelectListScreen = onSelectListScreen,
        onResetDatabase = onResetDatabase,
        modifier = modifier,
    ) { movie ->
//          Row (
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(8.dp)
        ) {
            Icon(
                imageVector = Icons.Default.Star,
                contentDescription = stringResource(id = R.string.movie),
                modifier = Modifier.clickable {
                    onSelectionToggle(movie.id)
                }
            )
            Display(text = movie.title)
        }
    }
//  }
}

Be sure to fixup the Modifier calls to pass fillMaxSize from the activity:

show in full file app/src/main/java/com/androidbyexample/compose/movies/MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
    // ...
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        setContent {
            MoviesTheme {
                Ui(
                    viewModel = viewModel,
                    modifier = Modifier.fillMaxSize()
                ) {
                    finish()
                }
            }
        }
    }
}

We're going to need to tweak our Label helper. Right now, we pass a string resource id, which it looks up. For our new screens, we'll be using arguments in strings, so we'll need to resolve them outside of the Label. We add an overload that takes a String and modify the original to resolve the string resource id and pass it to the new function.

show in full file app/src/main/java/com/androidbyexample/compose/movies/components/Label.kt
// ...

@Composable
fun Label(
    @StringRes textId: Int,
    modifier: Modifier = Modifier,
) {
    Label(
        text = stringResource(textId),
        modifier = modifier,
    )
}

@Composable
fun Label(
    text: String,
    modifier: Modifier = Modifier,
) {
    Text(
//      text = stringResource(id = textId),
        text = text,
        style = MaterialTheme.typography.titleMedium,
        modifier = modifier
            .padding(8.dp)
            .fillMaxWidth(),
    )
}

The new actor display screen will use RoleWithMovieDto in a list. We need to make it implement HasId to work with our generic ListUi. We'll derive the id property in the same way we did for RoleWithActorDto

show in full file repository/src/main/java/com/androidbyexample/compose/movies/repository/ActorDto.kt
// ...
data class RoleWithMovieDto(
    val movie: MovieDto,
    val character: String,
    val orderInCredits: Int,
//)
): HasId {
    override val id: String
        get() = "${movie.id}:$character"
}
internal fun RoleWithMovie.toDto() =
    RoleWithMovieDto(
        // ...

Now we add the new screens. These new screens are similar to the existing screens, but be careful of where you get the ids from. The MovieDisplayUi, for example, displays a list of RoleWithActorDtos, and when clicked, we need to call it.actor.id to get the proper id. (If you use just it.id here, the fetch will fail when looking up the actor)

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/ActorList.kt
// ...
import com.androidbyexample.compose.movies.repository.ActorDto

@Composable
fun ActorListUi(
    actors: List<ActorDto>,
    onActorClicked: (ActorDto) -> Unit,
    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedActors: () -> Unit,
    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onResetDatabase: () -> Unit,
) {
    ListScaffold(
        titleId = R.string.actors,
        items = actors,
        onItemClicked = onActorClicked,
        selectedIds = selectedIds,
        onSelectionToggle = onSelectionToggle,
        onClearSelections = onClearSelections,
        onDeleteSelectedItems = onDeleteSelectedActors,
        currentScreen = currentScreen,
        onSelectListScreen = onSelectListScreen,
        onResetDatabase = onResetDatabase
    ) { actor ->
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(8.dp)
        ) {
            Icon(
                imageVector = Icons.Default.Person,
                contentDescription = stringResource(id = R.string.actor),
                modifier = Modifier.clickable {
                    onSelectionToggle(actor.id)
                }
            )
            Display(text = actor.name)
        }
    }
}
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/RatingList.kt
// ...
import com.androidbyexample.compose.movies.repository.RatingDto

@Composable
fun RatingListUi(
    ratings: List<RatingDto>,
    onRatingClicked: (RatingDto) -> Unit,
    selectedIds: Set<String>,
    onSelectionToggle: (id: String) -> Unit,
    onClearSelections: () -> Unit,
    onDeleteSelectedRatings: () -> Unit,
    currentScreen: Screen,
    onSelectListScreen: (Screen) -> Unit,
    onResetDatabase: () -> Unit,
) {
    ListScaffold(
        titleId = R.string.ratings,
        items = ratings,
        onItemClicked = onRatingClicked,
        selectedIds = selectedIds,
        onSelectionToggle = onSelectionToggle,
        onClearSelections = onClearSelections,
        onDeleteSelectedItems = onDeleteSelectedRatings,
        currentScreen = currentScreen,
        onSelectListScreen = onSelectListScreen,
        onResetDatabase = onResetDatabase
    ) { rating ->
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(8.dp)
        ) {
            Icon(
                imageVector = Icons.Default.Star,
                contentDescription = stringResource(id = R.string.rating),
                modifier = Modifier.clickable {
                    onSelectionToggle(rating.id)
                }
            )
            Display(text = rating.name)
        }
    }
}

Next we create the display screens for actor and rating

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/ActorDisplay.kt
// ...
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ActorDisplayUi(
    id: String,
    fetchActor: suspend (String) -> ActorWithFilmographyDto,
    onMovieClicked: (MovieDto) -> Unit,
) {
    var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            actorWithFilmography = fetchActor(id)
        }
    }
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = actorWithFilmography?.actor?.name ?: stringResource(R.string.loading))
                }
            )
        }
    ) { paddingValues ->
        actorWithFilmography?.let { actorWithFilmography ->
            Column(
                modifier = Modifier
                    .padding(paddingValues)
            ) {
                Label(textId = R.string.name)
                Display(text = actorWithFilmography.actor.name)
                Label(
                    text = stringResource(
                        id = R.string.movies_starring,
                        actorWithFilmography.actor.name
                    )
                )
                List(
                    items = actorWithFilmography.filmography.sortedBy { it.movie.title },
                    onItemClicked = { onMovieClicked(it.movie) },
                    selectedIds = emptySet(),
                    onSelectionToggle = {},
                    onClearSelections = {},
                    modifier = Modifier.weight(1f)
                ) { role ->
                    Display(text = role.movie.title)
                }
            }
        }
    }
}
show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/RatingDisplay.kt
// ...
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RatingDisplayUi(
    id: String,
    fetchRating: suspend (String) -> RatingWithMoviesDto,
    onMovieClicked: (MovieDto) -> Unit,
) {
    var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) }
    LaunchedEffect(key1 = id) {
        withContext(Dispatchers.IO) {
            ratingWithMovies = fetchRating(id)
        }
    }
    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(text = ratingWithMovies?.rating?.name ?: stringResource(R.string.loading))
                }
            )
        }
    ) { paddingValues ->
        ratingWithMovies?.let { ratingWithMovies ->
            Column(
                modifier = Modifier
                    .padding(paddingValues)
            ) {
                Label(textId = R.string.name)
                Display(text = ratingWithMovies.rating.name)
                Label(textId = R.string.description)
                Display(text = ratingWithMovies.rating.description)
                Label(
                    text = stringResource(
                        id = R.string.movies_rated,
                        ratingWithMovies.rating.name
                    )
                )
                List(
                    items = ratingWithMovies.movies.sortedBy { it.title },
                    onItemClicked = onMovieClicked,
                    selectedIds = emptySet(),
                    onSelectionToggle = {},
                    onClearSelections = {},
                    modifier = Modifier.weight(1f)
                ) { movie ->
                    Display(
                        text = movie.title
                    )
                }
            }
        }
    }
}

Define new user-facing strings

show in full file app/src/main/res/values/strings.xml
<resources>
    // ...
    <string name="clear_selections">Clear Selections</string>
    <string name="delete_selected_items">Delete selected items</string>
    <string name="ratings">Ratings</string>
    <string name="rating">Rating</string>
    <string name="actors">Actors</string>
    <string name="actor">Actor</string>
    <string name="name">Name</string>
    <string name="movies_rated">Movies rated %1$s</string>
    <string name="movies_starring">Movies starring %1$s</string>
</resources>

Fix the onActorClicked parameter being passed to MovieDisplayUi to push the screen for the clicked RoleWithActorDto

Finally, we add the calls to the new screens in Ui

show in full file app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
// ...

@Composable
fun Ui(
    // ...
) {
    // ...
    when (val screen = viewModel.currentScreen) {
        // ...
        is MovieDisplay -> {
            MovieDisplayUi(
                id = screen.id,
                fetchMovie = viewModel::getMovieWithCast,
//              onActorClicked = { }, // empty for now. we'll fix later
                onActorClicked = { viewModel.pushScreen(ActorDisplay(it.actor.id)) },
                modifier = modifier,
            )
        }
        is ActorDisplay -> {
            ActorDisplayUi(
                id = screen.id,
                fetchActor = viewModel::getActorWithFilmography,
                onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }
            )
        }
        is RatingDisplay -> {
            RatingDisplayUi(
                id = screen.id,
                fetchRating = viewModel::getRatingWithMovies,
                onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) }
            )
        }
        MovieList -> {
            val movies by viewModel.moviesFlow.collectAsStateWithLifecycle(
                // ...
            )
        }
        ActorList -> {
            val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
            val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
            ActorListUi(
                actors = actors,
                onActorClicked = { actor ->
                    viewModel.pushScreen(ActorDisplay(actor.id))
                },
                selectedIds = selectedIds,
                onClearSelections = viewModel::clearSelectedIds,
                onSelectionToggle = viewModel::toggleSelection,
                onDeleteSelectedActors = viewModel::deleteSelectedActors,
                currentScreen = screen,
                onSelectListScreen = viewModel::setScreen,
                onResetDatabase = {
                    scope.launch(Dispatchers.IO) {
                        viewModel.resetDatabase()
                    }
                }
            )
        }
        RatingList -> {
            val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(initialValue = emptyList())
            val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet())
            RatingListUi(
                ratings = ratings,
                onRatingClicked = { rating ->
                    viewModel.pushScreen(RatingDisplay(rating.id))
                },
                selectedIds = selectedIds,
                onClearSelections = viewModel::clearSelectedIds,
                onSelectionToggle = viewModel::toggleSelection,
                onDeleteSelectedRatings = viewModel::deleteSelectedRatings,
                currentScreen = screen,
                onSelectListScreen = viewModel::setScreen,
                onResetDatabase = {
                    scope.launch(Dispatchers.IO) {
                        viewModel.resetDatabase()
                    }
                }
            )
        }
    }
}

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.

Note

We're still using a Star icon for movies. We'll pick a better icon in the final step.

You'll notice that we currently have no means of navigating to the actor and rating list screens. To do this, we'll add tabs at the bottom of the list screens to jump to other lists.

Navigation-wise, we'll treat each of these jumps as restarting navigation, replacing the stack with the destination. This isn't necessary, but I wanted to demonstrate a navigation alternative.

Now let's get a better movie icon...


All code changes

CHANGED: app/src/main/java/com/androidbyexample/compose/movies/MainActivity.kt
package com.androidbyexample.compose.movies

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import com.androidbyexample.compose.movies.screens.Ui
import com.androidbyexample.compose.movies.ui.theme.MoviesTheme

class MainActivity : ComponentActivity() {
    private val viewModel by viewModels<MovieViewModel> { MovieViewModel.Factory }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MoviesTheme {
                Ui(
                    viewModel = viewModel,
modifier = Modifier.fillMaxSize()
) { finish() } } } } }
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/MovieViewModel.kt
package com.androidbyexample.compose.movies

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.androidbyexample.compose.movies.repository.MovieDatabaseRepository
import com.androidbyexample.compose.movies.repository.MovieRepository
import com.androidbyexample.compose.movies.screens.MovieList
import com.androidbyexample.compose.movies.screens.Screen
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class MovieViewModel(
    private val repository: MovieRepository,
): ViewModel(), MovieRepository by repository {
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 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( MovieDatabaseRepository.create(application) ) as T } } } }
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/components/Label.kt
package com.androidbyexample.compose.movies.components

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, modifier: Modifier = Modifier, ) { Label( text = stringResource(textId), modifier = modifier, ) } @Composable fun Label( text: String, modifier: Modifier = Modifier, ) { Text( // text = stringResource(id = textId), text = text, style = MaterialTheme.typography.titleMedium, modifier = modifier .padding(8.dp) .fillMaxWidth(), ) }
ADDED: app/src/main/java/com/androidbyexample/compose/movies/components/ScreenSelectButton.kt
package com.androidbyexample.compose.movies.components

import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import com.androidbyexample.compose.movies.screens.Screen

@Composable fun RowScope.ScreenSelectButton( currentScreen: Screen, targetScreen: Screen, imageVector: ImageVector, @StringRes labelId: Int, onSelectListScreen: (Screen) -> Unit, ) = NavigationBarItem( selected = currentScreen == targetScreen, icon = { Icon( imageVector = imageVector, contentDescription = stringResource(id = labelId) ) }, label = { Text(text = stringResource(id = labelId)) }, onClick = { onSelectListScreen(targetScreen) } )
ADDED: app/src/main/java/com/androidbyexample/compose/movies/screens/ActorDisplay.kt
package com.androidbyexample.compose.movies.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.components.Label
import com.androidbyexample.compose.movies.repository.ActorWithFilmographyDto
import com.androidbyexample.compose.movies.repository.MovieDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class) @Composable fun ActorDisplayUi( id: String, fetchActor: suspend (String) -> ActorWithFilmographyDto, onMovieClicked: (MovieDto) -> Unit, ) { var actorWithFilmography by remember { mutableStateOf<ActorWithFilmographyDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { actorWithFilmography = fetchActor(id) } } Scaffold( topBar = { TopAppBar( title = { Text(text = actorWithFilmography?.actor?.name ?: stringResource(R.string.loading)) } ) } ) { paddingValues -> actorWithFilmography?.let { actorWithFilmography -> Column( modifier = Modifier .padding(paddingValues) ) { Label(textId = R.string.name) Display(text = actorWithFilmography.actor.name) Label( text = stringResource( id = R.string.movies_starring, actorWithFilmography.actor.name ) ) List( items = actorWithFilmography.filmography.sortedBy { it.movie.title }, onItemClicked = { onMovieClicked(it.movie) }, selectedIds = emptySet(), onSelectionToggle = {}, onClearSelections = {}, modifier = Modifier.weight(1f) ) { role -> Display(text = role.movie.title) } } } } }
ADDED: app/src/main/java/com/androidbyexample/compose/movies/screens/ActorList.kt
package com.androidbyexample.compose.movies.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.repository.ActorDto

@Composable fun ActorListUi( actors: List<ActorDto>, onActorClicked: (ActorDto) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedActors: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit, ) { ListScaffold( titleId = R.string.actors, items = actors, onItemClicked = onActorClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedActors, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase ) { actor -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Person, contentDescription = stringResource(id = R.string.actor), modifier = Modifier.clickable { onSelectionToggle(actor.id) } ) Display(text = actor.name) } } }
ADDED: app/src/main/java/com/androidbyexample/compose/movies/screens/ListScaffold.kt
package com.androidbyexample.compose.movies.screens

import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.compose.movies.R
import com.androidbyexample.compose.movies.components.ScreenSelectButton
import com.androidbyexample.compose.movies.repository.HasId

@OptIn(ExperimentalMaterial3Api::class)
@Composable fun <T: HasId> ListScaffold( @StringRes titleId: Int, items: List<T>, onItemClicked: (T) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedItems: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit, modifier: Modifier = Modifier, itemContent: @Composable ColumnScope.(T) -> Unit, ) { Scaffold( topBar = { if (selectedIds.isEmpty()) { TopAppBar( title = { Text(text = stringResource(titleId)) }, actions = { IconButton(onClick = onResetDatabase) { Icon( imageVector = Icons.Default.Refresh, contentDescription = stringResource(R.string.reset_database) ) } } ) } else { TopAppBar( navigationIcon = { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.clear_selections), modifier = Modifier.clickable(onClick = onClearSelections), ) }, title = { Text(text = selectedIds.size.toString(), modifier = Modifier.padding(8.dp)) }, actions = { IconButton(onClick = onDeleteSelectedItems) { Icon( imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete_selected_items) ) } }, ) } }, bottomBar = { NavigationBar { ScreenSelectButton( targetScreen = RatingList, imageVector = Icons.Default.Star, labelId = R.string.ratings, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = MovieList, imageVector = Icons.Default.Star, labelId = R.string.movies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) ScreenSelectButton( targetScreen = ActorList, imageVector = Icons.Default.Person, labelId = R.string.actors, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen ) } }, modifier = modifier ) { paddingValues -> List( items = items, onItemClicked = onItemClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, modifier = Modifier .padding(paddingValues) .fillMaxSize(), itemContent = itemContent, ) } }
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/MovieList.kt
package com.androidbyexample.compose.movies.screens

//import androidx.activity.compose.BackHandler
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.fillMaxSize
import androidx.compose.foundation.layout.padding
//import androidx.compose.foundation.lazy.LazyColumn
//import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
//import androidx.compose.material.icons.automirrored.filled.ArrowBack
//import androidx.compose.material.icons.filled.Delete
//import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
//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.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.repository.MovieDto

//@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) // for TopAppBar
@Composable
fun MovieListUi(
    movies: List<MovieDto>,
    modifier: Modifier = Modifier,
    onMovieClicked: (MovieDto) -> Unit,
selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit,
currentScreen: Screen, onSelectListScreen: (Screen) -> Unit,
onDeleteSelectedMovies: () -> Unit,
onResetDatabase: () -> Unit, ) {
// Scaffold( // topBar = { // if (selectedIds.isEmpty()) { // TopAppBar( // title = { Text(text = stringResource(R.string.movies)) }, // actions = { // IconButton(onClick = onResetDatabase) { // Icon( // imageVector = Icons.Default.Refresh, // contentDescription = stringResource(R.string.reset_database) // ) // } // } // ) // } else { // TopAppBar( // navigationIcon = { // Icon( // imageVector = Icons.AutoMirrored.Filled.ArrowBack, // contentDescription = stringResource(R.string.clear_selections), // modifier = Modifier.clickable(onClick = onClearSelections), // ) // }, // title = { // Text( // text = selectedIds.size.toString(), // modifier = Modifier.padding(8.dp) // ) // }, // actions = { // IconButton(onClick = onDeleteSelectedMovies) { // Icon( // imageVector = Icons.Default.Delete, // contentDescription = stringResource(R.string.delete_selected_items) // ) // } // }, // ) // } // }, // modifier = modifier, // ) { innerPadding -> // List( ListScaffold( titleId = R.string.movies, items = movies, onItemClicked = onMovieClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, // modifier = Modifier // .padding(innerPadding) // .fillMaxSize() onDeleteSelectedItems = onDeleteSelectedMovies, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase, modifier = modifier, ) { movie -> // Row ( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Star, contentDescription = stringResource(id = R.string.movie), modifier = Modifier.clickable { onSelectionToggle(movie.id) } ) Display(text = movie.title) } } // }
}
ADDED: app/src/main/java/com/androidbyexample/compose/movies/screens/RatingDisplay.kt
package com.androidbyexample.compose.movies.screens

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.components.Label
import com.androidbyexample.compose.movies.repository.MovieDto
import com.androidbyexample.compose.movies.repository.RatingWithMoviesDto
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

@OptIn(ExperimentalMaterial3Api::class) @Composable fun RatingDisplayUi( id: String, fetchRating: suspend (String) -> RatingWithMoviesDto, onMovieClicked: (MovieDto) -> Unit, ) { var ratingWithMovies by remember { mutableStateOf<RatingWithMoviesDto?>(null) } LaunchedEffect(key1 = id) { withContext(Dispatchers.IO) { ratingWithMovies = fetchRating(id) } } Scaffold( topBar = { TopAppBar( title = { Text(text = ratingWithMovies?.rating?.name ?: stringResource(R.string.loading)) } ) } ) { paddingValues -> ratingWithMovies?.let { ratingWithMovies -> Column( modifier = Modifier .padding(paddingValues) ) { Label(textId = R.string.name) Display(text = ratingWithMovies.rating.name) Label(textId = R.string.description) Display(text = ratingWithMovies.rating.description) Label( text = stringResource( id = R.string.movies_rated, ratingWithMovies.rating.name ) ) List( items = ratingWithMovies.movies.sortedBy { it.title }, onItemClicked = onMovieClicked, selectedIds = emptySet(), onSelectionToggle = {}, onClearSelections = {}, modifier = Modifier.weight(1f) ) { movie -> Display( text = movie.title ) } } } } }
ADDED: app/src/main/java/com/androidbyexample/compose/movies/screens/RatingList.kt
package com.androidbyexample.compose.movies.screens

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.androidbyexample.compose.movies.R
import com.androidbyexample.compose.movies.components.Display
import com.androidbyexample.compose.movies.repository.RatingDto

@Composable fun RatingListUi( ratings: List<RatingDto>, onRatingClicked: (RatingDto) -> Unit, selectedIds: Set<String>, onSelectionToggle: (id: String) -> Unit, onClearSelections: () -> Unit, onDeleteSelectedRatings: () -> Unit, currentScreen: Screen, onSelectListScreen: (Screen) -> Unit, onResetDatabase: () -> Unit, ) { ListScaffold( titleId = R.string.ratings, items = ratings, onItemClicked = onRatingClicked, selectedIds = selectedIds, onSelectionToggle = onSelectionToggle, onClearSelections = onClearSelections, onDeleteSelectedItems = onDeleteSelectedRatings, currentScreen = currentScreen, onSelectListScreen = onSelectListScreen, onResetDatabase = onResetDatabase ) { rating -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp) ) { Icon( imageVector = Icons.Default.Star, contentDescription = stringResource(id = R.string.rating), modifier = Modifier.clickable { onSelectionToggle(rating.id) } ) Display(text = rating.name) } } }
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/Screens.kt
package com.androidbyexample.compose.movies.screens

sealed interface Screen
data object MovieList: Screen data object ActorList: Screen data object RatingList: Screen data class MovieDisplay(val id: String): Screen data class ActorDisplay(val id: String): Screen data class RatingDisplay(val id: String): Screen
CHANGED: app/src/main/java/com/androidbyexample/compose/movies/screens/Ui.kt
package com.androidbyexample.compose.movies.screens

import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.compose.movies.MovieViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@Composable
fun Ui(
    viewModel: MovieViewModel,
    modifier: Modifier = Modifier,
    onExit: () -> Unit,
) {
    BackHandler {
        viewModel.popScreen()
    }

    val scope = rememberCoroutineScope()

    when (val screen = viewModel.currentScreen) {
        null -> onExit()
        is MovieDisplay -> {
            MovieDisplayUi(
                id = screen.id,
                fetchMovie = viewModel::getMovieWithCast,
// onActorClicked = { }, // empty for now. we'll fix later onActorClicked = { viewModel.pushScreen(ActorDisplay(it.actor.id)) },
modifier = modifier, ) }
is ActorDisplay -> { ActorDisplayUi( id = screen.id, fetchActor = viewModel::getActorWithFilmography, onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) } ) } is RatingDisplay -> { RatingDisplayUi( id = screen.id, fetchRating = viewModel::getRatingWithMovies, onMovieClicked = { viewModel.pushScreen(MovieDisplay(it.id)) } ) }
MovieList -> { val movies by viewModel.moviesFlow.collectAsStateWithLifecycle( initialValue = emptyList() )
val selectedIds by viewModel .selectedIdsFlow .collectAsStateWithLifecycle(initialValue = emptySet())
MovieListUi( movies = movies, modifier = modifier,
selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection,
onDeleteSelectedMovies = viewModel::deleteSelectedMovies,
currentScreen = screen, onSelectListScreen = viewModel::setScreen,
onResetDatabase = { scope.launch (Dispatchers.IO) { viewModel.resetDatabase() } }, onMovieClicked = { movie -> viewModel.pushScreen(MovieDisplay(movie.id)) }, ) }
ActorList -> { val actors by viewModel.actorsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) ActorListUi( actors = actors, onActorClicked = { actor -> viewModel.pushScreen(ActorDisplay(actor.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedActors = viewModel::deleteSelectedActors, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) } RatingList -> { val ratings by viewModel.ratingsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) val selectedIds by viewModel.selectedIdsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) RatingListUi( ratings = ratings, onRatingClicked = { rating -> viewModel.pushScreen(RatingDisplay(rating.id)) }, selectedIds = selectedIds, onClearSelections = viewModel::clearSelectedIds, onSelectionToggle = viewModel::toggleSelection, onDeleteSelectedRatings = viewModel::deleteSelectedRatings, currentScreen = screen, onSelectListScreen = viewModel::setScreen, onResetDatabase = { scope.launch(Dispatchers.IO) { viewModel.resetDatabase() } } ) }
} }
CHANGED: app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">Movies</string>
    <string name="movies">Movies</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="loading">…Loading…</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="ratings">Ratings</string> <string name="rating">Rating</string> <string name="actors">Actors</string> <string name="actor">Actor</string> <string name="name">Name</string> <string name="movies_rated">Movies rated %1$s</string> <string name="movies_starring">Movies starring %1$s</string>
</resources>
CHANGED: repository/src/main/java/com/androidbyexample/compose/movies/repository/ActorDto.kt
package com.androidbyexample.compose.movies.repository

import com.androidbyexample.compose.movies.data.ActorEntity
import com.androidbyexample.compose.movies.data.ActorWithFilmography
import com.androidbyexample.compose.movies.data.RoleWithMovie

data class ActorDto( override val id: String, val name: String, ): HasId
internal fun ActorEntity.toDto() = ActorDto(id = id, name = name) internal fun ActorDto.toEntity() = ActorEntity(id = id, name = name) data class ActorWithFilmographyDto( val actor: ActorDto, val filmography: List<RoleWithMovieDto>, )
data class RoleWithMovieDto( val movie: MovieDto, val character: String, val orderInCredits: Int, //) ): HasId { override val id: String get() = "${movie.id}:$character" }
internal fun RoleWithMovie.toDto() = RoleWithMovieDto( movie = movie.toDto(), character = role.character, orderInCredits = role.orderInCredits, ) internal fun ActorWithFilmography.toDto() = ActorWithFilmographyDto( actor = actor.toDto(), filmography = rolesWithMovies.map { it.toDto() } )