As I've begun creating all new UI-components using Compose instead of xml, I am unsure how to handle dialogs that are created in Compose.
Using dialogs outside of Compose only requires a context for dialogs:
val builder = AlertDialog.Builder(context as Activity, R.style.PermissionInfoDialog)
[...]
builder.show()
This lets you trigger the dialog from "anywhere" as long as you have a context (yes, I realise how this can be misused), but as far as I can understand, for compose, I need a "composable view" to add a custom compose dialog? It's manageable when you have a specific fragment that can show a specific dialog, but if you want more generic one that can be re-used, such as:
handle generic error messages and show error to user (from "all views/fragments/activities")
request permissions in multiple parts of the UI
etc.
then this becomes more cluttered.
In my specific scenario, I have a number of different fragments that need the ability to request permissions. The ability to request the permission (which requires that we explain to the user why we need it) is prefaced with a custom dialog. This abstract fragment holds this logic, but the individual ComposeView or xml (since this app is a mix of old and new stuff) that is needed (?) to add my Compose dialog to, is not known to the abstract fragment. Sure, I could have a function that let the abstract fragment request "somewhere to show the dialog" and let each fragment provide a ComposeView, but this - to me - feels unnatural and forced.
Is there something I'm missing and/or some "common practice" regarding how to solve this? I guess I'm looking for something similar (logic-wise) to the old solution of simply having a reference to a context (or similar) to show the dialog.
Please add dialog in compose using AlertDialog.
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title")
},
text = {
Text(
"This area typically contains the supportive text " +
"which presents the details regarding the Dialog's purpose."
)
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { openDialog.value = false }
) {
Text("Dismiss")
}
}
}
)
}
Related
My app has a lot of colour coding in it, and I can't figure out how to cleanly change the colour of a Snackbar action in a scaffold based on the action that shows the snackbar.
I have a composable, and inside there is a lazy list with an item that can be clicked. When you click it, that row has a specific colour associated with it.
Now, the click listener isn't a composable itself, I can't create a snackbar in it. I have to use the scaffolds snackbarHost
onClick {
scope.launch {
val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
message = "message",
actionLabel = "action"
)
when (snackbarResult) {
SnackbarResult.ActionPerformed -> do something
SnackbarResult.Dismissed -> do something
}
}
]
From here, I can't choose or set a colour.
In the scaffold, where I can make the snackbar, I can't get a colour back to it as it only has the snackbarData
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { snackbarData ->
Snackbar(
actionColor = I want a color here!
snackbarData = snackbarData
)
}
},
Using this set up, if 2 snackbars are triggered back to back, I can action both. You action the second, and then you can action the first. I like this functionality.
The only way I can get a colour in this way, is to set a global variable in the onClick but that is incredibly gross. Changing a local variable in the same file does not work.
Alternatively, I can trigger a recomposition via the ViewModel which will trigger showing a snackbar with the correct colour, but then I lose the functionality of triggering 2 items and still being capable of actioning both snackbars one after the other.
I even tried putting the colour into the action label so i could split it out, but I can't instantiate the existing SnackbarDataImpl to then recreate it without it because it's private. I would have to copy the entire scaffold implementation to simply add a colour this way as the host has to come from the scaffold
Is there some better way I can do this? It's way too complicated for something as simple as wanting to set a colour based on the action?
Maybe you could use something like this:
var color by rememberSaveable { mutableStateOf(Color.Red) }
Snackbar(
actionColor = color,
...
)
Probably your best bet is to have your ViewModel store the snackbar color:
class MyViewModel : ViewModel() {
var snackBarColor by mutableStateOf(Color.Blue)
(...)
}
And in the Scaffold:
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { snackbarData ->
Snackbar(
actionColor = myViewModel.snackBarColor,
snackbarData = snackbarData
)
}
},
So now when calling the Snackbar, also set the snackBarColor in the ViewModel:
onClick {
myViewModel.snackBarColor = Color.Red
scope.launch {
val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
message = "message",
actionLabel = "action"
)
when (snackbarResult) {
SnackbarResult.ActionPerformed -> do something
SnackbarResult.Dismissed -> do something
}
}
}
As long you have the same instance of the MyViewModel both in the Scaffold and where you are calling the onClick, the Scaffold will observe the snackBarColor from MyViewModel and update automatically when it changes.
The use case is that you have 10s or 100s of items inside of a dropdown menu, the dropdown options have some ordering - as with number values or alphabetical listing of words - and selections are made in succession.
When the user reopens the menu, you'd like for it to open in the same region as their last selection, so that for instance you don't jump from "car" to "apple" but rather from "car" to "cat". Or if they just opted to view order number 358, they can quickly view order number 359.
Using views, you could create a Spinner and put all of your items in an ArrayAdapter and then call spinner.setSelection() to scroll directly to the index you want.
DropdownMenu doesn't have anything like HorizontalPager's scrollToPage(). So what solutions might exist to achieve this?
So far, I've tried adding verticalScroll() to the DropdownMenu's modifier and trying to do arithmetic with the scrollState. But it crashes at runtime with an error saying the component has infinite height, the same error you get if you try to nest scrollable components like a LazyColumn inside of a Column with verticalScroll.
It's a known issue.
DropdownMenu has its own vertical scroll modifier inside, and there is no API to work with it.
Until this problem is fixed by providing a suitable API, the only workaround I can think of is to create your own view - you can take the source code of DropdownMenu as reference.
I'll post a more detailed answer here because I don't want to mislead anyone with my comment above.
If you're in Android Studio, click the three dots on the mouse-hover quick documentation box and select "edit source" to open the source for DropdownMenu in AndroidMenu.android.kt. Then observe that it uses a composable called DropdownMenuItemContent. Edit source again and you're in Menu.kt.
You'll see this:
#Composable
internal fun DropdownMenuContent(
...
...
...
{
Column(
modifier = modifier
.padding(vertical = DropdownMenuVerticalPadding)
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),//<-We want this
content = content
)
}
So in your custom composable just replace that rememberScrollState() with your favorite variable name for a ScrollState.
And then chain that reference all the way back up to your original view.
Getting Access to the ScrollState
#Composable
fun MyCustomDropdownMenu(
expanded:Boolean,
scrollStateProvidedByTopParent:ScrollState,
...
...
)
{...}
#Composable
fun MyCustomDropdownMenuContent(
scrollStateProvidedByTopParent:ScrollState,
...
...
)
{...}
//And now for your actual content
#Composable
fun TopParent(){
val scrollStateProvidedByTopParent=rememberScrollState()
val spinnerExpanded by remember{mutableStateOf(false)}
...
...
Box{
Row(modifier=Modifier.clickable(onClick={spinnerExpanded=!spinnerExpanded}))//<-More about this line in the sequel
{
Text("Options")
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
MyCustomDropdownMenu(
expanded = spinnerExpanded,
scrollStateProvidedByTopParent=scrollStateProvidedByTopParent,
onDismissRequest = { spinnerExpanded = false })
{//your spinner content}
}
}
}
The above only specifies how to access the ScrollState of the DropdownMenu. But once you have the ScrollState, you'll have to do some arithmetic to get the scroll position right when it opens. Here's one way that seems alright.
Calculating the scroll distance
Even after setting the contents of the menu items explicitly, the distance was never quite right if I relied on those values. So I used an onTextLayout callback inside the Text of my menu items in order to get the true Text height at the time of rendering. Then I use that value for the arithmetic. It looks like this:
#Composable
fun TopParent(){
val scrollStateProvidedByTopParent=rememberScrollState()
val spinnerExpanded by remember{mutableStateOf(false)}
val chosenText:String by remember{mutableStateOf(myListOfSpinnerOptions[0])
val height by remember{mutableStateOf(0)}
val heightHasBeenChecked by remember{mutableStateOf(false)}
val coroutineScope=rememberCoroutineScope()
...
...
Box{
Row(modifier=Modifier.clickable(onClick={spinnerExpanded=!spinnerExpanded
coroutineScope.launch{scrollStateProvidedByTopParent.scrollTo(height*myListOfSpinnerOptions.indexOf[chosenText])}}))//<-This gets some arithmetic for scrolling distance
{
Text("Options")
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
MyCustomDropdownMenu(
expanded = spinnerExpanded,
scrollStateProvidedByTopParent=scrollStateProvidedByTopParent,
onDismissRequest = { spinnerExpanded = false }) {
myListOfSpinnerOptions.forEach{option->
DropdownMenuItem(onClick={
chosenText=option
spinnerExpanded=false
}){
Text(option,onTextLayout={layoutResult->
if (!heightHasBeenChecked){
height=layoutResults.size.height
heightHasBeenChecked=true
}
}
)
}
}
}
}
}
So it seems like the recommended thing in Jetpack Compose is to hoist state out of your composables, to make them stateless, reusable, and testable, and allow using them in previews easily.
So instead of having something like
#Composable
fun MyInputField() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
You'd hoist the state, like this
#Composable
fun MyInputField(text: String, onTextChange: (String) -> Unit) {
TextField(value = text, onValueChange = onTextChange)
}
This is fine, however what of some more complex uses?
Let's pretend I have a screen represented by a composable, with multiple interactions between the View and the ViewModel. This screen is split into multiple inner composable (think for instance one for a header, one for the body, which in turn is split into several smaller composables)
You can't create a ViewModel (with viewModel() at least, you can instantiate one manually) inside a composable and use this composable in a Preview (previews don't support creating viewmodel like this)
Using a ViewModel inside the inner composables would make them stateful, wouldn't it ?
So the "cleanest" solution I see, would be to instantiate my viewmodel only at the highest composable level, and then pass to the children composables only vals representing the state, and callbacks to the ViewModel functions.
But that's wild, I'm not passing down all my ViewModel state and functions through individual parameters to all composables needing them.
Grouping them in a data class for example could be a solution
data class UiState(
val textInput: String,
val numberPicked: Int,
……
and maybe create another one for callbacks ?
But that's still creating a whole new class just to mimic what the viewmodel already has.
I don't actually see what the best way of doing this could be, and I find nothing about that anywhere
A good way to manage complex states is to encapsulate required complex behavior into a class and use remember function while having stateless widgets as most as you can and change any properties of state whenever it's required.
SearchTextField is a component that uses only state hoisting, SearchBar has back arrow and SearchTextField and also itself is a stateless composable. Communication between these two and parent of Searchbar is handled via callback functions only which makes both SearchTextField re-suable and easy to preview with a default state in preview. HomeScreen contains this state and where you manage changes.
Full implementation is posted here.
#Composable
fun <R, S> rememberSearchState(
query: TextFieldValue = TextFieldValue(""),
focused: Boolean = false,
searching: Boolean = false,
suggestions: List<S> = emptyList(),
searchResults: List<R> = emptyList()
): SearchState<R, S> {
return remember {
SearchState(
query = query,
focused = focused,
searching = searching,
suggestions = suggestions,
searchResults = searchResults
)
}
}
remember function to keep state for this only to be evaluated during the composition.
class SearchState<R, S>(
query: TextFieldValue,
focused: Boolean,
searching: Boolean,
suggestions: List<S>,
searchResults: List<R>
) {
var query by mutableStateOf(query)
var focused by mutableStateOf(focused)
var searching by mutableStateOf(searching)
var suggestions by mutableStateOf(suggestions)
var searchResults by mutableStateOf(searchResults)
val searchDisplay: SearchDisplay
get() = when {
!focused && query.text.isEmpty() -> SearchDisplay.InitialResults
focused && query.text.isEmpty() -> SearchDisplay.Suggestions
searchResults.isEmpty() -> SearchDisplay.NoResults
else -> SearchDisplay.Results
}
}
And change state in any part of UI by passing state to other composable or by ViewModel as
fun HomeScreen(
modifier: Modifier = Modifier,
viewModel: HomeViewModel,
navigateToTutorial: (String) -> Unit,
state: SearchState<TutorialSectionModel, SuggestionModel> = rememberSearchState()
) {
Column(
modifier = modifier.fillMaxSize()
) {
SearchBar(
query = state.query,
onQueryChange = { state.query = it },
onSearchFocusChange = { state.focused = it },
onClearQuery = { state.query = TextFieldValue("") },
onBack = { state.query = TextFieldValue("") },
searching = state.searching,
focused = state.focused,
modifier = modifier
)
LaunchedEffect(state.query.text) {
state.searching = true
delay(100)
state.searchResults = viewModel.getTutorials(state.query.text)
state.searching = false
}
when (state.searchDisplay) {
SearchDisplay.InitialResults -> {
}
SearchDisplay.NoResults -> {
}
SearchDisplay.Suggestions -> {
}
SearchDisplay.Results -> {
}
}
}
}
Jetmagic is an open source framework that deals exactly with this issue while also solving other major issues that Google neglected when developing Compose. Concerning your request, you don't pass in viewmodels at all as parameters. Jetmagic follows the "hoisted state" pattern, but it manages the viewmodels for you and keeps them associated with your composables. It treats composables as resources in a way that is similar to how the older view system treats xml layouts. Instead of directly calling a composable function, you ask Jetmagic's framework to provide you with an "instance" of the composable that best matches the device's configuration. Keep in mind, under the older xml-based system, you could effectively have multiple layouts for the same screen (such as one for portrait mode and another for landscape mode). Jetmagic picks the correct one for you. When it does this, it provides you with an object that it uses to manage the state of the composable and it's related viewmodel.
You can easily access the viewmodel anywhere within your screen's hierarchy without the need to pass the viewmodel down the hierarchy as parameters. This is done in part using CompositionLocalProvider.
Jetmagic is designed to handle the top-level composables that make up your screen. Within your composable hierarchy, you still call composables as you normally do but using state hoisting where it makes sense.
The best thing is to download Jetmagic and try it out. It has a great demo that illustrates the solution you are looking for:
https://github.com/JohannBlake/Jetmagic
I completely confused with compose conception.
I have a code
#Composable
fun HomeScreen(viewModel: HomeViewModel = getViewModel()) {
Scaffold {
val isTimeEnable by viewModel.isTimerEnable.observeAsState()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(Color.Black),
) {
Switch(
checked = isTimeEnable ?: false,
onCheckedChange = {
viewModel.setTimerEnable(it)
},
)
Clock(viewModel.timeSelected.value!!) {
viewModel.setTime(it)
}
}
}
}
#Composable
fun Clock(date: Long, selectTime: (date: Date) -> Unit) {
NumberClock(Date(date)) {
val time = SimpleDateFormat("HH:mm", Locale.ROOT).format(it)
Timber.d("Selected time: time")
selectTime(it)
}
}
Why Clock widget recomposes when I tap switch. If I remove line selectTime(it) from Clock widget callback recomposition doesn't happen.
Compose version: 1.0.2
This is because in terms of compose, you are creating a new selectTime lambda every time, so recomposition is necessary. If you pass setTime function as a reference, compose will know that it is the same function, so no recomposition is needed:
Clock(viewModel.timeSelected.value!!, viewModel::setTime)
Alternatively if you have more complex handler, you can remember it. Double brackets ({{ }}) are critical here, because you need to remember the lambda.
Clock(
date = viewModel.timeSelected.value!!,
selectTime = remember(viewModel) {
{
viewModel.setTimerEnable(it)
}
}
)
I know it looks kind of strange, you can use rememberLambda which will make your code more readable:
selectTime = rememberLambda(viewModel) {
viewModel.setTimerEnable(it)
}
Note that you need to pass all values that may change as keys, so remember will be recalculated on demand.
In general, recomposition is not a bad thing. Of course, if you can decrease it, you should do that, but your code should work fine even if it is recomposed many times. For example, you should not do heavy calculations right inside composable to do this, but instead use side effects.
So if recomposing Clock causes weird UI effects, there is probably something wrong with your NumberClock that cannot survive the recomposition. If so, please add the NumberClock code to your question for advice on how to improve it.
This is the intended behaviour. You are clearly modifying the isTimeEnabled field inside your viewmodel when the user toggles the switch (by calling vm.setTimeenabled). Now, it is apparent that the isTimeEnabled in your viewmodel is a LiveData instance, and you are referring to that instance from within your Composable by calling observeAsState on it. Hence, when you modify the value from the switch's onValueChange, you are essentially modifying the state that the Composable depends on. Hence, to render the updated state, a recomposition is triggered
Disclaimer: I know that Compose just entered alpha01 and thus I do not expect that every functionality
is available. However, layout and handling of specific layout cases is IMHO an important topic
that should be addressed early 😉.
The current view based ConstraintLayout provides some specific handling in case a child view is
marked as GONE, refer to the
ConstrainLayout documentation.
I checked the Compose ConstraintLayout documentation, the available modifiers and so on, however
did not find anything that points in this direction. I also could not find any hint regarding
INVISIBLE and how/if Compose ConstraintLayout handles it like the view based ConstraintLayout.
In general, the current view based layouts (LinearLayout for example) handle INVISIBLE and GONE in a
similar fashion:
if a view is in state INVISIBLE then the view is part of the layout with its sizes, just not
shown. The overall layout of other views does not change and they stay in their positions.
if a view is in state GONE its sizes a usually treated as 0 and the layout is recomputed and
changed, other views usually change their positions.
Here a simple Compose ConstraintLayout UI, just 4 buttons in a row, chained to have them nicely
spread.
// if dontShow is 0 then show all buttons, otherwise make the button with this number
// somehow INVISIBLE. This feature is not yet implemented.
#Composable
fun fourButtonsCL(dontShow: Int) {
ConstraintLayout(Modifier.fillMaxSize()) {
val (btn1, btn2, btn3, btn4) = createRefs()
TextButton(onClick = {}, Modifier.constrainAs(btn1) {}.background(teal200)) { Text("Button1") }
TextButton(onClick = {}, Modifier.constrainAs(btn2) {}.background(teal200)) { Text("Button2") }
TextButton(onClick = {}, Modifier.constrainAs(btn3) {}.background(teal200)) { Text("Button3") }
TextButton(onClick = {}, Modifier.constrainAs(btn4) {}.background(teal200)) { Text("Button4") }
createHorizontalChain(btn1, btn2, btn3, btn4)
}
}
#Preview(showBackground = true)
#Composable
fun previewThreeButtons() {
ComposeAppTheme {
fourButtonsCL()
}
}
Assume I would like to make Button3 invisible, but keep the other 3 buttons positioned where they
are. Thus just a hole between Button2 and Button4. How to achieve this without creating yet another
Composable or adding additional logic. While the logic in this simple case may be just a view lines
of code, more complex layouts would the need some more complex logic. In veiw based ConstraintLayout
wwe just need to modify the child view.
The other assumption: make Button3 disappear completely from the layout (GONE) and re-compute the
layout, remaining buttons become wider and evenly spread out. At a first glance this looks simple,
and in this very simple example it maybe easy. However in more complex layouts this could require
some or even a lot of re-wiring of constraints of the embedded Composables.
Thus the question is:
how does compose handles these cases for Column and Row layouts (like in view based LinearLayout)
and for ConstraintLayout in particular? However with the following restriction 😉: without defining
many new Composables and/or without adding complex layout logic inside the Composables (re-wiring
constraints for example).
Did I miss some modifier? Is this even planned or possible in the Composable layouts? What would be
the preferred way to solve such layout cases in Compose?
Based on #CommonsWare comment to the question I could solve the INVISIBLE
option, see code below.
Currently (in alpha-01) the implementation of ConstraintLayout seems to be incomplete, at least a few TODO comments in the code indicate this. This
seems to include the yet missing support of the GONE feature.
I saw some of these:
// TODO(popam, b/158069248): add parameter for gone margin
Also the chain feature does not yet perform layout rendering in the same way as
in the view based ConstraintLayout.
object FourElementsNoDSL {
const val elementA = "ElementA"
const val elementB = "ElementB"
const val elementC = "ElementC"
const val elementD = "ElementD"
private val noDSLConstraintSet = ConstraintSet {
// Create references with defines ids, here using a string as id. Could be an Int as well,
// actually it's defined as 'Any'
val elemA = createRefFor(elementA)
val elemB = createRefFor(elementB)
val elemC = createRefFor(elementC)
val elemD = createRefFor(elementD)
// Simple chain only. Instead of this simple chain we can use (for example):
// constrain(elemA) {start.linkTo(parent.start) }
// to set a constraint as known in XML
// constrain(elemA) {start.linkTo(parent.start, 16.dp) }
// constrain(elemB) {start.linkTo(elemA.end) }
// constrain(elemC) {start.linkTo(elemB.end) }
// constrain(elemD) {end.linkTo(parent.end) }
createHorizontalChain(elemA, elemB, elemC, elemD)
}
#Composable
fun fourButtonsCLNoDSL(doNotShow: List<String>) {
ConstraintLayout(constraintSet = noDSLConstraintSet, modifier = Modifier.fillMaxSize()) {
// This block contains the children
Text(text = "A",
modifier = Modifier.layoutId(elementA)
.drawOpacity(if (doNotShow.contains(elementA)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "B",
modifier = Modifier.layoutId(elementB)
.drawOpacity(if (doNotShow.contains(elementB)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "C",
modifier = Modifier.layoutId(elementC)
.drawOpacity(if (doNotShow.contains(elementC)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp)
)
Text(text = "D",
modifier = Modifier.layoutId(elementD)
.drawOpacity(if (doNotShow.contains(elementD)) 0f else 1f)
.padding(0.dp),
style = TextStyle(fontSize = 20.sp))
}
}
}
#Preview(showBackground = true)
#Composable
fun previewFourFieldsNoDSL() {
val noShow = listOf(FourElementsNoDSL.elementC)
PlaygroundTheme {
FourElementsNoDSL.fourButtonsCLNoDSL(noShow)
}
}
The object FourElementsNoDSL defines the layout, provides element ids and so on.
This is roughly comparable to an XML file that contains such layout.
noDSL means, that this layout does not use the Compose ConstraintLayout's Kotlin
DSL. Currently the DSL does not provide a mechanism to setup element references
(used in layoutId) with defined ids as its done in this example.