LazyColum only recompose when scroll - android

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)
}

Related

New card item appearse only after phone rotation

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.

animation o a lazycolumn android

I have a list with 10 items one of them have this elements "rankingCurrentPlace", "rankingPastPlace" and "isUser:true".
What i need to do its an animation on the lazycolumn if the api esponse is like this
"isUser:true", "rankingPastPlace:3" , "rankingCurrentPlace:7"
i need to show an animation in the list where the row starts in the third place and descend to the seventh place
is there a way to do this?
this is what I actually have
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
items(
items = leaderboard,
key = { leaderBoard ->
leaderBoard.rankingPlace
}
) { leaderBoard ->
RowComposable( modifier = Modifier
.fillMaxWidth(),
topicsItem = leaderBoard,)
}
This answer works except when swapping first item with any item even with basic swap function without animation. I think it would be better to ask a new question about why swapping first item doesn't work or if it is bug. Other than that works as expected. If you need to move to items that are not in screen you can lazyListState.layoutInfo.visibleItemsInfo and compare with initial item and scroll to it before animation
1.Have a SnapshotStateList of data to trigger recomposition when we swap 2 items
class MyData(val uuid: String, val value: String)
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData( uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
2.Function to swap items
private fun swap(list: SnapshotStateList<MyData>, from: Int, to: Int) {
val size = list.size
if (from in 0 until size && to in 0 until size) {
val temp = list[from]
list[from] = list[to]
list[to] = temp
}
}
3.Function to swap items one by one. There is a bug with swapping first item. Even if it's with function above when swapping first item other one moves up without showing animation via Modififer.animateItemPlacement().
#Composable
private fun animatedSwap(
lazyListState: LazyListState,
items: SnapshotStateList<MyData>,
from: Int,
to: Int,
onFinish: () -> Unit
) {
LaunchedEffect(key1 = Unit) {
val difference = from - to
val increasing = difference < 0
var currentValue: Int = from
repeat(abs(difference)) {
val temp = currentValue
if (increasing) {
currentValue++
} else {
currentValue--
}
swap(items, temp, currentValue)
if (!increasing && currentValue == 0) {
delay(300)
lazyListState.scrollToItem(0)
}
delay(350)
}
onFinish()
}
}
4.List with items that have Modifier.animateItemPlacement()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
Demo
#OptIn(ExperimentalFoundationApi::class)
#Composable
private fun AnimatedList() {
Column(modifier = Modifier.fillMaxSize()) {
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData(uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
var fromString by remember {
mutableStateOf("7")
}
var toString by remember {
mutableStateOf("3")
}
var animate by remember { mutableStateOf(false) }
if (animate) {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
animatedSwap(
lazyListState = lazyListState,
items = items,
from = from,
to = to
) {
animate = false
}
}
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = fromString,
onValueChange = {
fromString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
TextField(
value = toString,
onValueChange = {
toString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
animate = true
}
) {
Text("Swap")
}
}
}
Edit: Animating with Animatable
Another method for animating is using Animatable with Integer vector.
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
val coroutineScope = rememberCoroutineScope()
val animatable = remember { Animatable(0, IntToVector) }
And can be used as
private fun alternativeAnimate(
from: Int,
to: Int,
coroutineScope: CoroutineScope,
animatable: Animatable<Int, AnimationVector1D>,
items: SnapshotStateList<MyData>
) {
val difference = from - to
var currentValue: Int = from
coroutineScope.launch {
animatable.snapTo(from)
animatable.animateTo(to,
tween(350 * abs(difference), easing = LinearEasing),
block = {
val nextValue = this.value
if (abs(currentValue -nextValue) ==1) {
swap(items, currentValue, nextValue)
currentValue = nextValue
}
}
)
}
}
on button click, i'm getting values from TextField fo i convert from String
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
alternativeAnimate(from, to, coroutineScope, animatable, items)
}
) {
Text("Swap")
}
Result
I suggest you to get your items from a data class. If your other items does not contain the variables you mentioned you can make them nullable in data class and put a condition checker in your lazycolumn
Like this
data class Items(
val otherItems: Other,
val rankingCurrentPlace: Int?,
val rankingLastPlace: Int?,
val isUser: Boolean?
)
Then you can make a list from this data class and pass it to lazycolumn
LazyColumn{
items(list){
(elements with condition)
}
}

How do I get my DatePicker Dialog to return a date and display it? Issue updating viewmodel

A quick preface, I'm probably overlooking something rather simple but I'm willing to admit I'm not 100% sure of what I'm doing! The project I'm working on is a just for fun project to tinker around with Kotlin and Jetpack Compose by making an Android application.
So to my problem, I'm trying to use a DatePicker dialog using the vanpra compose-material-dialogs. I can get the dialog datepicker to appear but I can't get my selected date to return to my textfield I made after selecting the date from the dialog. It seems I'm running in to some issues updating the viewmodel correctly. Any help at all is greatly appreciated! Thank you!
Below is my DatePicker composable:
#Composable
fun TradeDatePicker(
value: LocalDate,
onValueChanged: (LocalDate) -> Unit,
modifier:Modifier = Modifier,
) {
val dialogState = rememberMaterialDialogState()
MaterialDialog(
dialogState = dialogState,
buttons = {
positiveButton("OK")
negativeButton("CANCEL")
},
backgroundColor = MaterialTheme.colors.background
) {
this.datepicker(onDateChange = onValueChanged)
}
Box(
modifier = modifier
.border(
width = 1.dp,
color = MaterialTheme.colors.onPrimary,
shape = RoundedCornerShape(50),
)
.clip(ButtonShape) //clip keeps animation within box borders
.clickable {
dialogState.show()
}
){
Row (
modifier = Modifier
.padding(16.dp),
){
Text(
text = "Trade Date",
modifier = Modifier
.weight(1F),
)
Icon(
Icons.Default.DateRange,
contentDescription = "Select Date",
)
}
}
}
Here is my screen where you select the datepicker field to display the datepicker dialog, it seems I'm trying to pass a LocalDate to the viewmodel but it requires a long(that's how I thought the date got returned from the dialog, as a long):
#Composable
fun AddEditTradeScreen(
navController: NavController,
viewModel: AddEditTradeViewModel = hiltViewModel()
){
val tradesymbolState = viewModel.tradeSymbol.value
val tradequantState = viewModel.tradeQuantity.value
val tradeDateState = viewModel.tradeDate
val scaffoldState = rememberScaffoldState()
val value: LocalDate
LaunchedEffect(key1 = true){
viewModel.eventFlow.collectLatest { event ->
when(event){
is AddEditTradeViewModel.UiEvent.ShowSnackbar ->{
scaffoldState.snackbarHostState.showSnackbar(
message = event.message
)
}
is AddEditTradeViewModel.UiEvent.SaveTrade -> {
navController.navigateUp()
}
}
}
}
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
viewModel.onEvent(AddEditTradeEvent.SaveTrade)
},
backgroundColor = MaterialTheme.colors.primary
) {
Icon(imageVector = Icons.Default.Save, contentDescription = "Save Trade")
}
},
scaffoldState = scaffoldState
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Enter Your Tradeeee",
style = MaterialTheme.typography.h4,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(20.dp))
TransparentHintTextField(
text = tradesymbolState.text,
hint = tradesymbolState.hint,
onValueChange = {
viewModel.onEvent(AddEditTradeEvent.EnteredSymbol(it))
},
onFocusChange = {
viewModel.onEvent(AddEditTradeEvent.ChangeSymbolFocus(it))
},
isHintVisible = tradesymbolState.isHintVisible,
singleLine = true,
textStyle = MaterialTheme.typography.h5
)
Spacer(modifier = Modifier.height(16.dp))
TransparentHintTextField(
text = tradequantState.text,
hint = tradequantState.hint,
onValueChange = {
viewModel.onEvent(AddEditTradeEvent.EnteredQuantity(it))
},
onFocusChange = {
viewModel.onEvent(AddEditTradeEvent.ChangeQuantityFocus(it))
},
isHintVisible = tradequantState.isHintVisible,
textStyle = MaterialTheme.typography.body1,
//modifier = Modifier.fillMaxHeight()
)
Spacer(modifier = Modifier.height(16.dp))
TradeDatePicker(
value = LocalDate.now(),
onValueChanged = {
viewModel.onEvent(AddEditTradeEvent.EnteredTradeDate(it))
},
modifier = Modifier
.fillMaxWidth()
)
}
}
}
This is my viewmodel that I'm trying to update:
#HiltViewModel
class AddEditTradeViewModel #Inject constructor(
private val tradeUseCases: TradeUseCases,
savedStateHandle: SavedStateHandle
) : ViewModel(){
private val _tradeSymbol = mutableStateOf(
TradeTextFieldState(
hint = "Enter Trade Symobol"
))
val tradeSymbol: State<TradeTextFieldState> = _tradeSymbol
private val _tradeQuantity = mutableStateOf(
TradeTextFieldState(
hint = "Enter Number of Shares"
))
val tradeQuantity: State<TradeTextFieldState> = _tradeQuantity
private var _tradeDate : String? by remember {
mutableStateOf(null)
}
val tradeDate = { date: Long? ->
_tradeDate = date?.toString()?:""
}
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private var currentTradeId: Int? = null
init{
savedStateHandle.get<Int>("tradeId")?.let { tradeId ->
if (tradeId != -1){
viewModelScope.launch {
tradeUseCases.getTrade(tradeId)?.also { trade ->
currentTradeId = trade.id
_tradeSymbol.value = tradeSymbol.value.copy(
text = trade.tradeSymb,
isHintVisible = false
)
_tradeQuantity.value = tradeQuantity.value.copy(
text = trade.tradequant,
isHintVisible = false
)
}
}
}
}
}
fun onEvent(event: AddEditTradeEvent){
when(event){
is AddEditTradeEvent.EnteredSymbol -> {
_tradeSymbol.value = tradeSymbol.value.copy(
text = event.value
)
}
is AddEditTradeEvent.ChangeSymbolFocus -> {
_tradeSymbol.value = tradeSymbol.value.copy(
isHintVisible = !event.focusState.isFocused &&
tradeSymbol.value.text.isBlank()
)
}
is AddEditTradeEvent.EnteredQuantity -> {
_tradeQuantity.value = tradeQuantity.value.copy(
text = event.value
)
}
is AddEditTradeEvent.ChangeQuantityFocus -> {
_tradeQuantity.value = tradeQuantity.value.copy(
isHintVisible = !event.focusState.isFocused &&
_tradeQuantity.value.text.isBlank()
)
}
is AddEditTradeEvent.EnteredTradeDate -> {
_tradeDate.value = tradeDate.value.copy(
text = event.value
)
}
is AddEditTradeEvent.SaveTrade -> {
viewModelScope.launch{
try {
tradeUseCases.addTrade(
Trade(
tradeSymb = tradeSymbol.value.text,
tradequant= tradeQuantity.value.text,
timestamp = System.currentTimeMillis(),
//tradestatus = "Long",
//color = MaterialTheme.colors.primary.green
id = currentTradeId
)
)
_eventFlow.emit(UiEvent.SaveTrade)
} catch (e: InvalidTradeException){
_eventFlow.emit(
UiEvent.ShowSnackbar(
message = e.message ?: "Couldn't save trade"
)
)
}
}
}
}
}
}
Lastly this is the event sealed class, not sure I'm using this correctly but in here for the "EnteredTradeDate" I declare it as a long and I'm not sure this is correct since I'm having trouble passing it from the screen to the viewmodel.
sealed class AddEditTradeEvent {
data class EnteredSymbol(val value: String): AddEditTradeEvent()
data class ChangeSymbolFocus(val focusState: FocusState): AddEditTradeEvent()
data class EnteredQuantity(val value: String): AddEditTradeEvent()
data class ChangeQuantityFocus(val focusState: FocusState): AddEditTradeEvent()
data class EnteredTradeDate(val value: Long) : AddEditTradeEvent()
data class ChangeDateFocus(val focusState: FocusState): AddEditTradeEvent()
object SaveTrade: AddEditTradeEvent()
}

How to make a #Preview in JetpackCompose, when the component depends of some data provide by ViewModel

I was developing an App where I try to implement some new technologies, as Jetpack Compose. And in general, it's a great tool, except the fact that it has hard pre-visualize system (#Preview) thn the regular xml design files.
My problem comes when I try to create a #Preview of the component which represent the different rows, where I load my data recover from network.
In my case I made this:
#Preview(
name ="ListScreenPreview ",
showSystemUi = true,
showBackground = true,
device = Devices.NEXUS_9)
#Composable
fun myPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
PokedexEntry(
model = pokemonMokData,
navController = rememberNavController(),
viewModel = hiltViewModel())
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Cacamon",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
To represent this #Composable:
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
Surface(
color = MaterialTheme.colors.background,
modifier = Modifier.fillMaxSize()
)
{
Column {
Spacer(modifier = Modifier.height(20.dp))
Image(
painter = painterResource(id = R.drawable.ic_international_pok_mon_logo),
contentDescription = "Pokemon",
modifier = Modifier
.fillMaxWidth()
.align(CenterHorizontally)
)
SearchBar(
hint = "Search...",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
viewModel.searchPokemonList(it)
}
Spacer(modifier = Modifier.height(16.dp))
PokemonList(navController = navController,
viewModel = viewModel)
}
}
}
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = " ",
onSearch: (String) -> Unit = { }
) {
var text by remember {
mutableStateOf("")
}
var isHintDisplayed by remember {
mutableStateOf(hint != "")
}
Box(modifier = modifier) {
BasicTextField(value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
.padding(horizontal = 20.dp, vertical = 12.dp)
.onFocusChanged {
isHintDisplayed = !it.isFocused
}
)
if (isHintDisplayed) {
Text(
text = hint,
color = Color.LightGray,
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 12.dp)
)
}
}
}
#Composable
fun PokemonList(
navController: NavController,
viewModel: PokemonListViewModel
) {
val pokemonList by remember { viewModel.pokemonList }
val endReached by remember { viewModel.endReached }
val loadError by remember { viewModel.loadError }
val isLoading by remember { viewModel.isLoading }
val isSearching by remember { viewModel.isSearching }
LazyColumn(contentPadding = PaddingValues(16.dp)) {
val itemCount = if (pokemonList.size % 2 == 0) {
pokemonList.size / 2
} else {
pokemonList.size / 2 + 1
}
items(itemCount) {
if (it >= itemCount - 1 && !endReached && !isLoading && !isSearching) {
viewModel.loadPokemonPaginated()
}
PokedexRow(rowIndex = it, models = pokemonList, navController = navController, viewModel = viewModel)
}
}
Box(
contentAlignment = Center,
modifier = Modifier.fillMaxSize()
) {
if (isLoading) {
CircularProgressIndicator(color = MaterialTheme.colors.primary)
}
if (loadError.isNotEmpty()) {
RetrySection(error = loadError) {
viewModel.loadPokemonPaginated()
}
}
}
}
#SuppressLint("LogNotTimber")
#Composable
fun PokedexEntry(
model: PokedexListModel,
navController: NavController,
modifier: Modifier = Modifier,
viewModel: PokemonListViewModel
) {
val defaultDominantColor = MaterialTheme.colors.surface
var dominantColor by remember {
mutableStateOf(defaultDominantColor)
}
Box(
contentAlignment = Center,
modifier = modifier
.shadow(5.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.aspectRatio(1f)
.background(
Brush.verticalGradient(
listOf(dominantColor, defaultDominantColor)
)
)
.clickable {
navController.navigate(
"pokemon_detail_screen/${dominantColor.toArgb()}/${model.pokemonName}/${model.number}"
)
}
) {
Column {
CoilImage(
imageRequest = ImageRequest.Builder(LocalContext.current)
.data(model.imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}.build(),
imageLoader = ImageLoader.Builder(LocalContext.current)
.availableMemoryPercentage(0.25)
.crossfade(true)
.build(),
contentDescription = model.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
loading = {
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val indicator = createRef()
CircularProgressIndicator(
//Set constrains dynamically
modifier = Modifier.constrainAs(indicator) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
}
},
// shows an error text message when request failed.
failure = {
Text(text = "image request failed.")
}
)
Log.d("pokemonlist", model.imageUrl)
Text(
text = model.pokemonName,
fontFamily = RobotoCondensed,
fontSize = 20.sp,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
#Composable
fun PokedexRow(
rowIndex: Int,
models: List<PokedexListModel>,
navController: NavController,
viewModel: PokemonListViewModel
) {
Column {
Row {
PokedexEntry(
model = models[rowIndex * 2],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
Spacer(modifier = Modifier.width(16.dp))
if (models.size >= rowIndex * 2 + 2) {
PokedexEntry(
model = models[rowIndex * 2 + 1],
navController = navController,
modifier = Modifier.weight(1f),
viewModel = viewModel
)
} else {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
#Composable
fun RetrySection(
error: String,
onRetry: () -> Unit,
) {
Column() {
Text(error, color = Color.Red, fontSize = 18.sp)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { onRetry() },
modifier = Modifier.align(CenterHorizontally)
) {
Text(text = "Retry")
}
}
}
I try to annotate with the #Nullable navController and viewmodel of the PokemonListScreen #Composable, but doesn't work either. I'm still seeing an empty screen:
So I try to search into the Jetpack documentation but, it's just defining quite simple Composables.
So if you have some more knowledge about it and can help, thanks in advance !
The main problem is if I wanna Preview that #Composable, although I made #Nullable to the viewmodel parameter, which I guess it's the problem here, AS still demand to initialize. Because I guess the right way to pass argument to a preview is by #PreviewArgument annotation.
[EDIT]
After some digging, I found AS is returning the following error under the Preview Screen:
So, there anyway to avoid viewmodel error??
[SOLUTION]
Finally a apply the following solution which make works, because the cause of the problem is due to Hilt have some inconpatibilities with Jetpack Compose previews:
Create an interface of the your ViewModel which recover all the variables and methods.
Make yourcurrent viemodel class extends of the interface.
Create a 2ยบ class which extends on the interface and pass that to your #Preview
#SuppressLint("UnrememberedMutableState")
#Preview(
name ="ListScreenPreview",
showSystemUi = true,
showBackground = true,
device = Devices.PIXEL)
#Composable
fun MyPokemonRowPreview(
#PreviewParameter(PokemonListScreenProvider::class) pokemonMokData: PokedexListModel
) {
JetpackComposePokedexTheme {
PokedexRow(
rowIndex = 0,
models = PokemonListScreenProvider().values.toList(),
navController = rememberNavController(),
viewModel = PokemonListViewModelMock(
0, mutableStateOf(""), mutableStateOf(value = false),
mutableStateOf(false), mutableStateOf(listOf(pokemonMokData))
)
)
}
}
class PokemonListScreenProvider: PreviewParameterProvider<PokedexListModel> {
override val values: Sequence<PokedexListModel> = sequenceOf(
PokedexListModel(
pokemonName = "Machasaurio",
number = 0,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png"
),
PokedexListModel(
pokemonName = "Tontaro",
number = 73,
imageUrl = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png"
)
)
}
PokemonListViewModelInterface
interface PokemonListViewModelInterface {
var curPage : Int
var loadError: MutableState<String>
var isLoading: MutableState<Boolean>
var endReached: MutableState<Boolean>
var pokemonList: MutableState<List<PokedexListModel>>
fun searchPokemonList(query: String)
fun loadPokemonPaginated()
fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit)
}
PokemonListViewModelMock
class PokemonListViewModelMock (
override var curPage: Int,
override var loadError: MutableState<String>,
override var isLoading: MutableState<Boolean>,
override var endReached: MutableState<Boolean>,
override var pokemonList: MutableState<List<PokedexListModel>>
): PokemonListViewModelInterface{
override fun searchPokemonList(query: String) {
TODO("Not yet implemented")
}
override fun loadPokemonPaginated() {
TODO("Not yet implemented")
}
override fun calcDominantColor(drawable: Drawable, onFinish: (Color) -> Unit) {
TODO("Not yet implemented")
}
}
The actual Preview is the following, and although the image doesn't display, is shown correctly:
You could create another composable which invokes the viewmodel logic via lambda functions instead of using the viewmodel itself. Extract your uiState to a separate class, so it can be used as a StateFlow in your viewmodel, which in turn can be observed from the composable.
#Composable
fun PokemonListScreen(
navController: NavController,
viewModel: PokemonListViewModel
) {
/*
rememberStateWithLifecyle is an extension function based on
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
*/
val uiState by rememberStateWithLifecycle(viewModel.uiState)
PokemonListScreen(
uiState = uiState,
onLoadPokemons = viewModel::loadPokemons,
onSearchPokemon = {viewModel.searchPokemon(it)},
onCalculateDominantColor = {viewModel.calcDominantColor(it)},
onNavigate = {route -> navController.navigate(route, null, null)},
)
}
#Composable
private fun PokemonListScreen(
uiState: PokemonUiState,
onLoadPokemons:()->Unit,
onSearchPokemon: (String) -> Unit,
onCalculateDominantColor: (Drawable) -> Color,
onNavigate:(String)->Unit,
) {
}
#HiltViewModel
class PokemonListViewModel #Inject constructor(/*your datasources*/) {
private val loading = MutableStateFlow(false)
private val loadError = MutableStateFlow(false)
private val endReached = MutableStateFlow(false)
private val searching = MutableStateFlow(false)
private val pokemons = MutableStateFlow<Pokemon?>(null)
val uiState: StateFlow<PokemonUiState> = combine(
loading,
loadError,
endReached,
searching,
pokemons
) { loading, error, endReached, searching, pokemons ->
PokemonUiState(
isLoading = loading,
loadError = error,
endReached = endReached,
isSearching = searching,
pokemonList = pokemons,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = PokemonUiState.Empty,
)
}
data class PokemonUiState(
val pokemonList: List<Pokemon> = emptyList(),
val endReached: Boolean = false,
val loadError: Boolean = false,
val isLoading: Boolean = false,
val isSearching: Boolean = false,
) {
companion object {
val Empty = PokemonUiState()
}
}
I'm not sure of the depth of this application, but a potential idea would be to code to an interface and not an implementation.
That is, create an interface with all of the functions you need (that may already exist in your ViewModel), have your PokemonListViewModel implement it, and create another mock class that implements it as well. Pass the mock into your preview and leave the real implementation with PokemonListViewModel
interface PokeListViewModel {
...
// your other val's
val isLoading: Boolean
fun searchPokemonList(pokemon: String)
fun loadPokemonPaginated()
// your other functions
...
}
Once you create your interface you can simply update your composables to be expecting an object that "is a" PokeListViewModel, for example.
Hopefully this helps

dynamically change text in cards - Jetpack Compose state

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
}

Categories

Resources