Issue with preferedWrapContent and ConstraintLayout - android-jetpack-compose

I'm using a Composable which take a content #Composable in parameter, and it seems that with the final version of ConstraintLayout, there is no update.
Here is the code
#Composable
fun Example(
modifier : Modifier = Modifier,
content : #Composable () -> Unit
) {
ConstraintLayout(modifier = modifier
.fillMaxSize()
.background(color = Color.Blue)
) {
val (title, someContent) = createRefs()
Text(text = "a text", modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
height = Dimension.wrapContent
width = Dimension.wrapContent
})
Box(modifier = Modifier
.constrainAs(someContent) {
width = Dimension.fillToConstraints
linkTo(start = parent.start, end = parent.end)
linkTo(top = parent.top, bottom = parent.bottom, bias = 1.0f)
height = Dimension.preferredWrapContent
}
.background(color = Color.Yellow)) {
content()
}
}
}
The Box height is initialized with the fist element received by the composable and does not change anymore.
For example, I send to this composable a Column with [Button + result of a Webservice]. The button is displayed at startup, and then after some times I received the result of the API, it does not recompose correctly, the size of the Box stays wrapcontent with the button only!
Am I doing something wrong ?
Moreover, it seems that the behaviour in a compose MotionLayout is the same (no update)

A recomposition only occurs on a composable when at least one of the parameter values change. Because you are set the content parameter to a function, the parameter never changes.
The content parameter is pointing to a function reference and that reference never changes during recomposition.
If you want your composable to recompose with the updated data, you need to add a mutable state variable inside your composable that your viewmodel updates, or pass in a parameter that will change when you have received new data.
Here's an example of updating a parameter value using a state variable in a view model. Updating the state variable recomposes your composable and provides it with the updated variable (name):
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
val vm: MyViewModel = viewModel()
vm.loadData()
Example(name = vm.name.value) { name ->
Text("Name: $name")
}
}
}
}
class MyViewModel: ViewModel() {
val name = mutableStateOf("")
fun loadData() {
viewModelScope.launch {
delay(2000)
name.value = "Joe Cool"
}
}
}
#Composable
fun Example(
modifier : Modifier = Modifier,
name: String,
content : #Composable (name: String) -> Unit
) {
ConstraintLayout(modifier = modifier
.fillMaxSize()
.background(color = Color.Blue)
) {
val (title, someContent) = createRefs()
Text(text = "a text", modifier = Modifier.constrainAs(title) {
top.linkTo(parent.top)
height = Dimension.wrapContent
width = Dimension.wrapContent
})
Box(modifier = Modifier
.constrainAs(someContent) {
width = Dimension.fillToConstraints
linkTo(start = parent.start, end = parent.end)
linkTo(top = parent.top, bottom = parent.bottom, bias = 1.0f)
height = Dimension.preferredWrapContent
}
.background(color = Color.Yellow)) {
content(name)
}
}
}

Related

How to preview compose layout when you're getting data from api

I'm setting up ui for my Lazy column and I'm getting data from api
#Preview
#Composable
fun MatchesRow(data: Data ) {
Card(
modifier = Modifier
.height(180.dp),
backgroundColor = MaterialTheme.colors.background
) {
}}
i get erros because my parameters are empty , how can i Preview ??
you have to create two function, one of which receives the data from the ViewModel, and the other is the case you mentioned
Note : One Stateful and one Stateless
But in order to be able to preview, you must act as follows:
#Composable
fun MatchesRow(data: Data ) {
Card(
modifier = Modifier
.height(180.dp),
backgroundColor = MaterialTheme.colors.background
) {
}}
#Preview
#Composable
fun MatchesRowPreview() {
val data = .....
MatchesRow(
data = data
)
}
If you want to preview a composable with a parameter, you can use #PreviewParameter annotation.
Say your data class looks like this,
data class Data(
val dataValue: String
)
and you have this composable with text inside a red box in the middle of the screen,
#Preview
#Composable
fun DataDetails(details: Data) {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.wrapContentSize()
.background(Color.Red)
) {
Text(
modifier = Modifier.padding(8.dp),
text = details.dataValue
)
}
}
}
and you want to preview it, you have to create a PreviewParemeterProvider first,
class DataPreviewProvider : PreviewParameterProvider<Data> {
override val values = listOf(Data(dataValue = "Hello")).asSequence()
}
and annotate the parameter with #PreviewParameter and pass the DataPreviewProvider to it like this.
#Preview
#Composable
fun DataDetails(#PreviewParameter(DataPreviewProvider::class) details: Data) {
...
}
I would also suggest to just simply create another composable solely for "previewing" purposes.
#Preview
#Composable
fun DataDetailsPreview(
#PreviewParameter(DataPreviewProvider::class) dataDetails: Data) {
DataDetails(userDetailsDisplay)
}
You can also check this #PreviewParemeter

How to save data in a composable function with view model constructor

I have a composable function for the home screen, whenever I navigate to a new composable and come back to this function everything is re created. Do I have to move the view model somewhere higher?
home screen
#Composable
fun HomeScreen(viewModel: NFTViewModel = viewModel()) {
val feedState by viewModel.nftResponse.observeAsState()
if (feedState?.status == Result.Status.SUCCESS) {
val nfts = feedState?.data?.content
LazyColumn {
itemsIndexed(nfts!!) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
.clickable { },
elevation = 8.dp
) {
Column(
modifier = Modifier.padding(15.dp)
) {
Image(
painter = rememberAsyncImagePainter(
item.firebaseUri
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.height(250.dp)
)
Text(
buildAnnotatedString {
append("Contract Address: ")
withStyle(
style = SpanStyle(fontWeight = FontWeight.W900, color = Color(0xFF4552B8))
) {
append(item.contractAddress)
}
}
)
Text(
buildAnnotatedString {
append("Chain: ")
withStyle(style = SpanStyle(fontWeight = FontWeight.W900)) {
append(item.chain.displayName)
}
append("")
}
)
Text(
buildAnnotatedString {
append("Price: ")
withStyle(style = SpanStyle(fontWeight = FontWeight.W900)) {
append(item.price)
}
append("")
}
)
}
}
}
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = R.color.white))
.wrapContentSize(Alignment.Center)
) {
Text(
text = "Feed",
fontWeight = FontWeight.Bold,
color = Color.DarkGray,
modifier = Modifier.align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center,
fontSize = 25.sp
)
}
}
}
Navigation
#Composable
fun Navigation(navController: NavHostController) {
NavHost(navController, startDestination = NavigationItem.Home.route) {
composable(NavigationItem.Home.route) {
val vm: NFTViewModel = viewModel()
HomeScreen(vm)
}
composable(NavigationItem.Add.route) {
AddScreen()
}
composable(NavigationItem.Wallets.route) {
WalletsScreen()
}
composable(NavigationItem.Popular.route) {
PopularScreen()
}
composable(NavigationItem.Profile.route) {
ProfileScreen()
}
}
}
It seems like I need to save the view model as a state or something? I seem to be missing something. Basically I dont want to trigger getFeed() everytime the composable function is called, where do I save the data in the compose function, in the view model?
EDIT now calling getFeed from init in the view model
class NFTViewModel : ViewModel() {
private val nftRepository = NFTRepository()
private var successResult: Result<NftResponse>? = null
private var _nftResponse = MutableLiveData<Result<NftResponse>>()
val nftResponse: LiveData<Result<NftResponse>>
get() = _nftResponse
init {
successResult?.let {
_nftResponse.postValue(successResult!!)
} ?: kotlin.run {
getFeed() //this is always called still successResult is always null
}
}
private fun getFeed() {
viewModelScope.launch {
_nftResponse.postValue(Result(Result.Status.LOADING, null, null))
val data = nftRepository.getFeed()
if (data.status == Result.Status.SUCCESS) {
successResult = data
_nftResponse.postValue(Result(Result.Status.SUCCESS, data.data, data.message))
} else {
_nftResponse.postValue(Result(Result.Status.ERROR, null, data.message))
}
}
}
override fun onCleared() {
Timber.tag(Constants.TIMBER).d("onCleared")
super.onCleared()
}
}
However it still seems like a new view model is being created.
It seems like I need to save the view model as a state or something?
You don't have to. ViewModels are already preserved as part of their owner scope. The same ViewModel instance will be returned to you if you retrieve the ViewModels correctly.
I seem to be missing something.
You are initializing a new instance of your NFTViewModel every time the navigation composable recomposes (gets called) instead of retrieving the NFTViewModel from its ViewModelStoreOwner.
You should retrieve ViewModels by calling viewModel() or if you are using Hilt and #HiltViewModel then call hiltViewModel() instead.
No Hilt
val vm: NFTViewModel = viewModel()
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
The created ViewModel is associated with the given viewModelStoreOwner and will be retained as long as the owner is alive (e.g. if it is an activity, until it is finished or process is killed).
If using Hilt (i.e. your ViewModel has the #HiltViewModel annotation)
val vm: NFTViewModel = hiltViewModel()
Returns an existing #HiltViewModel-annotated ViewModel or creates a new one scoped to the current navigation graph present on the NavController back stack.
If no navigation graph is currently present then the current scope will be used, usually, a fragment or an activity.
The above will preserve your view model state, however you are still resetting the state inside your composable if your composable exits the composition and then re-enters it at a later time, which happens every time you navigate to a different screen (to a different "screen" composable, if it is just a dialog then the previous composable won't leave the composition, because it will be still displayed in the background).
Due to this part of the code
#Composable
fun HomeScreen(viewModel: NFTViewModel) {
val feedState by viewModel.nftResponse.observeAsState()
// this resets to false when HomeScreen leaves and later
// re-enters the composition
val fetched = remember { mutableStateOf(false) }
if (!fetched.value) {
fetched.value = true
viewModel.getFeed()
}
fetched will always be false when you navigate to (and back to) HomeScreen and thus getFeed() will be called.
If you don't want to call getFeed() when you navigate back to HomeScreen you have to store the fetched value somewhere else, probably inside your NFTViewModel and only reset it to false when you want that getFeed() is called again.

How to bind string (update that string when change him) from viewmodel with Jetpack Compose

I want to implement MVVM arquitecture and i am trying to bind two string variables from my model but I cannot
I have this model and viewmodel:
data class LoginModel(var correo:String = "", val password:String = "")
class LoginViewModel:ViewModel() {
var model by mutableStateOf(LoginModel())
fun setEmail(text:String){
model.correo = text
}
}
And i have this #Composable function:
#Composable
fun LoginPage(navController: NavController,viewModel: LoginViewModel = viewModel()) {
val paddingBox=30.dp //padding for box
BoxWithConstraints(modifier = Modifier
.fillMaxSize()
.padding(paddingBox)) {
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceEvenly) {
Row(modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(15.dp))) {
//var text by remember { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = viewModel.model.correo,
onValueChange = {viewModel.setEmail(it)},
modifier= Modifier
.fillMaxWidth()
.background(androidx.compose.ui.graphics.Color.White),
shape = RoundedCornerShape(percent = 20),
trailingIcon = {
Icon(imageVector= Icons.Filled.Email,"correo")
},
singleLine = true)
}
//container of elements
}
}
}
but UI is not being updated. I write in emulator but nothing is changed. why ??
Your viewmodel contains LoginModel's object as the mutable state. But inside the setEmail method, you are just updating the underlying value of that mutable state's object. The underlying change will not be considered by the compose compiler as the object is still the same.
You can do something like this,
Change the correo to val from var.
data class LoginModel(val correo: String = "", val password: String = "")
And the viewmodel
class LoginViewModel : ViewModel() {
var model by mutableStateOf(LoginModel())
fun setEmail(text: String) {
model = model.copy(correo = text)
}
}
If you update the object itself, it will reflect in UI as this will make the recomposition.

Compose StateFlow being kept even after ViewModel that creates it is recreated

I'm having a real strange issue here. I have a ViewModel that has a StateFlow. That ViewModel is recreated in specific circumstances and sets it's StateFlow value to 0.
I also have Compose view that reads value of this StateFlow and displays text according to it.
Then I change that state to 2, for example. And then recreate the whole Compose view and ViewModel.
But, when I recreate the whole view, and the ViewModel, for brief moment, StateFlow keeps it's old state (even though ViewModel is recreated altogether with the View and state is set to 0), and then switches to the new one, which is zero (this only works if you make a change mentioned below).
This can cause tha crash if we have lists that have different amout of items, and we pass them when view is recreated, because then we will read index value that does not exist and our app will crash.
Changing the list ViewModelTwo(mutableListOf("text4")) to the ViewModelTwo(mutableListOf("text4", "text5", "text6")) will stop the crash. But look at the logs, and you'll see what's going on. First it goes to 2, then to 0, which is default.
I have github repo setup for Compose-Jb. You can open it in Android Studio: https://github.com/bnovakovic/composableIssue
Sorry for using android compose tags, but I could not find Compose-JB tag.
And for convinience, here are the code snippets.
Any help is appreciated
Main.kt
#Composable
#Preview
fun App(viewModelOne: ViewModelOne) {
val showComposable by viewModelOne.stateOne.collectAsState()
MaterialTheme {
// Depending on the state we decide to create different ViewModel
val viewModelTwo: ViewModelTwo = when (showComposable) {
0 -> ViewModelTwo(mutableListOf("text1", "text2", "text3"))
1 -> ViewModelTwo(mutableListOf("text4"))
else -> ViewModelTwo(mutableListOf("blah1", "blah2", "blah3"))
}
// New composable is always created with new ViewModelTwo that has default index of 0, yet the app still crashes
TestComposableTwo(viewModelTwo)
Row {
Button(onClick = {
viewModelOne.changeState()
}) {
Text("Click button below, than me")
}
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
val viewModelOne = ViewModelOne();
App(viewModelOne)
}
}
TestComposableView
#Composable
fun TestComposableTwo(viewModelTwo: ViewModelTwo) {
val currentIndex by viewModelTwo.currentListItem.collectAsState()
println("Index is: $currentIndex")
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
// At the point where we recreate this composable view, currentIndex keeps it's old value, and then changes it
// to the new one causing the app to crash since new list does not have index of 1
Text(text = viewModelTwo.stringList[currentIndex])
Button(onClick = {
viewModelTwo.changeIndex()
}) {
Text("Click me before clicking button above")
}
}
}
ViewModel1
class ViewModelOne {
private val viewModelScope = CoroutineScope(Dispatchers.IO)
private val _stateOne = MutableStateFlow(0)
val stateOne = _stateOne.asStateFlow()
fun changeState() {
viewModelScope.launch {
val currentValue = stateOne.value + 1
_stateOne.emit(currentValue)
}
}
}
ViewModel2
class ViewModelTwo(val stringList: List<String>) {
private val viewModelScope = CoroutineScope(Dispatchers.IO)
private val _currentListItem = MutableStateFlow(0)
val currentListItem = _currentListItem.asStateFlow()
fun changeIndex() {
viewModelScope.launch {
_currentListItem.emit(2)
}
}
}
It seems overly complex to make a new ViewModelTwo in the Composable based on a value in the first ViewModel. Try to make your ViewModel take care of the logic and your Composables just show the data.
I would suggest using a single ViewModel with a single MutableStateFlow.
And you don't need the emit or the viewModelScope in this case.
Something like this should work:
class MainViewModel {
private val _state = MutableStateFlow(State(listOf("text1", "text2", "text3"), 0))
val state: StateFlow<State> = _state
fun onChangeStateClick() {
_state.value = State(listOf("text4"), 0) // Or other logic here if needed
}
fun onChangeIndexClick() {
_state.update {
if (it.currentIndex < it.stringList.count() - 1) {
it.copy(currentIndex = it.currentIndex + 1)
} else it.copy(currentIndex = 0)
}
}
data class State(
val stringList: List<String>,
val currentIndex: Int
)
}
#Composable
#Preview
fun App(viewModel: MainViewModel) {
val state by viewModel.state.collectAsState()
MaterialTheme {
TestComposableTwo(
state.stringList[state.currentIndex],
onButtonClick = { viewModel.onChangeIndexClick() }
)
Row {
Button(onClick = {
viewModel.onChangeStateClick()
}) {
Text("Click button below, than me")
}
}
}
}
#Composable
fun TestComposableTwo(text: String, onButtonClick: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Text(text = text)
Button(onClick = onButtonClick) {
Text("Click me before clicking button above")
}
}
}

Jetpack Compose - How can we call a #Composable function inside an onClick()

I know its not possible to call composable functions inside onClick.
#Composable invocations can only happen from the context of a #Composable function
Compose version - alpha06
But I'm stuck with the below requirement.
The requirement is,
Call a server api call inside an onClick.
LazyColumnFor(items = list) { reports ->
Box(Modifier.clickable(
onClick = {
//API call
val liveDataReportsDetails =
viewModel.getReportDetails("xxxx")
LiveDataComponentForReportsDetails(liveDataReportsDetails)
}
)) {
ReportListItem(
item = reports
)
}
}
So you're right, composable functions cannot be called from within onClicks from either a button or a modifier. So you need to create a value like:
private val showDialog = mutableStateOf(false)
When set to true you want to invoke the composable code, like:
if(showDialog.value) {
alert()
}
Alert being something like:
#Composable
fun alert() {
AlertDialog(
title = {
Text(text = "Test")
},
text = {
Text("Test")
},
onDismissRequest = {
},
buttons = {
Button(onClick = { showDialog.value = false }) {
Text("test")
}
}
)
}
Now finish with changing the boolean where intended, like:
Box(Modifier.clickable(
onClick = {
showDialog.value = true
}
))
I hope this explanation helps, of course the value doesn't have to be a boolean, but you get the concept :).

Resources