diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/NimbusNavigator.kt b/compose/src/main/java/br/zup/com/nimbus/compose/NimbusNavigator.kt index 1afe83f..8db20ef 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/NimbusNavigator.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/NimbusNavigator.kt @@ -10,7 +10,7 @@ import java.util.UUID const val SHOW_VIEW = "showView" const val VIEW_URL = "viewUrl" -const val VIEW_JSON_DESCRIPTION = "json" +const val JSON = "json" const val SHOW_VIEW_DESTINATION_PARAM = "${SHOW_VIEW}?${VIEW_URL}" const val SHOW_VIEW_DESTINATION = "$SHOW_VIEW_DESTINATION_PARAM={${VIEW_URL}}" diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/ModalDialog.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/ModalDialog.kt index f6d50cb..c8e0d2a 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/ModalDialog.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/ModalDialog.kt @@ -2,10 +2,10 @@ package br.zup.com.nimbus.compose.internal import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -18,15 +18,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import br.zup.com.nimbus.compose.CoroutineDispatcherLib import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext /** * Figured out by trial and error @@ -40,11 +39,12 @@ private const val DIALOG_BUILD_TIME = 300L @Composable internal fun ModalTransitionDialog( onDismissRequest: () -> Unit, - onCanDismissRequest: () -> Boolean, + modifier: Modifier = Modifier + .fillMaxSize() + .background(Color.White), dismissOnBackPress: Boolean = true, - modifier: Modifier = Modifier.fillMaxSize(), modalTransitionDialogHelper: ModalTransitionDialogHelper = ModalTransitionDialogHelper(), - content: @Composable (ModalTransitionDialogHelper) -> Unit, + content: @Composable (ModalTransitionDialogHelper) -> Unit ) { val onCloseFlow: MutableStateFlow = remember { MutableStateFlow(false) } @@ -52,53 +52,45 @@ internal fun ModalTransitionDialog( val animateContentBackTrigger = remember { mutableStateOf(false) } LaunchedEffect(key1 = Unit) { - withContext(CoroutineDispatcherLib.backgroundPool) { - launch { - delay(DIALOG_BUILD_TIME) - animateContentBackTrigger.value = true - } - launch { - onCloseFlow.collectLatest { shouldClose -> - if(shouldClose) - startDismissWithExitAnimation(animateContentBackTrigger, onDismissRequest) - } + launch { + delay(DIALOG_BUILD_TIME) + animateContentBackTrigger.value = true + } + launch { + onCloseFlow.collectLatest { shouldClose -> + if(shouldClose) + startDismissWithExitAnimation(animateContentBackTrigger, onDismissRequest) } } } Dialog( onDismissRequest = { - coroutineScope.launch(CoroutineDispatcherLib.backgroundPool) { - startDismissWithExitAnimation(animateContentBackTrigger, - onDismissRequest, - onCanDismissRequest) + coroutineScope.launch { + startDismissWithExitAnimation(animateContentBackTrigger, onDismissRequest) } }, properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = dismissOnBackPress, dismissOnClickOutside = false) ) { - Box(modifier = modifier) { - AnimatedModalBottomSheetTransition( - visible = animateContentBackTrigger.value) { - modalTransitionDialogHelper.coroutineScope = coroutineScope - modalTransitionDialogHelper.onCloseFlow = onCloseFlow - content(modalTransitionDialogHelper) - } + AnimatedModalBottomSheetTransition( + modifier = modifier, + visible = animateContentBackTrigger.value) { + modalTransitionDialogHelper.onCloseFlow = onCloseFlow + modalTransitionDialogHelper.coroutineScope = coroutineScope + content(modalTransitionDialogHelper) } } } private suspend fun startDismissWithExitAnimation( animateContentBackTrigger: MutableState, - onDismissRequest: () -> Unit, - onCanDismissRequest: () -> Boolean = { true }, + onDismissRequest: () -> Unit ) { - if (onCanDismissRequest()) { - animateContentBackTrigger.value = false - delay(ANIMATION_TIME) - onDismissRequest() - } + animateContentBackTrigger.value = false + delay(ANIMATION_TIME) + onDismissRequest() } /** @@ -110,27 +102,28 @@ internal class ModalTransitionDialogHelper { var coroutineScope: CoroutineScope? = null var onCloseFlow: MutableStateFlow? = null fun triggerAnimatedClose() { - coroutineScope?.launch(CoroutineDispatcherLib.backgroundPool) { + coroutineScope?.launch { onCloseFlow?.tryEmit(true) } } } internal const val ANIMATION_TIME = 500L +internal const val DELAY_SHOW_CONTENT = ANIMATION_TIME + 100L -@OptIn(ExperimentalAnimationApi::class) @Composable internal fun AnimatedModalBottomSheetTransition( visible: Boolean, - content: @Composable AnimatedVisibilityScope.() -> Unit, + modifier: Modifier = Modifier + .fillMaxSize() + .background(Color.White), + content: @Composable AnimatedVisibilityScope.() -> Unit ) { var animateContentShowTrigger by remember { mutableStateOf(false) } if (visible) { LaunchedEffect(key1 = Unit) { - withContext(CoroutineDispatcherLib.backgroundPool) { - delay(ANIMATION_TIME) - animateContentShowTrigger = true - } + delay(DELAY_SHOW_CONTENT) + animateContentShowTrigger = true } } AnimatedVisibility( @@ -144,8 +137,11 @@ internal fun AnimatedModalBottomSheetTransition( targetOffsetY = { fullHeight -> fullHeight } ), content = { - if (animateContentShowTrigger) - content() + Box(modifier = modifier) { + if (animateContentShowTrigger) + content() + } + } ) } diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusModalView.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusModalView.kt index 13070ce..e7bf1bf 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusModalView.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusModalView.kt @@ -2,57 +2,28 @@ package br.zup.com.nimbus.compose.internal import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp +import com.zup.nimbus.core.network.ViewRequest @Composable internal fun NimbusModalView( - nimbusViewModel: NimbusViewModel, - modalParentHelper: ModalTransitionDialogHelper, + viewRequest: ViewRequest, + onDismiss: () -> Unit, + modifier: Modifier = Modifier + .fillMaxSize() + .background(Color.White) ) { - var nimbusViewModelModalState: NimbusViewModelModalState by remember { - mutableStateOf(NimbusViewModelModalState.HiddenModalState) - } - val modalTransitionDialogHelper = ModalTransitionDialogHelper() - val navHostHelper = NimbusNavHostHelper() - if (nimbusViewModelModalState is NimbusViewModelModalState.OnShowModalModalState) { - val showModalState = - (nimbusViewModelModalState as? NimbusViewModelModalState.OnShowModalModalState) ModalTransitionDialog( - modalTransitionDialogHelper = modalTransitionDialogHelper, - onDismissRequest = { - nimbusViewModel.setModalHiddenState() - }, - onCanDismissRequest = { - //Can dismiss the modal if we cannot pop more pages from navigation host - !navHostHelper.pop() - } + onDismissRequest = onDismiss, ) { NimbusNavHost( - modalParentHelper = modalTransitionDialogHelper, - nimbusNavHostHelper = navHostHelper, - viewRequest = showModalState?.viewRequest, - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .background(Color.White) - .padding(16.dp), + modalParentHelper = it, + viewRequest = viewRequest, + modifier = modifier ) } - } else if (nimbusViewModelModalState is NimbusViewModelModalState.OnHideModalState) { - modalParentHelper.triggerAnimatedClose() - } - CollectFlow(nimbusViewModel.nimbusViewModelModalState) { - if (it != nimbusViewModelModalState) - nimbusViewModelModalState = it - } } diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavHost.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavHost.kt index 8068f7b..6f2afb8 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavHost.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavHost.kt @@ -1,6 +1,10 @@ package br.zup.com.nimbus.compose.internal import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController @@ -13,8 +17,11 @@ import br.zup.com.nimbus.compose.Nimbus import br.zup.com.nimbus.compose.NimbusTheme.nimbus import br.zup.com.nimbus.compose.ProvideNavigatorState import br.zup.com.nimbus.compose.SHOW_VIEW_DESTINATION -import br.zup.com.nimbus.compose.VIEW_JSON_DESCRIPTION +import br.zup.com.nimbus.compose.JSON +import br.zup.com.nimbus.compose.NimbusTheme import br.zup.com.nimbus.compose.VIEW_URL +import br.zup.com.nimbus.compose.model.NimbusPageState +import br.zup.com.nimbus.compose.model.Page import com.zup.nimbus.core.network.ViewRequest import java.util.UUID @@ -41,6 +48,9 @@ internal fun NimbusNavHost( navigationState.handleNavigation(navController) } + val nimbusViewModelModalState: NimbusViewModelModalState by + nimbusViewModel.nimbusViewModelModalState.collectAsState() + NimbusDisposableEffect( onCreate = { initNavHost(nimbusViewModel, viewRequest, json) @@ -57,19 +67,29 @@ internal fun NimbusNavHost( route = SHOW_VIEW_DESTINATION, arguments = listOf(navArgument(VIEW_URL) { type = NavType.StringType - defaultValue = viewRequest?.url ?: VIEW_JSON_DESCRIPTION + defaultValue = viewRequest?.url ?: JSON }) ) { backStackEntry -> - nimbusViewModel.getPageBy( + NimbusBackHandler(onDismiss = + { + modalParentHelper.triggerAnimatedClose() + }) + + nimbusViewModelModalState.HandleModalState( + onDismiss = { + nimbusViewModel.setModalHiddenState() + }, + onHideModal = { + modalParentHelper.triggerAnimatedClose() + } + ) + + val page = nimbusViewModel.getPageBy( backStackEntry.getPageUrl() - )?.let { page -> - NimbusBackHandler() - page.Compose() - NimbusModalView( - nimbusViewModel = nimbusViewModel, - modalParentHelper = modalParentHelper - ) - } + ) + + val pageRemember by remember(page?.id) { mutableStateOf(page) } + pageRemember?.Compose() } } } @@ -89,7 +109,7 @@ private fun configureNavHostHelper( private fun initNavHost( nimbusViewModel: NimbusViewModel, viewRequest: ViewRequest?, - json: String + json: String, ) { if (viewRequest != null) @@ -102,12 +122,12 @@ private fun initNavHost( /** * This helper can be used to control some behaviour from outside the NimbusNavHost composable */ -internal class NimbusNavHostHelper { +class NimbusNavHostHelper { - var nimbusNavHostExecutor: NimbusNavHostExecutor? = null - fun isFirstScreen(): Boolean = nimbusNavHostExecutor?.isFirstScreen() ?: false + var nimbusNavHostExecutor: NimbusNavHostExecutor? = null + fun isFirstScreen(): Boolean = nimbusNavHostExecutor?.isFirstScreen() ?: false - fun pop(): Boolean = nimbusNavHostExecutor?.pop() ?: false + fun pop(): Boolean = nimbusNavHostExecutor?.pop() ?: false interface NimbusNavHostExecutor { fun isFirstScreen(): Boolean diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavigationtExtensions.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavigationtExtensions.kt index ff8de26..9ff8d02 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavigationtExtensions.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusNavigationtExtensions.kt @@ -1,9 +1,13 @@ package br.zup.com.nimbus.compose.internal +import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController +import br.zup.com.nimbus.compose.ErrorHandler +import br.zup.com.nimbus.compose.LoadingHandler import br.zup.com.nimbus.compose.SHOW_VIEW_DESTINATION_PARAM import br.zup.com.nimbus.compose.VIEW_URL +import br.zup.com.nimbus.compose.model.NimbusPageState import br.zup.com.nimbus.compose.model.Page internal fun NavBackStackEntry.getPageUrl() : String? { @@ -33,6 +37,40 @@ internal fun NimbusViewModelNavigationState.handleNavigation( } } +@Composable +internal fun NimbusViewModelModalState.HandleModalState( + onDismiss: () -> Unit, + onHideModal: () -> Unit, +) { + if (this is NimbusViewModelModalState.OnShowModalModalState) { + NimbusModalView( + viewRequest = this.viewRequest, + onDismiss = onDismiss + ) + } else if (this is NimbusViewModelModalState.OnHideModalState) { + onHideModal() + } +} + +@Composable +internal fun NimbusPageState.HandleNimbusPageState( + loadingView: LoadingHandler, + errorView: ErrorHandler, +) { + when (this) { + is NimbusPageState.PageStateOnLoading -> { + loadingView() + } + is NimbusPageState.PageStateOnError -> { + errorView(this.throwable, + this.retry) + } + is NimbusPageState.PageStateOnShowPage -> { + RenderedNode(flow = NodeFlow(this.node)) + } + } +} + internal fun Page.removePagesAfter(pages: MutableList) { val index = pages.indexOf(this) if (index < pages.lastIndex) diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusViewModel.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusViewModel.kt index dcff89a..8316e24 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusViewModel.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/NimbusViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import br.zup.com.nimbus.compose.CoroutineDispatcherLib -import br.zup.com.nimbus.compose.VIEW_JSON_DESCRIPTION +import br.zup.com.nimbus.compose.JSON import br.zup.com.nimbus.compose.model.Page import com.zup.nimbus.core.ServerDrivenNavigator import com.zup.nimbus.core.ServerDrivenView @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch internal sealed class NimbusViewModelModalState { - object HiddenModalState : NimbusViewModelModalState() + object RootState : NimbusViewModelModalState() object OnHideModalState : NimbusViewModelModalState() data class OnShowModalModalState(val viewRequest: ViewRequest) : NimbusViewModelModalState() } @@ -33,7 +33,7 @@ internal class NimbusViewModel( ) : ViewModel() { private var _nimbusViewModelModalState: MutableStateFlow = - MutableStateFlow(NimbusViewModelModalState.HiddenModalState) + MutableStateFlow(NimbusViewModelModalState.RootState) val nimbusViewModelModalState: StateFlow get() = _nimbusViewModelModalState @@ -81,7 +81,7 @@ internal class NimbusViewModel( } fun setModalHiddenState() { - setNimbusViewModelModalState(NimbusViewModelModalState.HiddenModalState) + setNimbusViewModelModalState(NimbusViewModelModalState.RootState) } fun pop(): Boolean { @@ -197,9 +197,9 @@ internal class NimbusViewModel( val view = ServerDrivenView( nimbus = nimbusConfig, getNavigator = { serverDrivenNavigator }, - description = VIEW_JSON_DESCRIPTION + description = JSON ) - val url = VIEW_JSON_DESCRIPTION + val url = JSON val page = Page( id = url, view = view diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/internal/RenderedNode.kt b/compose/src/main/java/br/zup/com/nimbus/compose/internal/RenderedNode.kt index f925ed7..dfa88dc 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/internal/RenderedNode.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/internal/RenderedNode.kt @@ -24,7 +24,7 @@ fun RenderedNode(flow: NodeFlow) { val state = flow.collectAsState() val (node, children) = state.value val ui = NimbusTheme.nimbus.uiLibraryManager - val handler = remember { ui.getComponent(node.component) } + val handler = remember(node.component) { ui.getComponent(node.component) } DisposableEffect(Unit) { onDispose { diff --git a/compose/src/main/java/br/zup/com/nimbus/compose/model/Page.kt b/compose/src/main/java/br/zup/com/nimbus/compose/model/Page.kt index c6e167a..9ddf5f1 100644 --- a/compose/src/main/java/br/zup/com/nimbus/compose/model/Page.kt +++ b/compose/src/main/java/br/zup/com/nimbus/compose/model/Page.kt @@ -1,12 +1,10 @@ package br.zup.com.nimbus.compose.model import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import br.zup.com.nimbus.compose.NimbusTheme -import br.zup.com.nimbus.compose.internal.CollectFlow -import br.zup.com.nimbus.compose.internal.NodeFlow -import br.zup.com.nimbus.compose.internal.RenderedNode +import br.zup.com.nimbus.compose.internal.HandleNimbusPageState import com.zup.nimbus.core.ServerDrivenView import com.zup.nimbus.core.tree.ServerDrivenNode import com.zup.nimbus.core.tree.dynamic.node.RootNode @@ -55,25 +53,8 @@ data class Page( @Composable fun Compose() { - val (localState, setMutableLocalState) = remember { - mutableStateOf(NimbusPageState.PageStateOnLoading) - } - - CollectFlow(state) { value -> - setMutableLocalState(value) - } - - when (localState) { - is NimbusPageState.PageStateOnLoading -> { - NimbusTheme.nimbus.loadingView() - } - is NimbusPageState.PageStateOnError -> { - NimbusTheme.nimbus.errorView(localState.throwable, localState.retry) - } - is NimbusPageState.PageStateOnShowPage -> { - RenderedNode(flow = NodeFlow(localState.node)) - } - } + val localState: NimbusPageState by state.collectAsState() + localState.HandleNimbusPageState(NimbusTheme.nimbus.loadingView, NimbusTheme.nimbus.errorView) } } diff --git a/compose/src/test/java/br/zup/com/nimbus/compose/internal/NimbusViewModelTest.kt b/compose/src/test/java/br/zup/com/nimbus/compose/internal/NimbusViewModelTest.kt index 0832cd9..926cfc2 100644 --- a/compose/src/test/java/br/zup/com/nimbus/compose/internal/NimbusViewModelTest.kt +++ b/compose/src/test/java/br/zup/com/nimbus/compose/internal/NimbusViewModelTest.kt @@ -356,7 +356,7 @@ class NimbusViewModelTest : BaseTest() { @DisplayName("Then should return receive a hidden state emission") @Test fun testGivenAPopWithOnlyOnePageShouldReturnFalse() = runTest { - val expectedModalState = NimbusViewModelModalState.HiddenModalState + val expectedModalState = NimbusViewModelModalState.RootState //When viewModel.setModalHiddenState()