Skip to content

Commit 455efa1

Browse files
committed
chore: migrate to Compose Destinations 2.x - core navigation infrastructure
- Add new WireRootNavGraph, WireNavHostEngine, and TabletDialogRoutePolicy - Add destination annotations for app, cells, and sketch modules - Remove old WireMainNavGraph in favor of new nav graph architecture - Update MainNavHost, Navigator, NavigationUtils, and HomeDestination - Update core navigation styles and animations - Update NavGraph definitions for auth, login, home, and team migration - Update WireActivity, AppLockActivity, and related infrastructure - Add AndroidNavigationConventionPlugin for build-logic
1 parent 90a4fbd commit 455efa1

File tree

31 files changed

+751
-323
lines changed

31 files changed

+751
-323
lines changed

app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import com.wire.android.appLogger
2222
import com.wire.android.di.ApplicationScope
2323
import com.wire.android.navigation.BackStackMode
2424
import com.wire.android.navigation.NavigationCommand
25-
import com.wire.android.ui.destinations.HomeScreenDestination
26-
import com.wire.android.ui.destinations.NewLoginScreenDestination
27-
import com.wire.android.ui.destinations.WelcomeScreenDestination
25+
import com.ramcosta.composedestinations.generated.app.destinations.HomeScreenDestination
26+
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination
27+
import com.ramcosta.composedestinations.generated.app.destinations.WelcomeScreenDestination
2828
import com.wire.kalium.logic.data.auth.AccountInfo
2929
import com.wire.kalium.logic.data.logout.LogoutReason
3030
import com.wire.kalium.logic.data.user.UserId

app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import androidx.annotation.DrawableRes
2222
import androidx.annotation.StringRes
2323
import com.ramcosta.composedestinations.spec.Direction
2424
import com.wire.android.R
25-
import com.wire.android.ui.destinations.AllConversationsScreenDestination
26-
import com.wire.android.ui.destinations.ArchiveScreenDestination
27-
import com.wire.android.ui.destinations.GlobalCellsScreenDestination
28-
import com.wire.android.ui.destinations.MeetingsScreenDestination
29-
import com.wire.android.ui.destinations.SettingsScreenDestination
30-
import com.wire.android.ui.destinations.VaultScreenDestination
31-
import com.wire.android.ui.destinations.WhatsNewScreenDestination
25+
import com.ramcosta.composedestinations.generated.app.destinations.AllConversationsScreenDestination
26+
import com.ramcosta.composedestinations.generated.app.destinations.ArchiveScreenDestination
27+
import com.ramcosta.composedestinations.generated.app.destinations.GlobalCellsScreenDestination
28+
import com.ramcosta.composedestinations.generated.app.destinations.MeetingsScreenDestination
29+
import com.ramcosta.composedestinations.generated.app.destinations.SettingsScreenDestination
30+
import com.ramcosta.composedestinations.generated.app.destinations.VaultScreenDestination
31+
import com.ramcosta.composedestinations.generated.app.destinations.WhatsNewScreenDestination
3232
import com.wire.android.util.ui.UIText
3333

3434
@Suppress("LongParameterList")

app/src/main/kotlin/com/wire/android/navigation/MainNavHost.kt

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,36 @@ package com.wire.android.navigation
2121
import androidx.compose.animation.ExperimentalAnimationApi
2222
import androidx.compose.runtime.Composable
2323
import androidx.compose.runtime.remember
24+
import androidx.compose.ui.Alignment
2425
import androidx.compose.ui.Modifier
2526
import androidx.hilt.navigation.compose.hiltViewModel
2627
import com.ramcosta.composedestinations.DestinationsNavHost
27-
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
28+
import com.ramcosta.composedestinations.generated.app.destinations.ConversationScreenDestination
29+
import com.ramcosta.composedestinations.generated.app.destinations.LoginScreenDestination
30+
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginPasswordScreenDestination
31+
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginScreenDestination
32+
import com.ramcosta.composedestinations.generated.app.destinations.NewLoginVerificationCodeScreenDestination
33+
import com.ramcosta.composedestinations.generated.app.navgraphs.LoginGraph
34+
import com.ramcosta.composedestinations.generated.app.navgraphs.NewConversationGraph
35+
import com.ramcosta.composedestinations.generated.app.navgraphs.NewLoginGraph
36+
import com.ramcosta.composedestinations.generated.app.navgraphs.PersonalToTeamMigrationGraph
37+
import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph
38+
import com.ramcosta.composedestinations.generated.app.navtype.groupConversationDetailsNavBackArgsNavType
39+
import com.ramcosta.composedestinations.generated.app.navtype.imagesPreviewNavBackArgsNavType
40+
import com.ramcosta.composedestinations.generated.app.navtype.mediaGalleryNavBackArgsNavType
41+
import com.ramcosta.composedestinations.generated.sketch.destinations.DrawingCanvasScreenDestination
42+
import com.ramcosta.composedestinations.generated.sketch.navtype.drawingCanvasNavBackArgsNavType
2843
import com.ramcosta.composedestinations.manualcomposablecalls.composable
2944
import com.ramcosta.composedestinations.navigation.dependency
45+
import com.ramcosta.composedestinations.navigation.destination
46+
import com.ramcosta.composedestinations.navigation.navGraph
3047
import com.ramcosta.composedestinations.scope.resultBackNavigator
3148
import com.ramcosta.composedestinations.scope.resultRecipient
32-
import com.ramcosta.composedestinations.spec.Route
33-
import com.wire.android.feature.sketch.destinations.DrawingCanvasScreenDestination
49+
import com.ramcosta.composedestinations.spec.Direction
3450
import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs
35-
import com.wire.android.navigation.style.DefaultNestedNavGraphAnimations
36-
import com.wire.android.navigation.style.DefaultRootNavGraphAnimations
37-
import com.wire.android.ui.NavGraphs
3851
import com.wire.android.ui.authentication.login.email.LoginEmailViewModel
52+
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolder
3953
import com.wire.android.ui.authentication.login.sso.SSOUrlConfigHolderImpl
40-
import com.wire.android.ui.destinations.ConversationScreenDestination
41-
import com.wire.android.ui.destinations.NewLoginPasswordScreenDestination
42-
import com.wire.android.ui.destinations.NewLoginVerificationCodeScreenDestination
4354
import com.wire.android.ui.home.conversations.ConversationScreen
4455
import com.wire.android.ui.home.newconversation.NewConversationViewModel
4556
import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel
@@ -49,88 +60,100 @@ import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel
4960
fun MainNavHost(
5061
navigator: Navigator,
5162
loginTypeSelector: LoginTypeSelector?,
52-
startDestination: Route,
63+
startDestination: Direction,
5364
modifier: Modifier = Modifier,
5465
) {
55-
val navHostEngine = rememberAnimatedNavHostEngine(
56-
rootDefaultAnimations = DefaultRootNavGraphAnimations,
57-
defaultAnimationsForNestedNavGraph = mapOf(
58-
NavGraphs.createPersonalAccount to DefaultNestedNavGraphAnimations,
59-
NavGraphs.createTeamAccount to DefaultNestedNavGraphAnimations,
60-
NavGraphs.newConversation to DefaultNestedNavGraphAnimations,
61-
)
62-
)
63-
64-
AdjustDestinationStylesForTablets()
66+
val navHostEngine = rememberWireNavHostEngine(Alignment.Center)
6567
DestinationsNavHost(
6668
modifier = modifier,
67-
navGraph = WireMainNavGraph,
69+
navGraph = WireRootGraph,
70+
defaultTransitions = WireRootGraph.defaultTransitions,
6871
engine = navHostEngine,
69-
startRoute = startDestination,
72+
start = startDestination,
7073
navController = navigator.navController,
7174
dependenciesContainerBuilder = {
7275
// 👇 To make Navigator available to all destinations as a non-navigation parameter
7376
dependency(navigator)
7477

78+
// Always provide a default SSO holder at root scope so destinations can resolve it
79+
// even when navigated directly without going through the expected nested graph route.
80+
val rootEntry = remember(navBackStackEntry) {
81+
navController.getBackStackEntry(WireRootGraph.route)
82+
}
83+
val rootSSOHolder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(rootEntry.savedStateHandle)
84+
dependency(rootSSOHolder)
85+
7586
// 👇 To make LoginTypeSelector available to all destinations as a non-navigation parameter if provided
7687
if (loginTypeSelector != null) dependency(loginTypeSelector)
7788

7889
// 👇 To tie NewConversationViewModel to nested NewConversationNavGraph, making it shared between all screens that belong to it
79-
dependency(NavGraphs.newConversation) {
90+
navGraph(NewConversationGraph) {
8091
val parentEntry = remember(navBackStackEntry) {
81-
navController.getBackStackEntry(NavGraphs.newConversation.route)
92+
navController.getBackStackEntry(NewConversationGraph.route)
8293
}
8394
hiltViewModel<NewConversationViewModel>(parentEntry)
8495
}
8596

8697
// 👇 To reuse LoginEmailViewModel from NewLoginPasswordScreen on NewLoginVerificationCodeScreen
87-
dependency(NewLoginVerificationCodeScreenDestination) {
98+
destination(NewLoginVerificationCodeScreenDestination) {
8899
val loginPasswordEntry = remember(navBackStackEntry) {
89100
navController.getBackStackEntry(NewLoginPasswordScreenDestination.route)
90101
}
91102
hiltViewModel<LoginEmailViewModel>(loginPasswordEntry)
92103
}
93104

94105
// 👇 To tie SSOUrlConfigHolder to nested LoginNavGraph, making it shared between all screens that belong to it
95-
dependency(NavGraphs.login) {
106+
navGraph(LoginGraph) {
96107
val parentEntry = remember(navBackStackEntry) {
97-
navController.getBackStackEntry(NavGraphs.login.route)
108+
navController.getBackStackEntry(LoginGraph.route)
98109
}
99-
SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
110+
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
111+
dependency(holder)
100112
}
101113

102114
// 👇 To tie SSOUrlConfigHolder to nested NewLoginNavGraph, making it shared between all screens that belong to it
103-
dependency(NavGraphs.newLogin) {
115+
navGraph(NewLoginGraph) {
104116
val parentEntry = remember(navBackStackEntry) {
105-
navController.getBackStackEntry(NavGraphs.newLogin.route)
117+
navController.getBackStackEntry(NewLoginGraph.route)
106118
}
107-
SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
119+
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(parentEntry.savedStateHandle)
120+
dependency(holder)
121+
}
122+
123+
// Some flows navigate directly to screen destinations instead of the nav graph route.
124+
// Provide the dependency at destination scope as a safe fallback.
125+
destination(LoginScreenDestination) {
126+
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(navBackStackEntry.savedStateHandle)
127+
dependency(holder)
128+
}
129+
destination(NewLoginScreenDestination) {
130+
val holder: SSOUrlConfigHolder = SSOUrlConfigHolderImpl(navBackStackEntry.savedStateHandle)
131+
dependency(holder)
108132
}
109133

110134
// 👇 To tie TeamMigrationViewModel to PersonalToTeamMigrationNavGraph, making it shared between all screens that belong to it
111-
dependency(NavGraphs.personalToTeamMigration) {
135+
navGraph(PersonalToTeamMigrationGraph) {
112136
val parentEntry = remember(navBackStackEntry) {
113-
navController.getBackStackEntry(NavGraphs.personalToTeamMigration.route)
137+
navController.getBackStackEntry(PersonalToTeamMigrationGraph.route)
114138
}
115139
hiltViewModel<TeamMigrationViewModel>(parentEntry)
116140
}
117141
},
118142
manualComposableCallsBuilder = {
119143
/**
120-
* In compose-destinations v1 it's not possible for code generation to use destination generated in another module,
121-
* that's why it's necessary to use "open" approach and manually call the composable function for the destination.
122-
* In v2 this will be possible, so that we will be able to use regular `ResultRecipient` instead of `OpenResultRecipient`
123-
* and provide it directly inside the destination's composable without the need to passing it manually.
124-
* https://github.com/raamcosta/compose-destinations/issues/508#issuecomment-1883166574
144+
* Keep manual composable calls for cross-module result wiring until we refactor
145+
* those destinations to rely on generated dependencies directly.
125146
*/
126147
composable(ConversationScreenDestination) {
127148
ConversationScreen(
128149
navigator = navigator,
129-
groupDetailsScreenResultRecipient = resultRecipient(),
130-
mediaGalleryScreenResultRecipient = resultRecipient(),
131-
imagePreviewScreenResultRecipient = resultRecipient(),
132-
drawingCanvasScreenResultRecipient = resultRecipient<DrawingCanvasScreenDestination, DrawingCanvasNavBackArgs>(),
133-
resultNavigator = resultBackNavigator(),
150+
groupDetailsScreenResultRecipient = resultRecipient(groupConversationDetailsNavBackArgsNavType),
151+
mediaGalleryScreenResultRecipient = resultRecipient(mediaGalleryNavBackArgsNavType),
152+
imagePreviewScreenResultRecipient = resultRecipient(imagesPreviewNavBackArgsNavType),
153+
drawingCanvasScreenResultRecipient = resultRecipient<DrawingCanvasScreenDestination, DrawingCanvasNavBackArgs>(
154+
drawingCanvasNavBackArgsNavType
155+
),
156+
resultNavigator = resultBackNavigator(groupConversationDetailsNavBackArgsNavType),
134157
)
135158
}
136159
}

app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.content.Context
2323
import androidx.navigation.NavBackStackEntry
2424
import androidx.navigation.NavController
2525
import androidx.navigation.NavDestination
26+
import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph
2627
import com.ramcosta.composedestinations.navigation.DestinationsNavOptionsBuilder
2728
import com.ramcosta.composedestinations.spec.DestinationSpec
2829
import com.ramcosta.composedestinations.spec.Direction
@@ -33,21 +34,20 @@ import com.ramcosta.composedestinations.utils.route
3334
import com.ramcosta.composedestinations.utils.toDestinationsNavigator
3435
import com.wire.android.appLogger
3536
import com.wire.android.util.CustomTabsHelper
36-
import com.wire.kalium.logger.obfuscateId
3737

3838
@SuppressLint("RestrictedApi")
3939
@Suppress("CyclomaticComplexMethod")
4040
internal fun NavController.navigateToItem(command: NavigationCommand) {
4141

42-
fun firstDestination() = currentBackStack.value.firstOrNull { it.route() is DestinationSpec<*> }
43-
fun lastDestination() = currentBackStack.value.lastOrNull { it.route() is DestinationSpec<*> }
42+
fun firstDestination() = currentBackStack.value.firstOrNull { it.route() is DestinationSpec }
43+
fun lastDestination() = currentBackStack.value.lastOrNull { it.route() is DestinationSpec }
4444
fun lastNestedGraph() = lastDestination()?.takeIf { it.navGraph() != navGraph }?.navGraph()
4545
fun firstDestinationWithRoute(route: String) =
4646
currentBackStack.value.firstOrNull { it.destination.route?.getBaseRoute() == route.getBaseRoute() }
4747

4848
fun lastDestinationFromOtherGraph(graph: NavGraphSpec) = currentBackStack.value.lastOrNull { it.navGraph() != graph }
4949

50-
appLogger.d("[$TAG] -> command: ${command.destination.route.obfuscateId()} backStackMode:${command.backStackMode}")
50+
appLogger.d("[$TAG] -> command: ${command.destination.route} backStackMode:${command.backStackMode}")
5151
toDestinationsNavigator().navigate(command.destination) {
5252
when (command.backStackMode) {
5353
BackStackMode.CLEAR_WHOLE, BackStackMode.CLEAR_TILL_START -> {
@@ -100,24 +100,21 @@ private fun DestinationsNavOptionsBuilder.popUpTo(
100100
getNavBackStackEntry: () -> NavBackStackEntry?,
101101
) {
102102
getNavBackStackEntry()?.let { entry ->
103-
appLogger.d("[$TAG] -> popUpTo:${entry.destination.route?.obfuscateId()} inclusive:${getInclusive(entry)}")
103+
appLogger.d("[$TAG] -> popUpTo:${entry.destination.route} inclusive:${getInclusive(entry)}")
104104
popUpTo(entry.route()) {
105105
this.inclusive = getInclusive(entry)
106106
}
107107
}
108108
}
109109

110-
internal fun NavDestination.toDestination(): DestinationSpec<*>? =
111-
this.route?.let { currentRoute -> WireMainNavGraph.findDestination(currentRoute) }
110+
internal fun NavDestination.toDestination(): DestinationSpec? =
111+
this.route?.let { currentRoute -> WireRootGraph.findDestination(currentRoute) }
112112

113-
fun String.getBaseRoute(): String =
114-
this.indexOfAny(listOf("?", "/")).let {
115-
if (it != -1) {
116-
this.substring(0, it)
117-
} else {
118-
this
119-
}
120-
}
113+
fun String.getBaseRoute(): String {
114+
var slashCount = 0
115+
val end = indexOfFirst { c -> (c == '/' && ++slashCount == 2) || c == '?' }
116+
return if (end >= 0) substring(0, end) else this
117+
}
121118

122119
fun Direction.handleNavigation(context: Context, handleOtherDirection: (Direction) -> Unit) = when (this) {
123120
is ExternalUriDirection -> CustomTabsHelper.launchUri(context, this.uri)
@@ -127,6 +124,6 @@ fun Direction.handleNavigation(context: Context, handleOtherDirection: (Directio
127124
}
128125

129126
@SuppressLint("RestrictedApi")
130-
fun NavController.startDestination() = currentBackStack.value.firstOrNull { it.route() is DestinationSpec<*> }
127+
fun NavController.startDestination() = currentBackStack.value.firstOrNull { it.route() is DestinationSpec }
131128

132129
private const val TAG = "NavigationUtils"

app/src/main/kotlin/com/wire/android/navigation/Navigator.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
2121
import androidx.compose.runtime.remember
2222
import androidx.navigation.NavHostController
2323
import com.ramcosta.composedestinations.utils.findDestination
24+
import com.ramcosta.composedestinations.generated.app.navgraphs.WireRootGraph
2425

2526
class Navigator(
2627
val finish: () -> Unit,
@@ -70,7 +71,7 @@ fun rememberNavigator(
7071
finish: () -> Unit,
7172
): Navigator {
7273
val navController = rememberTrackingAnimatedNavController {
73-
WireMainNavGraph.findDestination(it)?.let { it::class.simpleName } // there is a proguard rule for Routes
74+
WireRootGraph.findDestination(it)?.let { it::class.simpleName } // there is a proguard rule for Routes
7475
}
7576
return remember(finish, isAllowedToNavigate, navController) { Navigator(finish, navController, isAllowedToNavigate) }
7677
}

0 commit comments

Comments
 (0)