diff --git a/app/build.gradle b/app/build.gradle index bbe5cca35..4362ec8a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,6 +73,7 @@ dependencies { implementation projects.core.kotlin implementation projects.core.designsystem implementation projects.core.logger + implementation projects.core.resources implementation projects.data.repository.api implementation projects.data.model implementation projects.feature.home.api @@ -108,13 +109,20 @@ dependencies { implementation libs.androidx.lifecycle.runtime implementation libs.androidx.lifecycle.compiler - implementation libs.androidx.navigation.compose + implementation libs.androidx.navigation3.runtime + implementation libs.androidx.navigation3.ui + implementation libs.androidx.lifecycle.viewmodel.navigation3 implementation libs.androidx.activity.compose - implementation libs.androidx.hilt.navigation.compose + implementation libs.androidx.hilt.lifecycle.viewmodel.compose implementation libs.compose.foundation - implementation libs.compose.material3 implementation libs.compose.ui + implementation libs.compose.material3 + implementation libs.compose.material3.adaptivenavigation + implementation libs.compose.material3.adaptive.navigation3 + implementation libs.compose.animation.graphics + + implementation libs.materialmotion.compose.core implementation libs.androidx.profileinstaller diff --git a/app/dependencies/releaseRuntimeClasspath.txt b/app/dependencies/releaseRuntimeClasspath.txt index 9968d27c7..0fb728e95 100644 --- a/app/dependencies/releaseRuntimeClasspath.txt +++ b/app/dependencies/releaseRuntimeClasspath.txt @@ -1,11 +1,11 @@ -androidx.activity:activity-compose:1.10.1 -androidx.activity:activity-ktx:1.10.1 -androidx.activity:activity:1.10.1 -androidx.annotation:annotation-experimental:1.4.1 +androidx.activity:activity-compose:1.12.1 +androidx.activity:activity-ktx:1.12.1 +androidx.activity:activity:1.12.1 +androidx.annotation:annotation-experimental:1.5.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 -androidx.appcompat:appcompat-resources:1.7.0 -androidx.appcompat:appcompat:1.7.0 +androidx.appcompat:appcompat-resources:1.7.1 +androidx.appcompat:appcompat:1.7.1 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 @@ -13,18 +13,24 @@ androidx.browser:browser:1.8.0 androidx.collection:collection-jvm:1.5.0 androidx.collection:collection-ktx:1.5.0 androidx.collection:collection:1.5.0 -androidx.compose.animation:animation-android:1.7.8 -androidx.compose.animation:animation-core-android:1.7.8 -androidx.compose.animation:animation-core:1.7.8 -androidx.compose.animation:animation-graphics-android:1.7.8 -androidx.compose.animation:animation-graphics:1.7.8 -androidx.compose.animation:animation:1.7.8 -androidx.compose.foundation:foundation-android:1.7.8 -androidx.compose.foundation:foundation-layout-android:1.7.8 -androidx.compose.foundation:foundation-layout:1.7.8 -androidx.compose.foundation:foundation:1.7.8 -androidx.compose.material3.adaptive:adaptive-android:1.0.0 -androidx.compose.material3.adaptive:adaptive:1.0.0 +androidx.compose.animation:animation-android:1.10.0-beta02 +androidx.compose.animation:animation-core-android:1.10.0-beta02 +androidx.compose.animation:animation-core:1.10.0-beta02 +androidx.compose.animation:animation-graphics-android:1.10.0-beta02 +androidx.compose.animation:animation-graphics:1.10.0-beta02 +androidx.compose.animation:animation:1.10.0-beta02 +androidx.compose.foundation:foundation-android:1.10.0-beta02 +androidx.compose.foundation:foundation-layout-android:1.10.0-beta02 +androidx.compose.foundation:foundation-layout:1.10.0-beta02 +androidx.compose.foundation:foundation:1.10.0-beta02 +androidx.compose.material3.adaptive:adaptive-android:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-layout:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-navigation3-android:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive-navigation:1.3.0-alpha05 +androidx.compose.material3.adaptive:adaptive:1.3.0-alpha05 androidx.compose.material3:material3-adaptive-navigation-suite-android:1.3.1 androidx.compose.material3:material3-adaptive-navigation-suite:1.3.1 androidx.compose.material3:material3-android:1.3.1 @@ -37,30 +43,34 @@ androidx.compose.material:material-icons-extended:1.7.8 androidx.compose.material:material-ripple-android:1.7.8 androidx.compose.material:material-ripple:1.7.8 androidx.compose.material:material:1.7.8 -androidx.compose.runtime:runtime-android:1.8.2 -androidx.compose.runtime:runtime-saveable-android:1.8.2 -androidx.compose.runtime:runtime-saveable:1.8.2 -androidx.compose.runtime:runtime:1.8.2 -androidx.compose.ui:ui-android:1.8.2 -androidx.compose.ui:ui-geometry-android:1.8.2 -androidx.compose.ui:ui-geometry:1.8.2 -androidx.compose.ui:ui-graphics-android:1.8.2 -androidx.compose.ui:ui-graphics:1.8.2 -androidx.compose.ui:ui-text-android:1.8.2 -androidx.compose.ui:ui-text:1.8.2 -androidx.compose.ui:ui-tooling-preview-android:1.8.2 -androidx.compose.ui:ui-tooling-preview:1.8.2 -androidx.compose.ui:ui-unit-android:1.8.2 -androidx.compose.ui:ui-unit:1.8.2 -androidx.compose.ui:ui-util-android:1.8.2 -androidx.compose.ui:ui-util:1.8.2 -androidx.compose.ui:ui:1.8.2 +androidx.compose.runtime:runtime-android:1.10.0-beta02 +androidx.compose.runtime:runtime-annotation-android:1.10.0-beta02 +androidx.compose.runtime:runtime-annotation:1.10.0-beta02 +androidx.compose.runtime:runtime-retain-android:1.10.0-beta02 +androidx.compose.runtime:runtime-retain:1.10.0-beta02 +androidx.compose.runtime:runtime-saveable-android:1.10.0-beta02 +androidx.compose.runtime:runtime-saveable:1.10.0-beta02 +androidx.compose.runtime:runtime:1.10.0-beta02 +androidx.compose.ui:ui-android:1.10.0-beta02 +androidx.compose.ui:ui-geometry-android:1.10.0-beta02 +androidx.compose.ui:ui-geometry:1.10.0-beta02 +androidx.compose.ui:ui-graphics-android:1.10.0-beta02 +androidx.compose.ui:ui-graphics:1.10.0-beta02 +androidx.compose.ui:ui-text-android:1.10.0-beta02 +androidx.compose.ui:ui-text:1.10.0-beta02 +androidx.compose.ui:ui-tooling-preview-android:1.10.0-beta02 +androidx.compose.ui:ui-tooling-preview:1.10.0-beta02 +androidx.compose.ui:ui-unit-android:1.10.0-beta02 +androidx.compose.ui:ui-unit:1.10.0-beta02 +androidx.compose.ui:ui-util-android:1.10.0-beta02 +androidx.compose.ui:ui-util:1.10.0-beta02 +androidx.compose.ui:ui:1.10.0-beta02 androidx.compose:compose-bom:2025.02.00 androidx.concurrent:concurrent-futures-ktx:1.1.0 androidx.concurrent:concurrent-futures:1.1.0 -androidx.core:core-ktx:1.16.0-alpha02 +androidx.core:core-ktx:1.17.0 androidx.core:core-viewtree:1.0.0 -androidx.core:core:1.16.0-alpha02 +androidx.core:core:1.17.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview-poolingcontainer:1.0.0 androidx.customview:customview:1.0.0 @@ -76,7 +86,9 @@ androidx.datastore:datastore-preferences-external-protobuf:1.1.3 androidx.datastore:datastore-preferences-proto:1.1.3 androidx.datastore:datastore-preferences:1.1.3 androidx.datastore:datastore:1.1.3 +androidx.documentfile:documentfile:1.0.0 androidx.drawerlayout:drawerlayout:1.0.0 +androidx.dynamicanimation:dynamicanimation:1.0.0 androidx.emoji2:emoji2-views-helper:1.4.0 androidx.emoji2:emoji2:1.4.0 androidx.exifinterface:exifinterface:1.3.7 @@ -85,37 +97,43 @@ androidx.graphics:graphics-path:1.0.1 androidx.hilt:hilt-common:1.3.0 androidx.hilt:hilt-lifecycle-viewmodel-compose:1.3.0 androidx.hilt:hilt-lifecycle-viewmodel:1.3.0 -androidx.hilt:hilt-navigation-compose:1.3.0 androidx.hilt:hilt-work:1.3.0 androidx.interpolator:interpolator:1.0.0 -androidx.lifecycle:lifecycle-common-java8:2.9.1 -androidx.lifecycle:lifecycle-common-jvm:2.9.1 -androidx.lifecycle:lifecycle-common:2.9.1 -androidx.lifecycle:lifecycle-livedata-core-ktx:2.9.1 -androidx.lifecycle:lifecycle-livedata-core:2.9.1 -androidx.lifecycle:lifecycle-livedata:2.9.1 -androidx.lifecycle:lifecycle-process:2.9.1 -androidx.lifecycle:lifecycle-runtime-android:2.9.1 -androidx.lifecycle:lifecycle-runtime-compose-android:2.9.1 -androidx.lifecycle:lifecycle-runtime-compose:2.9.1 -androidx.lifecycle:lifecycle-runtime-ktx-android:2.9.1 -androidx.lifecycle:lifecycle-runtime-ktx:2.9.1 -androidx.lifecycle:lifecycle-runtime:2.9.1 -androidx.lifecycle:lifecycle-service:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-android:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-compose-android:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.9.1 -androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.1 -androidx.lifecycle:lifecycle-viewmodel:2.9.1 +androidx.legacy:legacy-support-core-utils:1.0.0 +androidx.lifecycle:lifecycle-common-java8:2.10.0 +androidx.lifecycle:lifecycle-common-jvm:2.10.0 +androidx.lifecycle:lifecycle-common:2.10.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.10.0 +androidx.lifecycle:lifecycle-livedata-core:2.10.0 +androidx.lifecycle:lifecycle-livedata:2.10.0 +androidx.lifecycle:lifecycle-process:2.10.0 +androidx.lifecycle:lifecycle-runtime-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-compose-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-compose:2.10.0 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.10.0 +androidx.lifecycle:lifecycle-runtime-ktx:2.10.0 +androidx.lifecycle:lifecycle-runtime:2.10.0 +androidx.lifecycle:lifecycle-service:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-navigation3-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate-android:2.10.0 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.10.0 +androidx.lifecycle:lifecycle-viewmodel:2.10.0 androidx.loader:loader:1.0.0 -androidx.navigation:navigation-common-android:2.9.6 -androidx.navigation:navigation-common:2.9.6 -androidx.navigation:navigation-compose-android:2.9.6 -androidx.navigation:navigation-compose:2.9.6 -androidx.navigation:navigation-runtime-android:2.9.6 -androidx.navigation:navigation-runtime:2.9.6 +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 +androidx.navigation3:navigation3-runtime-android:1.0.0 +androidx.navigation3:navigation3-runtime:1.0.0 +androidx.navigation3:navigation3-ui-android:1.0.0 +androidx.navigation3:navigation3-ui:1.0.0 +androidx.navigationevent:navigationevent-android:1.0.1 +androidx.navigationevent:navigationevent-compose-android:1.0.1 +androidx.navigationevent:navigationevent-compose:1.0.1 +androidx.navigationevent:navigationevent:1.0.1 +androidx.print:print:1.0.0 androidx.profileinstaller:profileinstaller:1.4.1 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.room:room-common-jvm:2.7.0-rc01 @@ -123,11 +141,11 @@ androidx.room:room-common:2.7.0-rc01 androidx.room:room-ktx:2.7.0-rc01 androidx.room:room-runtime-android:2.7.0-rc01 androidx.room:room-runtime:2.7.0-rc01 -androidx.savedstate:savedstate-android:1.3.0 -androidx.savedstate:savedstate-compose-android:1.3.0 -androidx.savedstate:savedstate-compose:1.3.0 -androidx.savedstate:savedstate-ktx:1.3.0 -androidx.savedstate:savedstate:1.3.0 +androidx.savedstate:savedstate-android:1.4.0 +androidx.savedstate:savedstate-compose-android:1.4.0 +androidx.savedstate:savedstate-compose:1.4.0 +androidx.savedstate:savedstate-ktx:1.4.0 +androidx.savedstate:savedstate:1.4.0 androidx.sqlite:sqlite-android:2.5.0-rc01 androidx.sqlite:sqlite-framework-android:2.5.0-rc01 androidx.sqlite:sqlite-framework:2.5.0-rc01 @@ -135,13 +153,14 @@ androidx.sqlite:sqlite:2.5.0-rc01 androidx.startup:startup-runtime:1.2.0 androidx.tracing:tracing-ktx:1.2.0 androidx.tracing:tracing:1.2.0 +androidx.transition:transition:1.6.0 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 -androidx.window:window-core-android:1.4.0-beta02 -androidx.window:window-core:1.4.0-beta02 -androidx.window:window:1.4.0-beta02 +androidx.window:window-core-android:1.5.0 +androidx.window:window-core:1.5.0 +androidx.window:window:1.5.0 androidx.work:work-runtime:2.10.0 com.google.accompanist:accompanist-drawablepainter:0.36.0 com.google.code.findbugs:jsr305:3.0.2 @@ -174,18 +193,20 @@ io.github.fornewid:material-motion-compose-core:1.1.3 io.github.fornewid:photo-compose:1.0.1 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 -org.jetbrains.androidx.lifecycle:lifecycle-common:2.8.4 -org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.8.4 -org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.8.4 -org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.4 +org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.5 +org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.5 +org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.5 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.5 +org.jetbrains.androidx.savedstate:savedstate-compose:1.3.5 +org.jetbrains.androidx.savedstate:savedstate:1.3.5 org.jetbrains.compose.animation:animation-core:1.7.3 org.jetbrains.compose.animation:animation:1.7.3 org.jetbrains.compose.annotation-internal:annotation:1.7.3 org.jetbrains.compose.collection-internal:collection:1.7.3 org.jetbrains.compose.foundation:foundation-layout:1.7.3 org.jetbrains.compose.foundation:foundation:1.7.3 -org.jetbrains.compose.runtime:runtime-saveable:1.7.3 -org.jetbrains.compose.runtime:runtime:1.7.3 +org.jetbrains.compose.runtime:runtime-saveable:1.9.2 +org.jetbrains.compose.runtime:runtime:1.9.2 org.jetbrains.compose.ui:ui-geometry:1.7.3 org.jetbrains.compose.ui:ui-graphics:1.7.3 org.jetbrains.compose.ui:ui-text:1.7.3 diff --git a/app/src/main/java/soup/movie/di/AppModule.kt b/app/src/main/java/soup/movie/di/AppModule.kt new file mode 100644 index 000000000..99c29dabb --- /dev/null +++ b/app/src/main/java/soup/movie/di/AppModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import soup.movie.feature.navigator.Navigator +import soup.movie.ui.main.NavigatorImpl + +@Module +@InstallIn(ActivityRetainedComponent::class) +interface AppModule { + + @Binds + fun bindsNavigator( + impl: NavigatorImpl, + ): Navigator +} diff --git a/app/src/main/java/soup/movie/di/ApplicationModule.kt b/app/src/main/java/soup/movie/di/ApplicationModule.kt index 0cfc41965..3e02314d0 100644 --- a/app/src/main/java/soup/movie/di/ApplicationModule.kt +++ b/app/src/main/java/soup/movie/di/ApplicationModule.kt @@ -26,7 +26,7 @@ import soup.movie.feature.navigator.MainNavigator interface ApplicationModule { @Binds - fun provideMainNavigator( + fun bindsMainNavigator( impl: MainNavigatorImpl, ): MainNavigator } diff --git a/app/src/main/java/soup/movie/ui/main/MainActivity.kt b/app/src/main/java/soup/movie/ui/main/MainActivity.kt index f75402ee0..16051d671 100644 --- a/app/src/main/java/soup/movie/ui/main/MainActivity.kt +++ b/app/src/main/java/soup/movie/ui/main/MainActivity.kt @@ -19,26 +19,107 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.scene.DialogSceneStrategy +import androidx.navigation3.ui.NavDisplay import dagger.hilt.android.AndroidEntryPoint +import soup.compose.material.motion.animation.materialSharedAxisZ import soup.movie.R +import soup.movie.core.designsystem.icon.MovieIcons import soup.movie.core.designsystem.theme.MovieTheme +import soup.movie.feature.home.HomeScreenKey +import soup.movie.feature.navigator.EntryProviderInstaller +import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject + lateinit var navigator: NavigatorImpl + + @Inject + lateinit var entryProviderScopes: Set<@JvmSuppressWildcards EntryProviderInstaller> + private val viewModel: MainViewModel by viewModels() + @OptIn(ExperimentalMaterial3AdaptiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { setTheme(R.style.Theme_Moop) super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { MovieTheme { - MainNavGraph() + NavigationSuiteScaffold( + navigationSuiteItems = { + TOP_LEVEL_ROUTES.forEach { (key, value) -> + val isSelected = key == navigator.state.topLevelRoute + item( + icon = { + Icon( + painter = rememberAnimatedVectorPainter( + animatedImageVector = AnimatedImageVector.animatedVectorResource(value.icon), + atEnd = isSelected, + ), + contentDescription = null, + ) + }, + label = { + Text(text = stringResource(value.description)) + }, + selected = isSelected, + onClick = { navigator.navigate(key) }, + ) + } + }, + ) { + val dialogSceneStrategy = remember { DialogSceneStrategy() } + val directive = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo()) + .copy(horizontalPartitionSpacerSize = 0.dp) + NavDisplay( + entries = navigator.state.toEntries( + entryProvider = entryProvider { + entryProviderScopes.forEach { builder -> this.builder() } + }, + ), + onBack = { navigator.goBack() }, + sceneStrategy = rememberListDetailSceneStrategy( + backNavigationBehavior = BackNavigationBehavior.PopLatest, + directive = directive, + ) then dialogSceneStrategy, + transitionSpec = { materialSharedAxisZ(forward = true) }, + popTransitionSpec = { materialSharedAxisZ(forward = false) }, + predictivePopTransitionSpec = { materialSharedAxisZ(forward = false) }, + ) + } } } viewModel.onInit() } } + +private val TOP_LEVEL_ROUTES = mapOf( + HomeScreenKey.Home to NavBarItem(icon = MovieIcons.AvdHomeNowSelected, description = soup.movie.resources.R.string.menu_home), + HomeScreenKey.Favorite to NavBarItem(icon = MovieIcons.AvdFavoriteSelected, description = soup.movie.resources.R.string.menu_favorite), +) + +data class NavBarItem( + val icon: Int, + val description: Int, +) diff --git a/app/src/main/java/soup/movie/ui/main/MainNavGraph.kt b/app/src/main/java/soup/movie/ui/main/MainNavGraph.kt deleted file mode 100644 index 7e02f6f96..000000000 --- a/app/src/main/java/soup/movie/ui/main/MainNavGraph.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2022 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.ui.main - -import androidx.compose.runtime.Composable -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import soup.movie.feature.detail.rememberDetailComposableFactory -import soup.movie.feature.home.rememberHomeComposableFactory -import soup.movie.feature.search.rememberSearchComposableFactory -import soup.movie.feature.settings.rememberSettingsComposableFactory - -private sealed interface Screen { - - @Serializable - data object Main : Screen - - @Serializable - data object Search : Screen - - @Serializable - data object Settings : Screen - - @Serializable - data class Detail(val movieId: String) : Screen -} - -@Composable -fun MainNavGraph() { - val navController = rememberNavController() - NavHost( - navController, - startDestination = Screen.Main, - ) { - composable { - val factory = rememberHomeComposableFactory() - factory.HomeNavGraph( - onSearchClick = { - navController.navigate(Screen.Search) - }, - onSettingsClick = { - navController.navigate(Screen.Settings) - }, - onMovieItemClick = { - navController.navigate(Screen.Detail(movieId = it.id)) - }, - ) - } - composable { - val factory = rememberSearchComposableFactory() - factory.SearchScreen( - upPress = { navController.navigateUp() }, - onItemClick = { - navController.navigate(Screen.Detail(movieId = it.id)) - }, - ) - } - composable { - val factory = rememberSettingsComposableFactory() - factory.SettingsNavGraph() - } - composable { backStackEntry -> - val movieId = backStackEntry.toRoute().movieId - val factory = rememberDetailComposableFactory() - factory.DetailNavGraph(movieId = movieId) - } - } -} diff --git a/app/src/main/java/soup/movie/ui/main/NavigationState.kt b/app/src/main/java/soup/movie/ui/main/NavigationState.kt new file mode 100644 index 000000000..d9a641f1d --- /dev/null +++ b/app/src/main/java/soup/movie/ui/main/NavigationState.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.ui.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavEntryDecorator +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import soup.movie.feature.home.HomeScreenKey +import javax.inject.Inject + +class NavigationState @Inject constructor() { + val startRoute: NavKey = HomeScreenKey.Home + + var topLevelRoute: NavKey by mutableStateOf(startRoute) + + val backStacks: Map> = mapOf( + HomeScreenKey.Home to mutableStateListOf(HomeScreenKey.Home), + HomeScreenKey.Favorite to mutableStateListOf(HomeScreenKey.Favorite), + ) + + val stacksInUse: List + get() = if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) + } +} + +@Composable +fun NavigationState.toEntries( + entryDecorators: List> = + listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider: (key: NavKey) -> NavEntry, +): SnapshotStateList> { + val decoratedEntries = backStacks.mapValues { (_, stack) -> + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = entryDecorators, + entryProvider = entryProvider, + ) + } + + return stacksInUse + .flatMap { decoratedEntries[it] ?: emptyList() } + .toMutableStateList() +} diff --git a/app/src/main/java/soup/movie/ui/main/NavigatorImpl.kt b/app/src/main/java/soup/movie/ui/main/NavigatorImpl.kt new file mode 100644 index 000000000..cf7159a38 --- /dev/null +++ b/app/src/main/java/soup/movie/ui/main/NavigatorImpl.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.ui.main + +import androidx.navigation3.runtime.NavKey +import dagger.hilt.android.scopes.ActivityRetainedScoped +import soup.movie.feature.navigator.Navigator +import javax.inject.Inject + +@ActivityRetainedScoped +class NavigatorImpl @Inject constructor( + val state: NavigationState, +) : Navigator { + + override fun navigate(route: NavKey) { + if (route in state.backStacks.keys) { + if (route == state.topLevelRoute) { + state.backStacks[route]?.let { + it.removeRange(1, it.size) + } + } else { + state.topLevelRoute = route + } + } else { + state.backStacks[state.topLevelRoute]?.add(route) + } + } + + override fun goBack() { + val currentStack = state.backStacks[state.topLevelRoute] ?: error("Stack for ${state.topLevelRoute} not found") + val currentRoute = currentStack.last() + + if (currentRoute == state.topLevelRoute) { + state.topLevelRoute = state.startRoute + } else { + currentStack.removeLastOrNull() + } + } +} diff --git a/feature/detail/api/build.gradle b/feature/detail/api/build.gradle index 224a6ab97..64f463528 100644 --- a/feature/detail/api/build.gradle +++ b/feature/detail/api/build.gradle @@ -1,7 +1,5 @@ plugins { id "moop.android.library" - id "moop.android.compose" - id "moop.android.hilt" } android { @@ -9,11 +7,10 @@ android { } dependencies { - implementation projects.core.kotlin - implementation projects.core.designsystem - implementation projects.core.resources + implementation projects.feature.navigator.api implementation libs.kotlin.stdlib + implementation libs.kotlin.serialization - implementation libs.compose.foundation + implementation libs.androidx.navigation3.runtime } diff --git a/feature/detail/api/src/main/java/soup/movie/feature/detail/DetailComposableFactory.kt b/feature/detail/api/src/main/java/soup/movie/feature/detail/DetailComposableFactory.kt deleted file mode 100644 index c14b262b6..000000000 --- a/feature/detail/api/src/main/java/soup/movie/feature/detail/DetailComposableFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2024 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.detail - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -interface DetailComposableFactory { - @Composable - fun DetailNavGraph(movieId: String) -} - -@Composable -fun rememberDetailComposableFactory(): DetailComposableFactory { - val context = LocalContext.current - return remember(context) { - EntryPointAccessors - .fromApplication(context, DetailComposableFactoryEntryPoint::class.java) - .detailComposableFactory() - } -} - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface DetailComposableFactoryEntryPoint { - fun detailComposableFactory(): DetailComposableFactory -} diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailComposableFactoryImpl.kt b/feature/detail/api/src/main/java/soup/movie/feature/detail/DetailScreenKey.kt similarity index 57% rename from feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailComposableFactoryImpl.kt rename to feature/detail/api/src/main/java/soup/movie/feature/detail/DetailScreenKey.kt index 1d29263a9..65a1496a3 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailComposableFactoryImpl.kt +++ b/feature/detail/api/src/main/java/soup/movie/feature/detail/DetailScreenKey.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 SOUP + * Copyright 2025 SOUP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package soup.movie.feature.detail.impl +package soup.movie.feature.detail -import androidx.compose.runtime.Composable -import soup.movie.feature.detail.DetailComposableFactory -import javax.inject.Inject +import kotlinx.serialization.Serializable +import soup.movie.feature.navigator.ScreenKey -class DetailComposableFactoryImpl @Inject constructor() : DetailComposableFactory { +sealed interface DetailScreenKey : ScreenKey { - @Composable - override fun DetailNavGraph(movieId: String) { - soup.movie.feature.detail.impl.DetailNavGraph(movieId = movieId) - } + @Serializable + data class Movie(val movieId: String) : DetailScreenKey + + @Serializable + data class Poster(val posterUrl: String) : DetailScreenKey } diff --git a/feature/detail/impl/build.gradle b/feature/detail/impl/build.gradle index 1c7b7da73..80908aead 100644 --- a/feature/detail/impl/build.gradle +++ b/feature/detail/impl/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation projects.core.datetime implementation projects.data.repository.api implementation projects.data.model + implementation projects.feature.navigator.api implementation projects.feature.home.api implementation projects.feature.detail.api @@ -26,10 +27,13 @@ dependencies { implementation libs.kotlin.serialization implementation libs.androidx.activity.compose - implementation libs.androidx.hilt.navigation.compose - implementation libs.androidx.navigation.compose + implementation libs.androidx.hilt.lifecycle.viewmodel.compose + implementation libs.androidx.navigation3.ui + implementation libs.androidx.navigation3.runtime + implementation libs.androidx.lifecycle.viewmodel.navigation3 implementation libs.compose.foundation implementation libs.compose.material3 + implementation libs.compose.material3.adaptive.navigation3 implementation libs.compose.ui implementation libs.compose.animation.graphics implementation libs.readmore.material diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailContent.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailContent.kt index b5190428f..e23781a6f 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailContent.kt +++ b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailContent.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import soup.movie.core.designsystem.theme.MovieTheme @@ -77,24 +77,27 @@ internal fun DetailContent( modifier = Modifier.padding(paddingValues), ) } + is DetailUiModel.Failure -> { DetailError( onRetryClick = { viewModel.onRetryClick() }, - modifier = Modifier.padding(paddingValues).fillMaxSize(), + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), ) } } } - val context = LocalContext.current val showOpenDateAlarmMessage by viewModel.showOpenDateAlarmMessage.collectAsState() + val opendateAlarmMessage = stringResource(R.string.action_toast_opendate_alarm) LaunchedEffect(showOpenDateAlarmMessage) { if (showOpenDateAlarmMessage) { coroutineScope.launch { snackbarHostState.showSnackbar( - message = context.getString(R.string.action_toast_opendate_alarm), + message = opendateAlarmMessage, ) viewModel.onOpenDateAlarmMessageShown() } diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailNavGraph.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailNavGraph.kt deleted file mode 100644 index b0b16dac3..000000000 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailNavGraph.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2022 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.detail.impl - -import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import soup.compose.material.motion.animation.materialSharedAxisZIn -import soup.compose.material.motion.animation.materialSharedAxisZOut - -private sealed interface DetailScreen { - - @Serializable - data class Home(val movieId: String) : DetailScreen - - @Serializable - data class Poster(val posterUrl: String) : DetailScreen -} - -@Composable -fun DetailNavGraph(movieId: String) { - val navController = rememberNavController() - NavHost( - navController, - startDestination = DetailScreen.Home(movieId), - enterTransition = { materialSharedAxisZIn(forward = true) }, - exitTransition = { materialSharedAxisZOut(forward = true) }, - popEnterTransition = { materialSharedAxisZIn(forward = false) }, - popExitTransition = { materialSharedAxisZOut(forward = false) }, - ) { - composable { - val viewModel = hiltViewModel() - DetailScreen( - viewModel = viewModel, - onPosterClick = { - navController.navigate(DetailScreen.Poster(posterUrl = it)) - }, - ) - } - composable { backStackEntry -> - val posterUrl = backStackEntry.toRoute().posterUrl - DetailPoster( - posterUrl = posterUrl, - ) - } - } -} diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailPoster.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailPoster.kt index e719ecfec..ad4666931 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailPoster.kt +++ b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailPoster.kt @@ -17,11 +17,9 @@ package soup.movie.feature.detail.impl import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import kotlinx.coroutines.launch import soup.compose.photo.ExperimentalPhotoApi import soup.compose.photo.PhotoBox @@ -40,16 +38,14 @@ fun DetailPoster( photoState.animateToInitialState() } } - Surface(color = Color.Black) { - PhotoBox(state = photoState) { - AsyncImage( - posterUrl, - contentDescription = null, - modifier = Modifier.fillMaxSize(), - onSuccess = { - photoState.setPhotoIntrinsicSize(it.intrinsicSize) - }, - ) - } + PhotoBox(state = photoState, modifier = Modifier.fillMaxSize()) { + AsyncImage( + posterUrl, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + onSuccess = { + photoState.setPhotoIntrinsicSize(it.intrinsicSize) + }, + ) } } diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailViewModel.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailViewModel.kt index 7bfe47c43..f021f23aa 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailViewModel.kt +++ b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/DetailViewModel.kt @@ -15,9 +15,11 @@ */ package soup.movie.feature.detail.impl -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow @@ -30,21 +32,21 @@ import soup.movie.data.repository.MovieRepository import soup.movie.datetime.MM_DD import soup.movie.datetime.today import soup.movie.datetime.yesterday +import soup.movie.feature.detail.DetailScreenKey import soup.movie.log.Logger import soup.movie.model.MovieDetailModel import soup.movie.model.MovieModel import soup.movie.model.OpenDateAlarmModel import java.time.temporal.ChronoUnit -import javax.inject.Inject -@HiltViewModel -class DetailViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = DetailViewModel.Factory::class) +class DetailViewModel @AssistedInject constructor( + @Assisted private val input: DetailScreenKey.Movie, private val repository: MovieRepository, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher, ) : ViewModel() { - private val movieId: String = savedStateHandle["movieId"]!! + private val movieId: String = input.movieId private val _uiModel = MutableStateFlow(DetailUiModel.None) val uiModel: StateFlow = _uiModel @@ -221,4 +223,9 @@ class DetailViewModel @Inject constructor( return 0 } } + + @AssistedFactory + interface Factory { + fun create(input: DetailScreenKey.Movie): DetailViewModel + } } diff --git a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/di/FeatureDetailModule.kt b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/di/FeatureDetailModule.kt index 39a7623b2..eca3f0182 100644 --- a/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/di/FeatureDetailModule.kt +++ b/feature/detail/impl/src/main/java/soup/movie/feature/detail/impl/di/FeatureDetailModule.kt @@ -15,19 +15,57 @@ */ package soup.movie.feature.detail.impl.di -import dagger.Binds +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.scene.DialogSceneStrategy import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import soup.movie.feature.detail.DetailComposableFactory -import soup.movie.feature.detail.impl.DetailComposableFactoryImpl +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet +import soup.movie.feature.detail.DetailScreenKey +import soup.movie.feature.detail.impl.DetailPoster +import soup.movie.feature.detail.impl.DetailScreen +import soup.movie.feature.detail.impl.DetailViewModel +import soup.movie.feature.navigator.EntryProviderInstaller +import soup.movie.feature.navigator.Navigator @Module -@InstallIn(SingletonComponent::class) -interface FeatureDetailModule { +@InstallIn(ActivityRetainedComponent::class) +object FeatureDetailModule { - @Binds - fun bindsDetailComposableFactoryImpl( - impl: DetailComposableFactoryImpl, - ): DetailComposableFactory + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + @IntoSet + @Provides + fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { + entry( + metadata = ListDetailSceneStrategy.detailPane("root"), + ) { key -> + val viewModel = hiltViewModel( + creationCallback = { factory -> + factory.create(key) + }, + ) + DetailScreen( + viewModel = viewModel, + onPosterClick = { + navigator.navigate(DetailScreenKey.Poster(posterUrl = it)) + }, + ) + } + entry( + metadata = DialogSceneStrategy.dialog( + DialogProperties( + usePlatformDefaultWidth = false, + decorFitsSystemWindows = false, + ), + ), + ) { key -> + DetailPoster( + posterUrl = key.posterUrl, + ) + } + } } diff --git a/feature/home/api/build.gradle b/feature/home/api/build.gradle index 26c521623..b3d2e5ed9 100644 --- a/feature/home/api/build.gradle +++ b/feature/home/api/build.gradle @@ -12,8 +12,11 @@ dependencies { implementation projects.core.kotlin implementation projects.core.designsystem implementation projects.data.model + implementation projects.feature.navigator.api implementation libs.kotlin.stdlib + implementation libs.kotlin.serialization + implementation libs.androidx.navigation3.runtime implementation libs.compose.foundation } diff --git a/feature/home/api/src/main/java/soup/movie/feature/home/HomeComposableFactory.kt b/feature/home/api/src/main/java/soup/movie/feature/home/HomeComposableFactory.kt index 34346643b..61af5154e 100644 --- a/feature/home/api/src/main/java/soup/movie/feature/home/HomeComposableFactory.kt +++ b/feature/home/api/src/main/java/soup/movie/feature/home/HomeComposableFactory.kt @@ -27,13 +27,6 @@ import soup.movie.model.MovieModel interface HomeComposableFactory { - @Composable - fun HomeNavGraph( - onSearchClick: () -> Unit, - onSettingsClick: () -> Unit, - onMovieItemClick: (MovieModel) -> Unit, - ) - @Composable fun MovieList( movies: List, diff --git a/feature/home/api/src/main/java/soup/movie/feature/home/HomeScreenKey.kt b/feature/home/api/src/main/java/soup/movie/feature/home/HomeScreenKey.kt new file mode 100644 index 000000000..211966d1f --- /dev/null +++ b/feature/home/api/src/main/java/soup/movie/feature/home/HomeScreenKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.feature.home + +import kotlinx.serialization.Serializable +import soup.movie.feature.navigator.ScreenKey + +sealed interface HomeScreenKey : ScreenKey { + + @Serializable + data object Home : HomeScreenKey + + @Serializable + data object Favorite : HomeScreenKey +} diff --git a/feature/home/impl/build.gradle b/feature/home/impl/build.gradle index e912fb747..6a30c2a37 100644 --- a/feature/home/impl/build.gradle +++ b/feature/home/impl/build.gradle @@ -17,16 +17,20 @@ dependencies { implementation projects.data.settings.api implementation projects.data.repository.api implementation projects.data.model + implementation projects.feature.navigator.api implementation projects.feature.home.api + implementation projects.feature.detail.api + implementation projects.feature.search.api + implementation projects.feature.settings.api implementation libs.kotlin.stdlib implementation libs.androidx.activity.compose - implementation libs.androidx.hilt.navigation.compose - implementation libs.compose.animation.graphics + implementation libs.androidx.hilt.lifecycle.viewmodel.compose + implementation libs.androidx.navigation3.runtime implementation libs.compose.foundation implementation libs.compose.material3 - implementation libs.compose.material3.adaptivenavigation + implementation libs.compose.material3.adaptive.navigation3 implementation libs.compose.ui testImplementation projects.testing diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeComposableFactoryImpl.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeComposableFactoryImpl.kt index 859630a6e..b7d3e4929 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeComposableFactoryImpl.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeComposableFactoryImpl.kt @@ -17,27 +17,12 @@ package soup.movie.feature.home.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel import soup.movie.feature.home.HomeComposableFactory import soup.movie.model.MovieModel import javax.inject.Inject class HomeComposableFactoryImpl @Inject constructor() : HomeComposableFactory { - @Composable - override fun HomeNavGraph( - onSearchClick: () -> Unit, - onSettingsClick: () -> Unit, - onMovieItemClick: (MovieModel) -> Unit, - ) { - HomeNavGraph( - viewModel = hiltViewModel(), - onSearchClick = onSearchClick, - onSettingsClick = onSettingsClick, - onMovieItemClick = onMovieItemClick, - ) - } - @Composable override fun MovieList( movies: List, diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeNavGraph.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeNavGraph.kt deleted file mode 100644 index d74ae6af0..000000000 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeNavGraph.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2022 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.home.impl - -import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi -import androidx.compose.animation.graphics.res.animatedVectorResource -import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter -import androidx.compose.animation.graphics.vector.AnimatedImageVector -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import soup.movie.core.designsystem.icon.MovieIcons -import soup.movie.feature.home.impl.favorite.HomeFavoriteScreen -import soup.movie.model.MovieModel -import soup.movie.resources.R - -@Composable -fun HomeNavGraph( - viewModel: HomeViewModel, - onSearchClick: () -> Unit, - onSettingsClick: () -> Unit, - onMovieItemClick: (MovieModel) -> Unit, -) { - val currentMainTab by viewModel.selectedMainTab.collectAsState() - HomeScaffold( - currentTab = currentMainTab, - tabs = MainTabUiModel.entries.toTypedArray(), - onTabSelected = { mainTab -> - viewModel.onMainTabSelected(mainTab) - }, - ) { - when (currentMainTab) { - MainTabUiModel.Home -> { - HomeScreen( - viewModel = viewModel, - onSearchClick = onSearchClick, - onMovieItemClick = onMovieItemClick, - ) - } - MainTabUiModel.Favorite -> { - HomeFavoriteScreen( - viewModel = hiltViewModel(), - onSettingsClick = onSettingsClick, - onItemClick = onMovieItemClick, - ) - } - } - } -} - -@OptIn(ExperimentalAnimationGraphicsApi::class) -@Composable -private fun HomeScaffold( - currentTab: MainTabUiModel, - tabs: Array, - onTabSelected: (MainTabUiModel) -> Unit, - modifier: Modifier = Modifier, - onTabReselected: (MainTabUiModel) -> Unit = onTabSelected, - content: @Composable () -> Unit, -) { - NavigationSuiteScaffold( - navigationSuiteItems = { - tabs.forEach { tab -> - val selected = currentTab == tab - item( - icon = { - Icon( - rememberAnimatedVectorPainter( - animatedImageVector = when (tab) { - MainTabUiModel.Home -> - AnimatedImageVector.animatedVectorResource(MovieIcons.AvdHomeNowSelected) - - MainTabUiModel.Favorite -> - AnimatedImageVector.animatedVectorResource(MovieIcons.AvdFavoriteSelected) - }, - atEnd = selected, - ), - contentDescription = null, - ) - }, - label = { - Text( - text = when (tab) { - MainTabUiModel.Home -> stringResource(R.string.menu_home) - MainTabUiModel.Favorite -> stringResource(R.string.menu_favorite) - }, - ) - }, - selected = selected, - onClick = { - if (selected) { - onTabReselected(tab) - } else { - onTabSelected(tab) - } - }, - ) - } - }, - modifier = modifier, - content = content, - ) -} diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeScreen.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeScreen.kt index 84abeeed4..64264e706 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeScreen.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeScreen.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import kotlinx.coroutines.launch import soup.movie.core.designsystem.icon.MovieIcons import soup.movie.core.designsystem.theme.MovieTheme diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeUiModel.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeTabUiModel.kt similarity index 93% rename from feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeUiModel.kt rename to feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeTabUiModel.kt index c4c4efa33..9c22e95ad 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeUiModel.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeTabUiModel.kt @@ -15,10 +15,6 @@ */ package soup.movie.feature.home.impl -enum class MainTabUiModel { - Home, Favorite -} - enum class HomeTabUiModel { Now, Plan } diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeViewModel.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeViewModel.kt index b0737ef1d..b22238538 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeViewModel.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/HomeViewModel.kt @@ -26,18 +26,9 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor() : ViewModel() { - private val _selectedMainTab = MutableStateFlow(MainTabUiModel.Home) - val selectedMainTab: StateFlow = _selectedMainTab - private val _selectedHomeTab = MutableStateFlow(HomeTabUiModel.Now) val selectedHomeTab: StateFlow = _selectedHomeTab - fun onMainTabSelected(mainTab: MainTabUiModel) { - viewModelScope.launch { - _selectedMainTab.emit(mainTab) - } - } - fun onHomeTabSelected(homeTab: HomeTabUiModel) { viewModelScope.launch { _selectedHomeTab.emit(homeTab) diff --git a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/di/FeatureHomeModule.kt b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/di/FeatureHomeModule.kt index 56ed31fb9..170401b74 100644 --- a/feature/home/impl/src/main/java/soup/movie/feature/home/impl/di/FeatureHomeModule.kt +++ b/feature/home/impl/src/main/java/soup/movie/feature/home/impl/di/FeatureHomeModule.kt @@ -15,12 +15,26 @@ */ package soup.movie.feature.home.impl.di +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import soup.movie.feature.detail.DetailScreenKey import soup.movie.feature.home.HomeComposableFactory +import soup.movie.feature.home.HomeScreenKey import soup.movie.feature.home.impl.HomeComposableFactoryImpl +import soup.movie.feature.home.impl.HomeScreen +import soup.movie.feature.home.impl.favorite.HomeFavoriteScreen +import soup.movie.feature.navigator.EntryProviderInstaller +import soup.movie.feature.navigator.Navigator +import soup.movie.feature.search.SearchScreenKey +import soup.movie.feature.settings.SettingsScreenKey @Module @InstallIn(SingletonComponent::class) @@ -31,3 +45,40 @@ interface FeatureHomeModule { impl: HomeComposableFactoryImpl, ): HomeComposableFactory } + +@Module +@InstallIn(ActivityRetainedComponent::class) +object HomeModule { + + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + @IntoSet + @Provides + fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { + entry( + metadata = ListDetailSceneStrategy.listPane("root"), + ) { + HomeScreen( + viewModel = hiltViewModel(), + onSearchClick = { + navigator.navigate(SearchScreenKey.Root) + }, + onMovieItemClick = { + navigator.navigate(DetailScreenKey.Movie(movieId = it.id)) + }, + ) + } + entry( + metadata = ListDetailSceneStrategy.listPane("root"), + ) { + HomeFavoriteScreen( + viewModel = hiltViewModel(), + onSettingsClick = { + navigator.navigate(SettingsScreenKey.Settings) + }, + onItemClick = { + navigator.navigate(DetailScreenKey.Movie(movieId = it.id)) + }, + ) + } + } +} diff --git a/feature/navigator/api/build.gradle b/feature/navigator/api/build.gradle index a18bcfab1..98afb8065 100644 --- a/feature/navigator/api/build.gradle +++ b/feature/navigator/api/build.gradle @@ -10,4 +10,5 @@ dependencies { implementation projects.core.logger implementation libs.kotlin.stdlib + implementation libs.androidx.navigation3.runtime } diff --git a/feature/navigator/api/src/main/java/soup/movie/feature/navigator/EntryProviderInstaller.kt b/feature/navigator/api/src/main/java/soup/movie/feature/navigator/EntryProviderInstaller.kt new file mode 100644 index 000000000..089967da5 --- /dev/null +++ b/feature/navigator/api/src/main/java/soup/movie/feature/navigator/EntryProviderInstaller.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.feature.navigator + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey + +typealias EntryProviderInstaller = EntryProviderScope.() -> Unit + +interface Navigator { + fun navigate(route: NavKey) + fun goBack() +} diff --git a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsComposableFactoryImpl.kt b/feature/navigator/api/src/main/java/soup/movie/feature/navigator/ScreenKey.kt similarity index 59% rename from feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsComposableFactoryImpl.kt rename to feature/navigator/api/src/main/java/soup/movie/feature/navigator/ScreenKey.kt index e7dabebac..156c7c828 100644 --- a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsComposableFactoryImpl.kt +++ b/feature/navigator/api/src/main/java/soup/movie/feature/navigator/ScreenKey.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 SOUP + * Copyright 2025 SOUP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package soup.movie.feature.settings.impl +package soup.movie.feature.navigator -import androidx.compose.runtime.Composable -import soup.movie.feature.settings.SettingsComposableFactory -import javax.inject.Inject +import androidx.navigation3.runtime.NavKey -class SettingsComposableFactoryImpl @Inject constructor() : SettingsComposableFactory { - - @Composable - override fun SettingsNavGraph() { - soup.movie.feature.settings.impl.SettingsNavGraph() +interface ScreenKey : NavKey { + companion object { + const val SCENE_KEY_ROOT: String = "root" + const val SCENE_KEY_SETTINGS: String = "settings" } } diff --git a/feature/search/api/build.gradle b/feature/search/api/build.gradle index 2ef6c0840..9e19d86e6 100644 --- a/feature/search/api/build.gradle +++ b/feature/search/api/build.gradle @@ -1,7 +1,5 @@ plugins { id "moop.android.library" - id "moop.android.compose" - id "moop.android.hilt" } android { @@ -9,9 +7,7 @@ android { } dependencies { - implementation projects.core.kotlin - implementation projects.core.designsystem - implementation projects.data.model - - implementation libs.compose.foundation + implementation projects.feature.navigator.api + implementation libs.kotlin.serialization + implementation libs.androidx.navigation3.runtime } diff --git a/feature/search/api/src/main/java/soup/movie/feature/search/SearchComposableFactory.kt b/feature/search/api/src/main/java/soup/movie/feature/search/SearchComposableFactory.kt deleted file mode 100644 index 45f705ccc..000000000 --- a/feature/search/api/src/main/java/soup/movie/feature/search/SearchComposableFactory.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.search - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import soup.movie.model.MovieModel - -interface SearchComposableFactory { - - @Composable - fun SearchScreen( - upPress: () -> Unit, - onItemClick: (MovieModel) -> Unit, - ) -} - -@Composable -fun rememberSearchComposableFactory(): SearchComposableFactory { - val context = LocalContext.current - return remember(context) { - EntryPointAccessors - .fromApplication(context, SearchComposableFactoryEntryPoint::class.java) - .searchComposableFactory() - } -} - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface SearchComposableFactoryEntryPoint { - fun searchComposableFactory(): SearchComposableFactory -} diff --git a/feature/search/api/src/main/java/soup/movie/feature/search/SearchScreenKey.kt b/feature/search/api/src/main/java/soup/movie/feature/search/SearchScreenKey.kt new file mode 100644 index 000000000..da9eee16c --- /dev/null +++ b/feature/search/api/src/main/java/soup/movie/feature/search/SearchScreenKey.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.feature.search + +import kotlinx.serialization.Serializable +import soup.movie.feature.navigator.ScreenKey + +sealed interface SearchScreenKey : ScreenKey { + + @Serializable + data object Root : SearchScreenKey +} diff --git a/feature/search/impl/build.gradle b/feature/search/impl/build.gradle index 628ad655c..c74bdd76a 100644 --- a/feature/search/impl/build.gradle +++ b/feature/search/impl/build.gradle @@ -14,15 +14,18 @@ dependencies { implementation projects.core.resources implementation projects.data.repository.api implementation projects.data.model - implementation projects.feature.search.api + implementation projects.feature.navigator.api implementation projects.feature.home.api + implementation projects.feature.detail.api + implementation projects.feature.search.api implementation libs.compose.foundation implementation libs.compose.material3 implementation libs.compose.ui - implementation libs.androidx.hilt.navigation.compose + implementation libs.androidx.hilt.lifecycle.viewmodel.compose implementation libs.androidx.lifecycle.viewmodel + implementation libs.androidx.navigation3.runtime testImplementation projects.testing androidTestImplementation projects.testing diff --git a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchComposableFactoryImpl.kt b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchComposableFactoryImpl.kt deleted file mode 100644 index af4e6cca7..000000000 --- a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/SearchComposableFactoryImpl.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2024 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.search.impl - -import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel -import soup.movie.feature.search.SearchComposableFactory -import soup.movie.model.MovieModel -import javax.inject.Inject - -class SearchComposableFactoryImpl @Inject constructor() : SearchComposableFactory { - - @Composable - override fun SearchScreen( - upPress: () -> Unit, - onItemClick: (MovieModel) -> Unit, - ) { - SearchScreen( - viewModel = hiltViewModel(), - upPress = upPress, - onItemClick = onItemClick, - ) - } -} diff --git a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/di/FeatureSearchModule.kt b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/di/FeatureSearchModule.kt index c25680037..5d3051afa 100644 --- a/feature/search/impl/src/main/java/soup/movie/feature/search/impl/di/FeatureSearchModule.kt +++ b/feature/search/impl/src/main/java/soup/movie/feature/search/impl/di/FeatureSearchModule.kt @@ -15,19 +15,33 @@ */ package soup.movie.feature.search.impl.di -import dagger.Binds +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import soup.movie.feature.search.SearchComposableFactory -import soup.movie.feature.search.impl.SearchComposableFactoryImpl +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet +import soup.movie.feature.detail.DetailScreenKey +import soup.movie.feature.navigator.EntryProviderInstaller +import soup.movie.feature.navigator.Navigator +import soup.movie.feature.search.SearchScreenKey +import soup.movie.feature.search.impl.SearchScreen @Module -@InstallIn(SingletonComponent::class) -interface FeatureSearchModule { +@InstallIn(ActivityRetainedComponent::class) +object FeatureSearchModule { - @Binds - fun bindsSearchComposableFactory( - impl: SearchComposableFactoryImpl, - ): SearchComposableFactory + @IntoSet + @Provides + fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { + entry { + SearchScreen( + viewModel = hiltViewModel(), + upPress = { navigator.goBack() }, + onItemClick = { + navigator.navigate(DetailScreenKey.Movie(movieId = it.id)) + }, + ) + } + } } diff --git a/feature/settings/api/build.gradle b/feature/settings/api/build.gradle index 54051df38..6b1dd1856 100644 --- a/feature/settings/api/build.gradle +++ b/feature/settings/api/build.gradle @@ -1,7 +1,5 @@ plugins { id "moop.android.library" - id "moop.android.compose" - id "moop.android.hilt" } android { @@ -10,9 +8,10 @@ android { dependencies { implementation projects.core.kotlin - implementation projects.core.resources + implementation projects.feature.navigator.api implementation libs.kotlin.stdlib + implementation libs.kotlin.serialization implementation libs.androidx.core - implementation libs.compose.foundation + implementation libs.androidx.navigation3.runtime } diff --git a/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsComposableFactory.kt b/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsComposableFactory.kt deleted file mode 100644 index 0d5c34638..000000000 --- a/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsComposableFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2024 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.settings - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent - -interface SettingsComposableFactory { - @Composable - fun SettingsNavGraph() -} - -@Composable -fun rememberSettingsComposableFactory(): SettingsComposableFactory { - val context = LocalContext.current - return remember(context) { - EntryPointAccessors - .fromApplication(context, SettingsComposableFactoryEntryPoint::class.java) - .settingsComposableFactory() - } -} - -@EntryPoint -@InstallIn(SingletonComponent::class) -interface SettingsComposableFactoryEntryPoint { - fun settingsComposableFactory(): SettingsComposableFactory -} diff --git a/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsScreenKey.kt b/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsScreenKey.kt new file mode 100644 index 000000000..c16a55f79 --- /dev/null +++ b/feature/settings/api/src/main/java/soup/movie/feature/settings/SettingsScreenKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2025 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package soup.movie.feature.settings + +import kotlinx.serialization.Serializable +import soup.movie.feature.navigator.ScreenKey + +sealed interface SettingsScreenKey : ScreenKey { + + @Serializable + data object Settings : SettingsScreenKey + + @Serializable + data object ThemeOption : SettingsScreenKey +} diff --git a/feature/settings/impl/build.gradle b/feature/settings/impl/build.gradle index 8965fd1fe..a81bf4045 100644 --- a/feature/settings/impl/build.gradle +++ b/feature/settings/impl/build.gradle @@ -16,17 +16,21 @@ dependencies { implementation projects.core.resources implementation projects.data.settings.api implementation projects.data.model + implementation projects.feature.navigator.api implementation projects.feature.settings.api implementation libs.kotlin.stdlib implementation libs.kotlin.serialization implementation libs.androidx.appcompat - implementation libs.androidx.hilt.navigation.compose + implementation libs.androidx.hilt.lifecycle.viewmodel.compose implementation libs.compose.foundation implementation libs.compose.material3 + implementation libs.compose.material3.adaptive.navigation3 implementation libs.compose.ui - implementation libs.androidx.navigation.compose + implementation libs.androidx.navigation3.ui + implementation libs.androidx.navigation3.runtime + implementation libs.androidx.lifecycle.viewmodel.navigation3 implementation libs.materialmotion.compose.core testImplementation projects.testing diff --git a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsNavGraph.kt b/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsNavGraph.kt deleted file mode 100644 index 8b3b7ce09..000000000 --- a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/SettingsNavGraph.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2022 SOUP - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package soup.movie.feature.settings.impl - -import androidx.compose.runtime.Composable -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable -import soup.compose.material.motion.animation.materialSharedAxisZIn -import soup.compose.material.motion.animation.materialSharedAxisZOut -import soup.movie.feature.settings.impl.home.SettingsScreen -import soup.movie.feature.settings.impl.home.SettingsViewModel -import soup.movie.feature.settings.impl.theme.ThemeOptionScreen -import soup.movie.feature.settings.impl.theme.ThemeOptionViewModel - -private sealed interface SettingsScreen { - - @Serializable - data object Home : SettingsScreen - - @Serializable - data object ThemeOption : SettingsScreen -} - -@Composable -fun SettingsNavGraph() { - val navController = rememberNavController() - NavHost( - navController, - startDestination = SettingsScreen.Home, - enterTransition = { materialSharedAxisZIn(forward = true) }, - exitTransition = { materialSharedAxisZOut(forward = true) }, - popEnterTransition = { materialSharedAxisZIn(forward = false) }, - popExitTransition = { materialSharedAxisZOut(forward = false) }, - ) { - composable { - val viewModel = hiltViewModel() - SettingsScreen( - viewModel = viewModel, - onThemeEditClick = { - navController.navigate(SettingsScreen.ThemeOption) - }, - ) - } - composable { - val viewModel = hiltViewModel() - ThemeOptionScreen(viewModel.items) - } - } -} diff --git a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/di/FeatureSettingsModule.kt b/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/di/FeatureSettingsModule.kt index efb50c40a..0de7d3f24 100644 --- a/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/di/FeatureSettingsModule.kt +++ b/feature/settings/impl/src/main/java/soup/movie/feature/settings/impl/di/FeatureSettingsModule.kt @@ -15,19 +15,47 @@ */ package soup.movie.feature.settings.impl.di -import dagger.Binds +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import soup.movie.feature.settings.SettingsComposableFactory -import soup.movie.feature.settings.impl.SettingsComposableFactoryImpl +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.multibindings.IntoSet +import soup.movie.feature.navigator.EntryProviderInstaller +import soup.movie.feature.navigator.Navigator +import soup.movie.feature.navigator.ScreenKey.Companion.SCENE_KEY_SETTINGS +import soup.movie.feature.settings.SettingsScreenKey +import soup.movie.feature.settings.impl.home.SettingsScreen +import soup.movie.feature.settings.impl.home.SettingsViewModel +import soup.movie.feature.settings.impl.theme.ThemeOptionScreen +import soup.movie.feature.settings.impl.theme.ThemeOptionViewModel @Module -@InstallIn(SingletonComponent::class) -interface FeatureSettingsModule { +@InstallIn(ActivityRetainedComponent::class) +object FeatureSettingsModule { - @Binds - fun bindsSettingsComposableFactory( - impl: SettingsComposableFactoryImpl, - ): SettingsComposableFactory + @OptIn(ExperimentalMaterial3AdaptiveApi::class) + @IntoSet + @Provides + fun provideEntryProviderInstaller(navigator: Navigator): EntryProviderInstaller = { + entry( + metadata = ListDetailSceneStrategy.listPane(SCENE_KEY_SETTINGS), + ) { + val viewModel = hiltViewModel() + SettingsScreen( + viewModel = viewModel, + onThemeEditClick = { + navigator.navigate(SettingsScreenKey.ThemeOption) + }, + ) + } + entry( + metadata = ListDetailSceneStrategy.detailPane(SCENE_KEY_SETTINGS), + ) { + val viewModel = hiltViewModel() + ThemeOptionScreen(viewModel.items) + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3d6dc75f..29bece98b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,14 +14,15 @@ dagger = "2.57.2" androidxhilt = "1.3.0" # AndroidX -androidx-activity = "1.10.1" -androidx-appcompat = "1.7.0" +androidx-activity = "1.12.1" +androidx-appcompat = "1.7.1" androidx-benchmark = "1.3.3" androidx-browser = "1.8.0" -androidx-core = "1.16.0-alpha02" +androidx-core = "1.17.0" androidx-datastore = "1.1.3" -androidx-lifecycle = "2.9.0-alpha11" +androidx-lifecycle = "2.10.0" androidx-navigation = "2.9.6" +androidx-navigation3 = "1.0.0" androidx-profileinstaller = "1.4.1" androidx-room = "2.7.0-rc01" androidx-startup = "1.2.0" @@ -30,6 +31,7 @@ androidx-work = "2.10.0" # Compose compose-bom = "2025.02.00" +compose-material3-adaptive = "1.3.0-alpha05" readmore = "1.5.6" materialmotion = "1.1.3" photo-compose = "1.0.1" @@ -60,7 +62,7 @@ dagger-hilt-testing = { module = "com.google.dagger:hilt-android-testing", versi androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidxhilt" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxhilt" } -androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxhilt" } +androidx-hilt-lifecycle-viewmodel-compose = { module = "androidx.hilt:hilt-lifecycle-viewmodel-compose", version.ref = "androidxhilt" } # AndroidX @@ -75,9 +77,13 @@ androidx-datastore-preferences = { module = "androidx.datastore:datastore-prefer androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "androidx-lifecycle" } androidx-lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" } + androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidx-profileinstaller" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } @@ -97,6 +103,7 @@ compose-foundation = { module = "androidx.compose.foundation:foundation" } compose-materialIconsExtended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-material3-adaptivenavigation = { module = "androidx.compose.material3:material3-adaptive-navigation-suite" } +compose-material3-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "compose-material3-adaptive" } compose-ui = { module = "androidx.compose.ui:ui" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }