I am writing a small gallery app for my cat. It has a button by clicking on which a new PhotoItem is added to the displayed list, but it appears only after phone rotation and I want it to appear on the screen right after button was clicked.
Right now everything is stored in a mutableList inside savedStateHandle.getStateFlow but I also tried regular MutableStateFlow and mutableStateOf and it didn't help. I havent really used jatpack compose and just can't figure what to do (
App
#SuppressLint("UnusedMaterialScaffoldPaddingParameter")
#Composable
fun BebrasPhotosApp() {
val galaryViewModel = viewModel<GalaryViewModel>()
val allPhotos by galaryViewModel.loadedPics.collectAsState()
Scaffold(topBar = { BebraTopAppBar() }, floatingActionButton = {
FloatingActionButton(
onClick = { galaryViewModel.addPicture() },
backgroundColor = MaterialTheme.colors.onBackground
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = "Add Photo",
tint = Color.White,
)
}
}) {
LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
items(allPhotos) {
PhotoItem(bebra = it)
}
}
}
}
ViewModel
class GalaryViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val loadedPics = savedStateHandle.getStateFlow(
"pics", initialValue = mutableListOf<Bebra>(
Bebra(R.string.photo_1, R.string.desc_1, R.drawable.bebra_pic_1, R.string.add_desc_1),
Bebra(R.string.photo_2, R.string.desc_2, R.drawable.bebra_pic_2, R.string.add_desc_2),
Bebra(R.string.photo_3, R.string.desc_3, R.drawable.bebra_pic_3, R.string.add_desc_3)
)
)
fun addPicture() {
val additionalBebraPhoto = Bebra(
R.string.photo_placeholder,
R.string.desc_placeholder,
R.drawable.placeholder_cat,
R.string.add_desc_placeholder
)
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
}
PhotoItem
#Composable
fun PhotoItem(bebra: Bebra, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
Card(elevation = 4.dp, modifier = modifier
.padding(8.dp)
.clickable { expanded = !expanded }) {
Column(
modifier = modifier
.padding(8.dp)
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
) {
Text(
text = stringResource(id = bebra.PicNumber),
style = MaterialTheme.typography.h1,
modifier = modifier.padding(bottom = 8.dp)
)
Text(
text = stringResource(id = bebra.PicDesc),
style = MaterialTheme.typography.body1,
modifier = modifier.padding(bottom = 8.dp)
)
Image(
painter = painterResource(id = bebra.Picture),
contentDescription = stringResource(id = bebra.PicDesc),
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(256.dp)
.clip(RoundedCornerShape(12))
)
if (expanded) {
BebraAdditionalDesc(bebra.additionalDesc)
}
}
}
}
Bebra Data class
data class Bebra(
#StringRes val PicNumber: Int,
#StringRes val PicDesc: Int,
#DrawableRes val Picture: Int,
#StringRes val additionalDesc: Int
)
So, I am also not super familiar with JC, but from first glance it looks like your method, addPicture() - which is called when the user taps on the button, does not update the state, therefore there's no recomposition happening, so the UI does not get updated.
Check:
fun addPicture() {
// ...
savedStateHandle.get<MutableList<Bebra>>("pics")!!.add(additionalBebraPhoto)
}
So here you are basically adding a new item to savedStateHandle, which I assume does not trigger a recomposition.
What I think you need to do, is to update loadedPics, somehow.
However, loadedPics is a StateFlow, to be able to update it you would need a MutableStateFlow.
For simplicity, this is how you would do it if you were operating with a list of strings:
// declare MutableStateFlow that can be updated and trigger recomposition
val _loadedPics = MutableStateFlow(
savedStateHandle.get<MutableList<String>>("pics") ?: mutableListOf()
)
// use this in the JC layout to listen to state changes
val loadedPics: StateFlow<List<String>> = _loadedPics
// addPicture:
val prevList = _loadedPics.value
prevList.add("item")
_loadedPics.value = prevList // triggers recomposition
// here you probably will want to save the item in the
// `savedStateHandle` as you already doing.
Related
I'm praticing Compose navigation. I made an App that displays a ListItem with images, on what I call HomeScreen, when a particular item is clicked it navigates to a destination containing more information/ Details, on what I called HomeInfoScreen. And to do this I used Navigation compose Arugments. I basically did this by benchmarking the Android Developers Rally Compose.
But the problem is whenever I click on any of the rows it takes me to the details of the first Item, no matter which of them I click.
I believe the problem is from coming from HomeScreen (remember, I said this was the destination I'm navigating from)
I've previously researched, and got an Idea of passing my model object as a parameter.
But I think I did something wrong, because I get errors, and I don't know how to fix it.
Please understand that I arranged the code, in multiple composables, in this format;
SecondBodyElement -> SecondBodyGrid ------> HomeContentScreen
And Finally I call HomeContentScreen on the HomeScreen.
SecondBodyElement;
#Composable
fun SecondBodyElement(
#StringRes text: Int,
#DrawableRes drawable: Int,
modifier: Modifier = Modifier,
onHomeCardClick: (Int) -> Unit
) {
Surface(
shape = MaterialTheme.shapes.small,
elevation = 10.dp,
modifier = modifier
.padding(horizontal = 12.dp, vertical = 12.dp)
.clip(shape = RoundedCornerShape(corner = CornerSize(8.dp)))
.clickable { onHomeCardClick(drawable) }
) {
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.h3,
maxLines = 3,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 16.dp)
)
}
}
}
SecondyBodyGrid;
#Composable
fun SecondBodyGrid(
onHomeCardClick: (Int) -> Unit = {},
) {
LazyVerticalGrid(
columns = GridCells.Fixed(1),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.height(1450.dp)
//.disabledVerticalPointerInputScroll()
) {
items(SecondBodyData) { item ->
SecondBodyElement(
onHomeCardClick = onHomeCardClick,
drawable = item.drawable,
text = item.text,
modifier = Modifier
.height(380.dp)
.clickable { onHomeCardClick(item.drawable + item.text) }
)
}
}
}
HomeContentScreen;
#Composable
fun HomeContentScreen(
modifier: Modifier = Modifier,
onHomeCardClick: (String) -> Unit,
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
val homeInfo = remember(accountType) { HomeInfoModel.getHomeInfo(accountType) }
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
HomeQuote()
HomeTitleSection(title = R.string.favorite_collections) {
SecondBodyGrid { onHomeCardClick(homeInfo.title) }
}
}
}
And Finally HomeScreen;
#Composable
fun HomeScreen(onHomeCardClick: (String) -> Unit) {
HomeContentScreen(onHomeCardClick = onHomeCardClick)
}
Please like I said I'm practicing, I don't know what you are going to need. But I'm going to add my NavHost (Nav Graph) and the Model file, just incase, If you need any other thing I'm more than happy to provide.
Model, HomeInfoModel;
data class HomeInfoData(
val id: Int,
val title: String,
val sex: String,
val age: Int,
val description: String,
val homeInfoImageId: Int = 0
)
object HomeInfoModel {
val homeInfoModelList = listOf(
HomeInfoData(
id = 1,
title = "There's Hope",
sex = "Male",
age = 14,
description = "Monty enjoys chicken treats and cuddling while watching Seinfeld.",
homeInfoImageId = R.drawable.ab1_inversions
),
....
)
fun getHomeInfo(accountName: String?): HomeInfoData {
return homeInfoModelList.first { it.title == accountName }
}
}
My NavHost (Nav Graph);
....
composable(route = Home.route) {
HomeScreen(
onHomeCardClick = { accountType ->
navController.navigateToHomeInfoScreen(accountType)
}
)
}
composable(
route = HomeInfoDestination.routeWithArgs,
arguments = HomeInfoDestination.arguments,
) { navBackStackEntry ->
// Retrieve the passed argument
val accountType =
navBackStackEntry.arguments?.getString(HomeInfoDestination.accountTypeArg)
// Pass accountType
HomeInfoScreen(accountType)
}
....
}
}
....
private fun NavHostController.navigateToHomeInfoScreen(accountType: String) {
this.navigateSingleTopTo("${HomeInfoDestination.route}/$accountType")
}
I think the problem is from the Homescreen but I don't really know, So I'm going to Add the HomeInfoScreen, Sorry making this question any longer.
HomeInfoScreen;
#Composable
fun HomeInfoScreen(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
DisplayHomeInfo(accountType)
}
#Composable
fun WelcomeText() {
Text(
text = "Welcome, to Home Information",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)
)
}
#Composable
fun HomeInfoDetails(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
val homeInfo = remember(accountType) { HomeInfoModel.getHomeInfo(accountType) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Image(
painter = painterResource(id = homeInfo.homeInfoImageId),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp)),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.title,
style = MaterialTheme.typography.h3
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = homeInfo.description,
style = MaterialTheme.typography.h5
)
}
}
// Step: Home screen - Scrolling
#Composable
fun DisplayHomeInfo(
accountType: String? = HomeInfoModel.homeInfoModelList.first().title
) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
WelcomeText()
HomeInfoDetails(accountType)
}
}
For Clarity Sake; How can I navigate to the exact item when it is clicked on the SuccessScreen.
I'll sinerely be greatful for any help. Thanks a lot in advance for your help.
In case somebody needs this, I've been able to do this. I followed a website's Developer's Breach tutorial.
I'm new on jetpack compose and I'm sure that I'm missing something but I don't know what?
my State model:
data class ChoiceSkillsState(
val isLoading: Boolean = false,
val errorWD: ErrorWD? = null,
val skills: List<Skill> = emptyList(),
)
The Skill model:
#Parcelize
data class Skill(
val id: Int,
val name: String,
val imageUrl: String? = null,
var children: List<SkillChild>? = null,
) : Parcelable {
#Parcelize
data class SkillChild(
val id: Int,
val name: String,
val imageUrl: String? = null,
var note: Int? = null,
) : Parcelable
}
fun Skill.asChildNoted(): Boolean {
if (!children.isNullOrEmpty()) {
children!!.forEach {
if (it.note != null) return true
}
}
return false
}
on my viewModel
private val _state = mutableStateOf(ChoiceSkillsState())
val state: State<ChoiceSkillsState> = _state
On some event I update my skillList on my state : ChoiceSkillState.
When I log, my data is updated correctly but my view is not recomposed..
There is my LazyColumn:
#Composable
private fun LazyColumnSkills(
skills: List<Skill>,
onClickSkill: (skill: Skill) -> Unit,
) {
LazyColumn(
contentPadding = PaddingValues(bottom = MaterialTheme.spacing.medium),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.small),
) {
items(
items = skills,
) { skill ->
ItemSkillParent(
skill = skill,
onClickSkill = onClickSkill
)
}
}
}
Then here is my ItemSkillParent:
#Composable
fun ItemSkillParent(
skill: Skill,
onClickSkill: (skill: Skill) -> Unit
) {
val backgroundColor =
if (skill.asChildNoted()) Orange
else OrangeLight3
val endIconRes =
if (skill.asChildNoted()) R.drawable.ic_apple
else R.drawable.ic_arrow_right
Box(
modifier = Modifier
.fillMaxWidth()
.clip(shape = MaterialTheme.shapes.itemSkill)
.background(backgroundColor)
.clickable { onClickSkill(skill) },
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 7.dp, horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier
.weight(1f)
.size(50.dp)
.clip(shape = MaterialTheme.shapes.itemSkillImage),
painter = rememberAsyncImagePainter(model = skill.imageUrl),
contentDescription = "Image skill",
contentScale = ContentScale.Crop
)
Text(
modifier = Modifier
.weight(6f)
.padding(horizontal = 10.dp),
text = skill.name,
style = MaterialTheme.typography.itemSkill
)
ButtonIconRoundedMini(
iconRes = endIconRes,
contentDesc = "Icon arrow right",
onClick = { onClickSkill(skill) }
)
}
}
}
My onClickSkill() will open a new Screen then pass result, then I will update my data with this :
fun updateSkill(skill: Skill) {
val skillsUpdated = _state.value.skills
skillsUpdated
.filter { it.id == skill.id }
.forEach { it.children = skill.children }
_state.value = _state.value.copy(skills = skillsUpdated)
}
As you can see, the background color and the iconResource should be changed, it's changing when only when I scroll.
Can someone explain me what's happening there ?
You should never use var in class properties if you want a property update to cause recomposition.
Check out Why is immutability important in functional programming?.
In this case you are updating the children property, but skillsUpdated and _state.value.skills are actually the same object - you can check the address, so Compose thinks it has not been changed.
After updating your children to val, you can use copy to update it.
val skillsUpdated = _state.value.skills.toMutableList()
for (i in skillsUpdated.indices) {
if (skillsUpdated[i].id != skill.id) continue
skillsUpdated[i] = skillsUpdated[i].copy(children = skill.children)
}
_state.value = _state.value.copy(skills = skillsUpdated.toImmutableList())
Note that converting a mutable list into an immutable list is also critical here, because otherwise the next time you try to update it, the list will be the same object: both toList and toMutableList return this when applied to a mutable list.
make your properties as state
val backgroundColor by remember {
mutableStateOf (if (skill.asChildNoted()) Orange
else OrangeLight3)
}
val endIconRes by remember {
mutableStateOf (if (skill.asChildNoted()) R.drawable.ic_apple else R.drawable.ic_arrow_right)
}
New to Compose and struggling hard with more complex state cases.
I cant seem to change the text dynamically in these cards, only set the text manually. Each button press should change the text in a box, starting left to right, leaving the next boxes empty.
What is wrong here?
UI:
val viewModel = HomeViewModel()
val guessArray = viewModel.guessArray
#Composable
fun HomeScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
) {
WordGrid()
Keyboard()
}
}
#Composable
fun WordGrid() {
CardRow()
}
#Composable
fun Keyboard() {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyKeyboardButton(text = "A", 35)
MyKeyboardButton(text = "B", 35)
}
}
#Composable
fun MyCard(text: String) {
Card(
modifier = Modifier
.padding(4.dp, 8.dp)
.height(55.dp)
.aspectRatio(1f),
backgroundColor = Color.White,
border = BorderStroke(2.dp, Color.Black),
) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
text = text,
fontSize = 20.sp,
)
}
}
}
#Composable
fun CardRow() {
guessArray.forEach { rowCards ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
rowCards.forEach {
MyCard(it)
println(it)
}
}
}
}
#Composable
fun MyKeyboardButton(text: String, width: Int) {
Button(
onClick = {
guessArray[viewModel.currentRow][viewModel.column] = text
viewModel.column += 1
},
modifier = Modifier
.width(width.dp)
.height(60.dp)
.padding(0.dp, 2.dp)
) {
Text(text = text, textAlign = TextAlign.Center)
}
}
ViewModel:
class HomeViewModel : ViewModel() {
var currentRow = 0
var guessArray = Array(5) { Array(6) { "" }.toMutableList() }
var column = 0
}
The grid is created, but the text is never changed.
To make Compose view recompose with the new value, a mutable state should be used.
You're already using it with remember in your composable, but it also needs to be used in your view model for properties, which should trigger recomposition.
class HomeViewModel : ViewModel() {
var currentRow = 0
val guessArray = List(5) { List(6) { "" }.toMutableStateList() }
var column = 0
}
I need to implement multiselect for LazyList, which will also change appBar content when list items are long clicked.
For ListView we could do that with just setting choiceMode to CHOICE_MODE_MULTIPLE_MODAL and setting MultiChoiceModeListener.
Is there a way to do this using Compose?
Since you're in control of the whole state in jetpack compose, you can easily create your own multi-select mode. This is an example.
First I created a ViewModel
val dirs: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.targetFolders)}.asFlow().asLiveData()
val all: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.allFolders)
}.asFlow().asLiveData()
val createFolder = mutableStateOf(false)
val refresh = mutableStateOf(false)
val enterSelectMode = mutableStateOf(false)
val selectedAll = mutableStateOf(false)
val selectedList = mutableStateOf(mutableListOf<String>())
fun updateList(path: String){
if(path in selectedList.value){
selectedList.value.remove(path)
}else{
selectedList.value.add(path)
}
}
}
Usage
Card(modifier = Modifier
.width(100.dp)
.height(120.dp)
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { **viewModel.enterSelectMode.value = true** },
onTap = {
if (viewModel.enterSelectMode.value) {
viewModel.enterSelectMode.value = false
}
}
)
}
,
shape = MaterialTheme.shapes.medium
) {
Image(painter = if (dirModel.dirCover != "") painter
else painterResource(id = R.drawable.thumbnail),
contentDescription = "dirThumbnail",
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(),
contentScale = ContentScale.FillHeight)
**AnimatedVisibility(
visible = viewModel.enterSelectMode.value,
enter = expandIn(),
exit = shrinkOut()
){
Row(verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.Start) {
Checkbox(checked = isSelected.value, onCheckedChange = {
isSelected.value = !isSelected.value
viewModel.updateList(dirModel.dirPath)
})
}
}**
}
Add selected field to some class representing the item. Then just compose proper code based on that field. In compose you don't have to look for some LazyColumn flag or anything like that. You are in control of the whole state of the list.
The same can be said about the AppBar, you can do a simple if there, like if (items.any { it.selected }) // display button
You can simply handle this by the below code:
Fore example This is your list:
LazyColumn {
items(items = filters) { item ->
FilterItem(item)
}
}
and this is your list item View:
#Composable
fun FilterItem(filter: FilterModel) {
val (selected, onSelected) = remember { mutableStateOf(false) }
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 8.dp)
.border(
width = 1.dp,
colorResource(
if (selected)
R.color.black
else
R.color.white
),
RoundedCornerShape(8.dp)
)) {
Text(
modifier = Modifier
.toggleable(
value =
selected,
onValueChange =
onSelected
)
.padding(8.dp),
text = filter.text)
}
}
and that's it use tooggleable on your click component modifier like here I did on text.
In my viewModel I have "state" for every single screen. e.g.
class MainState(val commonState: CommonState) {
val text = MutableStateFlow("text")
}
I pass viewModel to my JetpackCompose screen.
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = viewModel.state.mainState.text.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.state.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}
When I click button I change value of state and I expect UI will update but it does not. I have to click on TextField and then text in TextView updates.
Any suggestion why UI does not update automatically?
That's how I pass components and start whole screen in startActivity;
class HomeActivity : ComponentActivity() {
private val viewModel by viewModel<HomeViewModel>()
private val homeState: HomeState get() = viewModel.state
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RateMeAppTheme {
ContentScreen(viewModel, homeState)
}
}
}
}
In this simple case u should use mutableStateOf("text") in class MainState instead of mutableStateFlow
class MainState(val commonState: CommonState) {
val text = mutableStateOf("text")
}
Using MutableStateFlow
To use MutableStateFlow (which is not required in the current scenario) , we need to collect the flow.
Like the following:-
val state = viewModel.mainState.text.collectAsState() // we can collect a stateflow as state in a composable function
Then we can use the observed state value in the Text using:-
Text(text = state.value, ..)
Finally your composable function should look like:-
#Composable
fun MainScreen(text: String, viewModel: HomeViewModel) {
val textState = remember { mutableStateOf(TextFieldValue()) }
val state = viewModel.mainState.text.collectAsState()
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.value,
color = Color.Blue,
fontSize = 40.sp
)
Button(
onClick = { viewModel.mainState.text.value = "New text" },
colors = ButtonDefaults.buttonColors(
backgroundColor = Color.Green
),
modifier = Modifier.padding(16.dp)
) {
Text(text)
}
TextField(
value = textState.value,
onValueChange = { textState.value = it },
label = { Text("Input text") }
)
}
}