From cee93e78f0346f5e12b193461b65c59128692a65 Mon Sep 17 00:00:00 2001 From: xxfast Date: Mon, 22 Jan 2024 09:32:01 +1100 Subject: [PATCH 01/11] Add decompose router for page navigation --- app/build.gradle.kts | 3 +- app/ios/ios.xcodeproj/project.pbxproj | 4 +- .../router/app/TestDecomposeRouter.kt | 10 +- app/src/androidMain/AndroidManifest.xml | 3 +- .../decompose/router/app/MainActivity.kt | 5 +- .../router/app/screens/HomeScreen.kt | 50 ---- .../router/app/screens/HomeStateModels.kt | 11 - .../app/screens/details/DetailScreen.kt | 104 --------- .../router/app/screens/list/ListInstance.kt | 33 --- .../app/screens/list/ListStateModels.kt | 7 - .../app/screens/nested/NestedScreenModels.kt | 11 - .../decompose/router/screens/HomeScreen.kt | 80 +++++++ .../router/screens/HomeStateModels.kt | 7 + .../router/{app => }/screens/TestTags.kt | 2 +- .../router/screens/pages/PagesScreen.kt | 67 ++++++ .../router/screens/pages/PagesStateModels.kt | 7 + .../screens/router/nested/NestedScreen.kt | 217 ------------------ .../router/screens/stack/StackScreen.kt | 43 ++++ .../router/screens/stack/StackStateModels.kt | 12 + .../screens/stack/details/DetailScreen.kt | 60 +++++ .../stack}/details/DetailStateModels.kt | 2 +- .../router/screens/stack/list/ListInstance.kt | 32 +++ .../stack}/list/ListScreen.kt | 94 ++++---- .../screens/stack/list/ListStateModels.kt | 7 + .../decompose/router/app/Application.kt | 5 +- .../decompose/router/app/Application.kt | 2 +- .../decompose/router/app/Application.kt | 2 +- .../router/wear/content/RoutedContent.kt | 2 +- .../kotlin/io/github/xxfast/decompose}/Key.kt | 2 +- .../xxfast/decompose/{router => }/Key.kt | 2 +- .../decompose/{router => }/SavedState.kt | 2 +- .../io/github/xxfast/decompose/State.kt | 13 ++ .../decompose/router/RememberOnRoute.kt | 52 +++++ .../decompose/router/pages/RoutedContent.kt | 42 ++++ .../xxfast/decompose/router/pages/Router.kt | 58 +++++ .../router/{ => stack}/RoutedContent.kt | 4 +- .../decompose/router/{ => stack}/Router.kt | 65 +----- .../kotlin/io/github/xxfast/decompose}/Key.kt | 2 +- .../xxfast/decompose/{router => }/Key.kt | 2 +- .../decompose/router/DefaultRouterContext.kt | 1 - .../xxfast/decompose/{router => }/Key.kt | 2 +- 41 files changed, 566 insertions(+), 563 deletions(-) delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeScreen.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailScreen.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListInstance.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt rename app/src/commonMain/kotlin/io/github/xxfast/decompose/router/{app => }/screens/TestTags.kt (79%) create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesStateModels.kt delete mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/router/nested/NestedScreen.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackScreen.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackStateModels.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt rename app/src/commonMain/kotlin/io/github/xxfast/decompose/router/{app/screens => screens/stack}/details/DetailStateModels.kt (60%) create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListInstance.kt rename app/src/commonMain/kotlin/io/github/xxfast/decompose/router/{app/screens => screens/stack}/list/ListScreen.kt (52%) create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListStateModels.kt rename decompose-router/src/{desktopMain/kotlin/io/github/xxfast/decompose/router => androidMain/kotlin/io/github/xxfast/decompose}/Key.kt (78%) rename decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/{router => }/Key.kt (60%) rename decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/{router => }/SavedState.kt (92%) create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/State.kt create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RememberOnRoute.kt create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/RoutedContent.kt create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt rename decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/{ => stack}/RoutedContent.kt (85%) rename decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/{ => stack}/Router.kt (51%) rename decompose-router/src/{androidMain/kotlin/io/github/xxfast/decompose/router => desktopMain/kotlin/io/github/xxfast/decompose}/Key.kt (78%) rename decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/{router => }/Key.kt (78%) rename decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/{router => }/Key.kt (85%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbda33b..e57dd80 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,7 @@ kotlin { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) + implementation(compose.materialIconsExtended) implementation(libs.decompose) implementation(libs.decompose.compose.multiplatform) @@ -141,4 +142,4 @@ compose.desktop { compose.experimental { web.application { } -} \ No newline at end of file +} diff --git a/app/ios/ios.xcodeproj/project.pbxproj b/app/ios/ios.xcodeproj/project.pbxproj index a2f26e3..1a52062 100644 --- a/app/ios/ios.xcodeproj/project.pbxproj +++ b/app/ios/ios.xcodeproj/project.pbxproj @@ -327,7 +327,7 @@ "-framework", app, ); - PRODUCT_BUNDLE_IDENTIFIER = io.github.xxfast.decompose.router.app; + PRODUCT_BUNDLE_IDENTIFIER = io.github.xxfast.decompose.router.sample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -355,7 +355,7 @@ "-framework", app, ); - PRODUCT_BUNDLE_IDENTIFIER = io.github.xxfast.decompose.router.app; + PRODUCT_BUNDLE_IDENTIFIER = io.github.xxfast.decompose.router.sample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt index 90f4fdd..234987f 100644 --- a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollToNode import androidx.test.ext.junit.rules.ActivityScenarioRule -import io.github.xxfast.decompose.router.app.screens.BACK_BUTTON_TAG -import io.github.xxfast.decompose.router.app.screens.DETAILS_TAG -import io.github.xxfast.decompose.router.app.screens.FAVORITE_TAG -import io.github.xxfast.decompose.router.app.screens.LIST_TAG -import io.github.xxfast.decompose.router.app.screens.TITLE_BAR_TAG +import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG +import io.github.xxfast.decompose.router.screens.DETAILS_TAG +import io.github.xxfast.decompose.router.screens.FAVORITE_TAG +import io.github.xxfast.decompose.router.screens.LIST_TAG +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG import org.junit.Rule import org.junit.Test diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index 2fc18df..175824d 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -7,7 +7,8 @@ android:theme="@android:style/Theme.Material.NoActionBar"> + android:exported="true" + android:enableOnBackInvokedCallback="true"> diff --git a/app/src/androidMain/kotlin/io/github/xxfast/decompose/router/app/MainActivity.kt b/app/src/androidMain/kotlin/io/github/xxfast/decompose/router/app/MainActivity.kt index 8d1c54a..0d069eb 100644 --- a/app/src/androidMain/kotlin/io/github/xxfast/decompose/router/app/MainActivity.kt +++ b/app/src/androidMain/kotlin/io/github/xxfast/decompose/router/app/MainActivity.kt @@ -3,6 +3,7 @@ package io.github.xxfast.decompose.router.app import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider @@ -10,12 +11,14 @@ import androidx.core.view.WindowCompat import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.RouterContext import io.github.xxfast.decompose.router.defaultRouterContext -import io.github.xxfast.decompose.router.app.screens.HomeScreen +import io.github.xxfast.decompose.router.screens.HomeScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() + val rootRouterContext: RouterContext = defaultRouterContext() setContent { diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeScreen.kt deleted file mode 100644 index cc6f1d3..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeScreen.kt +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens - -import androidx.compose.runtime.Composable -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback.predictiveBackAnimation -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push -import io.github.xxfast.decompose.router.LocalRouterContext -import io.github.xxfast.decompose.router.Router -import io.github.xxfast.decompose.router.app.screens.HomeScreens.Details -import io.github.xxfast.decompose.router.app.screens.HomeScreens.Nested -import io.github.xxfast.decompose.router.content.RoutedContent -import io.github.xxfast.decompose.router.rememberRouter -import io.github.xxfast.decompose.router.app.screens.details.DetailScreen -import io.github.xxfast.decompose.router.app.screens.list.ListScreen -import io.github.xxfast.decompose.router.app.screens.nested.NestedScreen - -@OptIn(ExperimentalDecomposeApi::class) -@Composable -fun HomeScreen() { - val router: Router = rememberRouter(HomeScreens::class) { listOf(HomeScreens.List) } - - RoutedContent( - router = router, - animation = predictiveBackAnimation( - animation = stackAnimation(slide()), - onBack = { router.pop() }, - backHandler = LocalRouterContext.current.backHandler - ) - ) { screen -> - when (screen) { - HomeScreens.List -> ListScreen( - onSelect = { count -> router.push(Details(count)) }, - onSelectColor = { router.push(Nested) } - ) - - Nested -> NestedScreen( - onBack = { router.pop() }, - onSelect = { count -> router.push(Details(count)) }, - ) - - is Details -> DetailScreen( - count = screen.number, - onBack = { router.pop() } - ) - } - } -} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt deleted file mode 100644 index 3d10fc3..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/HomeStateModels.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens - -import kotlinx.serialization.Serializable - -@Serializable -sealed class HomeScreens { - @Serializable data object List: HomeScreens() - @Serializable data object Nested: HomeScreens() - @Serializable data class Details(val number: Int): HomeScreens() -} - diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailScreen.kt deleted file mode 100644 index cee9457..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailScreen.kt +++ /dev/null @@ -1,104 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens.details - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Check -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.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import io.github.xxfast.decompose.router.app.screens.BACK_BUTTON_TAG -import io.github.xxfast.decompose.router.app.screens.DETAILS_TAG -import io.github.xxfast.decompose.router.app.screens.TITLE_BAR_TAG -import io.github.xxfast.decompose.router.app.screens.TOOLBAR_TAG - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DetailScreen( - count: Int, - onBack: () -> Unit, -) { - var counted: Set by rememberSaveable { mutableStateOf(emptySet()) } - - if (counted.size == count) onBack() - - Scaffold( - topBar = { - LargeTopAppBar( - modifier = Modifier.testTag(TOOLBAR_TAG), - title = { - Text( - text = count.toString(), - modifier = Modifier.testTag(TITLE_BAR_TAG) - ) - }, - navigationIcon = { - IconButton( - modifier = Modifier - .testTag(BACK_BUTTON_TAG), - onClick = onBack - ) { - Icon(Icons.Default.ArrowBack, null) - } - }) - }) { paddingValues -> - LazyHorizontalGrid( - rows = Adaptive(100.dp), - modifier = Modifier - .padding(paddingValues) - .testTag(DETAILS_TAG), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(16.dp), - ) { - items(count) { i -> - val cornerSize: Dp by animateDpAsState(if (i in counted) 64.dp else 8.dp) - - Card( - modifier = Modifier - .width(100.dp) - .clickable { counted = if (i in counted) counted - i else counted + i } - .clip(RoundedCornerShape(cornerSize)), - colors = CardDefaults.cardColors( - containerColor = - if (i in counted) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.primaryContainer - ), - shape = RoundedCornerShape(cornerSize) - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - if (i in counted) Icon(imageVector = Icons.Default.Check, null) - else Text(text = i.toString(), style = MaterialTheme.typography.titleLarge) - } - } - } - } - } -} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListInstance.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListInstance.kt deleted file mode 100644 index 1e94ea5..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListInstance.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens.list - -import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance -import io.github.xxfast.decompose.router.SavedStateHandle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn -import kotlin.coroutines.CoroutineContext - -class ListInstance(savedStateHandle: SavedStateHandle) : Instance, CoroutineScope { - private val initialState: ListState = savedStateHandle.get() ?: ListState() - - // TODO: This has to be lazy for some weird reason - val state: StateFlow by lazy { - flow { - emit(ListState(Loading)) - delay(300L) - emit(ListState((1.. 100).toList())) - } - .stateIn(this, Lazily, initialState) - } - - override val coroutineContext: CoroutineContext = Dispatchers.Main - - override fun onDestroy() { - coroutineContext.cancel() - } -} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt deleted file mode 100644 index d77deed..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListStateModels.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens.list - -import kotlinx.serialization.Serializable - -@Serializable data class ListState(val items: List? = Loading) - -val Loading: Nothing? = null diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt deleted file mode 100644 index e01a409..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/nested/NestedScreenModels.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.xxfast.decompose.router.app.screens.nested - -import kotlinx.serialization.Serializable - -@Serializable -sealed class NestedScreens { - data object Home: NestedScreens() - data object Primary: NestedScreens() - data object Secondary: NestedScreens() - data object Tertiary: NestedScreens() -} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt new file mode 100644 index 0000000..a271681 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt @@ -0,0 +1,80 @@ +package io.github.xxfast.decompose.router.screens + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CropSquare +import androidx.compose.material.icons.rounded.ImportContacts +import androidx.compose.material.icons.rounded.Reorder +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.PagesScrollAnimation +import com.arkivanov.decompose.router.pages.select +import io.github.xxfast.decompose.router.pages.RoutedContent +import io.github.xxfast.decompose.router.pages.Router +import io.github.xxfast.decompose.router.pages.pagesOf +import io.github.xxfast.decompose.router.pages.rememberRouter +import io.github.xxfast.decompose.router.screens.HomeScreens.Page +import io.github.xxfast.decompose.router.screens.HomeScreens.Slot +import io.github.xxfast.decompose.router.screens.HomeScreens.Stack +import io.github.xxfast.decompose.router.screens.pages.PagesScreen +import io.github.xxfast.decompose.router.screens.stack.StackScreen + +@OptIn(ExperimentalDecomposeApi::class, ExperimentalFoundationApi::class) +@Composable +fun HomeScreen() { + val pager: Router = rememberRouter(HomeScreens::class) { pagesOf(Stack, Page, Slot) } + + Scaffold( + bottomBar = { + NavigationBar { + HomeScreens.entries.forEach { screen -> + NavigationBarItem( + selected = screen.ordinal == pager.pages.value.selectedIndex, + icon = { + Icon( + imageVector = when (screen) { + Stack -> Icons.Rounded.Reorder + Page -> Icons.Rounded.ImportContacts + Slot -> Icons.Rounded.CropSquare + }, + contentDescription = null, + ) + }, + label = { Text(screen.name) }, + onClick = { pager.select(screen.ordinal) } + ) + } + } + } + ) { scaffoldPadding -> + RoutedContent( + router = pager, + animation = PagesScrollAnimation.Disabled, + pager = { modifier, state, key, pageContent -> + HorizontalPager( + modifier = modifier, + state = state, + key = key, + pageContent = pageContent, + userScrollEnabled = false, + ) + }, + modifier = Modifier + .padding(scaffoldPadding) + ) { screen -> + when (screen) { + Stack -> StackScreen() + Page -> PagesScreen() + Slot -> Text("Slot") + } + } + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt new file mode 100644 index 0000000..158d1a0 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt @@ -0,0 +1,7 @@ +package io.github.xxfast.decompose.router.screens + +enum class HomeScreens { + Stack, + Page, + Slot, +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/TestTags.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt similarity index 79% rename from app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/TestTags.kt rename to app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt index f73e536..51a1b9d 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/TestTags.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router.app.screens +package io.github.xxfast.decompose.router.screens const val LIST_TAG = "list" const val TOOLBAR_TAG = "toolbar" diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt new file mode 100644 index 0000000..ba69240 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt @@ -0,0 +1,67 @@ +package io.github.xxfast.decompose.router.screens.pages + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.FirstPage +import androidx.compose.material3.Card +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.pages.Pages +import com.arkivanov.decompose.router.pages.selectFirst +import io.github.xxfast.decompose.router.pages.RoutedContent +import io.github.xxfast.decompose.router.pages.rememberRouter + +@OptIn( + ExperimentalDecomposeApi::class, + ExperimentalFoundationApi::class, + ExperimentalMaterial3Api::class +) +@Composable +fun PagesScreen() { + val router = rememberRouter(PagesScreens::class) { + Pages( + items = List(10) { PagesScreens(it) }, + selectedIndex = 0, + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Pages (${router.pages.value.items.size})") }, + actions = { + IconButton(onClick = { router.selectFirst() }) { + Icon(Icons.Rounded.FirstPage, null) + } + } + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { scaffoldPadding -> + RoutedContent(router = router, modifier = Modifier.padding(scaffoldPadding)) { page -> + Card(modifier = Modifier.padding(16.dp)) { + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = page.index.toString(), + style = MaterialTheme.typography.displayLarge, + modifier = Modifier.align(Alignment.Center) + ) + } + } + } + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesStateModels.kt new file mode 100644 index 0000000..4821b60 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesStateModels.kt @@ -0,0 +1,7 @@ +package io.github.xxfast.decompose.router.screens.pages + +import kotlinx.serialization.Serializable + +@Serializable class PagesScreens(val index: Int) + + diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/router/nested/NestedScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/router/nested/NestedScreen.kt deleted file mode 100644 index 5c3c943..0000000 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/router/nested/NestedScreen.kt +++ /dev/null @@ -1,217 +0,0 @@ -package io.github.xxfast.decompose.router.screens.router.nested - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.Orientation.Vertical -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material.icons.rounded.KeyboardArrowUp -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push -import io.github.xxfast.decompose.router.Router -import io.github.xxfast.decompose.router.RoutedContent -import io.github.xxfast.decompose.router.rememberRouter -import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG -import io.github.xxfast.decompose.router.screens.LIST_TAG -import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG -import io.github.xxfast.decompose.router.screens.TOOLBAR_TAG -import io.github.xxfast.decompose.router.screens.router.nested.NestedScreens.Home -import io.github.xxfast.decompose.router.screens.router.nested.NestedScreens.Primary -import io.github.xxfast.decompose.router.screens.router.nested.NestedScreens.Secondary -import io.github.xxfast.decompose.router.screens.router.nested.NestedScreens.Tertiary - -@Composable -fun NestedScreen( - onBack: () -> Unit, - onSelect: (Int) -> Unit -) { - val router: Router = rememberRouter(NestedScreens::class) { listOf(Home) } - - val items: List = buildList { repeat(50) { add(it) } } - - RoutedContent( - router = router, - animation = stackAnimation(slide(orientation = Vertical)) - ) { screen -> - - val (onPrimary, primaryContainer ) = MaterialTheme.colorScheme.onPrimaryContainer to MaterialTheme.colorScheme.primaryContainer - val (onSecondary, secondaryContainer ) = MaterialTheme.colorScheme.onSecondaryContainer to MaterialTheme.colorScheme.secondaryContainer - val (onTertiary, tertiaryContainer ) = MaterialTheme.colorScheme.onTertiaryContainer to MaterialTheme.colorScheme.tertiaryContainer - - when (screen) { - Home -> NestedHomeView( - onPrimary = { router.push(Primary)}, - onSecondary = { router.push(Secondary)}, - onTertiary = { router.push(Tertiary) }, - onBack = onBack - ) - - Primary -> NestedView( - title = "Primary", - items = items, - contentColor = onPrimary, - containerColor = primaryContainer, - onBack = { router.pop() }, - onSelect = onSelect - ) - - Secondary -> NestedView( - title = "Secondary", - items = items, - contentColor = onSecondary, - containerColor = secondaryContainer, - onBack = { router.pop() }, - onSelect = onSelect - ) - Tertiary -> NestedView( - title = "Tertiary", - items = items, - contentColor = onTertiary, - containerColor = tertiaryContainer, - onBack = { router.pop() }, - onSelect = onSelect - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun NestedHomeView( - onPrimary: () -> Unit, - onSecondary: () -> Unit, - onTertiary: () -> Unit, - onBack: () -> Unit, -) { - Scaffold( - topBar = { - LargeTopAppBar( - modifier = Modifier.testTag(TOOLBAR_TAG), - title = { - Text( - text = "Colors", - modifier = Modifier.testTag(TITLE_BAR_TAG) - ) - }, - navigationIcon = { - IconButton(onClick = onBack, modifier = Modifier.testTag(BACK_BUTTON_TAG)) { - Icon(Icons.Rounded.ArrowBack, null) } - } - ) - } - ) { paddingValues -> - Column(modifier = Modifier.padding(paddingValues).padding(16.dp)) { - Button( - onClick = onPrimary, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), - modifier = Modifier.fillMaxWidth().fillMaxHeight(.33f).padding(16.dp) - ) { - Text("Primary", style = MaterialTheme.typography.titleLarge) - } - Button( - onClick = onSecondary, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary), - modifier = Modifier.fillMaxWidth().fillMaxHeight(.5f).padding(16.dp) - ) { - Text("Secondary", style = MaterialTheme.typography.titleLarge) - } - Button( - onClick = onTertiary, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary), - modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(16.dp) - ) { - Text("Tertiary", style = MaterialTheme.typography.titleLarge) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun NestedView( - title: String, - items: List, - contentColor: Color, - containerColor: Color, - onBack: () -> Unit, - onSelect: (Int) -> Unit, -) { - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - onSurface = contentColor, - surface = containerColor, - ) - ) { - Scaffold( - topBar = { - LargeTopAppBar( - modifier = Modifier.testTag(TOOLBAR_TAG), - title = { - Text( - text = title, - modifier = Modifier.testTag(TITLE_BAR_TAG) - ) - }, - navigationIcon = { - IconButton(onClick = onBack, modifier = Modifier.testTag(BACK_BUTTON_TAG)) { - Icon(Icons.Rounded.KeyboardArrowUp, null) } - } - ) - } - ) { paddingValues -> - LazyVerticalGrid( - columns = Adaptive(100.dp), - modifier = androidx.compose.ui.Modifier - .padding(paddingValues) - .fillMaxSize() - .testTag(LIST_TAG), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(16.dp), - ) { - items(items) { item -> - Card( - modifier = Modifier - .height(100.dp) - .clip(MaterialTheme.shapes.medium) - .clickable { onSelect(item) }, - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = item.toString(), style = MaterialTheme.typography.titleLarge) - } - } - } - } - } - } -} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackScreen.kt new file mode 100644 index 0000000..7c93957 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackScreen.kt @@ -0,0 +1,43 @@ +package io.github.xxfast.decompose.router.screens.stack + +import androidx.compose.runtime.Composable +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback.predictiveBackAnimation +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.screens.stack.StackScreens.Details +import io.github.xxfast.decompose.router.screens.stack.StackScreens.List +import io.github.xxfast.decompose.router.screens.stack.details.DetailScreen +import io.github.xxfast.decompose.router.screens.stack.list.ListScreen +import io.github.xxfast.decompose.router.stack.RoutedContent +import io.github.xxfast.decompose.router.stack.Router +import io.github.xxfast.decompose.router.stack.rememberRouter + +@OptIn(ExperimentalDecomposeApi::class) +@Composable +fun StackScreen() { + val router: Router = rememberRouter(StackScreens::class) { listOf(List) } + + RoutedContent( + router = router, + animation = predictiveBackAnimation( + animation = stackAnimation(slide()), + onBack = { router.pop() }, + backHandler = LocalRouterContext.current.backHandler + ) + ) { screen -> + when (screen) { + List -> ListScreen( + onSelect = { item -> router.push(Details(item)) }, + ) + + is Details -> DetailScreen( + item = screen.item, + onBack = { router.pop() } + ) + } + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackStateModels.kt new file mode 100644 index 0000000..af52a55 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/StackStateModels.kt @@ -0,0 +1,12 @@ +package io.github.xxfast.decompose.router.screens.stack + +import kotlinx.serialization.Serializable + +@Serializable class Item(val index: Int) + +@Serializable +sealed class StackScreens { + @Serializable data object List: StackScreens() + @Serializable data class Details(val item: Item): StackScreens() +} + diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt new file mode 100644 index 0000000..514ed63 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt @@ -0,0 +1,60 @@ +package io.github.xxfast.decompose.router.screens.stack.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG +import io.github.xxfast.decompose.router.screens.TOOLBAR_TAG +import io.github.xxfast.decompose.router.screens.stack.Item + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DetailScreen( + item: Item, + onBack: () -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + modifier = Modifier.testTag(TOOLBAR_TAG), + title = { + Text( + text = item.index.toString(), + modifier = Modifier.testTag(TITLE_BAR_TAG) + ) + }, + navigationIcon = { + IconButton( + modifier = Modifier + .testTag(BACK_BUTTON_TAG), + onClick = onBack + ) { + Icon(Icons.Default.ArrowBack, null) + } + }) + }) { paddingValues -> + Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) { + Text( + text = item.toString(), + modifier = Modifier + .padding(16.dp) + .align(Alignment.Center) + ) + } + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailStateModels.kt similarity index 60% rename from app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt rename to app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailStateModels.kt index 2bcf6c6..0399f3a 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/details/DetailStateModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailStateModels.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router.app.screens.details +package io.github.xxfast.decompose.router.screens.stack.details import kotlinx.serialization.Serializable diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListInstance.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListInstance.kt new file mode 100644 index 0000000..29a02bf --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListInstance.kt @@ -0,0 +1,32 @@ +package io.github.xxfast.decompose.router.screens.stack.list + +import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance +import io.github.xxfast.decompose.SavedStateHandle +import io.github.xxfast.decompose.router.screens.stack.Item +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +class ListInstance(private val savedStateHandle: SavedStateHandle) : Instance, CoroutineScope { + private val initialState: ListState = savedStateHandle.get() ?: ListState() + + private val _state: MutableStateFlow = MutableStateFlow(initialState) + val state: StateFlow = _state + + override val coroutineContext: CoroutineContext = Dispatchers.Main + + fun add(item: Item = Item(_state.value.screens.size)) = launch { + val previous: ListState = _state.value + val updated: ListState = previous.copy(screens = previous.screens.plus(item)) + _state.emit(updated) + savedStateHandle.set(updated) + } + + override fun onDestroy() { + coroutineContext.cancel() + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt similarity index 52% rename from app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt rename to app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt index 9d9ae47..5cee63e 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/app/screens/list/ListScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt @@ -1,48 +1,51 @@ -package io.github.xxfast.decompose.router.app.screens.list +package io.github.xxfast.decompose.router.screens.stack.list import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells.Adaptive -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Favorite +import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import io.github.xxfast.decompose.router.rememberOnRoute -import io.github.xxfast.decompose.router.app.screens.FAVORITE_TAG -import io.github.xxfast.decompose.router.app.screens.LIST_TAG -import io.github.xxfast.decompose.router.app.screens.TITLE_BAR_TAG -import io.github.xxfast.decompose.router.app.screens.TOOLBAR_TAG +import io.github.xxfast.decompose.router.screens.LIST_TAG +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG +import io.github.xxfast.decompose.router.screens.TOOLBAR_TAG +import io.github.xxfast.decompose.router.screens.stack.Item +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.serializer @OptIn(ExperimentalMaterial3Api::class, InternalSerializationApi::class) @Composable fun ListScreen( - onSelect: (count: Int) -> Unit, - onSelectColor: () -> Unit, + onSelect: (screen: Item) -> Unit, ) { val instance: ListInstance = rememberOnRoute( type = ListInstance::class, @@ -52,58 +55,59 @@ fun ListScreen( } val state: ListState by instance.state.collectAsState() - val items: List? = state.items + val listState: LazyListState = rememberLazyListState() + val coroutineScope: CoroutineScope = rememberCoroutineScope() Scaffold( topBar = { - LargeTopAppBar( + TopAppBar( modifier = Modifier.testTag(TOOLBAR_TAG), title = { Text( - text = "Welcome", + text = "Stack (${state.screens.size})", modifier = Modifier.testTag(TITLE_BAR_TAG) ) }, - actions = { - IconButton( - onClick = onSelectColor, - modifier = Modifier - .testTag(FAVORITE_TAG) - ){ - Icon( - Icons.Rounded.Favorite, - contentDescription = null, - ) - } - } ) - }) { paddingValues -> - - if (items == Loading) Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxWidth() - ) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } else LazyVerticalGrid( - columns = Adaptive(100.dp), + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + instance.add() + coroutineScope.launch { listState.animateScrollToItem(state.screens.lastIndex) } + }, + content = { Icon(Icons.Rounded.Add, null) } + ) + }, + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { paddingValues -> + LazyColumn( + state = listState, modifier = Modifier .padding(paddingValues) .fillMaxSize() .testTag(LIST_TAG), verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(16.dp), ) { - items(items) { item -> + items(state.screens) { screen -> Card( modifier = Modifier - .height(100.dp) + .height(128.dp) + .fillMaxWidth() .clip(MaterialTheme.shapes.medium) - .clickable { onSelect(item) }, + .clickable { onSelect(screen) }, ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = item.toString(), style = MaterialTheme.typography.titleLarge) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = screen.index.toString(), + style = MaterialTheme.typography.displayMedium, + modifier = Modifier + .padding(8.dp) + ) } } } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListStateModels.kt new file mode 100644 index 0000000..0bf5762 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListStateModels.kt @@ -0,0 +1,7 @@ +package io.github.xxfast.decompose.router.screens.stack.list + +import io.github.xxfast.decompose.router.screens.stack.Item +import kotlinx.serialization.Serializable + +@Serializable +data class ListState(val screens: List = List(5) { Item(it) }) diff --git a/app/src/desktopMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt b/app/src/desktopMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt index f05341a..a4a8457 100644 --- a/app/src/desktopMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt +++ b/app/src/desktopMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt @@ -6,12 +6,9 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.jetbrains.lifecycle.LifecycleController -import com.arkivanov.essenty.lifecycle.LifecycleRegistry import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.RouterContext -import io.github.xxfast.decompose.router.app.screens.HomeScreen +import io.github.xxfast.decompose.router.screens.HomeScreen import io.github.xxfast.decompose.router.defaultRouterContext fun main() { diff --git a/app/src/iosMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt b/app/src/iosMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt index 494efb1..ae739e1 100644 --- a/app/src/iosMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt +++ b/app/src/iosMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt @@ -13,7 +13,7 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.PredictiveBackGestur import com.arkivanov.essenty.backhandler.BackDispatcher import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.RouterContext -import io.github.xxfast.decompose.router.app.screens.HomeScreen +import io.github.xxfast.decompose.router.screens.HomeScreen import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.autoreleasepool diff --git a/app/src/jsMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt b/app/src/jsMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt index 5aef9ca..f48fa17 100644 --- a/app/src/jsMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt +++ b/app/src/jsMain/kotlin/io/github/xxfast/decompose/router/app/Application.kt @@ -4,7 +4,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider import io.github.xxfast.decompose.router.LocalRouterContext import io.github.xxfast.decompose.router.RouterContext -import io.github.xxfast.decompose.router.app.screens.HomeScreen +import io.github.xxfast.decompose.router.screens.HomeScreen import io.github.xxfast.decompose.router.app.utils.BrowserViewportWindow import io.github.xxfast.decompose.router.defaultRouterContext import org.jetbrains.skiko.wasm.onWasmReady diff --git a/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt b/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt index c92e4ef..ae5f372 100644 --- a/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt +++ b/decompose-router-wear/src/androidMain/kotlin/io/github/xxfast/decompose/router/wear/content/RoutedContent.kt @@ -19,7 +19,7 @@ import com.arkivanov.decompose.Child import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.pop import io.github.xxfast.decompose.router.LocalRouterContext -import io.github.xxfast.decompose.router.Router +import io.github.xxfast.decompose.router.stack.Router import io.github.xxfast.decompose.router.RouterContext import kotlinx.serialization.Serializable diff --git a/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/Key.kt similarity index 78% rename from decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt rename to decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/Key.kt index 277b122..939c1dc 100644 --- a/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/Key.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import kotlin.reflect.KClass diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Key.kt similarity index 60% rename from decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt rename to decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Key.kt index 7cd7fd1..82abb1c 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/Key.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import kotlin.reflect.KClass diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/SavedState.kt similarity index 92% rename from decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt rename to decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/SavedState.kt index 27344c9..fe1fcea 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/SavedState.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/SavedState.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import com.arkivanov.essenty.instancekeeper.InstanceKeeper import kotlinx.serialization.Serializable diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/State.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/State.kt new file mode 100644 index 0000000..aa83365 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/State.kt @@ -0,0 +1,13 @@ +package io.github.xxfast.decompose + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import com.arkivanov.decompose.value.Value +import com.arkivanov.decompose.value.observe +import com.arkivanov.essenty.lifecycle.Lifecycle + +internal fun Value.asState(lifecycle: Lifecycle): State { + val state = mutableStateOf(value) + observe(lifecycle = lifecycle) { state.value = it } + return state +} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RememberOnRoute.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RememberOnRoute.kt new file mode 100644 index 0000000..69ee6fc --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RememberOnRoute.kt @@ -0,0 +1,52 @@ +package io.github.xxfast.decompose.router + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisallowComposableCalls +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import com.arkivanov.essenty.instancekeeper.getOrCreate +import com.arkivanov.essenty.statekeeper.StateKeeper +import io.github.xxfast.decompose.SavedStateHandle +import io.github.xxfast.decompose.key +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlin.reflect.KClass + +/*** + * Creates an instance of [T] that is scoped to the current route + * + * @param type class of [T] instance + * @param key key to remember the instance with. Defaults to [type]'s key + * @param block lambda to create an instance of [T] with a given [SavedStateHandle] + */ +@Composable +fun rememberOnRoute( + type: KClass, + strategy: KSerializer, + key: Any = type.key, + block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T +): T { + val component: RouterContext = LocalRouterContext.current + val stateKeeper: StateKeeper = component.stateKeeper + val instanceKeeper: InstanceKeeper = component.instanceKeeper + val instanceKey = "$key.instance" + val stateKey = "$key.state" + val (instance, savedState) = remember(key) { + val savedState: SavedStateHandle = instanceKeeper + .getOrCreate(stateKey) { SavedStateHandle(stateKeeper.consume(stateKey, strategy)) } + var instance: T? = instanceKeeper.get(instanceKey) as T? + if (instance == null) { + instance = block(savedState) + instanceKeeper.put(instanceKey, instance) + } + instance to savedState + } + + LaunchedEffect(Unit) { + if (!stateKeeper.isRegistered(stateKey)) + stateKeeper.register(stateKey, strategy) { savedState.value as C? } + } + + return instance +} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/RoutedContent.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/RoutedContent.kt new file mode 100644 index 0000000..8e61277 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/RoutedContent.kt @@ -0,0 +1,42 @@ +package io.github.xxfast.decompose.router.pages + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.Pages +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.PagesScrollAnimation +import com.arkivanov.decompose.extensions.compose.jetbrains.pages.defaultHorizontalPager +import com.arkivanov.decompose.router.pages.select +import io.github.xxfast.decompose.router.LocalRouterContext +import kotlinx.serialization.Serializable + +@OptIn(ExperimentalFoundationApi::class, ExperimentalDecomposeApi::class) +@Composable +fun RoutedContent( + router: Router, + modifier: Modifier = Modifier, + animation: PagesScrollAnimation = PagesScrollAnimation.Default, + pager: @Composable ( + Modifier, + PagerState, + key: (index: Int) -> Any, + pageContent: @Composable PagerScope.(index: Int) -> Unit, + ) -> Unit = defaultHorizontalPager(), + content: @Composable (C) -> Unit, +) { + Pages( + pages = router.pages, + onPageSelected = { index -> router.select(index) }, + modifier = modifier, + pager = pager, + scrollAnimation = animation, + ) { index, page -> + CompositionLocalProvider(LocalRouterContext provides page) { + content(router.pages.value.items[index].configuration) + } + } +} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt new file mode 100644 index 0000000..f312543 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt @@ -0,0 +1,58 @@ +package io.github.xxfast.decompose.router.pages + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.router.pages.ChildPages +import com.arkivanov.decompose.router.pages.Pages +import com.arkivanov.decompose.router.pages.PagesNavigation +import com.arkivanov.decompose.router.pages.childPages +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext +import io.github.xxfast.decompose.asState +import io.github.xxfast.decompose.router.getOrCreate +import io.github.xxfast.decompose.key +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializerOrNull +import kotlin.reflect.KClass + +@OptIn(ExperimentalDecomposeApi::class) +class Router internal constructor( + private val navigation: PagesNavigation, + val pages: State>, +) : PagesNavigation by navigation + +@OptIn(ExperimentalDecomposeApi::class, InternalSerializationApi::class) +@Composable +fun rememberRouter( + type: KClass, + key: Any = type.key, + handleBackButton: Boolean = true, + initialPages: () -> Pages, +): Router { + val routerContext: RouterContext = LocalRouterContext.current + val pagerKey = "$key.router" + + return remember(pagerKey) { + routerContext.getOrCreate(key = pagerKey) { + val navigation: PagesNavigation = PagesNavigation() + val stack: State> = routerContext + .childPages( + source = navigation, + serializer = type.serializerOrNull(), + initialPages = initialPages, + key = pagerKey, + handleBackButton = handleBackButton, + childFactory = { _, childComponentContext -> RouterContext(childComponentContext) }, + ) + .asState(routerContext.lifecycle) + + Router(navigation, stack) + } + } +} + +@OptIn(ExperimentalDecomposeApi::class) +fun pagesOf(vararg pages: C, selectedIndex: Int = 0): Pages = Pages(pages.toList(), selectedIndex) diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RoutedContent.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/RoutedContent.kt similarity index 85% rename from decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RoutedContent.kt rename to decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/RoutedContent.kt index 994f291..b7a6c98 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/RoutedContent.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/RoutedContent.kt @@ -1,10 +1,12 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose.router.stack import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext import kotlinx.serialization.Serializable /*** diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/Router.kt similarity index 51% rename from decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt rename to decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/Router.kt index 8330dad..dda61e2 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/Router.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/stack/Router.kt @@ -1,23 +1,16 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose.router.stack import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisallowComposableCalls -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.value.Value -import com.arkivanov.decompose.value.observe -import com.arkivanov.essenty.instancekeeper.InstanceKeeper -import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance -import com.arkivanov.essenty.instancekeeper.getOrCreate -import com.arkivanov.essenty.lifecycle.Lifecycle -import com.arkivanov.essenty.statekeeper.StateKeeper +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext +import io.github.xxfast.decompose.asState +import io.github.xxfast.decompose.router.getOrCreate +import io.github.xxfast.decompose.key import kotlinx.serialization.* import kotlin.reflect.KClass @@ -28,7 +21,7 @@ import kotlin.reflect.KClass * @param navigation decompose navigator to use * @param stack state of decompose child stack to use */ -class Router( +class Router internal constructor( private val navigation: StackNavigation, val stack: State>, ) : StackNavigation by navigation @@ -78,47 +71,3 @@ fun rememberRouter( } } } - -fun Value.asState(lifecycle: Lifecycle): State { - val state = mutableStateOf(value) - observe(lifecycle = lifecycle) { state.value = it } - return state -} - -/*** - * Creates an instance of [T] that is scoped to the current route - * - * @param type class of [T] instance - * @param key key to remember the instance with. Defaults to [type]'s key - * @param block lambda to create an instance of [T] with a given [SavedStateHandle] - */ -@Composable -fun rememberOnRoute( - type: KClass, - strategy: KSerializer, - key: Any = type.key, - block: @DisallowComposableCalls (savedState: SavedStateHandle) -> T -): T { - val component: RouterContext = LocalRouterContext.current - val stateKeeper: StateKeeper = component.stateKeeper - val instanceKeeper: InstanceKeeper = component.instanceKeeper - val instanceKey = "$key.instance" - val stateKey = "$key.state" - val (instance, savedState) = remember(key) { - val savedState: SavedStateHandle = instanceKeeper - .getOrCreate(stateKey) { SavedStateHandle(stateKeeper.consume(stateKey, strategy)) } - var instance: T? = instanceKeeper.get(instanceKey) as T? - if (instance == null) { - instance = block(savedState) - instanceKeeper.put(instanceKey, instance) - } - instance to savedState - } - - LaunchedEffect(Unit) { - if (!stateKeeper.isRegistered(stateKey)) - stateKeeper.register(stateKey, strategy) { savedState.value as C? } - } - - return instance -} diff --git a/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/Key.kt similarity index 78% rename from decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt rename to decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/Key.kt index 277b122..939c1dc 100644 --- a/decompose-router/src/androidMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/desktopMain/kotlin/io/github/xxfast/decompose/Key.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import kotlin.reflect.KClass diff --git a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/Key.kt similarity index 78% rename from decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt rename to decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/Key.kt index 277b122..939c1dc 100644 --- a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/Key.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import kotlin.reflect.KClass diff --git a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/DefaultRouterContext.kt b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/DefaultRouterContext.kt index bdda8ab..be20415 100644 --- a/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/DefaultRouterContext.kt +++ b/decompose-router/src/iosMain/kotlin/io/github/xxfast/decompose/router/DefaultRouterContext.kt @@ -1,7 +1,6 @@ package io.github.xxfast.decompose.router import com.arkivanov.essenty.backhandler.BackDispatcher -import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleRegistry import com.arkivanov.essenty.lifecycle.destroy import com.arkivanov.essenty.lifecycle.pause diff --git a/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt b/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/Key.kt similarity index 85% rename from decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt rename to decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/Key.kt index 2ec0c5a..8c4ba25 100644 --- a/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/router/Key.kt +++ b/decompose-router/src/jsMain/kotlin/io/github/xxfast/decompose/Key.kt @@ -1,4 +1,4 @@ -package io.github.xxfast.decompose.router +package io.github.xxfast.decompose import kotlin.reflect.KClass From 2c36180402bea7c1ff75801c61c6e43681686c08 Mon Sep 17 00:00:00 2001 From: xxfast Date: Mon, 22 Jan 2024 11:19:35 +1100 Subject: [PATCH 02/11] Add slot router --- .../decompose/router/screens/HomeScreen.kt | 3 +- .../router/screens/slot/SlotScreen.kt | 125 ++++++++++++++++++ .../router/screens/slot/SlotStateModals.kt | 7 + .../xxfast/decompose/router/pages/Router.kt | 8 +- .../decompose/router/slot/RoutedContent.kt | 29 ++++ .../xxfast/decompose/router/slot/Router.kt | 52 ++++++++ 6 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt create mode 100644 app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/RoutedContent.kt create mode 100644 decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/Router.kt diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt index a271681..26ea8d4 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt @@ -25,6 +25,7 @@ import io.github.xxfast.decompose.router.screens.HomeScreens.Page import io.github.xxfast.decompose.router.screens.HomeScreens.Slot import io.github.xxfast.decompose.router.screens.HomeScreens.Stack import io.github.xxfast.decompose.router.screens.pages.PagesScreen +import io.github.xxfast.decompose.router.screens.slot.SlotScreen import io.github.xxfast.decompose.router.screens.stack.StackScreen @OptIn(ExperimentalDecomposeApi::class, ExperimentalFoundationApi::class) @@ -73,7 +74,7 @@ fun HomeScreen() { when (screen) { Stack -> StackScreen() Page -> PagesScreen() - Slot -> Text("Slot") + Slot -> SlotScreen() } } } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt new file mode 100644 index 0000000..ee2f163 --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt @@ -0,0 +1,125 @@ +package io.github.xxfast.decompose.router.screens.slot + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG +import io.github.xxfast.decompose.router.slot.RoutedContent +import io.github.xxfast.decompose.router.slot.Router +import io.github.xxfast.decompose.router.slot.rememberRouter + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SlotScreen() { + val dialogRouter: Router = rememberRouter(DialogScreens::class) { null } + val bottomSheetRouter: Router = + rememberRouter(BottomSheetScreens::class) { null } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "Slot", + modifier = Modifier.testTag(TITLE_BAR_TAG) + ) + } + ) + }, + ) { scaffoldPadding -> + Column( + modifier = Modifier + .padding(scaffoldPadding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button( + onClick = { dialogRouter.activate(DialogScreens()) } + ) { + Text("Show Dialog") + } + + Button( + onClick = { bottomSheetRouter.activate(BottomSheetScreens()) } + ) { + Text("Show Bottom Sheet") + } + } + } + + RoutedContent(dialogRouter) { screens -> + AlertDialog( + onDismissRequest = { dialogRouter.dismiss() }, + confirmButton = { + TextButton(onClick = { dialogRouter.dismiss() }) { Text("Ok") } + }, + title = { Text("Dialog") }, + text = { + Text( + text = screens.toString(), + modifier = Modifier.padding(8.dp) + ) + } + ) + } + + RoutedContent(bottomSheetRouter) { screen -> + val sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { bottomSheetRouter.dismiss() }, + modifier = Modifier + .height(480.dp) + ) { + Scaffold( + topBar = { TopAppBar(title = { Text("Bottom Sheet") }) }, + bottomBar = { + BottomAppBar { + Spacer(modifier = Modifier.weight(1f)) + TextButton( + onClick = { bottomSheetRouter.dismiss() }, + modifier = Modifier.padding(16.dp) + ){ + Text("Ok") + } + } + } + ) { scaffoldPadding -> + Box( + modifier = Modifier.fillMaxSize().padding(scaffoldPadding) + ) { + Text( + text = screen.toString(), + modifier = Modifier + .padding(16.dp) + .align(Alignment.Center) + ) + } + } + } + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt new file mode 100644 index 0000000..6e341ce --- /dev/null +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt @@ -0,0 +1,7 @@ +package io.github.xxfast.decompose.router.screens.slot + +import kotlinx.serialization.Serializable + +@Serializable class DialogScreens + +@Serializable class BottomSheetScreens diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt index f312543..c2f077f 100644 --- a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/pages/Router.kt @@ -33,17 +33,17 @@ fun rememberRouter( initialPages: () -> Pages, ): Router { val routerContext: RouterContext = LocalRouterContext.current - val pagerKey = "$key.router" + val routerKey = "$key.router" - return remember(pagerKey) { - routerContext.getOrCreate(key = pagerKey) { + return remember(routerKey) { + routerContext.getOrCreate(key = routerKey) { val navigation: PagesNavigation = PagesNavigation() val stack: State> = routerContext .childPages( source = navigation, serializer = type.serializerOrNull(), initialPages = initialPages, - key = pagerKey, + key = routerKey, handleBackButton = handleBackButton, childFactory = { _, childComponentContext -> RouterContext(childComponentContext) }, ) diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/RoutedContent.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/RoutedContent.kt new file mode 100644 index 0000000..7d2a6a8 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/RoutedContent.kt @@ -0,0 +1,29 @@ +package io.github.xxfast.decompose.router.slot + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.router.slot.ChildSlot +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext +import kotlinx.serialization.Serializable + +/*** + * Composable to hoist content that are navigated by the router + * + * @param router Router to be used + * @param content + */ +@Composable +fun RoutedContent( + router: Router, + content: @Composable (C) -> Unit, +) { + val slot: ChildSlot by router.slot + val child: Child.Created? = slot.child + + if (child != null) CompositionLocalProvider(LocalRouterContext provides child.instance) { + content(child.configuration) + } +} diff --git a/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/Router.kt b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/Router.kt new file mode 100644 index 0000000..0f522c4 --- /dev/null +++ b/decompose-router/src/commonMain/kotlin/io/github/xxfast/decompose/router/slot/Router.kt @@ -0,0 +1,52 @@ +package io.github.xxfast.decompose.router.slot + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import com.arkivanov.decompose.router.slot.ChildSlot +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.childSlot +import io.github.xxfast.decompose.asState +import io.github.xxfast.decompose.key +import io.github.xxfast.decompose.router.LocalRouterContext +import io.github.xxfast.decompose.router.RouterContext +import io.github.xxfast.decompose.router.getOrCreate +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializerOrNull +import kotlin.reflect.KClass + +class Router internal constructor( + val navigation: SlotNavigation, + val slot: State>, +): SlotNavigation by navigation + +@OptIn(InternalSerializationApi::class) +@Composable +fun rememberRouter( + type: KClass, + key: Any = type.key, + handleBackButton: Boolean = true, + initialConfiguration: () -> C?, +): Router { + val routerContext: RouterContext = LocalRouterContext.current + val routerKey = "$key.router" + + return remember(routerKey) { + routerContext.getOrCreate(key = routerKey) { + val navigation: SlotNavigation = SlotNavigation() + val slot: State> = routerContext + .childSlot( + source = navigation, + serializer = type.serializerOrNull(), + initialConfiguration = initialConfiguration, + key = routerKey, + handleBackButton = handleBackButton, + childFactory = { _, childComponentContext -> RouterContext(childComponentContext) }, + ) + .asState(routerContext.lifecycle) + + Router(navigation, slot) + } + } +} From 7bf792e11f992ea9c6f9cdf91695e22d6288c8f6 Mon Sep 17 00:00:00 2001 From: xxfast Date: Mon, 22 Jan 2024 11:21:16 +1100 Subject: [PATCH 03/11] Add missing ios run configuration --- .idea/runConfigurations/ios.xml | 25 +----- .../xcshareddata/xcschemes/ios.xcscheme | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 app/ios/ios.xcodeproj/xcshareddata/xcschemes/ios.xcscheme diff --git a/.idea/runConfigurations/ios.xml b/.idea/runConfigurations/ios.xml index e1f97d8..7566ccc 100644 --- a/.idea/runConfigurations/ios.xml +++ b/.idea/runConfigurations/ios.xml @@ -1,24 +1,7 @@ - - - - - - true - true - false - false - + + + \ No newline at end of file diff --git a/app/ios/ios.xcodeproj/xcshareddata/xcschemes/ios.xcscheme b/app/ios/ios.xcodeproj/xcshareddata/xcschemes/ios.xcscheme new file mode 100644 index 0000000..8676930 --- /dev/null +++ b/app/ios/ios.xcodeproj/xcshareddata/xcschemes/ios.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 05979eb635d13278a3b841c91346ab19d94ac192 Mon Sep 17 00:00:00 2001 From: xxfast Date: Mon, 22 Jan 2024 11:57:56 +1100 Subject: [PATCH 04/11] Update API --- .../api/android/decompose-router-wear.api | 2 +- .../api/android/decompose-router.api | 83 +++++++++++++------ .../api/desktop/decompose-router.api | 83 +++++++++++++------ 3 files changed, 119 insertions(+), 49 deletions(-) diff --git a/decompose-router-wear/api/android/decompose-router-wear.api b/decompose-router-wear/api/android/decompose-router-wear.api index 556119f..474b180 100644 --- a/decompose-router-wear/api/android/decompose-router-wear.api +++ b/decompose-router-wear/api/android/decompose-router-wear.api @@ -1,4 +1,4 @@ public final class io/github/xxfast/decompose/router/wear/content/RoutedContentKt { - public static final fun RoutedContent (Lio/github/xxfast/decompose/router/Router;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/stack/Router;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } diff --git a/decompose-router/api/android/decompose-router.api b/decompose-router/api/android/decompose-router.api index 8886aee..ad68a9b 100644 --- a/decompose-router/api/android/decompose-router.api +++ b/decompose-router/api/android/decompose-router.api @@ -1,19 +1,23 @@ +public final class io/github/xxfast/decompose/KeyKt { + public static final fun getKey (Lkotlin/reflect/KClass;)Ljava/lang/String; +} + +public final class io/github/xxfast/decompose/SavedStateHandle : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun get ()Ljava/lang/Object; + public final fun getValue ()Ljava/lang/Object; + public fun onDestroy ()V + public final fun set (Ljava/lang/Object;)V +} + public final class io/github/xxfast/decompose/router/DefaultRouterContextKt { public static final fun defaultRouterContext (Landroidx/activity/ComponentActivity;)Lio/github/xxfast/decompose/router/RouterContext; public static final fun defaultRouterContext (Landroidx/fragment/app/Fragment;Landroidx/activity/OnBackPressedDispatcher;)Lio/github/xxfast/decompose/router/RouterContext; } -public final class io/github/xxfast/decompose/router/KeyKt { - public static final fun getKey (Lkotlin/reflect/KClass;)Ljava/lang/String; -} - -public final class io/github/xxfast/decompose/router/Router : com/arkivanov/decompose/router/stack/StackNavigation { - public static final field $stable I - public fun (Lcom/arkivanov/decompose/router/stack/StackNavigation;Landroidx/compose/runtime/State;)V - public final fun getStack ()Landroidx/compose/runtime/State; - public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public fun subscribe (Lkotlin/jvm/functions/Function1;)V - public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +public final class io/github/xxfast/decompose/router/RememberOnRouteKt { + public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; } public final class io/github/xxfast/decompose/router/RouterContext : com/arkivanov/decompose/ComponentContext { @@ -32,23 +36,54 @@ public final class io/github/xxfast/decompose/router/RouterContextKt { public static final fun getLocalRouterContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; } -public final class io/github/xxfast/decompose/router/RouterKt { - public static final fun asState (Lcom/arkivanov/decompose/value/Value;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Landroidx/compose/runtime/State; - public static final fun getLocalRouter ()Landroidx/compose/runtime/ProvidableCompositionLocal; - public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; - public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/Router; +public final class io/github/xxfast/decompose/router/pages/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/pages/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } -public final class io/github/xxfast/decompose/router/SavedStateHandle : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { +public final class io/github/xxfast/decompose/router/pages/Router : com/arkivanov/decompose/router/pages/PagesNavigation { public static final field $stable I - public fun (Ljava/lang/Object;)V - public final fun get ()Ljava/lang/Object; - public final fun getValue ()Ljava/lang/Object; - public fun onDestroy ()V - public final fun set (Ljava/lang/Object;)V + public final fun getPages ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class io/github/xxfast/decompose/router/pages/RouterKt { + public static final fun pagesOf ([Ljava/lang/Object;I)Lcom/arkivanov/decompose/router/pages/Pages; + public static synthetic fun pagesOf$default ([Ljava/lang/Object;IILjava/lang/Object;)Lcom/arkivanov/decompose/router/pages/Pages; + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/pages/Router; +} + +public final class io/github/xxfast/decompose/router/slot/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/slot/Router;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V +} + +public final class io/github/xxfast/decompose/router/slot/Router : com/arkivanov/decompose/router/slot/SlotNavigation { + public static final field $stable I + public final fun getNavigation ()Lcom/arkivanov/decompose/router/slot/SlotNavigation; + public final fun getSlot ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class io/github/xxfast/decompose/router/slot/RouterKt { + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/slot/Router; +} + +public final class io/github/xxfast/decompose/router/stack/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/stack/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/github/xxfast/decompose/router/stack/Router : com/arkivanov/decompose/router/stack/StackNavigation { + public static final field $stable I + public final fun getStack ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V } -public final class io/github/xxfast/decompose/router/content/RoutedContentKt { - public static final fun RoutedContent (Lio/github/xxfast/decompose/router/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +public final class io/github/xxfast/decompose/router/stack/RouterKt { + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/stack/Router; } diff --git a/decompose-router/api/desktop/decompose-router.api b/decompose-router/api/desktop/decompose-router.api index b2c9131..c2221ea 100644 --- a/decompose-router/api/desktop/decompose-router.api +++ b/decompose-router/api/desktop/decompose-router.api @@ -1,18 +1,22 @@ -public final class io/github/xxfast/decompose/router/DefaultRouterContextKt { - public static final fun defaultRouterContext (Lcom/arkivanov/essenty/backhandler/BackDispatcher;Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;Landroidx/compose/ui/window/WindowState;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/RouterContext; -} - -public final class io/github/xxfast/decompose/router/KeyKt { +public final class io/github/xxfast/decompose/KeyKt { public static final fun getKey (Lkotlin/reflect/KClass;)Ljava/lang/String; } -public final class io/github/xxfast/decompose/router/Router : com/arkivanov/decompose/router/stack/StackNavigation { +public final class io/github/xxfast/decompose/SavedStateHandle : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { public static final field $stable I - public fun (Lcom/arkivanov/decompose/router/stack/StackNavigation;Landroidx/compose/runtime/State;)V - public final fun getStack ()Landroidx/compose/runtime/State; - public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public fun subscribe (Lkotlin/jvm/functions/Function1;)V - public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V + public fun (Ljava/lang/Object;)V + public final fun get ()Ljava/lang/Object; + public final fun getValue ()Ljava/lang/Object; + public fun onDestroy ()V + public final fun set (Ljava/lang/Object;)V +} + +public final class io/github/xxfast/decompose/router/DefaultRouterContextKt { + public static final fun defaultRouterContext (Lcom/arkivanov/essenty/backhandler/BackDispatcher;Lcom/arkivanov/essenty/lifecycle/LifecycleRegistry;Landroidx/compose/ui/window/WindowState;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/RouterContext; +} + +public final class io/github/xxfast/decompose/router/RememberOnRouteKt { + public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; } public final class io/github/xxfast/decompose/router/RouterContext : com/arkivanov/decompose/ComponentContext { @@ -31,23 +35,54 @@ public final class io/github/xxfast/decompose/router/RouterContextKt { public static final fun getLocalRouterContext ()Landroidx/compose/runtime/ProvidableCompositionLocal; } -public final class io/github/xxfast/decompose/router/RouterKt { - public static final fun asState (Lcom/arkivanov/decompose/value/Value;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Landroidx/compose/runtime/State; - public static final fun getLocalRouter ()Landroidx/compose/runtime/ProvidableCompositionLocal; - public static final fun rememberOnRoute (Lkotlin/reflect/KClass;Lkotlinx/serialization/KSerializer;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; - public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/Router; +public final class io/github/xxfast/decompose/router/pages/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/pages/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/pages/PagesScrollAnimation;Lkotlin/jvm/functions/Function6;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } -public final class io/github/xxfast/decompose/router/SavedStateHandle : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { +public final class io/github/xxfast/decompose/router/pages/Router : com/arkivanov/decompose/router/pages/PagesNavigation { public static final field $stable I - public fun (Ljava/lang/Object;)V - public final fun get ()Ljava/lang/Object; - public final fun getValue ()Ljava/lang/Object; - public fun onDestroy ()V - public final fun set (Ljava/lang/Object;)V + public final fun getPages ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class io/github/xxfast/decompose/router/pages/RouterKt { + public static final fun pagesOf ([Ljava/lang/Object;I)Lcom/arkivanov/decompose/router/pages/Pages; + public static synthetic fun pagesOf$default ([Ljava/lang/Object;IILjava/lang/Object;)Lcom/arkivanov/decompose/router/pages/Pages; + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/pages/Router; +} + +public final class io/github/xxfast/decompose/router/slot/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/slot/Router;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V +} + +public final class io/github/xxfast/decompose/router/slot/Router : com/arkivanov/decompose/router/slot/SlotNavigation { + public static final field $stable I + public final fun getNavigation ()Lcom/arkivanov/decompose/router/slot/SlotNavigation; + public final fun getSlot ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V +} + +public final class io/github/xxfast/decompose/router/slot/RouterKt { + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/slot/Router; +} + +public final class io/github/xxfast/decompose/router/stack/RoutedContentKt { + public static final fun RoutedContent (Lio/github/xxfast/decompose/router/stack/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/github/xxfast/decompose/router/stack/Router : com/arkivanov/decompose/router/stack/StackNavigation { + public static final field $stable I + public final fun getStack ()Landroidx/compose/runtime/State; + public fun navigate (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V + public fun subscribe (Lkotlin/jvm/functions/Function1;)V + public fun unsubscribe (Lkotlin/jvm/functions/Function1;)V } -public final class io/github/xxfast/decompose/router/content/RoutedContentKt { - public static final fun RoutedContent (Lio/github/xxfast/decompose/router/Router;Landroidx/compose/ui/Modifier;Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimation;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +public final class io/github/xxfast/decompose/router/stack/RouterKt { + public static final fun rememberRouter (Lkotlin/reflect/KClass;Ljava/lang/Object;ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;II)Lio/github/xxfast/decompose/router/stack/Router; } From 5a0a5f69406dc3de098d2bcc63724378715fb096 Mon Sep 17 00:00:00 2001 From: xxfast Date: Mon, 22 Jan 2024 16:50:41 +1100 Subject: [PATCH 05/11] Add documentation for pages and slots --- .../router/screens/slot/SlotScreen.kt | 3 +- .../router/screens/slot/SlotStateModals.kt | 2 +- docs/decompose-router.tree | 2 + docs/topics/using-decompose-router.md | 11 +++ docs/topics/using-pages-navigation.md | 66 ++++++++++++++++++ docs/topics/using-slot-navigation.md | 68 +++++++++++++++++++ docs/topics/using-stack-navigation.md | 35 +++++----- 7 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 docs/topics/using-pages-navigation.md create mode 100644 docs/topics/using-slot-navigation.md diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt index ee2f163..4403660 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -58,7 +57,7 @@ fun SlotScreen() { verticalArrangement = Arrangement.Center ) { Button( - onClick = { dialogRouter.activate(DialogScreens()) } + onClick = { dialogRouter.activate(DialogScreens) } ) { Text("Show Dialog") } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt index 6e341ce..d7e53fb 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotStateModals.kt @@ -2,6 +2,6 @@ package io.github.xxfast.decompose.router.screens.slot import kotlinx.serialization.Serializable -@Serializable class DialogScreens +@Serializable object DialogScreens @Serializable class BottomSheetScreens diff --git a/docs/decompose-router.tree b/docs/decompose-router.tree index 9af6adf..fb2b94d 100644 --- a/docs/decompose-router.tree +++ b/docs/decompose-router.tree @@ -10,5 +10,7 @@ + + \ No newline at end of file diff --git a/docs/topics/using-decompose-router.md b/docs/topics/using-decompose-router.md index 9a72169..8b9c435 100644 --- a/docs/topics/using-decompose-router.md +++ b/docs/topics/using-decompose-router.md @@ -34,6 +34,17 @@ sealed class Screens { } ``` +Another example, collection of pages +```kotlin +@Serializable +enum class Pages { Page1, Page2, Page3 } +``` + +Or if your navigation graph is just a single page, it can even be a single object +```kotlin +@Serializable object Screen +``` + > `@Serializable` is optional but **preferred** if you want your screen state to > 1. Be retained across configuration changes (on Android) > 2. Survive process death (on Android) diff --git a/docs/topics/using-pages-navigation.md b/docs/topics/using-pages-navigation.md new file mode 100644 index 0000000..f5381b7 --- /dev/null +++ b/docs/topics/using-pages-navigation.md @@ -0,0 +1,66 @@ +# Pages Navigation + +Pages navigation is for managing a list of pages, where one selected page that is shown to the user. + +Define your navigation model, (as already covered in [model-driven navigation section](using-decompose-router.md#model-driven-navigation)) + +```kotlin +@Serializable +enum class PagesScreens { Page1, Page2, Page3 } +``` + +## Creating a router with page navigation model + +````kotlin +@Composable +fun PagesScreen() { + val router: Router = rememberRouter(PagesScreens::class) { pagesOf(Page1, Page2, Page3) } +} +```` + +> Due to this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900), you will still need to provide this +> type `PagesScreens:class` manually for now. +> Once resolved, you will be able to use the `inline` `refied` (and nicer) signature +> ```kotlin +> val router: Router = rememberRouter { pagesOf(Page1, Page2, Page3) } +> ``` +{style="warning"} + +## Consuming the state from the router + +Use `RoutedContent` to consume the state from the router. + +```kotlin +@Composable +fun PagesScreen() { + val router: Router = rememberRouter(PagesScreens::class) { pagesOf(Page1, Page2, Page3) } + + RoutedContent(router = router) { screen: PagesScreens -> + when (screen) { + Page1 -> Page1Screen() + Page2 -> Page2Screen() + Page3 -> Page3Screen() + } + } +} +``` + +## Navigating with page navigation router + +Decompose-router exposes the same Decompose page navigator extension [functions](https://arkivanov.github.io/Decompose/navigation/stack/navigation/#stacknavigator-extension-functions) + +```kotlin +val router: Router = rememberRouter(PagesScreens::class) { pagesOf(Page1, Page2, Page3) } + +// To go to second page +Button(onClick = { number -> router.select(1) }) + +// To go back to first screen +Button(onClick = { router.selectFirst() }) +``` + + + + Decompose API Documentation for pages + + \ No newline at end of file diff --git a/docs/topics/using-slot-navigation.md b/docs/topics/using-slot-navigation.md new file mode 100644 index 0000000..87f73a1 --- /dev/null +++ b/docs/topics/using-slot-navigation.md @@ -0,0 +1,68 @@ +# Slot Navigation + +Slot navigation is for managing one (or none) screen at a time, where user can activate or dismiss a screen + +Typical use cases are +- Dialogs +- Bottom Sheets + +Define your navigation model, (as already covered in [model-driven navigation section](using-decompose-router.md#model-driven-navigation)) + +```kotlin +@Serializable object SlotScreens +``` + +## Creating a router with slot navigation model + +````kotlin +@Composable +fun SlotScreen() { + val router: Router = rememberRouter(SlotScreens::class) { null } +} +```` + +> Due to this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900), you will still need to provide this +> type `SlotScreens:class` manually for now. +> Once resolved, you will be able to use the `inline` `refied` (and nicer) signature +> ```kotlin +> val router: Router = rememberRouter { null } +> ``` +{style="warning"} + +## Using the router to open up a dialog + +Use `RoutedContent` to use the router to open a dialog. + +```kotlin +@Composable +fun SlotScreen() { + val router: Router = rememberRouter(SlotScreens::class) { null } + + RoutedContent(router) { _ -> + AlertDialog( + onDismissRequest = { dialogRouter.dismiss() }, + ){ + // Dialog content here + ) + } +} +``` + +## Navigating with slot navigation router + +Decompose-router exposes the same Decompose page navigator extension [functions](https://arkivanov.github.io/Decompose/navigation/stack/navigation/#stacknavigator-extension-functions) + +```kotlin +// A example of how to show a dialog +Button( + onClick = { dialogRouter.activate(SlotScreens) } +) { + Text("Show Dialog") +} +``` + + + + Decompose API Documentation for slots + + \ No newline at end of file diff --git a/docs/topics/using-stack-navigation.md b/docs/topics/using-stack-navigation.md index 89a3315..ed60d55 100644 --- a/docs/topics/using-stack-navigation.md +++ b/docs/topics/using-stack-navigation.md @@ -1,15 +1,16 @@ # Stack Navigation -Define your navigation model, (as already covered -in [model-driven navigation section](using-decompose-router.md#model-driven-navigation)) +Stack navigation is for managing a stack of screens, where only screen at the top of the stack is shown to the user. + +Define your navigation model, (as already covered in [model-driven navigation section](using-decompose-router.md#model-driven-navigation)) ```kotlin @Serializable -sealed class Screens { +sealed class StackScreens { @Serializable - data object List : Screens() + data object List : StackScreens() @Serializable - data class Details(val number: Int) : Screens() + data class Details(val number: Int) : StackScreens() } ``` @@ -17,18 +18,18 @@ sealed class Screens { ````kotlin @Composable -fun HomeScreen() { - val router: Router = rememberRouter(Screens::class) { - listOf(Screens.List) // Root screen to be set here +fun StackScreen() { + val router: Router = rememberRouter(StackScreens::class) { + listOf(StackScreens.List) // Root screen to be set here } } ```` > Due to this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900), you will still need to provide this -> type `Screen:class` manually for now. +> type `StackScreens:class` manually for now. > Once resolved, you will be able to use the `inline` `refied` (and nicer) signature > ```kotlin -> val router: Router = rememberRouter { listOf(Screens.List) } +> val router: Router = rememberRouter { listOf(StackScreens.List) } > ``` {style="warning"} @@ -38,13 +39,13 @@ Use `RoutedContent` to consume the state from the router. ```kotlin @Composable -fun HomeScreen() { - val router: Router = rememberRouter(Screens::class) { listOf(Screens.List) } +fun StackScreen() { + val router: Router = rememberRouter(StackScreens::class) { listOf(StackScreens.List) } - RoutedContent(router = router) { screen: Screens -> + RoutedContent(router = router) { screen: StackScreens -> when (screen) { - HomeScreens.List -> ListScreen() - is Details -> DetailScreen(screen.number) + StackScreens.List -> ListScreen() + is StackScreens.Details -> DetailScreen(screen.number) } } } @@ -56,10 +57,10 @@ Decompose-router exposes the same Decompose stack navigator extension [functions ```kotlin -val router: Router = rememberRouter(HomeScreens::class) { listOf(HomeScreens.List) } +val router: Router = rememberRouter(StackScreens::class) { listOf(StackScreens.List) } // To go to details screen -Button(onClick = { number -> router.push(Details(number)) }) +Button(onClick = { number -> router.push(StackScreens.Details(number)) }) // To go back to list screen Button(onClick = { router.pop() }) From dc209827b02ffef4be7c31b000f79c9c05b9a5ce Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 07:54:18 +1100 Subject: [PATCH 06/11] Add test cases for pages and slot navigation --- .idea/runConfigurations/ios.xml | 2 +- .../router/app/TestDecomposeRouter.kt | 131 ------------------ .../decompose/router/app/TestElements.kt | 32 +++++ .../decompose/router/app/TestNestedRouters.kt | 69 +++++++++ .../decompose/router/app/TestPagesRouters.kt | 34 +++++ .../decompose/router/app/TestSlotRouters.kt | 53 +++++++ .../decompose/router/app/TestStackRouter.kt | 129 +++++++++++++++++ .../decompose/router/screens/HomeScreen.kt | 22 ++- .../router/screens/HomeStateModels.kt | 2 +- .../decompose/router/screens/TestTags.kt | 17 ++- .../router/screens/pages/PagesScreen.kt | 9 +- .../router/screens/slot/SlotScreen.kt | 14 +- .../screens/stack/details/DetailScreen.kt | 2 + .../router/screens/stack/list/ListScreen.kt | 4 +- 14 files changed, 372 insertions(+), 148 deletions(-) delete mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt create mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestElements.kt create mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt create mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestPagesRouters.kt create mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestSlotRouters.kt create mode 100644 app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt diff --git a/.idea/runConfigurations/ios.xml b/.idea/runConfigurations/ios.xml index 7566ccc..cedb443 100644 --- a/.idea/runConfigurations/ios.xml +++ b/.idea/runConfigurations/ios.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt deleted file mode 100644 index 234987f..0000000 --- a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestDecomposeRouter.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.github.xxfast.decompose.router.app - -import android.content.pm.ActivityInfo -import androidx.compose.ui.semantics.ProgressBarRangeInfo -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.hasProgressBarRangeInfo -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.AndroidComposeTestRule -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onChildAt -import androidx.compose.ui.test.onChildren -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToNode -import androidx.test.ext.junit.rules.ActivityScenarioRule -import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG -import io.github.xxfast.decompose.router.screens.DETAILS_TAG -import io.github.xxfast.decompose.router.screens.FAVORITE_TAG -import io.github.xxfast.decompose.router.screens.LIST_TAG -import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG -import org.junit.Rule -import org.junit.Test - -private val backButton = hasTestTag(BACK_BUTTON_TAG) -private val titleBar = hasTestTag(TITLE_BAR_TAG) -private val details = hasTestTag(DETAILS_TAG) -private val circularProgressIndicator = hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate) -private val lazyColumn = hasTestTag(LIST_TAG) -private val favouriteButton = hasTestTag(FAVORITE_TAG) - -typealias TestActivityRule = AndroidComposeTestRule, MainActivity> - -class TestDecomposeRouterWithActivity { - @get:Rule - val composeRule: TestActivityRule = createAndroidComposeRule() - - @OptIn(ExperimentalTestApi::class) - @Test - fun testBasicNavigation(): Unit = with(composeRule) { - // Check the initial state - onNode(circularProgressIndicator).assertExists() - onNode(titleBar).assertExists().assertTextEquals("Welcome") - - // Wait till the screen is populated - waitUntilAtLeastOneExists(lazyColumn) - - // Go to the 4th item - var testItem = "4" - onNode(lazyColumn).performScrollToNode(hasText(testItem)) - // Click on the 10th item - onNode(hasText(testItem)).performClick() - - // Verify if detail is shown is correct - onNode(titleBar).assertExists().assertTextEquals(testItem) - - // Do the little game - onNode(details).onChildren().fetchSemanticsNodes().forEachIndexed { index, _ -> - onNode(details).onChildAt(index).performClick() - } - - // Verify if auto-navigation works - onNode(titleBar).assertExists().assertTextEquals("Welcome") - - // Verify if state and scroll position is restored - onNode(circularProgressIndicator).assertDoesNotExist() - onNode(hasText(testItem)).assertExists() - - // Go to the 100th item - testItem = "100" - onNode(lazyColumn).performScrollToNode(hasText(testItem)) - onNode(hasText(testItem)).performClick() - onNode(titleBar).assertExists().assertTextEquals(testItem) - onNode(circularProgressIndicator).assertDoesNotExist() - onNode(hasText(testItem)).assertExists() - - // Test go back - onNode(backButton).assertExists().performClick() - onNode(titleBar).assertExists().assertTextEquals("Welcome") - } - - @OptIn(ExperimentalTestApi::class) - @Test - fun testRetainInstanceAcrossConfigurationChanges(): Unit = with(composeRule) { - // Wait till the screen is populated - waitUntilAtLeastOneExists(lazyColumn) - - // Trigger configuration change - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - - // Test if the loaded data is not lost - onNode(lazyColumn).assertExists() - onNode(circularProgressIndicator).assertDoesNotExist() - - // Go to the 50th item - val testItem = "50" - onNode(lazyColumn).performScrollToNode(hasText(testItem)) - // Click on the 10th item - onNode(hasText(testItem)).performClick() - - // Verify if detail is shown is correct - onNode(titleBar).assertExists().assertTextEquals(testItem) - - // Trigger configuration change again - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - - // Navigate back - onNode(backButton).assertExists().performClick() - onNode(titleBar).assertExists().assertTextEquals("Welcome") - - // Verify if state and scroll position is restored - onNode(circularProgressIndicator).assertDoesNotExist() - onNode(hasText(testItem)).assertExists() - } - - @Test - fun testNestedNavigation(): Unit = with(composeRule) { - onNode(favouriteButton).performClick() - onNode(titleBar).assertExists().assertTextEquals("Colors") - onNode(hasText("Primary")).performClick() - onNode(titleBar).assertExists().assertTextEquals("Primary") - onNode(backButton).performClick() - onNode(hasText("Secondary")).performClick() - onNode(titleBar).assertExists().assertTextEquals("Secondary") - onNode(hasText("10")).performClick() - onNode(titleBar).assertExists().assertTextEquals("10") - onNode(hasText("5")).performClick() - onNode(backButton).performClick() - onNode(titleBar).assertExists().assertTextEquals("Secondary") - } -} diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestElements.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestElements.kt new file mode 100644 index 0000000..8655f80 --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestElements.kt @@ -0,0 +1,32 @@ +package io.github.xxfast.decompose.router.app + +import androidx.compose.ui.test.hasTestTag +import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_BAR +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_PAGES +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_SLOT +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_STACK +import io.github.xxfast.decompose.router.screens.BOTTOM_SHEET +import io.github.xxfast.decompose.router.screens.BUTTON_BOTTOM_SHEET +import io.github.xxfast.decompose.router.screens.BUTTON_DIALOG +import io.github.xxfast.decompose.router.screens.DETAILS_TAG +import io.github.xxfast.decompose.router.screens.DIALOG +import io.github.xxfast.decompose.router.screens.FAB_ADD +import io.github.xxfast.decompose.router.screens.LIST_TAG +import io.github.xxfast.decompose.router.screens.PAGER +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG + +internal val backButton = hasTestTag(BACK_BUTTON_TAG) +internal val bottomNav = hasTestTag(BOTTOM_NAV_BAR) +internal val bottomNavPagesItem = hasTestTag(BOTTOM_NAV_PAGES) +internal val bottomNavSlotItem = hasTestTag(BOTTOM_NAV_SLOT) +internal val bottomNavStackItem = hasTestTag(BOTTOM_NAV_STACK) +internal val bottomSheet = hasTestTag(BOTTOM_SHEET) +internal val buttonBottomSheet = hasTestTag(BUTTON_BOTTOM_SHEET) +internal val buttonDialog = hasTestTag(BUTTON_DIALOG) +internal val details = hasTestTag(DETAILS_TAG) +internal val dialog = hasTestTag(DIALOG) +internal val fabAdd = hasTestTag(FAB_ADD) +internal val lazyColumn = hasTestTag(LIST_TAG) +internal val pager = hasTestTag(PAGER) +internal val titleBar = hasTestTag(TITLE_BAR_TAG) diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt new file mode 100644 index 0000000..0bfa04b --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt @@ -0,0 +1,69 @@ +package io.github.xxfast.decompose.router.app + +import android.content.pm.ActivityInfo +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToNode +import org.junit.Rule +import org.junit.Test + +class TestNestedRouters { + @get:Rule + val composeRule: TestActivityRule = createAndroidComposeRule() + + @Test + fun testInitialState(): Unit = with(composeRule) { + // Check the initial state + onNode(bottomNavStackItem).assertExists() + onNode(bottomNavPagesItem).assertExists() + onNode(bottomNavSlotItem).assertExists() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun testNestedNavigation(): Unit = with(composeRule) { + // Add 5 more items on to stack + repeat(5) { + onNode(fabAdd).performClick() + waitUntilAtLeastOneExists(hasText(it.toString())) + } + + // Go to 5th detail screen + var testItem = "5" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(details).assertExists().assertTextContains("Item@", substring = true) + + // Go to pages and swipe to the 5th page + onNode(bottomNavPagesItem).performClick() + onNode(pager).performScrollToIndex(5) + onNode(hasText("5")).assertExists() + + // Go to slots, open the bottom sheet and verify if it is visible + onNode(bottomNavSlotItem).performClick() + onNode(buttonBottomSheet).performClick() + onNode(bottomSheet).assertExists() + + // Verify all the screens of nested screens are restored + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onNode(bottomSheet).assertExists() + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + onNode(bottomSheet).assertDoesNotExist() + onNode(bottomNavStackItem).performClick() + onNode(details).assertExists().assertTextContains("Item@", substring = true) + onNode(bottomNavPagesItem).performClick() + onNode(hasText("5")).assertExists() + onNode(bottomNavSlotItem).performClick() + onNode(buttonBottomSheet).performClick() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onNode(bottomSheet).assertExists() + } +} diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestPagesRouters.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestPagesRouters.kt new file mode 100644 index 0000000..2505540 --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestPagesRouters.kt @@ -0,0 +1,34 @@ +package io.github.xxfast.decompose.router.app + +import android.content.pm.ActivityInfo +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToNode +import org.junit.Rule +import org.junit.Test + +class TestPagesRouters { + @get:Rule + val composeRule: TestActivityRule = createAndroidComposeRule() + + @Test + fun testPagesNavigation(): Unit = with(composeRule) { + // Go to pages and swipe to the 5th page + onNode(bottomNavPagesItem).performClick() + onNode(pager).performScrollToIndex(5) + onNode(hasText("5")).assertExists() + + // Verify pages screens are restored + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onNode(pager).assertExists() + onNode(hasText("5")).assertExists() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onNode(pager).assertExists() + onNode(hasText("5")).assertExists() + } +} diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestSlotRouters.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestSlotRouters.kt new file mode 100644 index 0000000..93b3660 --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestSlotRouters.kt @@ -0,0 +1,53 @@ +package io.github.xxfast.decompose.router.app + +import android.content.pm.ActivityInfo +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.compose.ui.test.performScrollToNode +import org.junit.Rule +import org.junit.Test + +class TestSlotRouters { + @get:Rule + val composeRule: TestActivityRule = createAndroidComposeRule() + + @OptIn(ExperimentalTestApi::class) + @Test + fun testSlotNavigation(): Unit = with(composeRule) { + // Go to slots, open the bottom sheet and verify if it is visible + onNode(bottomNavSlotItem).performClick() + onNode(buttonBottomSheet).performClick() + onNode(bottomSheet).assertExists() + + // Verify all the screens of nested screens are restored + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onNode(bottomSheet).assertExists() + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + onNode(bottomSheet).assertDoesNotExist() + onNode(buttonBottomSheet).performClick() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onNode(bottomSheet).assertExists() + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + + // Open the dialog and verify if it is visible + onNode(buttonDialog).performClick() + onNode(dialog).assertExists() + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + onNode(dialog).assertDoesNotExist() + onNode(buttonDialog).performClick() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onNode(dialog).assertExists() + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } +} diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt new file mode 100644 index 0000000..e0ac030 --- /dev/null +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt @@ -0,0 +1,129 @@ +package io.github.xxfast.decompose.router.app + +import android.content.pm.ActivityInfo +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.test.ext.junit.rules.ActivityScenarioRule +import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_BAR +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_PAGES +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_SLOT +import io.github.xxfast.decompose.router.screens.BOTTOM_NAV_STACK +import io.github.xxfast.decompose.router.screens.FAB_ADD +import io.github.xxfast.decompose.router.screens.LIST_TAG +import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG +import org.junit.Rule +import org.junit.Test + + + +typealias TestActivityRule = AndroidComposeTestRule, MainActivity> + +class TestStackRouter { + @get:Rule + val composeRule: TestActivityRule = createAndroidComposeRule() + + @Test + fun testInitialState(): Unit = with(composeRule) { + // Check the initial state + onNode(bottomNavStackItem).assertExists() + onNode(bottomNavStackItem).performClick() + onNode(titleBar).assertExists().assertTextEquals("Stack (5)") + onNode(lazyColumn).assertExists() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun testBasicNavigation(): Unit = with(composeRule) { + // Navigate to the 4th item and verify + var testItem = "4" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(details).assertExists().assertTextContains("Item@", substring = true) + + // Navigate back + onNode(backButton).performClick() + onNode(lazyColumn).assertExists() + onNode(hasText(testItem)).assertExists() + + // Add 5 more items + repeat(5) { + onNode(fabAdd).performClick() + waitUntilAtLeastOneExists(hasText(it.toString())) + } + + // Go back to the 5th item + testItem = "5" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(details).assertExists().assertTextContains("Item@", substring = true) + + // Navigate back and verify state and scroll position is restored + onNode(backButton).assertExists().performClick() + onNode(lazyColumn).assertExists() + onNode(titleBar).assertExists().assertTextContains("Stack", substring = true) + onNode(hasText(testItem)).assertExists() + + // Repeat the same test but this time navigate back with gestures + testItem = "9" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(details).assertExists().assertTextContains("Item@", substring = true) + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + onNode(lazyColumn).assertExists() + onNode(titleBar).assertExists().assertTextContains("Stack", substring = true) + onNode(hasText(testItem)).assertExists() + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun testRetainInstanceAcrossConfigurationChanges(): Unit = with(composeRule) { + // Add 5 more items + repeat(5) { + onNode(fabAdd).performClick() + waitUntilAtLeastOneExists(hasText(it.toString())) + } + + // Go to and click 5th item + var testItem = "5" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + + // Trigger configuration change and verify if the state and scroll position is restored back on the list screen + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(hasText(testItem)).assertExists() + + // Trigger configuration change again and verify scroll position is restored + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + onNode(hasText(testItem)).assertExists() + + // Repeat the same test but this time navigate back with gestures + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + testItem = "9" + onNode(lazyColumn).performScrollToNode(hasText(testItem)) + onNode(hasText(testItem)).performClick() + onNode(titleBar).assertExists().assertTextEquals(testItem) + onNode(details).assertExists().assertTextContains("Item@", substring = true) + activityRule.scenario.onActivity { activity -> + activity.onBackPressedDispatcher.onBackPressed() + } + onNode(lazyColumn).assertExists() + onNode(titleBar).assertExists().assertTextContains("Stack", substring = true) + onNode(hasText(testItem)).assertExists() + } +} diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt index 26ea8d4..3e038ab 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.jetbrains.pages.PagesScrollAnimation import com.arkivanov.decompose.router.pages.select @@ -21,7 +22,7 @@ import io.github.xxfast.decompose.router.pages.RoutedContent import io.github.xxfast.decompose.router.pages.Router import io.github.xxfast.decompose.router.pages.pagesOf import io.github.xxfast.decompose.router.pages.rememberRouter -import io.github.xxfast.decompose.router.screens.HomeScreens.Page +import io.github.xxfast.decompose.router.screens.HomeScreens.Pages import io.github.xxfast.decompose.router.screens.HomeScreens.Slot import io.github.xxfast.decompose.router.screens.HomeScreens.Stack import io.github.xxfast.decompose.router.screens.pages.PagesScreen @@ -31,11 +32,13 @@ import io.github.xxfast.decompose.router.screens.stack.StackScreen @OptIn(ExperimentalDecomposeApi::class, ExperimentalFoundationApi::class) @Composable fun HomeScreen() { - val pager: Router = rememberRouter(HomeScreens::class) { pagesOf(Stack, Page, Slot) } + val pager: Router = rememberRouter(HomeScreens::class) { pagesOf(Stack, Pages, Slot) } Scaffold( bottomBar = { - NavigationBar { + NavigationBar( + modifier = Modifier.testTag(BOTTOM_NAV_BAR) + ) { HomeScreens.entries.forEach { screen -> NavigationBarItem( selected = screen.ordinal == pager.pages.value.selectedIndex, @@ -43,14 +46,21 @@ fun HomeScreen() { Icon( imageVector = when (screen) { Stack -> Icons.Rounded.Reorder - Page -> Icons.Rounded.ImportContacts + Pages -> Icons.Rounded.ImportContacts Slot -> Icons.Rounded.CropSquare }, contentDescription = null, ) }, label = { Text(screen.name) }, - onClick = { pager.select(screen.ordinal) } + onClick = { pager.select(screen.ordinal) }, + modifier = Modifier.testTag( + when(screen) { + Stack -> BOTTOM_NAV_STACK + Pages -> BOTTOM_NAV_PAGES + Slot -> BOTTOM_NAV_SLOT + } + ) ) } } @@ -73,7 +83,7 @@ fun HomeScreen() { ) { screen -> when (screen) { Stack -> StackScreen() - Page -> PagesScreen() + Pages -> PagesScreen() Slot -> SlotScreen() } } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt index 158d1a0..673bc81 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/HomeStateModels.kt @@ -2,6 +2,6 @@ package io.github.xxfast.decompose.router.screens enum class HomeScreens { Stack, - Page, + Pages, Slot, } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt index 51a1b9d..30880ab 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/TestTags.kt @@ -1,8 +1,17 @@ package io.github.xxfast.decompose.router.screens -const val LIST_TAG = "list" -const val TOOLBAR_TAG = "toolbar" -const val FAVORITE_TAG = "favorite" const val BACK_BUTTON_TAG = "back" -const val TITLE_BAR_TAG = "titleBar" +const val BOTTOM_NAV_BAR = "bottomNav" +const val BOTTOM_NAV_PAGES = "bottomNavPages" +const val BOTTOM_NAV_SLOT = "bottomNavSlot" +const val BOTTOM_NAV_STACK = "bottomNavStack" +const val BOTTOM_SHEET = "bottomSheet" +const val BUTTON_BOTTOM_SHEET = "btnBottomSheet" +const val BUTTON_DIALOG = "btnDialog" const val DETAILS_TAG = "details" +const val DIALOG = "dialog" +const val FAB_ADD = "fabAdd" +const val LIST_TAG = "list" +const val PAGER = "pager" +const val TITLE_BAR_TAG = "titleBar" +const val TOOLBAR_TAG = "toolbar" diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt index ba69240..94889db 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/pages/PagesScreen.kt @@ -18,12 +18,14 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.pages.Pages import com.arkivanov.decompose.router.pages.selectFirst import io.github.xxfast.decompose.router.pages.RoutedContent import io.github.xxfast.decompose.router.pages.rememberRouter +import io.github.xxfast.decompose.router.screens.PAGER @OptIn( ExperimentalDecomposeApi::class, @@ -52,7 +54,12 @@ fun PagesScreen() { }, contentWindowInsets = WindowInsets(0, 0, 0, 0) ) { scaffoldPadding -> - RoutedContent(router = router, modifier = Modifier.padding(scaffoldPadding)) { page -> + RoutedContent( + router = router, + modifier = Modifier + .padding(scaffoldPadding) + .testTag(PAGER) + ) { page -> Card(modifier = Modifier.padding(16.dp)) { Box(modifier = Modifier.fillMaxSize()) { Text( diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt index 4403660..bc8e2c2 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/slot/SlotScreen.kt @@ -25,6 +25,10 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss +import io.github.xxfast.decompose.router.screens.BOTTOM_SHEET +import io.github.xxfast.decompose.router.screens.BUTTON_BOTTOM_SHEET +import io.github.xxfast.decompose.router.screens.BUTTON_DIALOG +import io.github.xxfast.decompose.router.screens.DIALOG import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG import io.github.xxfast.decompose.router.slot.RoutedContent import io.github.xxfast.decompose.router.slot.Router @@ -57,13 +61,15 @@ fun SlotScreen() { verticalArrangement = Arrangement.Center ) { Button( - onClick = { dialogRouter.activate(DialogScreens) } + onClick = { dialogRouter.activate(DialogScreens) }, + modifier = Modifier.testTag(BUTTON_DIALOG) ) { Text("Show Dialog") } Button( - onClick = { bottomSheetRouter.activate(BottomSheetScreens()) } + onClick = { bottomSheetRouter.activate(BottomSheetScreens()) }, + modifier = Modifier.testTag(BUTTON_BOTTOM_SHEET) ) { Text("Show Bottom Sheet") } @@ -82,7 +88,8 @@ fun SlotScreen() { text = screens.toString(), modifier = Modifier.padding(8.dp) ) - } + }, + modifier = Modifier.testTag(DIALOG) ) } @@ -93,6 +100,7 @@ fun SlotScreen() { onDismissRequest = { bottomSheetRouter.dismiss() }, modifier = Modifier .height(480.dp) + .testTag(BOTTOM_SHEET) ) { Scaffold( topBar = { TopAppBar(title = { Text("Bottom Sheet") }) }, diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt index 514ed63..294c7b4 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/details/DetailScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import io.github.xxfast.decompose.router.screens.BACK_BUTTON_TAG +import io.github.xxfast.decompose.router.screens.DETAILS_TAG import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG import io.github.xxfast.decompose.router.screens.TOOLBAR_TAG import io.github.xxfast.decompose.router.screens.stack.Item @@ -54,6 +55,7 @@ fun DetailScreen( modifier = Modifier .padding(16.dp) .align(Alignment.Center) + .testTag(DETAILS_TAG) ) } } diff --git a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt index 5cee63e..3ecd2bf 100644 --- a/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt +++ b/app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import io.github.xxfast.decompose.router.rememberOnRoute +import io.github.xxfast.decompose.router.screens.FAB_ADD import io.github.xxfast.decompose.router.screens.LIST_TAG import io.github.xxfast.decompose.router.screens.TITLE_BAR_TAG import io.github.xxfast.decompose.router.screens.TOOLBAR_TAG @@ -76,7 +77,8 @@ fun ListScreen( instance.add() coroutineScope.launch { listState.animateScrollToItem(state.screens.lastIndex) } }, - content = { Icon(Icons.Rounded.Add, null) } + content = { Icon(Icons.Rounded.Add, null) }, + modifier = Modifier.testTag(FAB_ADD) ) }, contentWindowInsets = WindowInsets(0, 0, 0, 0) From 950966d215b4759b6e8cce6046c5bb406f658903 Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 08:07:28 +1100 Subject: [PATCH 07/11] Update test action to run instrumentation tests on push --- .github/actions/get-avd-info/action.yml | 19 +++++++++++++ .github/workflows/test.yml | 37 +++++++++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .github/actions/get-avd-info/action.yml diff --git a/.github/actions/get-avd-info/action.yml b/.github/actions/get-avd-info/action.yml new file mode 100644 index 0000000..4db2371 --- /dev/null +++ b/.github/actions/get-avd-info/action.yml @@ -0,0 +1,19 @@ +name: 'Get AVD Info' +description: 'Get the AVD info based on its API level.' +inputs: + api-level: + required: true +outputs: + arch: + value: ${{ steps.get-avd-arch.outputs.arch }} + target: + value: ${{ steps.get-avd-target.outputs.target }} +runs: + using: "composite" + steps: + - id: get-avd-arch + run: echo "arch=$(if [ ${{ inputs.api-level }} -ge 30 ]; then echo x86_64; else echo x86; fi)" >> $GITHUB_OUTPUT + shell: bash + - id: get-avd-target + run: echo "target=$(echo default)" >> $GITHUB_OUTPUT + shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 872c5fe..e552bf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,15 +1,25 @@ name: Test on: - workflow_dispatch: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true jobs: test: name: Test android instrumentation - runs-on: macos-latest + runs-on: ubuntu-latest + timeout-minutes: 60 strategy: + fail-fast: true matrix: - config: [ { sdk: 21 }, { sdk: 33 }] + api-level: [25, 33, 34] steps: - uses: actions/checkout@v3 @@ -30,8 +40,23 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Run android instrumentation tests for sanity check + # API 30+ emulators only have x86_64 system images. + - name: Get AVD info + uses: ./.github/actions/get-avd-info + id: avd-info + with: + api-level: ${{ matrix.api-level }} + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Instrumentation tests uses: reactivecircus/android-emulator-runner@v2 with: - api-level: ${{ matrix.config.sdk }} - script: ./gradlew :decompose-router:connectedDebugAndroidTest --stacktrace + api-level: ${{ matrix.api-level }} + arch: ${{ steps.avd-info.outputs.arch }} + target: ${{ steps.avd-info.outputs.target }} + script: ./gradlew connectedDebugAndroidTest From 99aee47b033a67cd611b9cdc547160e3979f06cd Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 08:16:38 +1100 Subject: [PATCH 08/11] Bump min sdk to 25 --- .github/workflows/test.yml | 2 +- app/build.gradle.kts | 2 +- decompose-router-wear/build.gradle.kts | 2 +- decompose-router/build.gradle.kts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e552bf2..7655d50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ concurrency: jobs: test: - name: Test android instrumentation + name: Test on Android runs-on: ubuntu-latest timeout-minutes: 60 strategy: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e57dd80..7267b47 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,7 +108,7 @@ android { namespace = "io.github.xxfast.decompose.router.app" compileSdk = 34 defaultConfig { - minSdk = 21 + minSdk = 25 targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/decompose-router-wear/build.gradle.kts b/decompose-router-wear/build.gradle.kts index efb510b..6012f4d 100644 --- a/decompose-router-wear/build.gradle.kts +++ b/decompose-router-wear/build.gradle.kts @@ -34,7 +34,7 @@ android { namespace = "io.github.xxfast.decompose.router.wear" compileSdk = 34 defaultConfig { - minSdk = 21 + minSdk = 25 targetSdk = 34 } diff --git a/decompose-router/build.gradle.kts b/decompose-router/build.gradle.kts index 0993a4f..0fb7988 100644 --- a/decompose-router/build.gradle.kts +++ b/decompose-router/build.gradle.kts @@ -90,7 +90,7 @@ android { namespace = "io.github.xxfast.decompose.router" compileSdk = 34 defaultConfig { - minSdk = 21 + minSdk = 25 targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } From 993146d725eb1e3abf741f5d508b3ee07e61f342 Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 08:25:49 +1100 Subject: [PATCH 09/11] Refactor test cases to remove idling asserts --- .../github/xxfast/decompose/router/app/TestNestedRouters.kt | 2 -- .../io/github/xxfast/decompose/router/app/TestStackRouter.kt | 4 ---- 2 files changed, 6 deletions(-) diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt index 0bfa04b..43f215a 100644 --- a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestNestedRouters.kt @@ -24,13 +24,11 @@ class TestNestedRouters { onNode(bottomNavSlotItem).assertExists() } - @OptIn(ExperimentalTestApi::class) @Test fun testNestedNavigation(): Unit = with(composeRule) { // Add 5 more items on to stack repeat(5) { onNode(fabAdd).performClick() - waitUntilAtLeastOneExists(hasText(it.toString())) } // Go to 5th detail screen diff --git a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt index e0ac030..10a75c0 100644 --- a/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt +++ b/app/src/androidInstrumentedTest/kotlin/io/github/xxfast/decompose/router/app/TestStackRouter.kt @@ -39,7 +39,6 @@ class TestStackRouter { onNode(lazyColumn).assertExists() } - @OptIn(ExperimentalTestApi::class) @Test fun testBasicNavigation(): Unit = with(composeRule) { // Navigate to the 4th item and verify @@ -57,7 +56,6 @@ class TestStackRouter { // Add 5 more items repeat(5) { onNode(fabAdd).performClick() - waitUntilAtLeastOneExists(hasText(it.toString())) } // Go back to the 5th item @@ -87,13 +85,11 @@ class TestStackRouter { onNode(hasText(testItem)).assertExists() } - @OptIn(ExperimentalTestApi::class) @Test fun testRetainInstanceAcrossConfigurationChanges(): Unit = with(composeRule) { // Add 5 more items repeat(5) { onNode(fabAdd).performClick() - waitUntilAtLeastOneExists(hasText(it.toString())) } // Go to and click 5th item From ddca956c07d1651db7c015fd7fee0ec09febd0a7 Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 08:45:56 +1100 Subject: [PATCH 10/11] Fix slow gradle build task --- .github/workflows/build.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e01837a..826db89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,27 +39,38 @@ jobs: build: strategy: + fail-fast: true matrix: config: [ { target: jvm, os: ubuntu-latest }, { target: apple, os: macos-latest }, ] runs-on: ${{ matrix.config.os }} + timeout-minutes: 60 needs: - check + name: Build ${{ matrix.config.target }} steps: - uses: actions/checkout@v3 - - name: set up JDK 17 + + - name: Setup JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Build with Gradle run: ./gradlew build @@ -161,4 +172,4 @@ jobs: - name: Release to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 \ No newline at end of file + uses: actions/deploy-pages@v1 From ef12c1d1b344129649fbe1736a9d8107c58aa3b7 Mon Sep 17 00:00:00 2001 From: xxfast Date: Tue, 23 Jan 2024 09:01:44 +1100 Subject: [PATCH 11/11] Combine build and test actions --- .github/workflows/build.yml | 63 ++++++++++++++++++++++++++++++------- .github/workflows/test.yml | 62 ------------------------------------ 2 files changed, 52 insertions(+), 73 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 826db89..3036f2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,18 +39,39 @@ jobs: build: strategy: - fail-fast: true matrix: config: [ - { target: jvm, os: ubuntu-latest }, - { target: apple, os: macos-latest }, + { target: android, os: ubuntu-latest, tasks: testDebugUnitTest testReleaseUnitTest }, + { target: ios, os: macos-latest, tasks: iosX64Test iosSimulatorArm64Test }, + { target: js, os: ubuntu-latest, tasks: jsTest }, + { target: desktop, os: ubuntu-latest, tasks: desktopTest }, ] runs-on: ${{ matrix.config.os }} - timeout-minutes: 60 - needs: - - check - name: Build ${{ matrix.config.target }} + needs: check + steps: + - uses: actions/checkout@v3 + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup gradle + uses: gradle/gradle-build-action@v2 + + - name: Test ${{ matrix.config.target }} targets + run: ./gradlew ${{ matrix.config.tasks }} + + test: + name: Test on Android + runs-on: ubuntu-latest + timeout-minutes: 60 + needs: check + strategy: + fail-fast: true + matrix: + api-level: [ 25, 33, 34 ] steps: - uses: actions/checkout@v3 @@ -71,8 +92,26 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build + # API 30+ emulators only have x86_64 system images. + - name: Get AVD info + uses: ./.github/actions/get-avd-info + id: avd-info + with: + api-level: ${{ matrix.api-level }} + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: ${{ steps.avd-info.outputs.arch }} + target: ${{ steps.avd-info.outputs.target }} + script: ./gradlew connectedDebugAndroidTest release: name: Release to sonatype @@ -80,6 +119,7 @@ jobs: runs-on: macos-latest needs: - build + - test environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -118,6 +158,7 @@ jobs: runs-on: ubuntu-latest needs: - build + - test environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -143,7 +184,7 @@ jobs: uses: JetBrains/writerside-github-action@v4 with: instance: docs/decompose-router - artifact: webHelpKSTORE2-all.zip + artifact: webHelpDECOMPOSE-ROUTER2-all.zip docker-version: 232.10275 - name: Upload documentation @@ -151,7 +192,7 @@ jobs: with: name: docs path: | - artifacts/webHelpKSTORE2-all.zip + artifacts/webHelpDECOMPOSE-ROUTER2-all.zip artifacts/report.json retention-days: 7 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 7655d50..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Test - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -# This allows a subsequently queued workflow run to interrupt previous runs -concurrency: - group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' - cancel-in-progress: true - -jobs: - test: - name: Test on Android - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - fail-fast: true - matrix: - api-level: [25, 33, 34] - - steps: - - uses: actions/checkout@v3 - - - name: Setup JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - - - name: Setup Android SDK - uses: android-actions/setup-android@v2 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - # API 30+ emulators only have x86_64 system images. - - name: Get AVD info - uses: ./.github/actions/get-avd-info - id: avd-info - with: - api-level: ${{ matrix.api-level }} - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Instrumentation tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - arch: ${{ steps.avd-info.outputs.arch }} - target: ${{ steps.avd-info.outputs.target }} - script: ./gradlew connectedDebugAndroidTest