Skip to content

Commit

Permalink
feat: multi-modular navigation [WPB-10297] [WPB-10295] (#3363)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk authored Aug 23, 2024
1 parent 6e4ec6e commit 57a232d
Show file tree
Hide file tree
Showing 62 changed files with 678 additions and 332 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ dependencies {
// features
implementation(project(":features:sketch"))
implementation(project(":core:ui-common"))
implementation(project(":core:navigation"))

// kover
kover(project(":features:sketch"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,23 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine
import com.ramcosta.composedestinations.manualcomposablecalls.composable
import com.ramcosta.composedestinations.navigation.dependency
import com.ramcosta.composedestinations.scope.resultBackNavigator
import com.ramcosta.composedestinations.scope.resultRecipient
import com.ramcosta.composedestinations.spec.Route
import com.wire.android.feature.sketch.destinations.DrawingCanvasScreenDestination
import com.wire.android.feature.sketch.model.DrawingCanvasNavBackArgs
import com.wire.android.navigation.style.DefaultNestedNavGraphAnimations
import com.wire.android.navigation.style.DefaultRootNavGraphAnimations
import com.wire.android.ui.NavGraphs
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.home.conversations.ConversationScreen
import com.wire.android.ui.home.newconversation.NewConversationViewModel

@OptIn(ExperimentalMaterialNavigationApi::class, ExperimentalAnimationApi::class)
@Composable
fun NavigationGraph(
fun MainNavHost(
navigator: Navigator,
startDestination: Route,
) {
Expand All @@ -48,7 +55,7 @@ fun NavigationGraph(
)

DestinationsNavHost(
navGraph = NavGraphs.root,
navGraph = WireMainNavGraph,
engine = navHostEngine,
startRoute = startDestination,
navController = navigator.navController,
Expand All @@ -58,9 +65,30 @@ fun NavigationGraph(

// 👇 To tie NewConversationViewModel to nested NewConversationNavGraph, making it shared between all screens that belong to it
dependency(NavGraphs.newConversation) {
val parentEntry = remember(navBackStackEntry) { navController.getBackStackEntry(NavGraphs.newConversation.route) }
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(NavGraphs.newConversation.route)
}
hiltViewModel<NewConversationViewModel>(parentEntry)
}
},
manualComposableCallsBuilder = {
/**
* In compose-destinations v1 it's not possible for code generation to use destination generated in another module,
* that's why it's necessary to use "open" approach and manually call the composable function for the destination.
* In v2 this will be possible, so that we will be able to use regular `ResultRecipient` instead of `OpenResultRecipient`
* and provide it directly inside the destination's composable without the need to passing it manually.
* https://github.com/raamcosta/compose-destinations/issues/508#issuecomment-1883166574
*/
composable(ConversationScreenDestination) {
ConversationScreen(
navigator = navigator,
groupDetailsScreenResultRecipient = resultRecipient(),
mediaGalleryScreenResultRecipient = resultRecipient(),
imagePreviewScreenResultRecipient = resultRecipient(),
drawingCanvasScreenResultRecipient = resultRecipient<DrawingCanvasScreenDestination, DrawingCanvasNavBackArgs>(),
resultNavigator = resultBackNavigator(),
)
}
}
)
}
23 changes: 2 additions & 21 deletions app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,8 @@ import com.ramcosta.composedestinations.spec.NavGraphSpec
import com.ramcosta.composedestinations.utils.navGraph
import com.ramcosta.composedestinations.utils.route
import com.wire.android.appLogger
import com.wire.android.ui.NavGraphs
import com.wire.android.ui.destinations.Destination
import com.wire.android.util.CustomTabsHelper
import com.wire.kalium.logger.obfuscateId
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json

@SuppressLint("RestrictedApi")
internal fun NavController.navigateToItem(command: NavigationCommand) {
Expand Down Expand Up @@ -102,8 +98,8 @@ private fun NavOptionsBuilder.popUpTo(
}
}

internal fun NavDestination.toDestination(): Destination? =
this.route?.let { currentRoute -> NavGraphs.root.destinationsByRoute[currentRoute] }
internal fun NavDestination.toDestination(): DestinationSpec<*>? =
this.route?.let { currentRoute -> WireMainNavGraph.destinationsByRoute[currentRoute] }

fun String.getBaseRoute(): String =
this.indexOfAny(listOf("?", "/")).let {
Expand All @@ -118,19 +114,4 @@ fun Direction.handleNavigation(context: Context, handleOtherDirection: (Directio
else -> handleOtherDirection(this)
}

object ArgsSerializer {
@OptIn(ExperimentalSerializationApi::class)
private val instance: Json by lazy {
Json {
encodeDefaults = true
explicitNulls = false
// to enable the serialization of maps with complex keys
// e.g. Map<QualifiedIDEntity, PersistenceSession>
allowStructuredMapKeys = true
}
}

operator fun invoke() = instance
}

private const val TAG = "NavigationUtils"
3 changes: 1 addition & 2 deletions app/src/main/kotlin/com/wire/android/navigation/Navigator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavHostController
import com.wire.android.ui.NavGraphs

class Navigator(val finish: () -> Unit, val navController: NavHostController) {
private val isResumed: Boolean
Expand Down Expand Up @@ -57,7 +56,7 @@ class Navigator(val finish: () -> Unit, val navController: NavHostController) {
@Composable
fun rememberNavigator(finish: () -> Unit): Navigator {
val navController = rememberTrackingAnimatedNavController {
NavGraphs.root.destinationsByRoute[it]?.let { it::class.simpleName } // there is a proguard rule for Routes
WireMainNavGraph.destinationsByRoute[it]?.let { it::class.simpleName } // there is a proguard rule for Routes
}
return remember(finish, navController) { Navigator(finish, navController) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.util
package com.wire.android.navigation

import com.wire.android.util.ui.UIText
import com.wire.android.util.ui.toUIText
import com.ramcosta.composedestinations.spec.DestinationSpec
import com.ramcosta.composedestinations.spec.NavGraphSpec
import com.wire.android.feature.sketch.destinations.DrawingCanvasScreenDestination
import com.wire.android.ui.NavGraphs

/**
* Cache for the current conversation details.
* This is used to display the conversation name in the toolbar or can be used for other purposes.
*
* TODO: This is temporary, when we have navigation for sketch, we might do it with navigation arguments.
* TODO: Anyway, this might be useful, and we might keep it or discuss it.
*/
object CurrentConversationDetailsCache {

@Volatile
var conversationName: UIText = "".toUIText()
private set

@Synchronized
fun updateConversationName(newName: UIText) {
conversationName = newName
}
object WireMainNavGraph : NavGraphSpec {
override val route = "wire.main"
override val startRoute = NavGraphs.root.startRoute
val destinations: List<DestinationSpec<*>> = NavGraphs.root.destinations.plus(
DrawingCanvasScreenDestination
)
override val destinationsByRoute = destinations.associateBy { it.route }
override val nestedNavGraphs = NavGraphs.root.nestedNavGraphs
}
4 changes: 2 additions & 2 deletions app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.core.view.WindowCompat
import com.wire.android.appLogger
import com.wire.android.navigation.NavigationGraph
import com.wire.android.navigation.MainNavHost
import com.wire.android.navigation.rememberNavigator
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination
Expand Down Expand Up @@ -66,7 +66,7 @@ class AppLockActivity : AppCompatActivity() {
}
}

NavigationGraph(
MainNavHost(
navigator = navigator,
startDestination = startDestination
)
Expand Down
25 changes: 12 additions & 13 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import com.ramcosta.composedestinations.spec.Route
import com.wire.android.BuildConfig
import com.wire.android.R
Expand All @@ -61,8 +60,8 @@ import com.wire.android.feature.NavigationSwitchAccountActions
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.LocalNavigator
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.NavigationGraph
import com.wire.android.navigation.navigateToItem
import com.wire.android.navigation.MainNavHost
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.rememberNavigator
import com.wire.android.ui.calling.getIncomingCallIntent
import com.wire.android.ui.calling.ongoing.getOngoingCallIntent
Expand Down Expand Up @@ -240,15 +239,15 @@ class WireActivity : AppCompatActivity() {
}
)
CompositionLocalProvider(LocalNavigator provides navigator) {
NavigationGraph(
MainNavHost(
navigator = navigator,
startDestination = startDestination
)
}

// This setup needs to be done after the navigation graph is created, because building the graph takes some time,
// and if any NavigationCommand is executed before the graph is fully built, it will cause a NullPointerException.
SetUpNavigation(navigator.navController, onComplete)
SetUpNavigation(navigator, onComplete)
HandleScreenshotCensoring()
HandleDialogs(navigator::navigate)
}
Expand All @@ -259,11 +258,11 @@ class WireActivity : AppCompatActivity() {

@Composable
private fun SetUpNavigation(
navController: NavHostController,
navigator: Navigator,
onComplete: () -> Unit,
) {
val currentKeyboardController by rememberUpdatedState(LocalSoftwareKeyboardController.current)
val currentNavController by rememberUpdatedState(navController)
val currentNavigator by rememberUpdatedState(navigator)
LaunchedEffect(Unit) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
Expand All @@ -277,22 +276,22 @@ class WireActivity : AppCompatActivity() {
}
.collectLatest {
currentKeyboardController?.hide()
currentNavController.navigateToItem(it)
currentNavigator.navigate(it)
}
}
}
}

DisposableEffect(navController) {
DisposableEffect(navigator.navController) {
val updateScreenSettingsListener = NavController.OnDestinationChangedListener { _, _, _ ->
currentKeyboardController?.hide()
}
navController.addOnDestinationChangedListener(updateScreenSettingsListener)
navController.addOnDestinationChangedListener(currentScreenManager)
navigator.navController.addOnDestinationChangedListener(updateScreenSettingsListener)
navigator.navController.addOnDestinationChangedListener(currentScreenManager)

onDispose {
navController.removeOnDestinationChangedListener(updateScreenSettingsListener)
navController.removeOnDestinationChangedListener(currentScreenManager)
navigator.navController.removeOnDestinationChangedListener(updateScreenSettingsListener)
navigator.navController.removeOnDestinationChangedListener(currentScreenManager)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@
*/
package com.wire.android.ui.home.conversations

import android.os.Parcel
import android.os.Parcelable
import com.wire.android.ui.home.conversations.model.AssetBundle
import com.wire.kalium.logic.data.id.ConversationId
import kotlinx.serialization.Serializable
import com.wire.kalium.logic.data.id.QualifiedID
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

@Serializable
@Parcelize
@TypeParceler<ConversationId, QualifiedIdParceler>()
data class ConversationNavArgs(
val conversationId: ConversationId,
val searchedMessageId: String? = null,
val pendingBundles: ArrayList<AssetBundle>? = null,
val pendingTextBundle: String? = null
)
) : Parcelable

object QualifiedIdParceler : Parceler<QualifiedID> {
override fun create(parcel: Parcel) = parcel.readString().orEmpty().let {
QualifiedID(it.substringBeforeLast("@"), it.substringAfterLast("@"))
}
override fun QualifiedID.write(parcel: Parcel, flags: Int) = parcel.writeString(this.value + "@" + this.domain)
}
Loading

0 comments on commit 57a232d

Please sign in to comment.