Loading app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ fun LazyGridScope.stateItems() { @Composable private fun StatefulContentLoadingView() { val state = remember { mutableStateOf(ContentLoadingState.Loading) mutableStateOf<ContentLoadingState>(ContentLoadingState.Loading) } ContentLoadingView( Loading core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingViewPreview.kt +31 −21 Original line number Diff line number Diff line Loading @@ -30,27 +30,6 @@ internal fun ContentLoadingViewLoadingPreview() { } } @Composable @Preview(showBackground = true) internal fun ContentLoadingViewInteractivePreview() { PreviewWithThemes { val state = remember { mutableStateOf(ContentLoadingState.Loading) } DefaultContentLoadingView( state = state.value, modifier = Modifier .clickable { when (state.value) { ContentLoadingState.Loading -> state.value = ContentLoadingState.Content ContentLoadingState.Content -> state.value = ContentLoadingState.Loading } }, ) } } @Composable private fun DefaultContentLoadingView( state: ContentLoadingState, Loading @@ -67,3 +46,34 @@ private fun DefaultContentLoadingView( modifier = modifier.fillMaxSize(), ) } @Composable @Preview(showBackground = true) internal fun ContentLoadingViewInteractivePreview() { PreviewWithThemes { val state = remember { mutableStateOf(State(isLoading = true, content = "Hello world")) } ContentLoadingView( state = state.value, loading = { TextTitleMedium(text = "Loading...") }, content = { targetState -> TextTitleMedium(text = targetState.content) }, modifier = Modifier .clickable { val currentValue = state.value state.value = currentValue.copy(isLoading = currentValue.isLoading.not()) } .fillMaxSize(), ) } } private data class State( override val isLoading: Boolean, val content: String, ) : LoadingState core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingView.kt +41 −9 Original line number Diff line number Diff line Loading @@ -6,13 +6,30 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier /** * A container view that can animate between a loading view and a content view. * * @param STATE The type of the state being passed to this view. * * @param state The state relevant for displaying the content inside this view. * @param loading When `state.isLoading` is `true`, this composable function is displayed. * @param content When `state.isLoading` is `false`, this composable function is displayed with [state] being passed as * the argument. * * **IMPORTANT**: This is a delicate API whose usage should be carefully reviewed. It is using [AnimatedContent] and * inherits its caveats. * * The [loading] and [content] composable functions should only use the state being passed to them (if any). If you * disregard this advice, make sure to read the documentation of [AnimatedContent] to learn when the composable * functions are invoked and what that means for the external state a function fetches. */ @Composable fun ContentLoadingView( state: ContentLoadingState, fun <STATE : LoadingState> ContentLoadingView( state: STATE, loading: @Composable () -> Unit, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, content: @Composable () -> Unit, content: @Composable (STATE) -> Unit, ) { Box( modifier = modifier, Loading @@ -21,16 +38,31 @@ fun ContentLoadingView( AnimatedContent( targetState = state, label = "ContentLoadingView", contentKey = { targetState -> targetState.isLoading }, ) { targetState -> when (targetState) { ContentLoadingState.Loading -> loading() ContentLoadingState.Content -> content() if (targetState.isLoading) { loading() } else { content(targetState) } } } } enum class ContentLoadingState { Loading, Content, /** * Signals [ContentLoadingView] which of its composable function parameters to execute/display. */ interface LoadingState { val isLoading: Boolean } /** * Helper that can be use as `state` argument for [ContentLoadingView] when none of the composable function parameters * need access to any state. */ sealed class ContentLoadingState private constructor( override val isLoading: Boolean, ) : LoadingState { data object Loading : ContentLoadingState(isLoading = true) data object Content : ContentLoadingState(isLoading = false) } Loading
app-ui-catalog/src/main/java/app/k9mail/ui/catalog/ui/molecule/items/StateItems.kt +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ fun LazyGridScope.stateItems() { @Composable private fun StatefulContentLoadingView() { val state = remember { mutableStateOf(ContentLoadingState.Loading) mutableStateOf<ContentLoadingState>(ContentLoadingState.Loading) } ContentLoadingView( Loading
core/ui/compose/designsystem/src/debug/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingViewPreview.kt +31 −21 Original line number Diff line number Diff line Loading @@ -30,27 +30,6 @@ internal fun ContentLoadingViewLoadingPreview() { } } @Composable @Preview(showBackground = true) internal fun ContentLoadingViewInteractivePreview() { PreviewWithThemes { val state = remember { mutableStateOf(ContentLoadingState.Loading) } DefaultContentLoadingView( state = state.value, modifier = Modifier .clickable { when (state.value) { ContentLoadingState.Loading -> state.value = ContentLoadingState.Content ContentLoadingState.Content -> state.value = ContentLoadingState.Loading } }, ) } } @Composable private fun DefaultContentLoadingView( state: ContentLoadingState, Loading @@ -67,3 +46,34 @@ private fun DefaultContentLoadingView( modifier = modifier.fillMaxSize(), ) } @Composable @Preview(showBackground = true) internal fun ContentLoadingViewInteractivePreview() { PreviewWithThemes { val state = remember { mutableStateOf(State(isLoading = true, content = "Hello world")) } ContentLoadingView( state = state.value, loading = { TextTitleMedium(text = "Loading...") }, content = { targetState -> TextTitleMedium(text = targetState.content) }, modifier = Modifier .clickable { val currentValue = state.value state.value = currentValue.copy(isLoading = currentValue.isLoading.not()) } .fillMaxSize(), ) } } private data class State( override val isLoading: Boolean, val content: String, ) : LoadingState
core/ui/compose/designsystem/src/main/kotlin/app/k9mail/core/ui/compose/designsystem/molecule/ContentLoadingView.kt +41 −9 Original line number Diff line number Diff line Loading @@ -6,13 +6,30 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier /** * A container view that can animate between a loading view and a content view. * * @param STATE The type of the state being passed to this view. * * @param state The state relevant for displaying the content inside this view. * @param loading When `state.isLoading` is `true`, this composable function is displayed. * @param content When `state.isLoading` is `false`, this composable function is displayed with [state] being passed as * the argument. * * **IMPORTANT**: This is a delicate API whose usage should be carefully reviewed. It is using [AnimatedContent] and * inherits its caveats. * * The [loading] and [content] composable functions should only use the state being passed to them (if any). If you * disregard this advice, make sure to read the documentation of [AnimatedContent] to learn when the composable * functions are invoked and what that means for the external state a function fetches. */ @Composable fun ContentLoadingView( state: ContentLoadingState, fun <STATE : LoadingState> ContentLoadingView( state: STATE, loading: @Composable () -> Unit, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.Center, content: @Composable () -> Unit, content: @Composable (STATE) -> Unit, ) { Box( modifier = modifier, Loading @@ -21,16 +38,31 @@ fun ContentLoadingView( AnimatedContent( targetState = state, label = "ContentLoadingView", contentKey = { targetState -> targetState.isLoading }, ) { targetState -> when (targetState) { ContentLoadingState.Loading -> loading() ContentLoadingState.Content -> content() if (targetState.isLoading) { loading() } else { content(targetState) } } } } enum class ContentLoadingState { Loading, Content, /** * Signals [ContentLoadingView] which of its composable function parameters to execute/display. */ interface LoadingState { val isLoading: Boolean } /** * Helper that can be use as `state` argument for [ContentLoadingView] when none of the composable function parameters * need access to any state. */ sealed class ContentLoadingState private constructor( override val isLoading: Boolean, ) : LoadingState { data object Loading : ContentLoadingState(isLoading = true) data object Content : ContentLoadingState(isLoading = false) }