Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use type-safety arguments for navigation compose #309

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id "moop.android.application"
id "moop.android.compose"
id "moop.android.hilt"
id "org.jetbrains.kotlin.plugin.serialization"
alias libs.plugins.firebase.crashlytics
alias libs.plugins.dependencyGuard
alias(libs.plugins.baselineprofile)
Expand Down Expand Up @@ -105,6 +106,7 @@ dependencies {
runtimeOnly projects.core.imageloading.impl

implementation libs.kotlin.stdlib
implementation libs.kotlin.serialization
implementation libs.coroutines.core
implementation libs.coroutines.android

Expand Down
48 changes: 25 additions & 23 deletions app/src/main/java/soup/movie/ui/main/MainNavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,30 @@
package soup.movie.ui.main

import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
import soup.movie.core.designsystem.windowsizeclass.WindowWidthSizeClass
import soup.movie.feature.detail.rememberDetailComposableFactory
import soup.movie.feature.home.rememberHomeComposableFactory
import soup.movie.feature.search.rememberSearchComposableFactory
import soup.movie.feature.settings.rememberSettingsComposableFactory

private enum class Screen(val route: String) {
Main("main"),
Search("search"),
Settings("settings"),
Detail("detail"),
}
private sealed interface Screen {

@Serializable
data object Main : Screen

@Serializable
data object Search : Screen

@Serializable
data object Settings : Screen

private fun NavController.navigateToDetail(movieId: String) {
navigate(route = Screen.Detail.route + "/" + movieId)
@Serializable
data class Detail(val movieId: String) : Screen
}

@Composable
Expand All @@ -45,42 +49,40 @@ fun MainNavGraph(
val navController = rememberNavController()
NavHost(
navController,
startDestination = Screen.Main.route,
startDestination = Screen.Main,
) {
composable(Screen.Main.route) {
composable<Screen.Main> {
val factory = rememberHomeComposableFactory()
factory.HomeNavGraph(
widthSizeClass = widthSizeClass,
onSearchClick = {
navController.navigate(Screen.Search.route)
navController.navigate(Screen.Search)
},
onSettingsClick = {
navController.navigate(Screen.Settings.route)
navController.navigate(Screen.Settings)
},
onMovieItemClick = {
navController.navigateToDetail(movieId = it.id)
navController.navigate(Screen.Detail(movieId = it.id))
},
)
}
composable(Screen.Search.route) {
composable<Screen.Search> {
val factory = rememberSearchComposableFactory()
factory.SearchScreen(
upPress = { navController.navigateUp() },
onItemClick = {
navController.navigateToDetail(movieId = it.id)
navController.navigate(Screen.Detail(movieId = it.id))
},
)
}
composable(Screen.Settings.route) {
composable<Screen.Settings> {
val factory = rememberSettingsComposableFactory()
factory.SettingsNavGraph()
}
composable(
route = Screen.Detail.route + "/{movieId}",
arguments = listOf(navArgument("movieId") { nullable = false }),
) {
composable<Screen.Detail> { backStackEntry ->
val movieId = backStackEntry.toRoute<Screen.Detail>().movieId
val factory = rememberDetailComposableFactory()
factory.DetailNavGraph()
factory.DetailNavGraph(movieId = movieId)
}
}
}
2 changes: 2 additions & 0 deletions data/model/src/main/java/soup/movie/model/MovieDetailModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ package soup.movie.model
* @param boxOffice 박스오피스 정보
*/
data class MovieDetailModel(
val movie: MovieModel,

val id: String,
val score: Int,
val title: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package soup.movie.data.network.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import soup.movie.model.MovieDetailModel
import soup.movie.model.MovieModel
import soup.movie.model.TheaterRatingsModel

/**
* @param genres 장르
Expand Down Expand Up @@ -59,6 +61,24 @@ class MovieDetailResponse(

fun MovieDetailResponse.asModel(): MovieDetailModel {
return MovieDetailModel(
movie = MovieModel(
id = id,
score = score,
title = title,
posterUrl = posterUrl,
openDate = openDate,
isNow = isNow,
age = age,
nationFilter = nationFilter,
genres = genres,
boxOffice = boxOffice?.rank,
theater = TheaterRatingsModel(
cgv = cgv?.star,
lotte = lotte?.star,
megabox = megabox?.star,
),
),

id = id,
score = score,
title = title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import dagger.hilt.components.SingletonComponent

interface DetailComposableFactory {
@Composable
fun DetailNavGraph()
fun DetailNavGraph(movieId: String)
}

@Composable
Expand Down
3 changes: 3 additions & 0 deletions feature/detail/impl/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id "moop.android.library"
id "moop.android.compose"
id "moop.android.hilt"
id "org.jetbrains.kotlin.plugin.serialization"
}

android {
Expand All @@ -22,9 +23,11 @@ dependencies {
implementation projects.feature.detail.api

implementation libs.kotlin.stdlib
implementation libs.kotlin.serialization

implementation libs.androidx.activity.compose
implementation libs.androidx.hilt.navigation.compose
implementation libs.androidx.navigation.compose
implementation libs.compose.foundation
implementation libs.compose.material
implementation libs.compose.ui
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,13 @@
package soup.movie.feature.detail.impl

import androidx.compose.runtime.Composable
import androidx.hilt.navigation.compose.hiltViewModel
import soup.movie.feature.detail.DetailComposableFactory
import javax.inject.Inject

class DetailComposableFactoryImpl @Inject constructor() : DetailComposableFactory {

@Composable
override fun DetailNavGraph() {
DetailNavGraph(
viewModel = hiltViewModel(),
)
override fun DetailNavGraph(movieId: String) {
soup.movie.feature.detail.impl.DetailNavGraph(movieId = movieId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp
internal fun DetailContent(
viewModel: DetailViewModel,
uiModel: DetailUiModel,
onPosterClick: () -> Unit,
onPosterClick: (String) -> Unit,
onItemClick: (ContentItemUiModel) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -42,7 +42,7 @@ internal fun DetailContent(
DetailHeader(
uiModel = uiModel.header,
onPosterClick = {
onPosterClick()
onPosterClick(uiModel.header.movie.posterUrl)
},
modifier = Modifier.padding(bottom = 8.dp),
actions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,55 +15,53 @@
*/
package soup.movie.feature.detail.impl

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import soup.movie.core.designsystem.showToast
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
import soup.compose.material.motion.animation.materialSharedAxisZIn
import soup.compose.material.motion.animation.materialSharedAxisZOut

private sealed interface DetailScreen {

@Serializable
data class Home(val movieId: String) : DetailScreen

@Serializable
data class Poster(val posterUrl: String) : DetailScreen
}

@Composable
fun DetailNavGraph(
viewModel: DetailViewModel,
) {
val context = LocalContext.current
val uiModel: DetailUiModel by viewModel.uiModel.collectAsState()
Box {
val movie = (uiModel as? DetailUiModel.Success)?.header?.movie
var showPoster by remember { mutableStateOf(false) }
DetailScreen(
viewModel = viewModel,
uiModel = uiModel,
onPosterClick = {
showPoster = true
},
)
if (movie != null) {
AnimatedVisibility(
visible = showPoster,
enter = fadeIn(),
exit = fadeOut(),
) {
DetailPoster(
movie = movie,
upPress = { showPoster = false },
)
}
fun DetailNavGraph(movieId: String) {
val navController = rememberNavController()
NavHost(
navController,
startDestination = DetailScreen.Home(movieId),
enterTransition = { materialSharedAxisZIn(forward = true) },
exitTransition = { materialSharedAxisZOut(forward = true) },
popEnterTransition = { materialSharedAxisZIn(forward = false) },
popExitTransition = { materialSharedAxisZOut(forward = false) },
) {
composable<DetailScreen.Home> {
val viewModel = hiltViewModel<DetailViewModel>()
DetailScreen(
viewModel = viewModel,
onPosterClick = {
navController.navigate(DetailScreen.Poster(posterUrl = it))
},
)
}
}

LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
is ToastAction -> context.showToast(event.resId)
}
composable<DetailScreen.Poster> { backStackEntry ->
val posterUrl = backStackEntry.toRoute<DetailScreen.Poster>().posterUrl
DetailPoster(
posterUrl = posterUrl,
upPress = {
navController.navigateUp()
},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ import soup.compose.photo.ExperimentalPhotoApi
import soup.compose.photo.PhotoBox
import soup.compose.photo.rememberPhotoState
import soup.movie.core.imageloading.AsyncImage
import soup.movie.model.MovieModel

@OptIn(ExperimentalPhotoApi::class)
@Composable
internal fun DetailPoster(
movie: MovieModel,
posterUrl: String,
upPress: () -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
Expand All @@ -51,8 +50,8 @@ internal fun DetailPoster(
state = photoState,
) {
AsyncImage(
movie.posterUrl,
contentDescription = movie.title,
posterUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
onSuccess = {
photoState.setPhotoIntrinsicSize(it.intrinsicSize)
Expand Down
Loading
Loading