Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/v3 navigation #104

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.futured.kmptemplate.android.ui.screen.ProfileScreenUi
import app.futured.kmptemplate.android.ui.screen.ThirdScreenUi
import app.futured.kmptemplate.feature.navigation.profile.ProfileChild
import app.futured.kmptemplate.feature.navigation.profile.ProfileConfig
import app.futured.kmptemplate.feature.navigation.profile.ProfileNavHost
Expand Down Expand Up @@ -43,6 +44,7 @@ fun ProfileNavHostUi(
) { child ->
when (val childInstance = child.instance) {
is ProfileChild.Profile -> ProfileScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
is ProfileChild.Third -> ThirdScreenUi(screen = childInstance.screen, modifier = Modifier.fillMaxSize())
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ private fun Content(
Button(onClick = actions::onLogout) {
Text(kmpStringResource(MR.strings.generic_sign_out))
}
Button(onClick = actions::onThird) {
Text("Nav to third")
}
}
}
}
2 changes: 2 additions & 0 deletions iosApp/iosApp/Views/Navigation/ProfileTabNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct ProfileTabNavigationView: View {
switch onEnum(of: child) {
case .profile(let entry):
ProfileView(ProfileViewModel(entry.screen))
case .third(let entry):
ThirdView(ThirdViewModel(entry.screen))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions iosApp/iosApp/Views/Screen/Profile/ProfileView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct ProfileView<ViewModel: ProfileViewModelProtocol>: View {
VStack(spacing: 10) {
Text(Localizable.login_screen_title.localized)
Button(Localizable.generic_sign_out.localized, action: viewModel.onLogoutClick)
Button("Navigate to third", action: viewModel.onThirdClick)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Localize the navigation button text.

The button text "Navigate to third" is hardcoded, while other text in the view uses localization (e.g., Localizable.login_screen_title.localized). Consider adding a localized string for consistency.

Example fix:

-            Button("Navigate to third", action: viewModel.onThirdClick)
+            Button(Localizable.profile_navigate_to_third.localized, action: viewModel.onThirdClick)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Button("Navigate to third", action: viewModel.onThirdClick)
Button(Localizable.profile_navigate_to_third.localized, action: viewModel.onThirdClick)

}
}
}
4 changes: 4 additions & 0 deletions iosApp/iosApp/Views/Screen/Profile/ProfileViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI

protocol ProfileViewModelProtocol: DynamicProperty {
func onLogoutClick()
func onThirdClick()
}

struct ProfileViewModel {
Expand All @@ -17,4 +18,7 @@ extension ProfileViewModel: ProfileViewModelProtocol {
func onLogoutClick() {
actions.onLogout()
}
func onThirdClick() {
actions.onThird()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ import app.futured.kmptemplate.feature.ui.firstScreen.FirstScreenNavigation
import app.futured.kmptemplate.feature.ui.secondScreen.SecondComponent
import app.futured.kmptemplate.feature.ui.secondScreen.SecondScreenNavigation
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.navigate
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import kotlinx.coroutines.flow.StateFlow
import org.koin.core.annotation.Factory
import org.koin.core.annotation.InjectedParam
Expand All @@ -28,50 +25,45 @@ internal class HomeNavHostComponent(
@InjectedParam private val initialStack: List<HomeConfig>,
) : AppComponent<Unit, Nothing>(componentContext, Unit), HomeNavHost {

private val homeNavigator = StackNavigation<HomeConfig>()

override val actions: HomeNavHost.Actions = object : HomeNavHost.Actions {
override fun navigate(newStack: List<Child<HomeConfig, HomeChild>>) = homeNavigator.navigate { newStack.map { it.configuration } }
override fun pop() = homeNavigator.pop()
}
private val homeNavigator: HomeNavigation = HomeNavigator()

override val stack: StateFlow<ChildStack<HomeConfig, HomeChild>> = childStack(
source = homeNavigator,
source = homeNavigator.navigator,
serializer = HomeConfig.serializer(),
initialStack = { initialStack },
key = "HomeStack",
handleBackButton = true,
childFactory = { config, childCtx ->
when (config) {
HomeConfig.First -> HomeChild.First(
AppComponentFactory.createComponent<FirstComponent>(
AppComponentFactory.createScreenComponent<FirstComponent, FirstScreenNavigation>(
childContext = childCtx,
navigation = FirstScreenNavigation(
toSecond = { homeNavigator.pushNew(HomeConfig.Second) },
),
navigation = homeNavigator,
),
)

HomeConfig.Second -> HomeChild.Second(
AppComponentFactory.createComponent<SecondComponent>(
AppComponentFactory.createScreenComponent<SecondComponent, SecondScreenNavigation>(
childContext = childCtx,
navigation = SecondScreenNavigation(
pop = { homeNavigator.pop() },
toThird = { id -> homeNavigator.pushNew(HomeConfig.Third(ThirdScreenArgs(id))) },
),
navigation = homeNavigator,
),
)

is HomeConfig.Third -> HomeChild.Third(
AppComponentFactory.createComponent<ThirdComponent>(
AppComponentFactory.createScreenComponent<ThirdComponent, ThirdScreenNavigation>(
childContext = childCtx,
navigation = ThirdScreenNavigation(
pop = { homeNavigator.pop() },
),
navigation = homeNavigator,
config.args,
),
)
}
},
).asStateFlow()

override val actions: HomeNavHost.Actions = object : HomeNavHost.Actions {
override fun navigate(newStack: List<Child<HomeConfig, HomeChild>>) =
homeNavigator.navigator.navigate { newStack.map { it.configuration } }

override fun pop() = homeNavigator.navigator.pop()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package app.futured.kmptemplate.feature.navigation.home

import app.futured.kmptemplate.feature.ui.firstScreen.FirstComponent
import app.futured.kmptemplate.feature.ui.firstScreen.FirstScreenNavigation
import app.futured.kmptemplate.feature.ui.secondScreen.SecondComponent
import app.futured.kmptemplate.feature.ui.secondScreen.SecondScreenNavigation
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import org.koin.core.annotation.Single

internal interface HomeNavigation : FirstScreenNavigation, SecondScreenNavigation, ThirdScreenNavigation {
val navigator: StackNavigation<HomeConfig>
}

internal class HomeNavigator : HomeNavigation {
override val navigator = StackNavigation<HomeConfig>()

override fun FirstComponent.navigateToSecond() =
navigator.pushNew(HomeConfig.Second)

override fun SecondComponent.pop() =
navigator.pop()

override fun SecondComponent.navigateToThird(id: String) =
navigator.pushNew(HomeConfig.Third(ThirdScreenArgs(id)))

override fun ThirdComponent.pop() {
navigator.pop()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package app.futured.kmptemplate.feature.navigation.profile

import app.futured.kmptemplate.feature.ui.profileScreen.ProfileScreen
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreen
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.essenty.backhandler.BackHandlerOwner
Expand All @@ -23,8 +25,12 @@ sealed interface ProfileConfig {

@Serializable
data object Profile : ProfileConfig

@Serializable
data class Third(val args: ThirdScreenArgs) : ProfileConfig
}

sealed interface ProfileChild {
data class Profile(val screen: ProfileScreen) : ProfileChild
data class Third(val screen: ThirdScreen) : ProfileChild
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import app.futured.kmptemplate.feature.ui.base.AppComponentContext
import app.futured.kmptemplate.feature.ui.base.AppComponentFactory
import app.futured.kmptemplate.feature.ui.profileScreen.ProfileComponent
import app.futured.kmptemplate.feature.ui.profileScreen.ProfileScreenNavigation
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.navigate
import com.arkivanov.decompose.router.stack.pop
Expand All @@ -19,34 +20,41 @@ import org.koin.core.annotation.InjectedParam
@Factory
internal class ProfileNavHostComponent(
@InjectedParam componentContext: AppComponentContext,
@InjectedParam navigationActions: ProfileNavHostNavigation,
@InjectedParam toLogin: () -> Unit,
@InjectedParam private val initialStack: List<ProfileConfig>,
) : AppComponent<Unit, Nothing>(componentContext, Unit), ProfileNavHost {

private val stackNavigator = StackNavigation<ProfileConfig>()
private val navigator: ProfileNavHostNavigation = ProfileNavHostNavigator(toLogin)

override val stack: StateFlow<ChildStack<ProfileConfig, ProfileChild>> = childStack(
source = stackNavigator,
source = navigator.stackNavigator,
serializer = ProfileConfig.serializer(),
initialStack = { initialStack },
handleBackButton = true,
childFactory = { config, childCtx ->
when (config) {
ProfileConfig.Profile -> ProfileChild.Profile(
AppComponentFactory.createComponent<ProfileComponent>(
AppComponentFactory.createScreenComponent<ProfileComponent, ProfileScreenNavigation>(
childContext = childCtx,
navigation = navigator,
),
)

is ProfileConfig.Third -> ProfileChild.Third(
AppComponentFactory.createScreenComponent<ThirdComponent, ThirdScreenNavigation>(
childContext = childCtx,
navigation = ProfileScreenNavigation(
toLogin = navigationActions.toLogin,
),
navigation = navigator,
config.args,
),
)
}
},
).asStateFlow()

override val actions: ProfileNavHost.Actions = object : ProfileNavHost.Actions {
override fun pop() = stackNavigator.pop()
override fun navigate(newStack: List<Child<ProfileConfig, ProfileChild>>) = stackNavigator.navigate {
newStack.map { it.configuration }
}
override fun navigate(newStack: List<Child<ProfileConfig, ProfileChild>>) =
navigator.stackNavigator.navigate { newStack.map { it.configuration } }

override fun pop() = navigator.stackNavigator.pop()
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
package app.futured.kmptemplate.feature.navigation.profile

import app.futured.arkitekt.decompose.navigation.NavigationActions
import app.futured.kmptemplate.feature.ui.profileScreen.ProfileScreen
import app.futured.kmptemplate.feature.ui.profileScreen.ProfileScreenNavigation
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdComponent
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenNavigation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push

data class ProfileNavHostNavigation(
val toLogin: () -> Unit,
) : NavigationActions
internal interface ProfileNavHostNavigation : ProfileScreenNavigation, ThirdScreenNavigation {
val stackNavigator: StackNavigation<ProfileConfig>
}

internal class ProfileNavHostNavigator(
private val navigateToLogin: () -> Unit,
) : ProfileNavHostNavigation {
override val stackNavigator = StackNavigation<ProfileConfig>()

override fun ProfileScreen.toLogin() = navigateToLogin()

override fun ProfileScreen.navigateToThird(id: String) {
stackNavigator.push(ProfileConfig.Third(ThirdScreenArgs(id)))
}

override fun ThirdComponent.pop() {
stackNavigator.pop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import app.futured.arkitekt.decompose.ext.asStateFlow
import app.futured.kmptemplate.feature.navigation.deepLink.DeepLinkDestination
import app.futured.kmptemplate.feature.navigation.deepLink.DeepLinkResolver
import app.futured.kmptemplate.feature.navigation.signedIn.SignedInNavHostComponent
import app.futured.kmptemplate.feature.navigation.signedIn.SignedInNavHostNavigation
import app.futured.kmptemplate.feature.ui.base.AppComponent
import app.futured.kmptemplate.feature.ui.base.AppComponentContext
import app.futured.kmptemplate.feature.ui.base.AppComponentFactory
import app.futured.kmptemplate.feature.ui.loginScreen.LoginComponent
import app.futured.kmptemplate.feature.ui.loginScreen.LoginScreen
import app.futured.kmptemplate.feature.ui.loginScreen.LoginScreenNavigation
import app.futured.kmptemplate.feature.ui.thirdScreen.ThirdScreenArgs
import co.touchlab.kermit.Logger
import com.arkivanov.decompose.router.slot.ChildSlot
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.essenty.lifecycle.doOnCreate
Expand All @@ -27,36 +26,31 @@ internal class RootNavHostComponent(
private val deepLinkResolver: DeepLinkResolver,
) : AppComponent<RootNavHostViewState, Nothing>(componentContext, RootNavHostViewState), RootNavHost {

private val slotNavigator = SlotNavigation<RootConfig>()
private val rootNavigator: RootNavHostNavigation = RootNavHostNavigator()
private var pendingDeepLink: DeepLinkDestination? = null

private val logger = Logger.withTag("RootNavHostComponent")

override val slot: StateFlow<ChildSlot<RootConfig, RootChild>> = childSlot(
source = slotNavigator,
source = rootNavigator.slotNavigator,
serializer = RootConfig.serializer(),
initialConfiguration = { null },
handleBackButton = false,
childFactory = { config, childCtx ->
when (config) {
RootConfig.Login -> RootChild.Login(
screen = AppComponentFactory.createComponent<LoginComponent>(
RootConfig.Login -> {
val screen: LoginScreen = AppComponentFactory.createScreenComponent<LoginComponent, LoginScreenNavigation>(
childContext = childCtx,
navigation = LoginScreenNavigation(
toSignedIn = {
slotNavigator.activate(RootConfig.SignedIn())
},
),
),
)
navigation = rootNavigator,
)
RootChild.Login(screen = screen)
}

is RootConfig.SignedIn -> RootChild.SignedIn(
navHost = AppComponentFactory.createComponent<SignedInNavHostComponent>(
navHost = AppComponentFactory.createAppComponent<SignedInNavHostComponent>(
childContext = childCtx,
{ rootNavigator.slotNavigator.activate(RootConfig.Login) },
config.initialConfig,
SignedInNavHostNavigation(
toLogin = { slotNavigator.activate(RootConfig.Login) },
),
),
)
}
Expand All @@ -66,7 +60,7 @@ internal class RootNavHostComponent(
init {
doOnCreate {
if (!consumeDeepLink()) {
slotNavigator.activate(RootConfig.Login)
rootNavigator.slotNavigator.activate(RootConfig.Login)
}
}
}
Expand All @@ -86,12 +80,13 @@ internal class RootNavHostComponent(
*/
private fun consumeDeepLink(): Boolean {
val deepLink = pendingDeepLink ?: return false
when (deepLink) {
DeepLinkDestination.HomeTab -> slotNavigator.activate(RootConfig.deepLinkHome())
DeepLinkDestination.ProfileTab -> slotNavigator.activate(RootConfig.deepLinkProfile())
DeepLinkDestination.SecondScreen -> slotNavigator.activate(RootConfig.deepLinkSecondScreen())
is DeepLinkDestination.ThirdScreen -> slotNavigator.activate(RootConfig.deepLinkThirdScreen(ThirdScreenArgs(deepLink.argument)))
val deepLinkConfig = when (deepLink) {
DeepLinkDestination.HomeTab -> RootConfig.deepLinkHome()
DeepLinkDestination.ProfileTab -> RootConfig.deepLinkProfile()
DeepLinkDestination.SecondScreen -> RootConfig.deepLinkSecondScreen()
is DeepLinkDestination.ThirdScreen -> RootConfig.deepLinkThirdScreen(ThirdScreenArgs(deepLink.argument))
}
rootNavigator.slotNavigator.activate(deepLinkConfig)
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import org.koin.core.component.KoinComponent
object RootNavHostFactory : KoinComponent {
fun create(
componentContext: AppComponentContext,
): RootNavHost = AppComponentFactory.createComponent<RootNavHostComponent>(componentContext)
): RootNavHost = AppComponentFactory.createAppComponent<RootNavHostComponent>(componentContext)
}
Loading
Loading