diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3d0c5a..bb1f381 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,8 @@ compose = "1.6.7" compose-multiplatform = "1.6.10" dokka = "1.9.20" jbx-core-bundle = "1.0.0" +jbx-lifecycle = "2.8.0" +jbx-navigation = "2.7.0-alpha07" jbx-savedstate = "1.2.0" kotlin = "1.9.23" kotlinx-coroutines = "1.8.0" @@ -24,13 +26,14 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" } compose-ui-test-junit4-android = { module = "androidx.compose.ui:ui-test-junit4-android", version.ref = "compose" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } jbx-core-bundle = { module = "org.jetbrains.androidx.core:core-bundle", version.ref = "jbx-core-bundle" } +jbx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "jbx-lifecycle" } +jbx-lifecycle-viewmodel-savedstate = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "jbx-lifecycle" } +jbx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "jbx-navigation" } jbx-savedstate = { module = "org.jetbrains.androidx.savedstate:savedstate", version.ref = "jbx-savedstate" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } diff --git a/internal/playground/src/commonMain/kotlin/soil/playground/router/NavLink.kt b/internal/playground/src/commonMain/kotlin/soil/playground/router/NavLink.kt new file mode 100644 index 0000000..c4f9c85 --- /dev/null +++ b/internal/playground/src/commonMain/kotlin/soil/playground/router/NavLink.kt @@ -0,0 +1,26 @@ +package soil.playground.router + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +@Composable +inline fun NavLink( + to: String, + router: NavRouter = LocalNavRouter.current, + content: @Composable (NavLinkHandle) -> Unit +) { + val handle: NavLinkHandle = remember(to) { { router.push(to) } } + content(handle) +} + +@Composable +inline fun NavLink( + to: T, + router: NavRouter = LocalNavRouter.current, + content: @Composable (NavLinkHandle) -> Unit +) { + val handle: NavLinkHandle = remember(to) { { router.push(to) } } + content(handle) +} + +typealias NavLinkHandle = () -> Unit diff --git a/internal/playground/src/commonMain/kotlin/soil/playground/router/NavRouter.kt b/internal/playground/src/commonMain/kotlin/soil/playground/router/NavRouter.kt new file mode 100644 index 0000000..1a088a6 --- /dev/null +++ b/internal/playground/src/commonMain/kotlin/soil/playground/router/NavRouter.kt @@ -0,0 +1,28 @@ +package soil.playground.router + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.staticCompositionLocalOf + +@Stable +interface NavRouter { + fun push(route: String) + + fun push(route: T) + + fun back(): Boolean + + fun canBack(): Boolean +} + +interface NavRoute + +private val noRouter = object : NavRouter { + override fun push(route: String) = Unit + override fun push(route: T) = Unit + override fun back() = false + override fun canBack() = false +} + +val LocalNavRouter = staticCompositionLocalOf { + noRouter +} diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index 198d3ab..eb70dc9 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -89,8 +89,7 @@ kotlin { implementation(libs.ktor.core) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.voyager.navigator) - implementation(libs.voyager.screenModel) + implementation(libs.jbx.navigation.compose) } androidMain.dependencies { diff --git a/sample/composeApp/src/androidMain/kotlin/soil/kmp/integration/voyager/ScreenExt.android.kt b/sample/composeApp/src/androidMain/kotlin/soil/kmp/integration/voyager/ScreenExt.android.kt deleted file mode 100644 index 9d97340..0000000 --- a/sample/composeApp/src/androidMain/kotlin/soil/kmp/integration/voyager/ScreenExt.android.kt +++ /dev/null @@ -1,11 +0,0 @@ -package soil.kmp.integration.voyager - -import androidx.compose.runtime.Composable -import cafe.adriel.voyager.core.screen.Screen -import soil.space.AtomStore -import soil.space.compose.rememberViewModelStore - -@Composable -actual fun Screen.rememberScreenStore(key: String?): AtomStore { - return rememberViewModelStore(key) -} diff --git a/sample/composeApp/src/commonMain/kotlin/App.kt b/sample/composeApp/src/commonMain/kotlin/App.kt index 0b72be9..f0619e1 100644 --- a/sample/composeApp/src/commonMain/kotlin/App.kt +++ b/sample/composeApp/src/commonMain/kotlin/App.kt @@ -1,4 +1,3 @@ -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -12,11 +11,12 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import cafe.adriel.voyager.navigator.CurrentScreen -import cafe.adriel.voyager.navigator.Navigator -import soil.kmp.screen.HomeScreen +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController import soil.playground.FeedbackAction import soil.playground.LocalFeedbackHost import soil.playground.style.AppTheme @@ -30,35 +30,54 @@ fun App() { } } +@Composable +private fun Content( + navController: NavHostController = rememberNavController() +) = withAppTheme { + val backStackEntry by navController.currentBackStackEntryAsState() + val navigator = remember(navController) { Navigator(navController) } + val canNavigateBack = remember(backStackEntry) { navigator.canBack() } + val hostState = remember { SnackbarHostState() } + val feedbackAction = remember { FeedbackAction(hostState) } + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + AppBar( + canNavigateBack = canNavigateBack, + navigateUp = { navigator.back() } + ) + }, + snackbarHost = { + SnackbarHost(hostState) + } + ) { innerPadding -> + CompositionLocalProvider(LocalFeedbackHost provides feedbackAction) { + NavRouterHost( + navigator = navigator, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun Content() = withAppTheme { - Navigator(HomeScreen) { navigator -> - val hostState = remember { SnackbarHostState() } - val feedbackAction = remember { FeedbackAction(hostState) } - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - if (navigator.canPop) { - TopAppBar( - title = { }, - navigationIcon = { - IconButton(onClick = navigator::pop) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - } - ) - } - }, - snackbarHost = { - SnackbarHost(hostState) - } - ) { innerPadding -> - Box(modifier = Modifier.padding(innerPadding)) { - CompositionLocalProvider(LocalFeedbackHost provides feedbackAction) { - CurrentScreen() +fun AppBar( + canNavigateBack: Boolean, + navigateUp: () -> Unit, + modifier: Modifier = Modifier +) { + TopAppBar( + title = { }, + modifier = modifier, + navigationIcon = { + if (canNavigateBack) { + IconButton(onClick = navigateUp) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } } - } + ) } diff --git a/sample/composeApp/src/commonMain/kotlin/Navigator.kt b/sample/composeApp/src/commonMain/kotlin/Navigator.kt new file mode 100644 index 0000000..2f35ac2 --- /dev/null +++ b/sample/composeApp/src/commonMain/kotlin/Navigator.kt @@ -0,0 +1,133 @@ +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import soil.kmp.screen.HelloFormScreen +import soil.kmp.screen.HelloQueryDetailScreen +import soil.kmp.screen.HelloQueryScreen +import soil.kmp.screen.HelloSpaceScreen +import soil.kmp.screen.HomeScreen +import soil.kmp.screen.NavScreen +import soil.playground.router.LocalNavRouter +import soil.playground.router.NavRoute +import soil.playground.router.NavRouter +import soil.space.compose.rememberViewModelStore + +@Stable +class Navigator( + val navController: NavHostController +) : NavRouter { + override fun push(route: String) { + navController.navigate(route) + } + + override fun push(route: T) { + when (val screen = route as NavScreen) { + is NavScreen.Home -> push(NavScreenDestination.Home()) + is NavScreen.HelloQuery -> push(NavScreenDestination.HelloQuery()) + is NavScreen.HelloQueryDetail -> push(NavScreenDestination.HelloQueryDetail(screen.postId)) + is NavScreen.HelloForm -> push(NavScreenDestination.HelloForm()) + is NavScreen.HelloSpace -> push(NavScreenDestination.HelloSpace()) + } + } + + override fun back(): Boolean { + return navController.popBackStack() + } + + override fun canBack(): Boolean { + return navController.previousBackStackEntry != null + } +} + +@Composable +fun NavRouterHost( + navigator: Navigator, + modifier: Modifier +) { + val startDestination = remember(NavScreen.root) { NavScreen.root.destination.route } + CompositionLocalProvider(LocalNavRouter provides navigator) { + NavHost( + navController = navigator.navController, + startDestination = startDestination, + modifier = modifier + ) { + composable( + route = NavScreenDestination.Home.route + ) { + HomeScreen() + } + composable( + route = NavScreenDestination.HelloQuery.route + ) { + HelloQueryScreen() + } + composable( + route = NavScreenDestination.HelloQueryDetail.route, + arguments = NavScreenDestination.HelloQueryDetail.arguments + ) { + val id = checkNotNull(it.arguments?.getInt(NavScreenDestination.HelloQueryDetail.id.name)) + HelloQueryDetailScreen(postId = id) + } + composable( + route = NavScreenDestination.HelloForm.route + ) { + HelloFormScreen() + } + composable( + route = NavScreenDestination.HelloSpace.route + ) { + val rootEntry = navigator.navController.getBackStackEntry(startDestination) + HelloSpaceScreen( + navStore = rememberViewModelStore(rootEntry) + ) + } + } + } +} + +private sealed class NavScreenDestination( + val route: String +) { + data object Home : NavScreenDestination("/home") { + operator fun invoke() = route + } + + data object HelloQuery : NavScreenDestination("/helloQuery") { + operator fun invoke() = route + } + + data object HelloQueryDetail : NavScreenDestination("/helloQuery/{id}") { + val arguments get() = listOf(id) + val id: NamedNavArgument + get() = navArgument("id") { + type = NavType.IntType + } + + operator fun invoke(postId: Int) = "/helloQuery/$postId" + } + + data object HelloForm : NavScreenDestination("/helloForm") { + operator fun invoke() = route + } + + data object HelloSpace : NavScreenDestination("/helloSpace") { + operator fun invoke() = route + } +} + +private val NavScreen.destination: NavScreenDestination + get() = when (this) { + is NavScreen.Home -> NavScreenDestination.Home + is NavScreen.HelloQuery -> NavScreenDestination.HelloQuery + is NavScreen.HelloQueryDetail -> NavScreenDestination.HelloQueryDetail + is NavScreen.HelloForm -> NavScreenDestination.HelloForm + is NavScreen.HelloSpace -> NavScreenDestination.HelloSpace + } diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/AtomScreenModel.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/AtomScreenModel.kt deleted file mode 100644 index 062d975..0000000 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/AtomScreenModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package soil.kmp.integration.voyager - -import cafe.adriel.voyager.core.model.ScreenModel -import soil.space.compose.AtomSaveableStore -import soil.space.AtomStore - -class AtomScreenModel( - val store: AtomStore = AtomSaveableStore() -) : ScreenModel diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/ScreenExt.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/ScreenExt.kt deleted file mode 100644 index b757ce7..0000000 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/integration/voyager/ScreenExt.kt +++ /dev/null @@ -1,19 +0,0 @@ -package soil.kmp.integration.voyager - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.Navigator -import soil.space.AtomStore - -@Composable -expect fun Screen.rememberScreenStore(key: String? = null): AtomStore - -@Composable -fun Navigator.rememberNavigatorScreenStore( - key: String? = null, -): AtomStore { - val model = rememberNavigatorScreenModel(key) { AtomScreenModel() } - return remember(model) { model.store } -} diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloFormScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloFormScreen.kt index 7e4facf..807fb90 100644 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloFormScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloFormScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen import kotlinx.coroutines.delay import kotlinx.coroutines.launch import soil.form.compose.Controller @@ -44,20 +43,16 @@ import soil.playground.form.compose.rememberAsRadio import soil.playground.form.compose.rememberAsSelect import soil.playground.style.withAppTheme - -class HelloFormScreen : Screen { - - @Composable - override fun Content() { - val feedback = LocalFeedbackHost.current - val coroutineScope = rememberCoroutineScope() - HelloFormContent( - onSubmitted = { - coroutineScope.launch { feedback.showAlert("Form submitted successfully") } - }, - modifier = Modifier.fillMaxSize() - ) - } +@Composable +fun HelloFormScreen() { + val feedback = LocalFeedbackHost.current + val coroutineScope = rememberCoroutineScope() + HelloFormContent( + onSubmitted = { + coroutineScope.launch { feedback.showAlert("Form submitted successfully") } + }, + modifier = Modifier.fillMaxSize() + ) } // The form input fields are based on the Live Demo used in React Hook Form. diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryDetailScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryDetailScreen.kt new file mode 100644 index 0000000..385c39a --- /dev/null +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryDetailScreen.kt @@ -0,0 +1,108 @@ +package soil.kmp.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import soil.playground.query.compose.ContentLoading +import soil.playground.query.compose.ContentUnavailable +import soil.playground.query.compose.PostDetailItem +import soil.playground.query.compose.PostUserDetailItem +import soil.playground.query.compose.rememberGetPostQuery +import soil.playground.query.compose.rememberGetUserPostsQuery +import soil.playground.query.compose.rememberGetUserQuery +import soil.playground.query.data.Post +import soil.playground.query.data.Posts +import soil.playground.query.data.User +import soil.query.compose.rememberQueriesErrorReset +import soil.query.compose.runtime.Await +import soil.query.compose.runtime.Catch +import soil.query.compose.runtime.ErrorBoundary +import soil.query.compose.runtime.Suspense + +@Composable +fun HelloQueryDetailScreen(postId: Int) { + HelloQueryScreenDetailTemplate { + PostDetailContent( + postId = postId, + modifier = Modifier.fillMaxSize() + ) + } +} + +@Composable +private fun HelloQueryScreenDetailTemplate( + content: @Composable () -> Unit +) { + ErrorBoundary( + modifier = Modifier.fillMaxSize(), + fallback = { + ContentUnavailable( + error = it.err, + reset = it.reset, + modifier = Modifier.matchParentSize() + ) + }, + onError = { e -> println(e.toString()) }, + onReset = rememberQueriesErrorReset() + ) { + Suspense( + fallback = { ContentLoading(modifier = Modifier.matchParentSize()) }, + modifier = Modifier.fillMaxSize(), + content = content + ) + } +} + +@Composable +private fun PostDetailContent( + postId: Int, + modifier: Modifier = Modifier +) { + PostDetailContainer(postId) { post -> + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + PostDetailItem(post, modifier = Modifier.fillMaxWidth()) + PostUserDetailContainer(userId = post.userId) { user, posts -> + PostUserDetailItem(user = user, posts = posts) + } + } + } +} + +@Composable +private fun PostDetailContainer( + postId: Int, + content: @Composable (Post) -> Unit +) { + val query = rememberGetPostQuery(postId) + Await(query) { post -> + content(post) + } + Catch(query) +} + +@Composable +private fun PostUserDetailContainer( + userId: Int, + content: @Composable (User, Posts) -> Unit +) { + val userQuery = rememberGetUserQuery(userId) + val postsQuery = rememberGetUserPostsQuery(userId) + Suspense( + fallback = { ContentLoading(modifier = Modifier.matchParentSize()) }, + modifier = Modifier.fillMaxWidth() + ) { + Await(userQuery, postsQuery) { user, posts -> + content(user, posts) + } + } + Catch(userQuery, postsQuery) +} diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryScreen.kt index 6892cf9..05e23fb 100644 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloQueryScreen.kt @@ -1,39 +1,27 @@ package soil.kmp.screen import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow import io.ktor.client.plugins.ResponseException import soil.playground.Alert import soil.playground.query.compose.ContentLoadMore import soil.playground.query.compose.ContentLoading import soil.playground.query.compose.ContentUnavailable -import soil.playground.query.compose.PostDetailItem import soil.playground.query.compose.PostListItem -import soil.playground.query.compose.PostUserDetailItem -import soil.playground.query.compose.rememberGetPostQuery import soil.playground.query.compose.rememberGetPostsQuery -import soil.playground.query.compose.rememberGetUserPostsQuery -import soil.playground.query.compose.rememberGetUserQuery import soil.playground.query.data.PageParam -import soil.playground.query.data.Post import soil.playground.query.data.Posts -import soil.playground.query.data.User +import soil.playground.router.NavLink import soil.playground.style.withAppTheme import soil.query.compose.rememberQueriesErrorReset import soil.query.compose.runtime.Await @@ -41,17 +29,10 @@ import soil.query.compose.runtime.Catch import soil.query.compose.runtime.ErrorBoundary import soil.query.compose.runtime.Suspense -class HelloQueryScreen : Screen { - - @Composable - override fun Content() { - HelloQueryScreenTemplate { - val navigator = LocalNavigator.currentOrThrow - HelloQueryContent( - onSelect = { navigator.push(PostDetailScreen(postId = it.id)) }, - modifier = Modifier.fillMaxSize() - ) - } +@Composable +fun HelloQueryScreen() { + HelloQueryScreenTemplate { + HelloQueryContent(modifier = Modifier.fillMaxSize()) } } @@ -81,7 +62,6 @@ private fun HelloQueryScreenTemplate( @Composable private fun HelloQueryContent( - onSelect: (Post) -> Unit, modifier: Modifier = Modifier ) = withAppTheme { ListSectionContainer { state -> @@ -91,11 +71,13 @@ private fun HelloQueryContent( verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(state.posts, key = { it.id }) { post -> - PostListItem( - onClick = { onSelect(post) }, - post = post, - modifier = Modifier.fillMaxWidth() - ) + NavLink(to = NavScreen.HelloQueryDetail(post.id)) { + PostListItem( + onClick = it, + post = post, + modifier = Modifier.fillMaxWidth() + ) + } } val pageParam = state.loadMoreParam if (pageParam != null) { @@ -139,65 +121,3 @@ private data class ListSectionState( val loadMoreParam: PageParam?, val loadMore: suspend (PageParam) -> Unit ) - -private class PostDetailScreen( - val postId: Int -) : Screen { - @Composable - override fun Content() { - HelloQueryScreenTemplate { - PostDetailContent( - postId = postId, - modifier = Modifier.fillMaxSize() - ) - } - } -} - -@Composable -private fun PostDetailContent( - postId: Int, - modifier: Modifier = Modifier -) { - PostDetailContainer(postId) { post -> - Column( - modifier = modifier.verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - PostDetailItem(post, modifier = Modifier.fillMaxWidth()) - PostUserDetailContainer(userId = post.userId) { user, posts -> - PostUserDetailItem(user = user, posts = posts) - } - } - } -} - -@Composable -private fun PostDetailContainer( - postId: Int, - content: @Composable (Post) -> Unit -) { - val query = rememberGetPostQuery(postId) - Await(query) { post -> - content(post) - } - Catch(query) -} - -@Composable -private fun PostUserDetailContainer( - userId: Int, - content: @Composable (User, Posts) -> Unit -) { - val userQuery = rememberGetUserQuery(userId) - val postsQuery = rememberGetUserPostsQuery(userId) - Suspense( - fallback = { ContentLoading(modifier = Modifier.matchParentSize()) }, - modifier = Modifier.fillMaxWidth() - ) { - Await(userQuery, postsQuery) { user, posts -> - content(user, posts) - } - } - Catch(userQuery, postsQuery) -} diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloSpaceScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloSpaceScreen.kt index 142ac17..66251a8 100644 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloSpaceScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HelloSpaceScreen.kt @@ -16,33 +16,31 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import soil.kmp.integration.voyager.rememberNavigatorScreenStore -import soil.kmp.integration.voyager.rememberScreenStore import soil.playground.space.compose.Counter import soil.playground.style.withAppTheme +import soil.space.AtomStore import soil.space.atom import soil.space.atomScope import soil.space.compose.AtomRoot import soil.space.compose.rememberAtomState import soil.space.compose.rememberAtomValue +import soil.space.compose.rememberViewModelStore -class HelloSpaceScreen : Screen { - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - AtomRoot( - currentScreen to rememberScreenStore(), - navScreen to navigator.rememberNavigatorScreenStore(), - fallbackScope = { currentScreen } - ) { - HelloSpaceContent( - modifier = Modifier.fillMaxSize() - ) - } +@Composable +fun HelloSpaceScreen( + navStore: AtomStore +) { + AtomRoot( + currentScreen to rememberViewModelStore(), + navScreen to navStore, + fallbackScope = { currentScreen } + // If fallbackScope is set to navScreen, the value of Counter is preserved even if it navigates back and then forward again. + // fallbackScope = { navScreen } + ) { + HelloSpaceContent( + modifier = Modifier.fillMaxSize() + ) } } diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HomeScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HomeScreen.kt index 5ef89c1..18f57a1 100644 --- a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HomeScreen.kt +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/HomeScreen.kt @@ -14,43 +14,43 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow +import soil.playground.router.NavLink -object HomeScreen : Screen { - - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() +@Composable +fun HomeScreen() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.padding(16.dp) + .widthIn(max = 360.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.padding(16.dp) - .widthIn(max = 360.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { + NavLink(to = NavScreen.HelloQuery) { OutlinedButton( - onClick = { navigator.push(HelloQueryScreen()) }, + onClick = it, modifier = Modifier .fillMaxWidth() .height(80.dp) ) { Text("Query") } + } + NavLink(to = NavScreen.HelloForm) { OutlinedButton( - onClick = { navigator.push(HelloFormScreen()) }, + onClick = it, modifier = Modifier .fillMaxWidth() .height(80.dp) ) { Text("Form") } + } + NavLink(to = NavScreen.HelloSpace) { OutlinedButton( - onClick = { navigator.push(HelloSpaceScreen()) }, + onClick = it, modifier = Modifier .fillMaxWidth() .height(80.dp) diff --git a/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/NavScreen.kt b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/NavScreen.kt new file mode 100644 index 0000000..9feca78 --- /dev/null +++ b/sample/composeApp/src/commonMain/kotlin/soil/kmp/screen/NavScreen.kt @@ -0,0 +1,20 @@ +package soil.kmp.screen + +import soil.playground.router.NavRoute + +sealed interface NavScreen : NavRoute { + + data object Home : NavScreen + + data object HelloQuery : NavScreen + + data class HelloQueryDetail(val postId: Int) : NavScreen + + data object HelloForm : NavScreen + + data object HelloSpace : NavScreen + + companion object { + val root: NavScreen = Home + } +} diff --git a/sample/composeApp/src/skikoMain/kotlin/soil/kmp/integration/voyager/ScreenExt.skiko.kt b/sample/composeApp/src/skikoMain/kotlin/soil/kmp/integration/voyager/ScreenExt.skiko.kt deleted file mode 100644 index 6be1013..0000000 --- a/sample/composeApp/src/skikoMain/kotlin/soil/kmp/integration/voyager/ScreenExt.skiko.kt +++ /dev/null @@ -1,13 +0,0 @@ -package soil.kmp.integration.voyager - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import cafe.adriel.voyager.core.model.rememberScreenModel -import cafe.adriel.voyager.core.screen.Screen -import soil.space.AtomStore - -@Composable -actual fun Screen.rememberScreenStore(key: String?): AtomStore { - val model = rememberScreenModel(tag = key) { AtomScreenModel() } - return remember(model) { model.store } -} diff --git a/soil-space/build.gradle.kts b/soil-space/build.gradle.kts index 328b159..d17cac2 100644 --- a/soil-space/build.gradle.kts +++ b/soil-space/build.gradle.kts @@ -47,14 +47,14 @@ kotlin { commonMain.dependencies { implementation(compose.runtime) implementation(compose.runtimeSaveable) + implementation(libs.jbx.lifecycle.viewmodel.compose) + implementation(libs.jbx.lifecycle.viewmodel.savedstate) api(libs.jbx.savedstate) api(libs.jbx.core.bundle) } androidMain.dependencies { implementation(libs.androidx.core) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.lifecycle.viewmodel.savedstate) } val skikoMain by creating { diff --git a/soil-space/src/androidMain/kotlin/soil/space/compose/AtomViewModel.kt b/soil-space/src/commonMain/kotlin/soil/space/compose/AtomViewModel.kt similarity index 97% rename from soil-space/src/androidMain/kotlin/soil/space/compose/AtomViewModel.kt rename to soil-space/src/commonMain/kotlin/soil/space/compose/AtomViewModel.kt index 14fceb5..23dddf3 100644 --- a/soil-space/src/androidMain/kotlin/soil/space/compose/AtomViewModel.kt +++ b/soil-space/src/commonMain/kotlin/soil/space/compose/AtomViewModel.kt @@ -3,9 +3,9 @@ package soil.space.compose -import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.core.bundle.Bundle import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelStoreOwner @@ -44,7 +44,7 @@ class AtomViewModel( */ @Composable fun rememberViewModelStore( - key: String? + key: String? = null ): AtomStore { val vm = viewModel( factory = viewModelFactory {