From ecd604a1c10b0e82ad5b4068e33f5d078706bea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adriel=20Caf=C3=A9?= Date: Thu, 2 Sep 2021 22:42:09 -0300 Subject: [PATCH] feat: ScreenModel integrated with Coroutines, RxJava, Koin, Kodein --- README.md | 22 ++++-- gradle.properties | 2 +- gradle/libs.versions.toml | 11 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- sample-multi-module/app/build.gradle | 2 + sample/build.gradle | 7 ++ sample/src/main/AndroidManifest.xml | 6 +- .../java/cafe/adriel/voyager/sample/App.kt | 22 ++++-- .../adriel/voyager/sample/SampleActivity.kt | 41 +++++----- .../adriel/voyager/sample/SampleContent.kt | 76 +++++++++++++++++++ .../sample/androidNavigation/DetailsScreen.kt | 55 -------------- .../androidNavigation/DetailsViewModel.kt | 7 -- .../sample/androidNavigation/ListScreen.kt | 34 --------- .../sample/androidNavigation/ListViewModel.kt | 15 ---- .../androidViewModel/AndroidDetailsScreen.kt | 22 ++++++ .../AndroidDetailsViewModel.kt | 7 ++ .../androidViewModel/AndroidListScreen.kt | 19 +++++ .../androidViewModel/AndroidListViewModel.kt | 15 ++++ .../AndroidViewModelActivity.kt | 17 +++++ .../KodeinIntegrationActivity.kt | 17 +++++ .../sample/kodeinIntegration/KodeinScreen.kt | 20 +++++ .../kodeinIntegration/KodeinScreenModel.kt | 9 +++ .../KoinIntegrationActivity.kt | 17 +++++ .../sample/koinIntegration/KoinScreen.kt | 20 +++++ .../sample/koinIntegration/KoinScreenModel.kt | 9 +++ .../RxJavaIntegrationActivity.kt | 17 +++++ .../sample/rxjavaIntegration/RxJavaScreen.kt | 34 +++++++++ .../rxjavaIntegration/RxJavaScreenModel.kt | 26 +++++++ .../sample/screenModel/DetailsScreen.kt | 38 ++++++++++ .../sample/screenModel/DetailsScreenModel.kt | 23 ++++++ .../voyager/sample/screenModel/ListScreen.kt | 23 ++++++ .../sample/screenModel/ListScreenModel.kt | 9 +++ .../ScreenModelActivity.kt} | 4 +- settings.gradle | 5 +- .../adriel/voyager/androidx/AndroidScreen.kt | 2 +- ...lder.kt => AndroidScreenLifecycleOwner.kt} | 57 +++++++------- .../bottomSheet/BottomSheetNavigator.kt | 4 +- .../voyager/core/lifecycle/ScreenLifecycle.kt | 8 -- ...ScreenHooks.kt => ScreenLifecycleHooks.kt} | 6 +- .../core/lifecycle/ScreenLifecycleOwner.kt | 11 +++ .../core/lifecycle/ScreenLifecycleStore.kt | 20 +++++ .../adriel/voyager/core/model/ScreenModel.kt | 42 ++++++++++ .../voyager/core/model/ScreenModelStore.kt | 74 ++++++++++++++++++ voyager-kodein/.gitignore | 1 + voyager-kodein/build.gradle | 23 ++++++ voyager-kodein/consumer-rules.pro | 0 voyager-kodein/gradle.properties | 3 + voyager-kodein/src/main/AndroidManifest.xml | 2 + .../cafe/adriel/voyager/kodein/ScreenModel.kt | 24 ++++++ voyager-koin/.gitignore | 1 + voyager-koin/build.gradle | 23 ++++++ voyager-koin/consumer-rules.pro | 0 voyager-koin/gradle.properties | 3 + voyager-koin/src/main/AndroidManifest.xml | 2 + .../cafe/adriel/voyager/koin/ScreenModel.kt | 18 +++++ .../adriel/voyager/navigator/Navigator.kt | 25 ++---- .../internal/NavigatorBackHandler.kt | 2 +- .../navigator/internal/NavigatorDisposable.kt | 31 ++++++++ voyager-rxjava/.gitignore | 1 + voyager-rxjava/build.gradle | 22 ++++++ voyager-rxjava/consumer-rules.pro | 0 voyager-rxjava/gradle.properties | 3 + voyager-rxjava/src/main/AndroidManifest.xml | 2 + .../cafe/adriel/voyager/rxjava/ScreenModel.kt | 21 +++++ voyager-tab-navigator/build.gradle | 1 + .../cafe/adriel/voyager/navigator/tab/Tab.kt | 8 +- .../voyager/navigator/tab/TabNavigator.kt | 36 +++------ .../tab/internal/TabNavigatorSaver.kt | 28 ------- 68 files changed, 883 insertions(+), 274 deletions(-) create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/SampleContent.kt delete mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsScreen.kt delete mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsViewModel.kt delete mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt delete mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsViewModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListViewModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidViewModelActivity.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinIntegrationActivity.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreenModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinIntegrationActivity.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreenModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaIntegrationActivity.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreenModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreenModel.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt create mode 100644 sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreenModel.kt rename sample/src/main/java/cafe/adriel/voyager/sample/{androidNavigation/AndroidNavigationActivity.kt => screenModel/ScreenModelActivity.kt} (75%) rename voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/{ScreenLifecycleHolder.kt => AndroidScreenLifecycleOwner.kt} (52%) rename voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/{ScreenHooks.kt => ScreenLifecycleHooks.kt} (62%) create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleOwner.kt create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleStore.kt create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModel.kt create mode 100644 voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModelStore.kt create mode 100644 voyager-kodein/.gitignore create mode 100644 voyager-kodein/build.gradle create mode 100644 voyager-kodein/consumer-rules.pro create mode 100644 voyager-kodein/gradle.properties create mode 100644 voyager-kodein/src/main/AndroidManifest.xml create mode 100644 voyager-kodein/src/main/java/cafe/adriel/voyager/kodein/ScreenModel.kt create mode 100644 voyager-koin/.gitignore create mode 100644 voyager-koin/build.gradle create mode 100644 voyager-koin/consumer-rules.pro create mode 100644 voyager-koin/gradle.properties create mode 100644 voyager-koin/src/main/AndroidManifest.xml create mode 100644 voyager-koin/src/main/java/cafe/adriel/voyager/koin/ScreenModel.kt create mode 100644 voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt create mode 100644 voyager-rxjava/.gitignore create mode 100644 voyager-rxjava/build.gradle create mode 100644 voyager-rxjava/consumer-rules.pro create mode 100644 voyager-rxjava/gradle.properties create mode 100644 voyager-rxjava/src/main/AndroidManifest.xml create mode 100644 voyager-rxjava/src/main/java/cafe/adriel/voyager/rxjava/ScreenModel.kt delete mode 100644 voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/internal/TabNavigatorSaver.kt diff --git a/README.md b/README.md index 5f0237c9..738553a4 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,16 @@ Voyager is a lightweight and complete navigation library built for, and seamless Create scalable Single-Activity apps powered by a [pragmatic API](https://voyager.adriel.cafe/navigation/fundamentals). ```kotlin +class HomeScreenModel : ScreenModel { + // ... +} + class HomeScreen : Screen { @Composable override fun Content() { - // Your awesome UI goes here + val screenModel = rememberScreenModel() + // ... } } @@ -41,13 +46,16 @@ Turn on the Warp Drive and enjoy the trek 🖖 See the [project website](https://voyager.adriel.cafe) for documentation and APIs. ### Features +- [Linear navigation](https://voyager.adriel.cafe/navigation/fundamentals) - [BottomSheet navigation](https://voyager.adriel.cafe/navigation/bottomsheet-navigation) - [Tab navigation](https://voyager.adriel.cafe/navigation/tab-navigation) like [Youtube app](https://play.google.com/store/apps/details?id=com.google.android.youtube) -- [Nested navigation](https://voyager.adriel.cafe/navigation/nested-navigation) if you need to manage multiple stacks -- [State restoration](https://voyager.adriel.cafe/state-restoration) after Activity recreation +- [Nested navigation](https://voyager.adriel.cafe/navigation/nested-navigation) (multiple stacks, parent navigation) +- [ScreenModel](https://voyager.adriel.cafe/screenmodel/fundamentals) (a.k.a ViewModel) integrated with [Koin](https://voyager.adriel.cafe/screenmodel/koin-integration), [Kodein](https://voyager.adriel.cafe/screenmodel/kodein-integration), [Coroutines](https://voyager.adriel.cafe/screenmodel/coroutines-integration), [RxJava](https://voyager.adriel.cafe/screenmodel/rxjava-integration) +- [Android ViewModel](https://voyager.adriel.cafe/android-viewmodel) integration - Type-safe [multi-module navigation](https://voyager.adriel.cafe/navigation/multi-module-navigation) - State-aware [Stack API](https://voyager.adriel.cafe/stack-api) - Built-in [transitions](https://voyager.adriel.cafe/transitions) +- [State restoration](https://voyager.adriel.cafe/state-restoration) after Activity recreation - [Lifecycle](https://voyager.adriel.cafe/lifecycle) callbacks - [Back press](https://voyager.adriel.cafe/back-press) handling - [Deep linking](https://voyager.adriel.cafe/deep-links) support @@ -56,10 +64,10 @@ See the [project website](https://voyager.adriel.cafe) for documentation and API - [Compose for Desktop](https://github.com/JetBrains/compose-jb) support ### Samples -| [Stack API](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/stateStack) | [AndroidX ViewModel](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation) | [Basic nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation) | -|------------|----------|-------------| -| ![navigation-stack](https://user-images.githubusercontent.com/2512298/126323192-9b6349fe-7b96-4acf-b62e-c75165d909e1.gif) | ![navigation-androidx](https://user-images.githubusercontent.com/2512298/130377801-c350b4f5-bcca-4d28-9403-0d9d4c1e99f7.gif) | ![navigation-basic](https://user-images.githubusercontent.com/2512298/126323165-47760eec-2ba2-48ee-8e3a-841d50098d33.gif) | +| [Stack API](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/stateStack) | [Android ViewModel](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel) | [ScreenModel](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/screenModel) | [Basic nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/basicNavigation) | +|----------|----------|----------|----------| +| ![navigation-stack](https://user-images.githubusercontent.com/2512298/126323192-9b6349fe-7b96-4acf-b62e-c75165d909e1.gif) | ![navigation-android-viewmodel](https://user-images.githubusercontent.com/2512298/130377801-c350b4f5-bcca-4d28-9403-0d9d4c1e99f7.gif) | ![navigation-screenmodel](https://user-images.githubusercontent.com/2512298/131770829-fa85cb19-cc76-4fbf-9bdc-165997d5349d.gif) | ![navigation-basic](https://user-images.githubusercontent.com/2512298/126323165-47760eec-2ba2-48ee-8e3a-841d50098d33.gif) | | [BottomSheet nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/bottomSheetNavigation) | [Tab nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/tabNavigation) | [Multi-module nav.](https://github.com/adrielcafe/voyager/tree/main/sample-multi-module) | [Nested nav.](https://github.com/adrielcafe/voyager/tree/main/sample/src/main/java/cafe/adriel/voyager/sample/nestedNavigation) | -|------------|------------|----------|-------------| +|----------|----------|----------|----------| | ![navigation-bottom-sheet](https://user-images.githubusercontent.com/2512298/131191122-18025192-ce4d-4659-9afa-aacfdb488796.gif) | ![navigation-tab](https://user-images.githubusercontent.com/2512298/126323588-2f970953-0adb-47f8-b2fb-91c5854656bd.gif) | ![navigation-multi-module](https://user-images.githubusercontent.com/2512298/130662717-c15caf88-350e-42a0-837c-3453805b68f2.gif) | ![navigation-nested](https://user-images.githubusercontent.com/2512298/126323027-a2633aef-9402-4df8-9384-45935d7986cf.gif) | diff --git a/gradle.properties b/gradle.properties index 574f7a16..8a80bf95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ kotlin.code.style=official # Maven GROUP=cafe.adriel.voyager -VERSION_NAME=1.0.0-beta09 +VERSION_NAME=1.0.0-beta10 POM_DESCRIPTION=A pragmatic navigation library for Jetpack Compose POM_INCEPTION_YEAR=2021 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3239b218..8102a556 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,15 @@ [versions] -plugin-android = "7.0.0" +plugin-android = "7.0.2" plugin-ktlint = "10.1.0" plugin-maven = "0.17.0" -kotlin = "1.5.21" +kotlin = "1.5.30" +kodein = "7.7.0" koin = "3.1.2" +leakCanary = "2.7" appCompat = "1.3.1" lifecycle = "2.4.0-alpha03" -compose = "1.1.0-alpha02" +compose = "1.1.0-alpha03" composeActivity = "1.3.1" composeViewModel = "1.0.0-alpha07" @@ -19,10 +21,13 @@ plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. plugin-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "plugin-ktlint" } plugin-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "plugin-maven" } +leakCanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanary" } +kodein = { module = "org.kodein.di:kodein-di-framework-compose", version.ref = "kodein" } koin = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } appCompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompat" } lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } lifecycle-savedState = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } +compose-rxjava = { module = "androidx.compose.runtime:runtime-rxjava3", version.ref = "compose" } compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } compose-runtimeSaveable = { module = "androidx.compose.runtime:runtime-saveable", version.ref = "compose" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8ebc0cf4..e7d7cafc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sun Jun 06 10:15:22 BRT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sample-multi-module/app/build.gradle b/sample-multi-module/app/build.gradle index ca85ed5e..a4bd8c80 100644 --- a/sample-multi-module/app/build.gradle +++ b/sample-multi-module/app/build.gradle @@ -18,4 +18,6 @@ dependencies { implementation libs.compose.runtime implementation libs.compose.activity implementation libs.compose.material + + debugImplementation libs.leakCanary } \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index bca2adcd..b9ddbdd3 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -13,13 +13,20 @@ dependencies { implementation projects.voyagerBottomSheetNavigator implementation projects.voyagerTransitions implementation projects.voyagerAndroidx + implementation projects.voyagerKodein + implementation projects.voyagerKoin + implementation projects.voyagerRxjava + implementation libs.kodein implementation libs.koin implementation libs.appCompat + implementation libs.compose.rxjava implementation libs.compose.viewModel implementation libs.compose.compiler implementation libs.compose.runtime implementation libs.compose.activity implementation libs.compose.material implementation libs.compose.materialIcons + + debugImplementation libs.leakCanary } \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 13fae66d..64d24952 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -22,8 +22,12 @@ - + + + + + diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/App.kt b/sample/src/main/java/cafe/adriel/voyager/sample/App.kt index 2c936c13..ec19d635 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/App.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/App.kt @@ -1,24 +1,36 @@ package cafe.adriel.voyager.sample import android.app.Application -import cafe.adriel.voyager.sample.androidNavigation.DetailsViewModel -import cafe.adriel.voyager.sample.androidNavigation.ListViewModel +import cafe.adriel.voyager.sample.androidViewModel.AndroidDetailsViewModel +import cafe.adriel.voyager.sample.androidViewModel.AndroidListViewModel +import cafe.adriel.voyager.sample.kodeinIntegration.KodeinScreenModel +import cafe.adriel.voyager.sample.koinIntegration.KoinScreenModel +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.bindProvider import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin +import org.koin.dsl.factory import org.koin.dsl.module -class App : Application() { +class App : Application(), DIAware { + + override val di by DI.lazy { + bindProvider { KodeinScreenModel() } + } override fun onCreate() { super.onCreate() startKoin { modules( module { + factory() + viewModel { - ListViewModel(handle = get()) + AndroidListViewModel(handle = get()) } viewModel { parameters -> - DetailsViewModel(index = parameters.get()) + AndroidDetailsViewModel(index = parameters.get()) } } ) diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt index fd51fe13..6cb02ca1 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/SampleActivity.kt @@ -6,12 +6,10 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer 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.LazyColumn import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -19,10 +17,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.sample.androidNavigation.AndroidNavigationActivity +import cafe.adriel.voyager.sample.androidViewModel.AndroidViewModelActivity import cafe.adriel.voyager.sample.basicNavigation.BasicNavigationActivity import cafe.adriel.voyager.sample.bottomSheetNavigation.BottomSheetNavigationActivity +import cafe.adriel.voyager.sample.kodeinIntegration.KodeinIntegrationActivity +import cafe.adriel.voyager.sample.koinIntegration.KoinIntegrationActivity import cafe.adriel.voyager.sample.nestedNavigation.NestedNavigationActivity +import cafe.adriel.voyager.sample.rxjavaIntegration.RxJavaIntegrationActivity +import cafe.adriel.voyager.sample.screenModel.ScreenModelActivity import cafe.adriel.voyager.sample.stateStack.StateStackActivity import cafe.adriel.voyager.sample.tabNavigation.TabNavigationActivity @@ -38,24 +40,23 @@ class SampleActivity : ComponentActivity() { @Composable fun Content() { - Column( + LazyColumn( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .padding(24.dp) + modifier = Modifier.fillMaxSize().padding(24.dp) ) { - StartSampleButton("SnapshotStateStack") - Spacer(modifier = Modifier.height(24.dp)) - StartSampleButton("Basic Navigation") - Spacer(modifier = Modifier.height(24.dp)) - StartSampleButton("Tab Navigation") - Spacer(modifier = Modifier.height(24.dp)) - StartSampleButton("BottomSheet Navigation") - Spacer(modifier = Modifier.height(24.dp)) - StartSampleButton("Android Navigation") - Spacer(modifier = Modifier.height(24.dp)) - StartSampleButton("Nested Navigation") + item { + StartSampleButton("SnapshotStateStack") + StartSampleButton("Basic Navigation") + StartSampleButton("Tab Navigation") + StartSampleButton("BottomSheet Navigation") + StartSampleButton("Nested Navigation") + StartSampleButton("Android ViewModel") + StartSampleButton("ScreenModel") + StartSampleButton("Koin Integration") + StartSampleButton("Kodein Integration") + StartSampleButton("RxJava Integration") + } } } @@ -65,7 +66,7 @@ class SampleActivity : ComponentActivity() { Button( onClick = { context.startActivity(Intent(this, T::class.java)) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) ) { Text(text = text) } diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/SampleContent.kt b/sample/src/main/java/cafe/adriel/voyager/sample/SampleContent.kt new file mode 100644 index 00000000..87a3609b --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/SampleContent.kt @@ -0,0 +1,76 @@ +package cafe.adriel.voyager.sample + +import androidx.compose.foundation.clickable +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import java.util.UUID + +val sampleItems: List + get() = (0..99).map { "Item #$it | ${UUID.randomUUID().toString().substringBefore('-')}" } + +@Composable +fun LoadingContent() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ListContent(items: List, onClick: ((Int) -> Unit)? = null) { + LazyColumn { + itemsIndexed(items) { index, item -> + ListItem( + text = { Text(text = item) }, + modifier = if (onClick == null) Modifier else Modifier.clickable { onClick(index) } + ) + } + } +} + +@Composable +fun DetailsContent(instance: Any, item: String, onClick: () -> Unit) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = item, + style = MaterialTheme.typography.h5 + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = instance.toString().substringAfterLast('.'), + style = MaterialTheme.typography.body2 + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onClick, + content = { Text(text = "Back") } + ) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsScreen.kt deleted file mode 100644 index 3bb9ea8c..00000000 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsScreen.kt +++ /dev/null @@ -1,55 +0,0 @@ -package cafe.adriel.voyager.sample.androidNavigation - -import androidx.compose.foundation.layout.Arrangement -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.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.androidx.AndroidScreen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import org.koin.androidx.compose.getViewModel -import org.koin.core.parameter.parametersOf - -data class DetailsScreen( - val index: Int -) : AndroidScreen() { - - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val viewModel = getViewModel { parametersOf(index) } - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Text( - text = "Item #${viewModel.index}", - style = MaterialTheme.typography.h5 - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = viewModel.toString().substringAfterLast('.'), - style = MaterialTheme.typography.body2 - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = navigator::pop, - content = { Text(text = "Back") } - ) - } - } -} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsViewModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsViewModel.kt deleted file mode 100644 index 4af647cd..00000000 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/DetailsViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package cafe.adriel.voyager.sample.androidNavigation - -import androidx.lifecycle.ViewModel - -class DetailsViewModel( - val index: Int -) : ViewModel() diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt deleted file mode 100644 index 7487f8f3..00000000 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListScreen.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cafe.adriel.voyager.sample.androidNavigation - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ListItem -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.androidx.AndroidScreen -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import org.koin.androidx.compose.getStateViewModel - -class ListScreen : AndroidScreen() { - - @OptIn(ExperimentalMaterialApi::class) - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val viewModel = getStateViewModel() - - LazyColumn { - itemsIndexed(viewModel.items) { index, item -> - ListItem( - text = { Text(text = item) }, - modifier = Modifier.clickable { navigator.push(DetailsScreen(index)) } - ) - } - } - } -} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt deleted file mode 100644 index 8891682e..00000000 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/ListViewModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package cafe.adriel.voyager.sample.androidNavigation - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import java.util.UUID - -class ListViewModel(private val handle: SavedStateHandle) : ViewModel() { - - init { - handle["items"] = (0..100).map { "Item #$it | ${UUID.randomUUID().toString().substringBefore('-')}" } - } - - val items: List - get() = handle["items"] ?: error("Items not found") -} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt new file mode 100644 index 00000000..4812f492 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsScreen.kt @@ -0,0 +1,22 @@ +package cafe.adriel.voyager.sample.androidViewModel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.sample.DetailsContent +import org.koin.androidx.compose.getViewModel +import org.koin.core.parameter.parametersOf + +data class AndroidDetailsScreen( + val index: Int +) : AndroidScreen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val viewModel = getViewModel { parametersOf(index) } + + DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsViewModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsViewModel.kt new file mode 100644 index 00000000..aa1556b0 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidDetailsViewModel.kt @@ -0,0 +1,7 @@ +package cafe.adriel.voyager.sample.androidViewModel + +import androidx.lifecycle.ViewModel + +class AndroidDetailsViewModel( + val index: Int +) : ViewModel() diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt new file mode 100644 index 00000000..78725e10 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListScreen.kt @@ -0,0 +1,19 @@ +package cafe.adriel.voyager.sample.androidViewModel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.sample.ListContent +import org.koin.androidx.compose.getStateViewModel + +class AndroidListScreen : AndroidScreen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val viewModel = getStateViewModel() + + ListContent(viewModel.items, onClick = { index -> navigator.push(AndroidDetailsScreen(index)) }) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListViewModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListViewModel.kt new file mode 100644 index 00000000..1250e4df --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidListViewModel.kt @@ -0,0 +1,15 @@ +package cafe.adriel.voyager.sample.androidViewModel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import cafe.adriel.voyager.sample.sampleItems + +class AndroidListViewModel(private val handle: SavedStateHandle) : ViewModel() { + + init { + handle["items"] = sampleItems + } + + val items: List + get() = handle["items"] ?: error("Items not found") +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidViewModelActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidViewModelActivity.kt new file mode 100644 index 00000000..8fdc4250 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/androidViewModel/AndroidViewModelActivity.kt @@ -0,0 +1,17 @@ +package cafe.adriel.voyager.sample.androidViewModel + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import cafe.adriel.voyager.navigator.Navigator + +class AndroidViewModelActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Navigator(AndroidListScreen()) + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinIntegrationActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinIntegrationActivity.kt new file mode 100644 index 00000000..5866f42e --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinIntegrationActivity.kt @@ -0,0 +1,17 @@ +package cafe.adriel.voyager.sample.kodeinIntegration + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import cafe.adriel.voyager.navigator.Navigator + +class KodeinIntegrationActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Navigator(KodeinScreen()) + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreen.kt new file mode 100644 index 00000000..6101e0c9 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreen.kt @@ -0,0 +1,20 @@ +package cafe.adriel.voyager.sample.kodeinIntegration + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.kodein.rememberScreenModel +import cafe.adriel.voyager.sample.ListContent + +class KodeinScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val screenModel = rememberScreenModel() + + ListContent(screenModel.items) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreenModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreenModel.kt new file mode 100644 index 00000000..47f21519 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/kodeinIntegration/KodeinScreenModel.kt @@ -0,0 +1,9 @@ +package cafe.adriel.voyager.sample.kodeinIntegration + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.sample.sampleItems + +class KodeinScreenModel : ScreenModel { + + val items = sampleItems +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinIntegrationActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinIntegrationActivity.kt new file mode 100644 index 00000000..2db5b6cd --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinIntegrationActivity.kt @@ -0,0 +1,17 @@ +package cafe.adriel.voyager.sample.koinIntegration + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import cafe.adriel.voyager.navigator.Navigator + +class KoinIntegrationActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Navigator(KoinScreen()) + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreen.kt new file mode 100644 index 00000000..9a336234 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreen.kt @@ -0,0 +1,20 @@ +package cafe.adriel.voyager.sample.koinIntegration + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.koin.getScreenModel +import cafe.adriel.voyager.sample.ListContent + +class KoinScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val screenModel = getScreenModel() + + ListContent(screenModel.items) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreenModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreenModel.kt new file mode 100644 index 00000000..b602046f --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/koinIntegration/KoinScreenModel.kt @@ -0,0 +1,9 @@ +package cafe.adriel.voyager.sample.koinIntegration + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.sample.sampleItems + +class KoinScreenModel : ScreenModel { + + val items = sampleItems +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaIntegrationActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaIntegrationActivity.kt new file mode 100644 index 00000000..9f8db920 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaIntegrationActivity.kt @@ -0,0 +1,17 @@ +package cafe.adriel.voyager.sample.rxjavaIntegration + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import cafe.adriel.voyager.navigator.Navigator + +class RxJavaIntegrationActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Navigator(RxJavaScreen()) + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreen.kt new file mode 100644 index 00000000..d90ea941 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreen.kt @@ -0,0 +1,34 @@ +package cafe.adriel.voyager.sample.rxjavaIntegration + +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.currentCompositeKeyHash +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rxjava3.subscribeAsState +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.sample.ListContent +import cafe.adriel.voyager.sample.LoadingContent + +class RxJavaScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val screenModel = rememberScreenModel { RxJavaScreenModel() } + val state by screenModel.state.subscribeAsState(initial = RxJavaScreenModel.State.Loading) + + when (val result = state) { + is RxJavaScreenModel.State.Loading -> LoadingContent() + is RxJavaScreenModel.State.Result -> ListContent(result.items) + } + + LaunchedEffect(currentCompositeKeyHash) { + screenModel.getItems() + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreenModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreenModel.kt new file mode 100644 index 00000000..151beb63 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/rxjavaIntegration/RxJavaScreenModel.kt @@ -0,0 +1,26 @@ +package cafe.adriel.voyager.sample.rxjavaIntegration + +import cafe.adriel.voyager.rxjava.RxScreenModel +import cafe.adriel.voyager.rxjava.disposables +import cafe.adriel.voyager.sample.sampleItems +import io.reactivex.rxjava3.core.Single +import java.util.concurrent.TimeUnit + +class RxJavaScreenModel : RxScreenModel() { + + sealed class State { + object Loading : State() + data class Result(val items: List) : State() + } + + private val items = sampleItems + + fun getItems() { + Single + .just(items) + .delay(1_000, TimeUnit.MILLISECONDS) + .doOnSubscribe { mutableState.onNext(State.Loading) } + .subscribe { items -> mutableState.onNext(State.Result(items)) } + .let(disposables::add) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt new file mode 100644 index 00000000..1529a679 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreen.kt @@ -0,0 +1,38 @@ +package cafe.adriel.voyager.sample.screenModel + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.currentCompositeKeyHash +import androidx.compose.runtime.getValue +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.sample.DetailsContent +import cafe.adriel.voyager.sample.LoadingContent + +data class DetailsScreen( + val index: Int +) : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val screenModel = rememberScreenModel { DetailsScreenModel(index) } + val state by screenModel.state.collectAsState() + + when (val result = state) { + is DetailsScreenModel.State.Loading -> LoadingContent() + is DetailsScreenModel.State.Result -> DetailsContent(screenModel, result.item, navigator::pop) + } + + LaunchedEffect(currentCompositeKeyHash) { + screenModel.getItem(index) + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreenModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreenModel.kt new file mode 100644 index 00000000..ea02f828 --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/DetailsScreenModel.kt @@ -0,0 +1,23 @@ +package cafe.adriel.voyager.sample.screenModel + +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class DetailsScreenModel( + val index: Int +) : StateScreenModel(State.Loading) { + + sealed class State { + object Loading : State() + data class Result(val item: String) : State() + } + + fun getItem(index: Int) { + coroutineScope.launch { + delay(1_000) + mutableState.value = State.Result("Item #$index") + } + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt new file mode 100644 index 00000000..d52789bb --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreen.kt @@ -0,0 +1,23 @@ +package cafe.adriel.voyager.sample.screenModel + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.core.screen.uniqueScreenKey +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.sample.ListContent + +class ListScreen : Screen { + + override val key: ScreenKey = uniqueScreenKey + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val screenModel = rememberScreenModel { ListScreenModel() } + + ListContent(screenModel.items, onClick = { index -> navigator.push(DetailsScreen(index)) }) + } +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreenModel.kt b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreenModel.kt new file mode 100644 index 00000000..ce23c7ee --- /dev/null +++ b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ListScreenModel.kt @@ -0,0 +1,9 @@ +package cafe.adriel.voyager.sample.screenModel + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.sample.sampleItems + +class ListScreenModel : ScreenModel { + + val items = sampleItems +} diff --git a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/AndroidNavigationActivity.kt b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ScreenModelActivity.kt similarity index 75% rename from sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/AndroidNavigationActivity.kt rename to sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ScreenModelActivity.kt index 87bc33a2..73673012 100644 --- a/sample/src/main/java/cafe/adriel/voyager/sample/androidNavigation/AndroidNavigationActivity.kt +++ b/sample/src/main/java/cafe/adriel/voyager/sample/screenModel/ScreenModelActivity.kt @@ -1,11 +1,11 @@ -package cafe.adriel.voyager.sample.androidNavigation +package cafe.adriel.voyager.sample.screenModel import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import cafe.adriel.voyager.navigator.Navigator -class AndroidNavigationActivity : ComponentActivity() { +class ScreenModelActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/settings.gradle b/settings.gradle index 00865054..15b62066 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,7 +18,10 @@ include( ':voyager-tab-navigator', ':voyager-bottom-sheet-navigator', ':voyager-transitions', - ':voyager-androidx' + ':voyager-androidx', + ':voyager-kodein', + ':voyager-koin', + ':voyager-rxjava' ) enableFeaturePreview("VERSION_CATALOGS") diff --git a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt index 1979a9ff..dff642da 100644 --- a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt +++ b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreen.kt @@ -10,5 +10,5 @@ public abstract class AndroidScreen : Screen, ScreenLifecycleProvider { override val key: ScreenKey = uniqueScreenKey - override fun getLifecycleOwner(): ScreenLifecycleOwner = ScreenLifecycleHolder.get(key) + override fun getLifecycleOwner(): ScreenLifecycleOwner = AndroidScreenLifecycleOwner.get(this) } diff --git a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreenLifecycleOwner.kt similarity index 52% rename from voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt rename to voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreenLifecycleOwner.kt index f1c88f14..7ed09130 100644 --- a/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/ScreenLifecycleHolder.kt +++ b/voyager-androidx/src/main/java/cafe/adriel/voyager/androidx/AndroidScreenLifecycleOwner.kt @@ -3,6 +3,7 @@ package cafe.adriel.voyager.androidx import android.app.Activity import android.content.Context import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSavedStateRegistryOwner import androidx.lifecycle.Lifecycle @@ -14,14 +15,13 @@ import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner -import cafe.adriel.voyager.core.lifecycle.ScreenHooks +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleHooks import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleOwner -import cafe.adriel.voyager.core.screen.ScreenKey -import java.util.concurrent.ConcurrentHashMap +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore +import cafe.adriel.voyager.core.screen.Screen -public class ScreenLifecycleHolder private constructor( - private val key: ScreenKey -) : ScreenLifecycleOwner, +public class AndroidScreenLifecycleOwner private constructor() : + ScreenLifecycleOwner, LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { @@ -32,29 +32,32 @@ public class ScreenLifecycleHolder private constructor( private val controller = SavedStateRegistryController.create(this) - private val Context.canDispose: Boolean - get() = (this as? Activity)?.isChangingConfigurations?.not() ?: true + private val Context.isChangingConfigurations: Boolean + get() = (this as? Activity)?.isChangingConfigurations ?: false init { - controller.performRestore(null) + if (controller.savedStateRegistry.isRestored.not()) { + controller.performRestore(null) + } } @Composable - override fun getHooks(): ScreenHooks { + override fun getHooks(): ScreenLifecycleHooks { val context = LocalContext.current - return ScreenHooks( - providers = listOf( - LocalViewModelStoreOwner provides this, - LocalSavedStateRegistryOwner provides this, - ), - disposer = { - if (context.canDispose) { - viewModelStore.clear() - remove(key) + return remember(this) { + ScreenLifecycleHooks( + providers = listOf( + LocalViewModelStoreOwner provides this, + LocalSavedStateRegistryOwner provides this, + ), + onDispose = { + if (context.isChangingConfigurations.not()) { + viewModelStore.clear() + } } - } - ) + ) + } } override fun getLifecycle(): Lifecycle = registry @@ -63,15 +66,9 @@ public class ScreenLifecycleHolder private constructor( override fun getSavedStateRegistry(): SavedStateRegistry = controller.savedStateRegistry - internal companion object { - - private val holders = ConcurrentHashMap() + public companion object { - internal fun get(key: ScreenKey) = - holders.getOrPut(key) { ScreenLifecycleHolder(key) } - - private fun remove(key: ScreenKey) { - holders -= key - } + public fun get(screen: Screen): ScreenLifecycleOwner = + ScreenLifecycleStore.get(screen) { AndroidScreenLifecycleOwner() } } } diff --git a/voyager-bottom-sheet-navigator/src/main/java/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt b/voyager-bottom-sheet-navigator/src/main/java/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt index 21fc14b2..acbe7085 100644 --- a/voyager-bottom-sheet-navigator/src/main/java/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt +++ b/voyager-bottom-sheet-navigator/src/main/java/cafe/adriel/voyager/navigator/bottomSheet/BottomSheetNavigator.kt @@ -55,9 +55,7 @@ public fun BottomSheetNavigator( BottomSheetNavigator(navigator, sheetState, coroutineScope) } - CompositionLocalProvider( - LocalBottomSheetNavigator provides bottomSheetNavigator - ) { + CompositionLocalProvider(LocalBottomSheetNavigator provides bottomSheetNavigator) { ModalBottomSheetLayout( modifier = modifier, scrimColor = scrimColor, diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt index b4311d7c..4dd7cb8b 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycle.kt @@ -31,11 +31,3 @@ public interface ScreenLifecycleProvider { public fun getLifecycleOwner(): ScreenLifecycleOwner } - -public interface ScreenLifecycleOwner { - - @Composable - public fun getHooks(): ScreenHooks = ScreenHooks.Empty -} - -private object DefaultScreenLifecycleOwner : ScreenLifecycleOwner diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleHooks.kt similarity index 62% rename from voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt rename to voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleHooks.kt index c45a73c9..5b8249ab 100644 --- a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenHooks.kt +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleHooks.kt @@ -2,12 +2,12 @@ package cafe.adriel.voyager.core.lifecycle import androidx.compose.runtime.ProvidedValue -public data class ScreenHooks( +public data class ScreenLifecycleHooks( val providers: List> = emptyList(), - val disposer: () -> Unit = {} + val onDispose: () -> Unit = {} ) { internal companion object { - val Empty = ScreenHooks() + val Empty = ScreenLifecycleHooks() } } diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleOwner.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleOwner.kt new file mode 100644 index 00000000..aea185a8 --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleOwner.kt @@ -0,0 +1,11 @@ +package cafe.adriel.voyager.core.lifecycle + +import androidx.compose.runtime.Composable + +public interface ScreenLifecycleOwner { + + @Composable + public fun getHooks(): ScreenLifecycleHooks = ScreenLifecycleHooks.Empty +} + +internal object DefaultScreenLifecycleOwner : ScreenLifecycleOwner diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleStore.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleStore.kt new file mode 100644 index 00000000..01232c9e --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/lifecycle/ScreenLifecycleStore.kt @@ -0,0 +1,20 @@ +package cafe.adriel.voyager.core.lifecycle + +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import java.util.concurrent.ConcurrentHashMap + +public object ScreenLifecycleStore { + + private val owners = ConcurrentHashMap() + + public fun get( + screen: Screen, + factory: (ScreenKey) -> ScreenLifecycleOwner + ): ScreenLifecycleOwner = + owners.getOrPut(screen.key) { factory(screen.key) } + + public fun remove(screen: Screen) { + owners -= screen.key + } +} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModel.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModel.kt new file mode 100644 index 00000000..6cbc8843 --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModel.kt @@ -0,0 +1,42 @@ +package cafe.adriel.voyager.core.model + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisallowComposableCalls +import androidx.compose.runtime.remember +import cafe.adriel.voyager.core.screen.Screen +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.plus + +public val ScreenModel.coroutineScope: CoroutineScope + get() = ScreenModelStore.getOrPutDependency( + screenModel = this, + name = "ScreenModelCoroutineScope", + factory = { key -> MainScope() + CoroutineName(key) }, + onDispose = { scope -> scope.cancel() } + ) + +@Composable +public inline fun Screen.rememberScreenModel( + tag: String? = null, + factory: @DisallowComposableCalls () -> T +): T = + remember(ScreenModelStore.getKey(this, tag)) { + ScreenModelStore.getOrPut(this, tag, factory) + } + +public interface ScreenModel { + + public fun onDispose() {} +} + +public abstract class StateScreenModel(initial: S) : ScreenModel { + + protected val mutableState: MutableStateFlow = MutableStateFlow(initial) + public val state: StateFlow = mutableState.asStateFlow() +} diff --git a/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModelStore.kt b/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModelStore.kt new file mode 100644 index 00000000..1001dbfd --- /dev/null +++ b/voyager-core/src/main/java/cafe/adriel/voyager/core/model/ScreenModelStore.kt @@ -0,0 +1,74 @@ +package cafe.adriel.voyager.core.model + +import androidx.compose.runtime.DisallowComposableCalls +import cafe.adriel.voyager.core.screen.Screen +import java.util.concurrent.ConcurrentHashMap + +private typealias ScreenModelKey = String + +private typealias DependencyKey = String +private typealias DependencyInstance = Any +private typealias DependencyOnDispose = (Any) -> Unit +private typealias Dependency = Pair + +public object ScreenModelStore { + + @PublishedApi + internal val screenModels: ConcurrentHashMap = ConcurrentHashMap() + + @PublishedApi + internal val dependencies: ConcurrentHashMap = ConcurrentHashMap() + + @PublishedApi + internal inline fun getKey(screen: Screen, tag: String?): ScreenModelKey = + "${screen.key}:${T::class.qualifiedName}:${tag ?: "default"}" + + @PublishedApi + internal fun getDependencyKey(screenModel: ScreenModel, name: String): DependencyKey = + screenModels + .firstNotNullOfOrNull { + if (it.value == screenModel) it.key + else null + } + ?.let { "$it:$name" } + ?: error("ScreenModel not found: ${screenModel::class.qualifiedName}") + + @PublishedApi + internal inline fun getOrPut( + screen: Screen, + tag: String?, + factory: @DisallowComposableCalls () -> T + ): T = + screenModels.getOrPut(getKey(screen, tag), factory) as T + + public inline fun getOrPutDependency( + screenModel: ScreenModel, + name: String, + noinline onDispose: @DisallowComposableCalls (T) -> Unit = {}, + noinline factory: @DisallowComposableCalls (DependencyKey) -> T + ): T { + val key = getDependencyKey(screenModel, name) + + return dependencies + .getOrPut(key) { (factory(key) to onDispose) as Dependency } + .first as T + } + + public fun remove(screen: Screen) { + screenModels.onEach(screen) { key -> + screenModels[key]?.onDispose() + screenModels -= key + } + + dependencies.onEach(screen) { key -> + dependencies[key]?.let { (instance, onDispose) -> onDispose(instance) } + dependencies -= key + } + } + + private fun Map.onEach(screen: Screen, block: (String) -> Unit) = + asSequence() + .filter { it.key.startsWith(screen.key) } + .map { it.key } + .forEach(block) +} diff --git a/voyager-kodein/.gitignore b/voyager-kodein/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/voyager-kodein/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/voyager-kodein/build.gradle b/voyager-kodein/build.gradle new file mode 100644 index 00000000..66f6e237 --- /dev/null +++ b/voyager-kodein/build.gradle @@ -0,0 +1,23 @@ +apply plugin: "com.android.library" +apply from: "../android-module.gradle" + +android { + defaultConfig { + consumerProguardFiles "consumer-rules.pro" + } + kotlinOptions { + freeCompilerArgs += '-Xexplicit-api=strict' + } +} + +dependencies { + api projects.voyagerCore + + implementation libs.kodein + implementation libs.compose.runtime + + testRuntimeOnly libs.junit.engine + testImplementation libs.junit.api +} + +apply plugin: "com.vanniktech.maven.publish" \ No newline at end of file diff --git a/voyager-kodein/consumer-rules.pro b/voyager-kodein/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/voyager-kodein/gradle.properties b/voyager-kodein/gradle.properties new file mode 100644 index 00000000..21cda346 --- /dev/null +++ b/voyager-kodein/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=VoyagerKodein +POM_ARTIFACT_ID=voyager-kodein +POM_PACKAGING=aar \ No newline at end of file diff --git a/voyager-kodein/src/main/AndroidManifest.xml b/voyager-kodein/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1364a3e5 --- /dev/null +++ b/voyager-kodein/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/voyager-kodein/src/main/java/cafe/adriel/voyager/kodein/ScreenModel.kt b/voyager-kodein/src/main/java/cafe/adriel/voyager/kodein/ScreenModel.kt new file mode 100644 index 00000000..17095f73 --- /dev/null +++ b/voyager-kodein/src/main/java/cafe/adriel/voyager/kodein/ScreenModel.kt @@ -0,0 +1,24 @@ +package cafe.adriel.voyager.kodein + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import org.kodein.di.compose.localDI +import org.kodein.di.direct +import org.kodein.di.provider + +@Composable +public inline fun Screen.rememberScreenModel( + tag: Any? = null +): T = with(localDI()) { + rememberScreenModel(tag = tag?.toString()) { direct.provider(tag)() } +} + +@Composable +public inline fun Screen.rememberScreenModel( + tag: Any? = null, + arg: A +): T = with(localDI()) { + rememberScreenModel(tag = tag?.toString()) { direct.provider(tag, arg)() } +} diff --git a/voyager-koin/.gitignore b/voyager-koin/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/voyager-koin/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/voyager-koin/build.gradle b/voyager-koin/build.gradle new file mode 100644 index 00000000..36bdd76f --- /dev/null +++ b/voyager-koin/build.gradle @@ -0,0 +1,23 @@ +apply plugin: "com.android.library" +apply from: "../android-module.gradle" + +android { + defaultConfig { + consumerProguardFiles "consumer-rules.pro" + } + kotlinOptions { + freeCompilerArgs += '-Xexplicit-api=strict' + } +} + +dependencies { + api projects.voyagerCore + + implementation libs.koin + implementation libs.compose.runtime + + testRuntimeOnly libs.junit.engine + testImplementation libs.junit.api +} + +apply plugin: "com.vanniktech.maven.publish" \ No newline at end of file diff --git a/voyager-koin/consumer-rules.pro b/voyager-koin/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/voyager-koin/gradle.properties b/voyager-koin/gradle.properties new file mode 100644 index 00000000..717cdcc4 --- /dev/null +++ b/voyager-koin/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=VoyagerKoin +POM_ARTIFACT_ID=voyager-koin +POM_PACKAGING=aar \ No newline at end of file diff --git a/voyager-koin/src/main/AndroidManifest.xml b/voyager-koin/src/main/AndroidManifest.xml new file mode 100644 index 00000000..60eafe4b --- /dev/null +++ b/voyager-koin/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/voyager-koin/src/main/java/cafe/adriel/voyager/koin/ScreenModel.kt b/voyager-koin/src/main/java/cafe/adriel/voyager/koin/ScreenModel.kt new file mode 100644 index 00000000..5fbc95a4 --- /dev/null +++ b/voyager-koin/src/main/java/cafe/adriel/voyager/koin/ScreenModel.kt @@ -0,0 +1,18 @@ +package cafe.adriel.voyager.koin + +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import org.koin.androidx.compose.getKoin +import org.koin.core.parameter.ParametersDefinition +import org.koin.core.qualifier.Qualifier + +@Composable +public inline fun Screen.getScreenModel( + qualifier: Qualifier? = null, + noinline parameters: ParametersDefinition? = null, +): T { + val koin = getKoin() + return rememberScreenModel(tag = qualifier?.value) { koin.get(qualifier, parameters) } +} diff --git a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt index e9f8ae47..ee28648c 100644 --- a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt +++ b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/Navigator.kt @@ -2,19 +2,17 @@ package cafe.adriel.voyager.navigator import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.SaveableStateHolder -import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.stack.Stack -import cafe.adriel.voyager.core.stack.StackEvent import cafe.adriel.voyager.core.stack.toMutableStateStack import cafe.adriel.voyager.navigator.internal.NavigatorBackHandler +import cafe.adriel.voyager.navigator.internal.NavigatorDisposableEffect import cafe.adriel.voyager.navigator.internal.rememberNavigator public typealias NavigatorContent = @Composable (navigator: Navigator) -> Unit @@ -28,9 +26,6 @@ public val ProvidableCompositionLocal.currentOrThrow: T @Composable get() = current ?: error("CompositionLocal is null") -private val disposableEvents: Set = - setOf(StackEvent.Pop, StackEvent.Replace) - @Composable public fun CurrentScreen() { val navigator = LocalNavigator.currentOrThrow @@ -44,11 +39,13 @@ public fun CurrentScreen() { @Composable public fun Navigator( screen: Screen, + autoDispose: Boolean = true, onBackPressed: OnBackPressed = { true }, content: NavigatorContent = { CurrentScreen() } ) { Navigator( screens = listOf(screen), + autoDispose = autoDispose, onBackPressed = onBackPressed, content = content ) @@ -57,33 +54,25 @@ public fun Navigator( @Composable public fun Navigator( screens: List, + autoDispose: Boolean = true, onBackPressed: OnBackPressed = { true }, content: NavigatorContent = { CurrentScreen() } ) { require(screens.isNotEmpty()) { "Navigator must have at least one screen" } val navigator = rememberNavigator(screens, LocalNavigator.current) - val currentScreen = navigator.lastItem - val lifecycleOwner = rememberScreenLifecycleOwner(currentScreen) + val lifecycleOwner = rememberScreenLifecycleOwner(navigator.lastItem) val hooks = lifecycleOwner.getHooks() CompositionLocalProvider( LocalNavigator provides navigator, *hooks.providers.toTypedArray() ) { - content(navigator) + if (autoDispose) NavigatorDisposableEffect(navigator, hooks.onDispose) NavigatorBackHandler(navigator, onBackPressed) - DisposableEffect(currentScreen.key) { - onDispose { - if (navigator.lastEvent in disposableEvents) { - hooks.disposer() - navigator.stateHolder.removeState(currentScreen.key) - navigator.clearEvent() - } - } - } + content(navigator) } } diff --git a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt index ecf00396..9ea44061 100644 --- a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt +++ b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorBackHandler.kt @@ -8,7 +8,7 @@ import cafe.adriel.voyager.navigator.OnBackPressed @Composable internal fun NavigatorBackHandler( navigator: Navigator, - onBackPressed: OnBackPressed, + onBackPressed: OnBackPressed ) { if (onBackPressed != null) { BackHandler( diff --git a/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt new file mode 100644 index 00000000..ec56d0ca --- /dev/null +++ b/voyager-navigator/src/main/java/cafe/adriel/voyager/navigator/internal/NavigatorDisposable.kt @@ -0,0 +1,31 @@ +package cafe.adriel.voyager.navigator.internal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore +import cafe.adriel.voyager.core.model.ScreenModelStore +import cafe.adriel.voyager.core.stack.StackEvent +import cafe.adriel.voyager.navigator.Navigator + +private val disposableEvents: Set = + setOf(StackEvent.Pop, StackEvent.Replace) + +@Composable +internal fun NavigatorDisposableEffect( + navigator: Navigator, + onDispose: () -> Unit +) { + val currentScreen = navigator.lastItem + + DisposableEffect(currentScreen.key) { + onDispose { + if (navigator.lastEvent in disposableEvents) { + onDispose() + ScreenModelStore.remove(currentScreen) + ScreenLifecycleStore.remove(currentScreen) + navigator.stateHolder.removeState(currentScreen.key) + navigator.clearEvent() + } + } + } +} diff --git a/voyager-rxjava/.gitignore b/voyager-rxjava/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/voyager-rxjava/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/voyager-rxjava/build.gradle b/voyager-rxjava/build.gradle new file mode 100644 index 00000000..485f5c9b --- /dev/null +++ b/voyager-rxjava/build.gradle @@ -0,0 +1,22 @@ +apply plugin: "com.android.library" +apply from: "../android-module.gradle" + +android { + defaultConfig { + consumerProguardFiles "consumer-rules.pro" + } + kotlinOptions { + freeCompilerArgs += '-Xexplicit-api=strict' + } +} + +dependencies { + api projects.voyagerCore + + implementation libs.compose.rxjava + + testRuntimeOnly libs.junit.engine + testImplementation libs.junit.api +} + +apply plugin: "com.vanniktech.maven.publish" \ No newline at end of file diff --git a/voyager-rxjava/consumer-rules.pro b/voyager-rxjava/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/voyager-rxjava/gradle.properties b/voyager-rxjava/gradle.properties new file mode 100644 index 00000000..8ba52d18 --- /dev/null +++ b/voyager-rxjava/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=VoyagerRxJava +POM_ARTIFACT_ID=voyager-rxjava +POM_PACKAGING=aar \ No newline at end of file diff --git a/voyager-rxjava/src/main/AndroidManifest.xml b/voyager-rxjava/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c6fd574e --- /dev/null +++ b/voyager-rxjava/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/voyager-rxjava/src/main/java/cafe/adriel/voyager/rxjava/ScreenModel.kt b/voyager-rxjava/src/main/java/cafe/adriel/voyager/rxjava/ScreenModel.kt new file mode 100644 index 00000000..9af024e8 --- /dev/null +++ b/voyager-rxjava/src/main/java/cafe/adriel/voyager/rxjava/ScreenModel.kt @@ -0,0 +1,21 @@ +package cafe.adriel.voyager.rxjava + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.ScreenModelStore +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.subjects.BehaviorSubject + +public val ScreenModel.disposables: CompositeDisposable + get() = ScreenModelStore.getOrPutDependency( + screenModel = this, + name = "ScreenModelCompositeDisposable", + factory = { CompositeDisposable() }, + onDispose = { disposables -> disposables.clear() } + ) + +public abstract class RxScreenModel : ScreenModel { + + protected val mutableState: BehaviorSubject = BehaviorSubject.create() + public val state: Observable = mutableState +} diff --git a/voyager-tab-navigator/build.gradle b/voyager-tab-navigator/build.gradle index e9e5251d..e8b74029 100644 --- a/voyager-tab-navigator/build.gradle +++ b/voyager-tab-navigator/build.gradle @@ -12,6 +12,7 @@ android { dependencies { api projects.voyagerCore + implementation projects.voyagerNavigator implementation libs.compose.runtimeSaveable implementation libs.compose.ui diff --git a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/Tab.kt b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/Tab.kt index 8d893fbc..abf694fe 100644 --- a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/Tab.kt +++ b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/Tab.kt @@ -3,15 +3,11 @@ package cafe.adriel.voyager.navigator.tab import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.CurrentScreen @Composable public fun CurrentTab() { - val tabNavigator = LocalTabNavigator.current - val currentTab = tabNavigator.current - - tabNavigator.stateHolder.SaveableStateProvider(currentTab.key) { - currentTab.Content() - } + CurrentScreen() } public data class TabOptions( diff --git a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt index d74241a6..529f6fc0 100644 --- a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt +++ b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/TabNavigator.kt @@ -2,15 +2,10 @@ package cafe.adriel.voyager.navigator.tab import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.SaveableStateHolder -import androidx.compose.runtime.setValue +import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf -import cafe.adriel.voyager.core.lifecycle.rememberScreenLifecycleOwner -import cafe.adriel.voyager.navigator.tab.internal.rememberTabNavigator +import cafe.adriel.voyager.navigator.Navigator public typealias TabNavigatorContent = @Composable (tabNavigator: TabNavigator) -> Unit @@ -22,29 +17,22 @@ public fun TabNavigator( tab: Tab, content: TabNavigatorContent = { CurrentTab() } ) { - val tabNavigator = rememberTabNavigator(tab) - val currentTab = tabNavigator.current - val lifecycleOwner = rememberScreenLifecycleOwner(currentTab) - val hooks = lifecycleOwner.getHooks() - - CompositionLocalProvider( - LocalTabNavigator provides tabNavigator, - *hooks.providers.toTypedArray() - ) { - content(tabNavigator) - } + Navigator(tab, autoDispose = false, onBackPressed = null) { navigator -> + val tabNavigator = remember(navigator) { + TabNavigator(navigator) + } - DisposableEffect(tabNavigator) { - onDispose { - hooks.disposer() + CompositionLocalProvider(LocalTabNavigator provides tabNavigator) { + content(tabNavigator) } } } public class TabNavigator internal constructor( - tab: Tab, - public val stateHolder: SaveableStateHolder + private val navigator: Navigator ) { - public var current: Tab by mutableStateOf(tab) + public var current: Tab + get() = navigator.lastItem as Tab + set(tab) = navigator.replaceAll(tab) } diff --git a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/internal/TabNavigatorSaver.kt b/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/internal/TabNavigatorSaver.kt deleted file mode 100644 index 04b9f95a..00000000 --- a/voyager-tab-navigator/src/main/java/cafe/adriel/voyager/navigator/tab/internal/TabNavigatorSaver.kt +++ /dev/null @@ -1,28 +0,0 @@ -package cafe.adriel.voyager.navigator.tab.internal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.SaveableStateHolder -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.saveable.rememberSaveableStateHolder -import cafe.adriel.voyager.navigator.tab.Tab -import cafe.adriel.voyager.navigator.tab.TabNavigator - -@Composable -internal fun rememberTabNavigator( - tab: Tab -): TabNavigator { - val stateHolder = rememberSaveableStateHolder() - - return rememberSaveable(saver = tabNavigatorSaver(stateHolder)) { - TabNavigator(tab, stateHolder) - } -} - -private fun tabNavigatorSaver( - stateHolder: SaveableStateHolder -): Saver = - Saver( - save = { tabNavigator -> tabNavigator.current }, - restore = { tab -> TabNavigator(tab, stateHolder) } - )