I'm trying to convert my old XML layout to #Composable classes in a test app I made, but I encountered a problem with my "loading" screen.
The app has a button to fetch quotes from a free API and, when clicked, a loading screen should appear on top of the page, effectively blocking possible further interactions with the button.
The loading screen was previously RelativeLayout with a ProgressBar inside.
Now with Compose I cannot manage to have this loading screen to be "on top" because the buttons still show above it and remain clickable.
The same "wrong" behaviour can also be reproduced with XML layouts when using MaterialButtons, whereas with AppCompatButtons the issue is solved.
Is there a way to make this work in compose?
p.s. here is my solution with Compose
#Composable
fun QuoteButton(text: String, onClick: () -> Unit) {
Button(
onClick,
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 5.dp)
) {
Text(text = text)
}
}
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
content()
} else {
content()
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
QuoteLoading(isLoading = loadingState) {
Column {
QuoteDisplay(textState)
QuoteButton(getString(R.string.button_fetch_quote)) {
viewModel.setEvent(Event.GetQuote)
}
QuoteButton(getString(R.string.button_save_quote)) {
viewModel.setEvent(Event.SaveQuote)
}
QuoteButton(getString(R.string.button_clear_quotes)) {
viewModel.setEvent(Event.ClearQuote)
}
}
}
}
}
}
}
}
private val DarkColorPalette = darkColors(
primary = Color(0xFFBB86FC),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
private val LightColorPalette = lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC5)
)
#Composable
fun ComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: #Composable () -> Unit) {
MaterialTheme(
colors = if (darkTheme) DarkColorPalette else LightColorPalette,
content = content
)
}
First of all put your progress bar in a dialogue that is not cancellable by any input except loading has been finished.
#Composable
fun QuoteLoading(
isLoading: MutableState<Boolean>,
content: #Composable () -> Unit
) = if (isLoading.value) {
Box(
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.pointerInput(Unit) {}
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnBackPress = false,
dismissOnClickOutside = false),
content = {
CircularProgressIndicator()
}
)
}
content()
} else {
content()
}
Related
I am working on nested column in jetpack compose. I have one list which is huge amount of data coming from server. I was checked in Layout Inspector and I see that whenever my item is added in list it recompose and increase counts. So my doubt is if I add 100 item in list one by one, so my Nested Column will be 100 times recompose ? If not can someone help me on this please?
ListViewComposableActivity.kt
class ListViewComposableActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppBarScaffold(
displayHomeAsUpEnabled = true,
titleId = R.string.activity
) {
ListViewItemStateful()
}
}
}
}
ListViewItemStateful
#Composable
fun ListViewItemStateful(
viewModel: ListViewModel = koinViewModel(),
) {
ItemViewListStateless(
uiState = viewModel.uiState,
isEnable = viewModel.isEnable,
scanDeviceList = viewModel.scanResultList,
)
}
ItemViewListStateless
#Composable
fun ItemViewListStateless(
uiState: State,
isEnable: Boolean,
scanDeviceList: SnapshotStateList<ScanResults>,
) {
when (uiState) {
INITIAL,
FIRST -> {
ListContent(isEnable, scanDeviceList)
}
}
}
ListContent
#Composable
fun ListContent(isEnable: Boolean, scanDeviceList: SnapshotStateList<ScanResults>) {
AnimatedVisibility(true) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
if (isEnable) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
DeviceList(
scanDeviceList,
modifier = Modifier.align(Alignment.Start),
)
}
}
}
}
}
DeviceList
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList.forEachIndexed { index, scanResults ->
Text(text = scanResults.device.name)
}
}
}
}
ListViewModel.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.abc.app.common.BaseViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ListViewModel : BaseViewModel() {
val scanResultList by lazy { mutableStateListOf<ScanResults>() }
var isEnable by mutableStateOf(false)
private set
var uiState by mutableStateOf<State>(State.INITIAL)
private set
init {
viewModelScope.launch {
(0..10).forEach {
delay(2000)
scanResultList.add(ScanResults(Device("item $it")))
}
}
isEnable = true
uiState = State.FIRST
}
}
data class ScanResults(val device: Device)
data class Device(val name: String)
enum class State {
INITIAL,
FIRST
}
I am adding few items in list to show in layout inspector
In above image you can see the DeviceList is recompose 10 times.
I checked in Jetpack Compose: Debugging recomposition around 6:40 min he tried to solve recompose issue and there skipped recomposition count is clear. So why it's showing count in my component tree in recomposition and skipped section? Many thanks
UPDATE
When I was changed to #Thracian answer it still recomposition skip
#Composable
fun ColumnScope.DeviceList(
scanDeviceList:()-> SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList().isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList().forEachIndexed { index, scanResults ->
Item(scanResults.device)
}
}
}
}
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
In your question when you add a new item to SnapshotStateList whole Column is composed because Column doesn't create a composition scope due to inline keyword. If you create a scope that scope is recomposed when the value it reads changes. You can refer this question and answer as well.
Jetpack Compose Smart Recomposition
Add an item where Text reads device
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
Random color is something i use for displaying recomposition visually
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
Your current setup
With Item Composable that creates scope.
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List", color = getRandomColor())
scanDeviceList.forEachIndexed { index, scanResults ->
// Text(
// modifier = Modifier.border(2.dp, getRandomColor()),
// text = scanResults.device.name
// )
Item(scanResults.device)
}
}
}
}
And when you have many items, especially they don't fit viewport you can use LazyColumn instead of Column with verticalScroll to limit recomposition amount to number of items that are visible on viewport or visible area of LazyColumn
Expected result:
Reusable ExtendedFloatingActionButton should expand or collapse within a reusable Scaffold when I scroll.
Current result:
ExtendedFloatingActionButton does not expand or collapse when I scroll the list in my Scaffold
I followed this tutorial, but it was not created with reusability in mind. The part I'm confused about is the listState varible within the section called "Joining Everything Together" because I'm not sure where within my code I need to declare it as I have done this a few times in different areas.
#Composable
fun MyReusableScaffold(scaffoldTitle: String, scaffoldFab: #Composable () -> Unit,
scaffoldContent: #Composable (contentPadding: PaddingValues) -> Unit) {
Scaffold(
topBar = { LargeTopAppBar( title = { Text(text = scaffoldTitle) } ) },
floatingActionButton = { scaffoldFab },
content = { contentPadding -> scaffoldContent(contentPadding = contentPadding) }
)
}
#Composable
fun MyFAB(listState: LazyListState) {
ExtendedFloatingActionButton(
text = { Text(text = "title") },
icon = { Icon(Icons.Filled.Add, "") },
expanded = listState.isScrollingUp()
)
}
#Composable
fun <T> MyLazyColumn(modifier: Modifier,
...
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState
) {
...
}
}
#Composable
fun MyHomeScreen() {
MyScaffold(
scaffoldTitle = "title",
scaffoldFab = MyExtendedFAB(listState = LazyListState?)
scaffoldContent = { MyHomeScreenContent(contentPadding = it) },
)
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
}
The expansion state of the ExtendedFloatingActionButton is managed by the expanded.
In your example the expanded state is derived on the listState of the LazyColumn. It means that LazyColumn and ExtendedFloatingActionButton have to use both the same listState.
For example you can do something like:
#Composable
fun MyHomeScreen() {
val listState = rememberLazyListState()
val expandedFab by remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0
}
}
MyReusableScaffold(
scaffoldTitle = "title",
scaffoldFab = { MyFAB(expandedFab) },
scaffoldContent = { MyHomeScreenContent(contentPadding = it, listState = listState) },
)
}
fun MyFAB(expandedFab : Boolean) {
ExtendedFloatingActionButton(
//...
expanded = expandedFab
)
}
#Composable
fun MyLazyColumn(modifier: Modifier = Modifier,
listState: LazyListState
) {
LazyColumn(
state = listState
) { /** ... */ }
}
#Composable
fun MyHomeScreenContent(
modifier: Modifier = Modifier,
listState: LazyListState,
contentPadding: PaddingValues = PaddingValues()
) {
MyLazyColumn( listState = listState )
}
I have a Compose component with a box and 2 components inside it, but they are never visible both at the same time. I want to adjust size of this box to the first component and stay unchanged when this component will be not visible.
Box(
modifier = modifier.then(Modifier.background(bgColor)),
contentAlignment = Alignment.Center
) {
if (componentVisible1) {
Button(
modifier = Modifier.height(48.dp).widthIn(144.dp),
onClick = onClicked,
enabled = enabled
) {
Text(
text = "text1",
)
}
}
if (component2Visible) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp).background(buttonColor, CircleShape).padding(2.dp),
strokeWidth = 2.dp
)
}
}
Now, width of the box reduces when the component1 is not visible.
You can use SubcomposeLayout to pass a Composable's dimension if another one depends on it without recomposition or in your case even if it's not even in composition.
#Composable
internal fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (DpSize) -> Unit
) {
val density = LocalDensity.current
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceable: Placeable = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}.first()
val dependentPlaceable: Placeable =
subcompose(SlotsEnum.Dependent) {
dependentContent(
DpSize(
density.run { mainPlaceable.width.toDp() },
density.run { mainPlaceable.height.toDp() }
)
)
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}.first()
layout(mainPlaceable.width, mainPlaceable.height) {
dependentPlaceable.placeRelative(0, 0)
}
}
}
/**
* Enum class for SubcomposeLayouts with main and dependent Composables
*/
enum class SlotsEnum { Main, Dependent }
And you can use it to set dimensions of Box based on measuring or subcomposing your Button.
#Composable
private fun DimensionSample() {
var componentVisible1 by remember { mutableStateOf(false) }
var component2Visible by remember { mutableStateOf(true) }
Column {
Button(onClick = {
componentVisible1 = !componentVisible1
component2Visible = !component2Visible
}) {
Text(text = "Toggle")
}
val button = #Composable {
Button(
modifier = Modifier
.height(48.dp)
.widthIn(144.dp),
onClick = {}
) {
Text(
text = "text1",
)
}
}
val circularProgressIndicator = #Composable {
CircularProgressIndicator(
modifier = Modifier
.size(24.dp)
.background(Color.Yellow, CircleShape)
.padding(2.dp),
strokeWidth = 2.dp
)
}
DimensionSubcomposeLayout(mainContent = {
button()
}) {
Box(
modifier = Modifier
.size(it)
.then(Modifier.background(Color.Red)),
contentAlignment = Alignment.Center
) {
if (componentVisible1) {
button()
}
if (component2Visible) {
circularProgressIndicator()
}
}
}
}
}
I'm having some issues with ModalBottomSheetLayout trying to make it fully expanded. I tried some answers from other posts (like Make ModalBottomSheetLayout always Expanded) with no result.
This is the Composable function I've created:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ExpandedSheetDialog(
sheetContent: #Composable (() -> Unit),
screenContent: #Composable (() -> Unit),
modalState: ModalBottomSheetState
) {
ModalBottomSheetLayout(
sheetState = modalState,
sheetShape = RoundedCornerShape(topEnd = 10.dp, topStart = 10.dp),
sheetContent = { sheetContent.invoke() },
content = { screenContent.invoke() }
)
}
And this is the Preview:
#OptIn(ExperimentalMaterialApi::class)
#Preview(showBackground = true)
#Composable
fun ExpandedSheetDialogPreview() {
val scope: CoroutineScope = rememberCoroutineScope()
val modalSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Expanded)
ExpandedSheetDialog(
sheetContent = {
Button(
onClick = {
scope.launch {
modalSheetState.animateTo(ModalBottomSheetValue.Hidden)
}
},
text = "Hide"
)
},
screenContent = {
Column(
modifier = Modifier.padding(16.dp).fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = {
scope.launch {
modalSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
},
text = "Show Extended"
)
}
},
modalState = modalSheetState
)
}
I tried in several emulators, all of them with the same result, the "BottomSheetDialog" appears but is not fully expanded. What am I doing wrong?
I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}