Skip to content

Commit ea57a78

Browse files
committed
navigating to bottom sheet
1 parent 1e419e7 commit ea57a78

File tree

44 files changed

+1012
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1012
-178
lines changed

Diff for: app/src/main/kotlin/br/com/mob1st/bet/features/launch/presentation/MainActivity.kt

+24-17
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
7-
import androidx.compose.material3.Surface
7+
import androidx.compose.foundation.layout.fillMaxSize
88
import androidx.compose.runtime.Composable
99
import androidx.compose.runtime.CompositionLocalProvider
10+
import androidx.compose.ui.Modifier
1011
import androidx.navigation.compose.NavHost
1112
import androidx.navigation.compose.rememberNavController
1213
import br.com.mob1st.core.androidx.compose.LocalActivity
14+
import br.com.mob1st.core.androidx.navigation.ModalBottomSheetLayout
15+
import br.com.mob1st.core.androidx.navigation.rememberBottomSheetNavigator
1316
import br.com.mob1st.core.design.atoms.theme.TwoCentsTheme
1417
import br.com.mob1st.core.design.atoms.theme.UiContrast
1518
import br.com.mob1st.core.design.utils.LocalLocale
@@ -45,31 +48,35 @@ class MainActivity : ComponentActivity() {
4548
private fun App() {
4649
UiContrast {
4750
TwoCentsTheme {
48-
Surface {
49-
NavigationGraph()
50-
}
51+
NavigationGraph()
5152
}
5253
}
5354
}
5455

5556
@Composable
5657
internal fun NavigationGraph() {
57-
val navController = rememberNavController()
58+
val bottomSheetNavigator = rememberBottomSheetNavigator()
59+
val navController = rememberNavController(bottomSheetNavigator)
5860
val financesNavGraph = koinInject<FinancesNavGraph>()
5961
val budgetBuilderNavGraph = koinInject<BudgetBuilderNavGraph>()
60-
NavHost(
61-
navController = navController,
62-
startDestination = BudgetBuilderNavGraph.Root,
62+
ModalBottomSheetLayout(
63+
modifier = Modifier.fillMaxSize(),
64+
bottomSheetNavigator = bottomSheetNavigator,
6365
) {
64-
financesNavGraph.graph(
66+
NavHost(
6567
navController = navController,
66-
onClickClose = { },
67-
)
68-
budgetBuilderNavGraph.graph(
69-
navController,
70-
onComplete = {
71-
// go to home
72-
},
73-
)
68+
startDestination = BudgetBuilderNavGraph.Root,
69+
) {
70+
financesNavGraph.graph(
71+
navController = navController,
72+
onClickClose = { },
73+
)
74+
budgetBuilderNavGraph.graph(
75+
navController,
76+
onComplete = {
77+
// go to home
78+
},
79+
)
80+
}
7481
}
7582
}

Diff for: core/androidx/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ android {
1919
}
2020

2121
dependencies {
22+
implementation(libs.datastore.preferences)
2223
implementation(libs.kotlin.datetime)
24+
implementation(libs.kotlin.serialization.json)
2325
implementation(libs.timber)
24-
implementation(libs.datastore.preferences)
2526
implementation(libs.bundles.android)
2627
implementation(libs.bundles.compose)
2728
implementation(libs.bundles.lifecycle)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
@file:OptIn(ExperimentalMaterial3Api::class)
2+
3+
package br.com.mob1st.core.androidx.navigation
4+
5+
import androidx.activity.compose.BackHandler
6+
import androidx.compose.material3.ExperimentalMaterial3Api
7+
import androidx.compose.material3.SheetState
8+
import androidx.compose.material3.SheetValue
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.collectAsState
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.runtime.mutableStateOf
14+
import androidx.compose.runtime.produceState
15+
import androidx.compose.runtime.rememberCoroutineScope
16+
import androidx.compose.runtime.rememberUpdatedState
17+
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
18+
import androidx.compose.runtime.setValue
19+
import androidx.compose.runtime.snapshotFlow
20+
import androidx.compose.ui.util.fastForEach
21+
import androidx.navigation.FloatingWindow
22+
import androidx.navigation.NavBackStackEntry
23+
import androidx.navigation.NavDestination
24+
import androidx.navigation.NavOptions
25+
import androidx.navigation.Navigator
26+
import androidx.navigation.NavigatorState
27+
import androidx.navigation.compose.LocalOwnersProvider
28+
import kotlinx.coroutines.flow.MutableStateFlow
29+
import kotlinx.coroutines.flow.StateFlow
30+
import kotlinx.coroutines.flow.distinctUntilChanged
31+
import kotlinx.coroutines.flow.drop
32+
import kotlinx.coroutines.flow.transform
33+
import kotlinx.coroutines.launch
34+
import kotlin.coroutines.cancellation.CancellationException
35+
36+
@Navigator.Name("bottomSheet")
37+
public class BottomSheetNavigator constructor(
38+
internal val sheetState: SheetState,
39+
) : Navigator<BottomSheetNavigator.Destination>() {
40+
internal var sheetEnabled by mutableStateOf(false)
41+
private set
42+
43+
private var attached by mutableStateOf(false)
44+
45+
/**
46+
* Get the back stack from the [state]. In some cases, the [sheetInitializer] might be composed
47+
* before the Navigator is attached, so we specifically return an empty flow if we aren't
48+
* attached yet.
49+
*/
50+
private val backStack: StateFlow<List<NavBackStackEntry>>
51+
get() = if (attached) {
52+
state.backStack
53+
} else {
54+
MutableStateFlow(emptyList())
55+
}
56+
57+
/**
58+
* Get the transitionsInProgress from the [state]. In some cases, the [sheetInitializer] might be
59+
* composed before the Navigator is attached, so we specifically return an empty flow if we
60+
* aren't attached yet.
61+
*/
62+
private val transitionsInProgress: StateFlow<Set<NavBackStackEntry>>
63+
get() = if (attached) {
64+
state.transitionsInProgress
65+
} else {
66+
MutableStateFlow(emptySet())
67+
}
68+
69+
/**
70+
* Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState]
71+
*/
72+
public val navigatorSheetState: BottomSheetNavigatorSheetState =
73+
BottomSheetNavigatorSheetState(sheetState)
74+
75+
/**
76+
* A [Composable] function that hosts the current sheet content. This should be set as
77+
* sheetContent of your [ModalBottomSheetLayout].
78+
*/
79+
80+
internal var sheetContent: @Composable () -> Unit = {}
81+
internal var onDismissRequest: () -> Unit = {}
82+
83+
internal val sheetInitializer: @Composable () -> Unit = {
84+
val saveableStateHolder = rememberSaveableStateHolder()
85+
val transitionsInProgressEntries by transitionsInProgress.collectAsState()
86+
87+
// The latest back stack entry, retained until the sheet is completely hidden
88+
// While the back stack is updated immediately, we might still be hiding the sheet, so
89+
// we keep the entry around until the sheet is hidden
90+
val retainedEntry by produceState<NavBackStackEntry?>(
91+
initialValue = null,
92+
key1 = backStack,
93+
) {
94+
backStack
95+
.transform { backStackEntries ->
96+
// Always hide the sheet when the back stack is updated
97+
// Regardless of whether we're popping or pushing, we always want to hide
98+
// the sheet first before deciding whether to re-show it or keep it hidden
99+
try {
100+
sheetEnabled = false
101+
} catch (_: CancellationException) {
102+
// We catch but ignore possible cancellation exceptions as we don't want
103+
// them to bubble up and cancel the whole produceState coroutine
104+
} finally {
105+
emit(backStackEntries.lastOrNull())
106+
}
107+
}
108+
.collect {
109+
value = it
110+
}
111+
}
112+
113+
if (retainedEntry != null) {
114+
val currentOnSheetShown by rememberUpdatedState {
115+
transitionsInProgressEntries.forEach(state::markTransitionComplete)
116+
}
117+
LaunchedEffect(sheetState, retainedEntry) {
118+
snapshotFlow { sheetState.isVisible }
119+
// We are only interested in changes in the sheet's visibility
120+
.distinctUntilChanged()
121+
// distinctUntilChanged emits the initial value which we don't need
122+
.drop(1)
123+
.collect { visible ->
124+
if (visible) {
125+
currentOnSheetShown()
126+
}
127+
}
128+
}
129+
130+
LaunchedEffect(key1 = retainedEntry) {
131+
sheetEnabled = true
132+
133+
sheetContent = {
134+
retainedEntry!!.LocalOwnersProvider(saveableStateHolder) {
135+
val content =
136+
(retainedEntry!!.destination as Destination).content
137+
content(retainedEntry!!)
138+
}
139+
}
140+
onDismissRequest = {
141+
sheetEnabled = false
142+
143+
if (transitionsInProgressEntries.contains(retainedEntry)) {
144+
// Sheet dismissal can be started through popBackStack in which case we have a
145+
// transition that we'll want to complete
146+
state.markTransitionComplete(retainedEntry!!)
147+
} else {
148+
// If there is no transition in progress, the sheet has been dimissed by the
149+
// user (for example by tapping on the scrim or through an accessibility action)
150+
// In this case, we will immediately pop without a transition as the sheet has
151+
// already been hidden
152+
state.pop(popUpTo = retainedEntry!!, saveState = false)
153+
}
154+
}
155+
}
156+
157+
val scope = rememberCoroutineScope()
158+
BackHandler {
159+
scope
160+
.launch { sheetState.hide() }
161+
.invokeOnCompletion {
162+
if (!sheetState.isVisible) {
163+
onDismissRequest()
164+
}
165+
}
166+
}
167+
} else {
168+
LaunchedEffect(key1 = Unit) {
169+
sheetContent = {}
170+
onDismissRequest = {}
171+
}
172+
}
173+
}
174+
175+
override fun onAttach(state: NavigatorState) {
176+
super.onAttach(state)
177+
attached = true
178+
}
179+
180+
override fun createDestination(): Destination = Destination(
181+
navigator = this,
182+
content = {},
183+
)
184+
185+
override fun navigate(
186+
entries: List<NavBackStackEntry>,
187+
navOptions: NavOptions?,
188+
navigatorExtras: Extras?,
189+
) {
190+
entries.fastForEach { entry ->
191+
state.push(entry)
192+
}
193+
}
194+
195+
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
196+
state.pop(popUpTo, savedState)
197+
}
198+
199+
/**
200+
* [NavDestination] specific to [BottomSheetNavigator]
201+
*/
202+
@NavDestination.ClassType(Composable::class)
203+
public class Destination(
204+
navigator: BottomSheetNavigator,
205+
internal val content: @Composable (NavBackStackEntry) -> Unit,
206+
) : NavDestination(navigator), FloatingWindow
207+
}
208+
209+
public class BottomSheetNavigatorSheetState(private val sheetState: SheetState) {
210+
/**
211+
* @see SheetState.isVisible
212+
*/
213+
public val isVisible: Boolean
214+
get() = sheetState.isVisible
215+
216+
/**
217+
* @see SheetState.currentValue
218+
*/
219+
public val currentValue: SheetValue
220+
get() = sheetState.currentValue
221+
222+
/**
223+
* @see SheetState.targetValue
224+
*/
225+
public val targetValue: SheetValue
226+
get() = sheetState.targetValue
227+
}

0 commit comments

Comments
 (0)