How to shuffle list whenever click is triggered - android

I have Json Data through which I'm doing this .
fun getFact(context: Context) = viewModelScope.launch{
try {
val format = Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
}
val factJson = context.assets.open("Facts.json").bufferedReader().use {
it.readText()
}
val factList = format.decodeFromString<List<FootballFact>>(factJson)
_uiState.value = ViewState.Success(factList)
} catch (e: Exception) {
_uiState.value = ViewState.Error(exception = e)
}
}
This is the way i m getting my job from viewModle in Ui sceeen
viewModel.getFact(context)
when (val result =
viewModel.uiState.collectAsState().value) {
is ViewState.Error -> {
Toast.makeText(
context,
"Error ${result.exception}",
Toast.LENGTH_SHORT
).show()
}
is ViewState.Success -> {
val factsLists = mutableStateOf(result.fact)
val randomFact = factsLists.value[0]
FactCard(quote = randomFact.toString()) {
factsLists.value.shuffled()
}
}
}
I have fact card where i want to show that fact .also i have there a lambda for click where i want my factList to refresh every time whenever is clicked.
#Composable
fun FactCard(quote: String , onClick : ()-> Unit) {
val fact = remember { mutableStateOf(quote)}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.
.clickable { onClick() }
) {
Text(.. )
}
}
I don't know how to approach this, i think there is silly thing I'm doing.

factsLists.shuffled() returns a new list with the elements of this list randomly shuffled.

Composables can only recompose when you update state data. You aren't doing that. Your click event should return the new quote that you want to display. You then set fact.value to the new quote. Calling fact.value with a new value is what triggers a recompose:
when (val result = viewModel.uiState.collectAsState().value) {
is ViewState.Error -> {
Toast.makeText(
context,
"Error ${result.exception}",
Toast.LENGTH_SHORT
).show()
}
is ViewState.Success -> {
val factsLists = mutableStateOf(result.fact)
val randomFact = factsLists.value[0]
FactCard(quote = randomFact.toString()) {
return factsLists.value.shuffled()[0]
}
}
}
#Composable
fun FactCard(quote: String , onClick : ()-> String) {
var fact = remember { mutableStateOf(quote)}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.
.clickable {
fact.value = onClick()
}
) {
Text(.. )
}
}

Related

Kotlin Channel does not collect value

I'm trying to send a variable value from my ViewModel to my composable screen. I tried using the debugger to find out where it gets stuck. It seems like it sends the value but never actually receives it.
This is the code I'm using:
NewEvent.kt
#Composable
fun NewEvent(
viewModel: NewEventViewModel = viewModel(),
navController: NavController
){
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
}
changeEventType() gets called here
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}
NewEventViewModel.kt
private val newEventTypeChannel = Channel<EventType>()
val newEventType = newEventTypeChannel.receiveAsFlow()
fun changeEventType(newEventType: EventType){
viewModelScope.launch {
newEventTypeChannel.send(newEventType)
}
}
I downloaded a sample project from GitHub using this exact implementation and it worked, I'm not sure what I'm missing here.
If you want to display toast message, you don't have to create separate composable function for it. Since Toast is dynamic and does not need to be recomposed, you don't need composable function for it.
It would be more clear and better to implement it like this (in one composable function)
val context = LocalContext.current
LaunchedEffect(context){
viewModel.newEventType.collect { eventType ->
Toast.makeText(context, eventType.toString(), Toast.LENGTH_SHORT).show()
}
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false },
) {
eventTypeList.forEach {
if(it != viewModel.event.eventType && it != EventType.UNKNOWN) {
DropdownMenuItem(
onClick = { viewModel.changeEventType(it); menuExpanded = false },
text = { Text(stringResource(context.resources.getIdentifier(it.toString().lowercase(), "string", context.packageName))) }
)
}
}
}

Handling state using list in android Jetpack compose

I am trying to update a list which is kept as state of a composable view, but the composable view is not recomposed even though the values of list are changed.
var list = remember {mutableStateOf(getListOfItems())}
ItemListView({ selectedItem ->
list.value = updateList(selectedItem, list.value)
}, list.value)
private fun updateList(selectedItem: String,
itemsList: List<Product>): List<Product> {
for (item in itemsList){
// selected item in the view is updated
item.selected = item.name == selectedItem
}
return itemsList
}
Any idea why the composable is not getting updated? I noticed the issue happens when using a List as state.
You need to update value of MutableState, which is a List in your example, to trigger recomposition not any property of value you set to MutableState.
#Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
But there is another function called mutableStateListOf() which can trigger recomposition when you add or remove items, or update existing one with new instance.
val list =remember {
mutableStateListOf< Product>().apply {
addAll(getListOfItems())
}
}
The jetpack compose looks at your object itself to see if it has changed to decide whether to update it or not. You need to update the list itself to do that:
data class A(var v: Int)
#Composable
fun Test1() {
var list by remember { mutableStateOf(listOf(A(1), A(2), A(3))) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
Text(text = it.toString())
}
item {
// it does not work
Button(onClick = {
list[0].v = 2
}) { Text("Change value") }
// it works
Button(onClick = {
list = list.map { it.copy(v = it.v + 1) }
}) { Text("Change list") }
}
}
}
Also, you can use mutableStateListOf, which will monitor the addition and removal of elements.
#Composable
fun Test2() {
val list = remember { mutableStateListOf(A(1), A(2), A(3)) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
Text(text = it.toString())
}
item {
// it not work
Button(onClick = {
list[0].v = 2
}) { Text("Change value") }
// it work
Button(onClick = {
list.add(A(3))
}) { Text("Change list") }
}
}
}
In your case, you can represent the selection like this:
val selectedList = remember { mutableStateListOf<String>() }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
val selected = selectedList.contains(it.name)
Text(text = if (selected) "selected" else "not selected")
Button(onClick = {
if (!selected) selectedList.add(it.name)
}) { Text("Select it") }
}
}
Fixed this by taking a copy of list with new values :
private fun updateList(selectedItem: String,
itemsList: List<Product>): List<Product> {
val updatedList = itemsList.map { product ->
if(product.name == selectedItem) {
product.copy(selected = true)
} else {
product.copy(selected = false)
}
}
return updatedList
}

How to add item in LazyColumn in jetpack compose?

In the following viewModel code I am generating a list of items from graphQl server
private val _balloonsStatus =
MutableStateFlow<Status<List<BalloonsQuery.Edge>?>>(Status.Loading())
val balloonsStatus get() = _balloonsStatus
private val _endCursor = MutableStateFlow<String?>(null)
val endCursor get() = _endCursor
init {
loadBalloons(null)
}
fun loadBalloons(cursor: String?) {
viewModelScope.launch {
val data = repo.getBalloonsFromServer(cursor)
if (data.errors == null) {
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
_endCursor.value = data.data?.balloons?.pageInfo?.endCursor
} else {
_balloonsStatus.value = Status.Error(data.errors!![0].message)
_endCursor.value = null
}
}
}
and in the composable function I am getting this data by following this code:
#Composable
fun BalloonsScreen(
navHostController: NavHostController? = null,
viewModel: SharedBalloonViewModel
) {
val endCursor by viewModel.endCursor.collectAsState()
val balloons by viewModel.balloonsStatus.collectAsState()
AssignmentTheme {
Column(modifier = Modifier.fillMaxSize()) {
when (balloons) {
is Status.Error -> {
Log.i("Reyjohn", balloons.message!!)
}
is Status.Loading -> {
Log.i("Reyjohn", "loading..")
}
is Status.Success -> {
BalloonList(edgeList = balloons.data!!, navHostController = navHostController)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { viewModel.loadBalloons(endCursor) }) {
Text(text = "Load More")
}
}
}
}
#Composable
fun BalloonList(
edgeList: List<BalloonsQuery.Edge>,
navHostController: NavHostController? = null,
) {
LazyColumn {
items(items = edgeList) { edge ->
UserRow(edge.node, navHostController)
}
}
}
but the problem is every time I click on Load More button it regenerates the view and displays a new set of list, but I want to append the list at the end of the previous list. As far I understand that the list is regenerated as the flow I am listening to is doing the work behind this, but I am stuck here to get a workaround about how to achieve my target here, a kind hearted help would be much appreciated!
You can create a private list in ViewModel that adds List<BalloonsQuery.Edge>?>
and instead of
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
you can do something like
_balloonsStatus.value = Status.Success(myLiast.addAll(
data.data?.balloons?.edges))
should update Compose with the latest data appended to existing one

LazyColumn items stay in same position after some items removed

I have LazyColumn with swipe-to-delete. When I swipe an item, it is deleted by viewModel. The problem is that if I swipe the item away, the LazyColumn doesn't update the position of other items (as shown in GIF).
Here's my code implementation:
#ExperimentalMaterialApi
#Composable
fun Screen() {
val livedata = viewModel.itemsLiveData.observeAsState()
val stateList = remember { mutableStateListOf<Data>() }
stateList.addAll(livedata.value!!)
SwipableLazyColumn(stateList)
}
#ExperimentalMaterialApi
#Composable
fun SwipableLazyColumn(
stateList: SnapshotStateList<Data>
) {
LazyColumn {
items(items = stateList) { item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(EndToStart) || dismissState.isDismissed(StartToEnd)) {
viewModel.swipeToDelete(item)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(StartToEnd, EndToStart),
dismissThresholds = {
FractionalThreshold(0.25f)
},
background = {},
dismissContent = {
MyData(item)
}
)
}
}
}
I use SnapshotStateList as it's suggested here. Although I don't use swapList because it clears out all items
ViewModel:
class MyViewModel #Inject internal constructor(
private val itemRepository: ItemRepository
) : BaseViewModel(), LifecycleObserver {
private val itemsList = mutableListOf<MyData>()
private val _itemsLiveData = MutableLiveData<List<MyData>>()
val itemsLiveData: LiveData<List<MyData>> = _itemsLiveData
init {
loadItems()
}
private fun loadItems() {
viewModelScope.launch {
itemRepository.getItems().collect {
when (it) {
is Result.Success -> onItemsLoaded(it.data)
is Result.Error -> {
onItemsLoaded(emptyList())
}
}
}
}
}
private fun onItemsLoaded(itemsList: List<MyData>) {
itemsList.clear()
itemsList.addAll(notifications)
_itemsLiveData.value = if (itemsList.isNotEmpty()) {
itemsList
} else {
null
}
}
fun swipeToDelete(item: MyData) {
if (itemsList.size == 0) return
viewModelScope.launch {
when (
val result =
itemRepository.deletelItem(item)
) {
is Result.Success -> {
onItemDeleted(item)
}
is Result.Error -> {
showSnackBar(
"error"
)
}
}
}
}
private fun onItemDeleted(item: MyData) {
itemsList.remove(item)
_itemsLiveData.value = itemsList
}
}
You should refresh your list inside viemodel(delete item) and return modified list right here
var tempList = itemList
ItemList.clear()
ItemList.addAll(tempList)
You need to provide key for the LazyColumn's items.
By default, each item's state is keyed against the position of the
item in the list. However, this can cause issues if the data set
changes, since items which change position effectively lose any
remembered state.
Example
LazyColumn {
items(
items = stateList,
key = { _, listItem ->
listItem.hashCode()
},
) { item ->
// As it is ...
}
}
Reference

Jetpack compose update list element

I am currently trying to write an App for my thesis and currently, I am looking into different approaches. Since I really like Flutter and the Thesis requires me to use Java/Kotlin I would like to use Jetpack compose.
Currently, I am stuck trying to update ListElements.
I want to have a List that shows Experiments and their state/result. Once I hit the Button I want the experiments to run and after they are done update their state. Currently, the run Method does nothing besides setting the state to success.
The problem is I don't know how to trigger a recompose from the viewModel of the ExperimentRow once an experiment updates its state.
ExperimentsActivity:
class ExperimentsActivity : AppCompatActivity() {
val exViewModel by viewModels<ExperimentViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//For now this is just Dummy Data and will be replaced
exViewModel.experiments += listOf(
Experiment("Test1", exViewModel::experimentStateChanged),
Experiment("Strongbox", exViewModel::experimentStateChanged)
)
setContent {
TpmTheme {
// A surface container using the 'background' color from the theme
Surface {
ExperimentScreen(
exViewModel.experiments,
exViewModel::startTests
)
}
}
}
}
}
ExperimentViewModel:
class ExperimentViewModel : ViewModel() {
var experiments by mutableStateOf(listOf<Experiment>())
fun startTests() {
for (exp in experiments) {
exp.run()
}
}
fun experimentStateChanged(experiment: Experiment) {
Log.i("ViewModel", "Changed expState of ${experiment.name} to ${experiment.state}")
// HOW DO I TRIGGER A RECOMPOSE OF THE EXPERIMENTROW FOR THE experiment????
//experiments = experiments.toMutableList().also { it.plus(experiment) }
Log.i("Vi", "Size of Expirments: ${experiments.size}")
}
}
ExperimentScreen:
#Composable
fun ExperimentScreen(
experiments: List<Experiment>,
onStartExperiments: () -> Unit
) {
Column {
LazyColumnFor(
items = experiments,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp),
) { ep ->
ExperimentRow(
experiment = ep,
modifier = Modifier.fillParentMaxWidth(),
)
}
Button(
onClick = { onStartExperiments() },
modifier = Modifier.padding(16.dp).fillMaxWidth(),
) {
Text("Run Tests")
}
}
}
#Composable
fun ExperimentRow(experiment: Experiment, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(experiment.name)
Icon(
asset = experiment.state.vAsset,
)
}
Experiment:
class Experiment(val name: String, val onStateChanged: (Experiment) -> Unit) {
var state: ExperimentState = ExperimentState.DEFAULT
set(value) {
field = value
onStateChanged(this)
}
fun run() {
state = ExperimentState.SUCCESS;
}
}
enum class ExperimentState(val vAsset: VectorAsset) {
DEFAULT(Icons.Default.Info),
RUNNING(Icons.Default.Refresh),
SUCCESS(Icons.Default.Done),
FAILED(Icons.Default.Warning),
}
There's a few ways to address this but key thing is that you need to add a copy of element (with state changed) to experiments to trigger the recomposition.
One possible example would be
data class Experiment(val name: String, val state: ExperimentState, val onStateChanged: (Experiment) -> Unit) {
fun run() {
onStateChanged(this.copy(state = ExperimentState.SUCCESS))
}
}
and then
fun experimentStateChanged(experiment: Experiment) {
val index = experiments.toMutableList().indexOfFirst { it.name == experiment.name }
experiments = experiments.toMutableList().also {
it[index] = experiment
}
}
though I suspect there's probably cleaner way of doing this.

Categories

Resources