Skip to content

Commit

Permalink
refactor(kmp): migrate navigation suit scaffold to Compose Multiplatf…
Browse files Browse the repository at this point in the history
…orm.
  • Loading branch information
GerardPaligot committed Dec 12, 2024
1 parent d420e26 commit 370bb21
Show file tree
Hide file tree
Showing 12 changed files with 98 additions and 86 deletions.
2 changes: 1 addition & 1 deletion androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ dependencies {
implementation(libs.androidx.browser)

implementation(libs.jetbrains.navigation.compose)
implementation(libs.jetbrains.compose.material3.windowsizeclass)
implementation(libs.bundles.jetbrains.compose.adaptive)
implementation(libs.jetbrains.kotlinx.datetime)

implementation(compose.ui)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.paligot.confily.main
package com.paligot.confily.android

import android.content.res.Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.toSize
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.glance.appwidget.updateAll
Expand Down Expand Up @@ -45,6 +54,7 @@ class MainActivity : ComponentActivity() {
navController.handleDeepLink(intent)
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Expand All @@ -63,11 +73,16 @@ class MainActivity : ComponentActivity() {
}
navController = rememberNavController()
val scope = rememberCoroutineScope()
val config = LocalConfiguration.current
val windowSize = with(LocalDensity.current) { currentWindowSize().toSize().toDpSize() }
val adaptiveInfo = WindowSizeClass.calculateFromSize(windowSize)
val exportSubject = stringResource(Resource.string.text_export_subject)
val reportSubject = stringResource(Resource.string.text_report_subject)
val reportAppTarget = stringResource(Resource.string.text_report_app_target)
Main(
defaultEvent = BuildConfig.DEFAULT_EVENT,
isPortrait = config.isPortrait,
adaptiveInfo = adaptiveInfo,
launchUrl = { launchUrl(it) },
onContactExportClicked = { export ->
val uri: Uri = FileProvider.getUriForFile(
Expand Down Expand Up @@ -124,7 +139,10 @@ class MainActivity : ComponentActivity() {
onProfileCreated = {
scope.launch { NetworkingAppWidget().updateAll(context = this@MainActivity) }
},
navController = navController
navController = navController,
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
)
}
}
Expand Down
26 changes: 12 additions & 14 deletions features/main/main-di/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
plugins {
id("confily.android.library")
id("confily.quality")
id("confily.di")
}

android {
namespace = "com.paligot.confily.main.di"
}

dependencies {
implementation(projects.features.main.main)
implementation(projects.features.eventList.eventListDi)
implementation(projects.features.infos.infosDi)
implementation(projects.features.networking.networkingDi)
implementation(projects.features.partners.partnersDi)
implementation(projects.features.schedules.schedulesDi)
implementation(projects.features.speakers.speakersDi)
implementation(projects.shared.coreDi)

implementation(libs.koin.core)
implementation(libs.koin.android)
kotlin {
sourceSets.commonMain.dependencies {
implementation(projects.features.main.main)
implementation(projects.features.eventList.eventListDi)
implementation(projects.features.infos.infosDi)
implementation(projects.features.networking.networkingDi)
implementation(projects.features.partners.partnersDi)
implementation(projects.features.schedules.schedulesDi)
implementation(projects.features.speakers.speakersDi)
implementation(projects.shared.coreDi)
}
}
82 changes: 44 additions & 38 deletions features/main/main/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
id("confily.android.library")
id("confily.multiplatform.library")
id("confily.android.library.compose")
id("confily.quality")
}
Expand All @@ -8,41 +10,45 @@ android {
namespace = "com.paligot.confily.main"
}

dependencies {
api(projects.features.schedules.schedulesUi)
api(projects.features.speakers.speakersUi)
api(projects.style.theme)
implementation(projects.features.schedules.schedulesRoutes)
implementation(projects.features.eventList.eventListRoutes)
implementation(projects.features.schedules.schedulesPresentation)
implementation(projects.features.speakers.speakersPresentation)
implementation(projects.features.partners.partnersPresentation)
implementation(projects.features.networking.networkingPresentation)
implementation(projects.features.infos.infosPresentation)
implementation(projects.features.infos.infosPanes)
implementation(projects.features.eventList.eventListPresentation)
implementation(projects.features.navigation)
implementation(projects.style.components.adaptive)
implementation(projects.shared.core)
implementation(projects.shared.coreNavigation)
implementation(projects.shared.uiModels)
implementation(projects.shared.resources)

implementation(libs.koin.androidx.compose)

implementation(libs.bundles.jetbrains.compose.adaptive)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
implementation(libs.androidx.compose.runtime.livedata)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.preview)
debugImplementation(compose.uiTooling)

implementation(libs.jetbrains.kotlinx.collections)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation.compose)

implementation(platform(libs.google.firebase.bom))
implementation("com.google.firebase:firebase-crashlytics-ktx")
kotlin {
androidTarget()

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
useCommonJs()
browser()
}

sourceSets.commonMain.dependencies {
api(projects.features.schedules.schedulesUi)
api(projects.features.speakers.speakersUi)
api(projects.style.theme)
implementation(projects.features.schedules.schedulesRoutes)
implementation(projects.features.eventList.eventListRoutes)
implementation(projects.features.schedules.schedulesPresentation)
implementation(projects.features.speakers.speakersPresentation)
implementation(projects.features.partners.partnersPresentation)
implementation(projects.features.networking.networkingPresentation)
implementation(projects.features.infos.infosPresentation)
implementation(projects.features.eventList.eventListPresentation)
implementation(projects.features.navigation)
implementation(projects.style.components.adaptive)
implementation(projects.shared.core)
implementation(projects.shared.coreNavigation)
implementation(projects.shared.uiModels)
implementation(projects.shared.resources)

implementation(compose.material3)
// Can't use compose.material3AdaptiveNavigationSuite because of a bug in the plugin
// Check https://mvnrepository.com/artifact/org.jetbrains.compose.material3/material3-adaptive-navigation-suite
implementation("org.jetbrains.compose.material3:material3-adaptive-navigation-suite:1.7.1")
implementation(compose.components.resources)

implementation(libs.jetbrains.kotlinx.collections)
implementation(libs.jetbrains.lifecycle.viewmodel.compose)
implementation(libs.jetbrains.navigation.compose)
implementation(libs.bundles.jetbrains.compose.adaptive)

implementation(libs.koin.compose.viewmodel)
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package com.paligot.confily.main

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import com.paligot.confily.models.ui.ExportNetworkingUi
import com.paligot.confily.style.theme.ConfilyTheme
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Suppress("LongMethod", "UnusedPrivateMember")
@ExperimentalCoroutinesApi
@FlowPreview
@Composable
fun Main(
defaultEvent: String?,
isPortrait: Boolean,
adaptiveInfo: WindowSizeClass,
launchUrl: (String) -> Unit,
onContactExportClicked: (ExportNetworkingUi) -> Unit,
onReportByPhoneClicked: (String) -> Unit,
Expand All @@ -25,6 +31,7 @@ fun Main(
onScheduleStarted: () -> Unit,
onProfileCreated: () -> Unit,
navController: NavHostController,
modifier: Modifier = Modifier,
viewModel: MainViewModel = koinViewModel(parameters = { parametersOf(defaultEvent) })
) {
ConfilyTheme {
Expand All @@ -33,6 +40,8 @@ fun Main(
is MainUiState.Success -> {
MainNavigation(
startDestination = (uiState.value as MainUiState.Success).startDestination,
isPortrait = isPortrait,
adaptiveInfo = adaptiveInfo,
launchUrl = launchUrl,
onContactExportClicked = onContactExportClicked,
onReportByPhoneClicked = onReportByPhoneClicked,
Expand All @@ -41,6 +50,7 @@ fun Main(
onItineraryClicked = onItineraryClicked,
onScheduleStarted = onScheduleStarted,
onProfileCreated = onProfileCreated,
modifier = modifier,
navController = navController
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
package com.paligot.confily.main

import android.content.res.Configuration
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.material3.Icon
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowSize
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.toSize
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
Expand All @@ -36,14 +28,15 @@ import com.paligot.confily.speakers.presentation.speakerGraph
import com.paligot.confily.style.components.adaptive.isCompat
import com.paligot.confily.style.theme.appbars.iconColor
import org.jetbrains.compose.resources.stringResource
import org.koin.androidx.compose.koinViewModel
import org.koin.compose.viewmodel.koinViewModel
import kotlin.reflect.KClass

@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun MainNavigation(
startDestination: KClass<*>,
isPortrait: Boolean,
adaptiveInfo: WindowSizeClass,
launchUrl: (String) -> Unit,
onContactExportClicked: (ExportNetworkingUi) -> Unit,
onReportByPhoneClicked: (String) -> Unit,
Expand All @@ -56,12 +49,9 @@ fun MainNavigation(
navController: NavHostController = rememberNavController(),
viewModel: MainNavigationViewModel = koinViewModel()
) {
val config = LocalConfiguration.current
val uiState = viewModel.uiState.collectAsState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val windowSize = with(LocalDensity.current) { currentWindowSize().toSize().toDpSize() }
val adaptiveInfo = WindowSizeClass.calculateFromSize(windowSize)
val heightCompact = adaptiveInfo.heightSizeClass.isCompat
val layoutType = if (heightCompact) {
NavigationSuiteType.NavigationRail
Expand All @@ -70,9 +60,7 @@ fun MainNavigation(
}
NavigationSuiteScaffold(
layoutType = layoutType,
modifier = modifier.semantics {
testTagsAsResourceId = true
},
modifier = modifier,
navigationSuiteItems = {
when (uiState.value) {
is MainNavigationUiState.Success -> {
Expand All @@ -90,7 +78,7 @@ fun MainNavigation(
},
onClick = {
navController.navigate(action.route) {
popUpTo(navController.graph.findStartDestination().id) {
popUpTo(navController.graph.findStartDestination()) {
saveState = true
}
launchSingleTop = true
Expand All @@ -113,7 +101,7 @@ fun MainNavigation(
builder = {
eventGraph(navController)
scheduleGraph(
isPortrait = config.isPortrait,
isPortrait = isPortrait,
adaptiveInfo = adaptiveInfo,
navController = navController,
enterTransition = enterSlideInHorizontal(),
Expand All @@ -125,7 +113,7 @@ fun MainNavigation(
onScheduleStarted = onScheduleStarted
)
speakerGraph(
isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE,
isLandscape = isPortrait.not(),
adaptiveInfo = adaptiveInfo,
navController = navController,
enterTransition = enterSlideInHorizontal(),
Expand All @@ -144,7 +132,7 @@ fun MainNavigation(
}
)
partnerGraph(
isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE,
isLandscape = isPortrait.not(),
adaptiveInfo = adaptiveInfo,
navController = navController,
enterTransition = enterSlideInHorizontal(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package com.paligot.confily.main

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.paligot.confily.core.events.EventRepository
import com.paligot.confily.core.events.entities.FeatureFlags
import com.paligot.confily.core.navigation.NavigationAction
Expand Down Expand Up @@ -32,11 +30,8 @@ class MainNavigationViewModel(
private val userRepository: UserRepository
) : ViewModel() {
val uiState: StateFlow<MainNavigationUiState> = eventRepository.featureFlags()
.map { MainNavigationUiState.Success(navActions(it)) }
.catch {
Firebase.crashlytics.recordException(it)
MainNavigationUiState.Failure(it)
}
.map { MainNavigationUiState.Success(navActions(it)) as MainNavigationUiState }
.catch { emit(MainNavigationUiState.Failure(it)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
Expand Down
Loading

0 comments on commit 370bb21

Please sign in to comment.