Related
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)
}
}
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()
}
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
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
}
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") }
)
}
}