I want to read from the Room database as updates are made and render in Compose UI. I am only getting the initial state and updates to the table are not showing up unless I go to the previous screen and come back. What am I doing wrong?
Note: I understand I need to have a ViewModel in between but I suppose the issue will still be there.
UI:
#Composable
fun ListScreen(itemKey: String, itemRepository: ItemRepository) {
val itemList by itemRepository.getItemData(itemKey).collectAsState(initial = listOf())
LazyColumn(
modifier = Modifier.padding(vertical = 20.dp, horizontal = 10.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(itemList) {
Text(it, style = MaterialTheme.typography.body2)
}
}
}
Repository:
fun getItemData(itemKey: String): Flow<List<String>> = dao.getItems(itemKey)
DAO:
#Query("SELECT value FROM ItemEntity WHERE itemKey = :itemKey")
fun getItems(itemKey: String): Flow<List<String>>
Found the issue. The code that writes to the database and the one that reads from it were using different RoomDatabase objects. After making sure they are the same, live updates started to show up.
Related
I’ve got a problem with a LazyColumn of elements that have a favourite button: basically when I tap the favourite button, the item that is being favourited (a document in my case) is changed in the underlying data structure in the VM, but the view isn’t updated, so I never see any change in the button state.
class MainViewModel(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : ViewModel() {
var documentList = emptyList<PDFDocument>().toMutableStateList()
....
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
}
}
The composables are:
#Composable
fun DocumentRow(
document: PDFDocument,
onDocumentClicked: (String, Boolean) -> Unit,
onFavoriteValueChange: (Uri) -> Unit
) {
HeartIcon(
isFavorite = document.favorite,
onValueChanged = { onFavoriteValueChange(document.uri) }
)
}
#Composable
fun HeartIcon(
isFavorite: Boolean,
color: Color = Color(0xffE91E63),
onValueChanged: (Boolean) -> Unit
) {
IconToggleButton(
checked = isFavorite,
onCheckedChange = {
onValueChanged()
}
) {
Icon(
tint = color,
imageVector = if (isFavorite) {
Icons.Filled.Favorite
} else {
Icons.Default.FavoriteBorder
},
contentDescription = null
)
}
}
Am I doing something wrong? because when I call the toggleFavouriteDocument in the ViewModel, I see it’s marked or unmarked as favorite but there is no recomposition at all anywhere.
I might be missing it because you didn't post the rest of your code, but your documentList in the VM isn't observable, so how would the Composable know that it got changed? It needs to be something like Flow or LiveData, and it needs to be observed in the Composable. Something like this:
in ViewModel:
val documentList = MutableLiveData<List<PDFDocument>>()
in Composable:
val documentList by viewModel.documentList.observeAsState(List<PDFDocument>())
And you'll probably have to change the way you modify items in documentList. LiveData is weird about mutable collections inside MutableLiveData, and modifying individual items doesn't trigger a state change. You have to create a copy of the list with the modified items, and then re-port the whole list to the LiveData variable:
fun toggleFavoriteDocument(pdfDocument: PDFDocument) {
documentList.value?.let { oldList ->
// create a copy of existing list
val newList = mutableListOf<PDFDocument>()
newList.addAll(oldList)
// modify the item in the new list
newList.find {
it == pdfDocument
}?.let {
it.favorite = !it.favorite
}
// update the observable
documentList.postValue(newList)
}
}
Edit: There's also a potential problem with the way that you're trying to update the favorite value in the existing list. Without knowing how PDFDocument is implemented, I don't know if you can use the = operator. You should test that to make sure that newList.find { it == pdfDocument } actually finds the document
I have a state class
object SomeState {
data class State(
val mainPhotos: List<S3Photo>? = emptyList(),
)
}
VM load data via init and updates state
class SomeViewModel() {
var viewState by mutableStateOf(SomeState.State())
private set
init {
val photos = someSource.load()
viewState = viewState.cope(mainPhotos = photos)
}
}
Composable takes data from state
#Composable
fun SomeViewFun(
state = SomeState.State
) {
HorizontalPager(
count = state .mainPhotos?.size ?: 0,
) {
//view items
}
}
The problem is that count in HorizontalPager always == 0, but in logcat and debugger i see that list.size() == 57
I have a lot of screen with arch like this and they works normaly. But on this screen view state doesn't updates and i can't understand why.
UPDATE
VM passes to Composable like this
#Composable
fun SomeDistanation() {
val viewModel: SomeViewModel = hiltViewModel()
SomeViewFun(
state = viewModel.state
)
}
Also Composable take Flow<ViewEffect> and etc, but in this question it doesn't matter, because there is no user input or side effects
UPDATE 2
The problem was in data source. All code in question work correctly. Problem closed.
object wrapping is completely redundant (no fields, no functions), you can remove it (also, change the name so it won't confuse with compose's State):
data class MyState(
val mainPhotos: List<S3Photo>? = emptyList(),
)
According to Android Developers, you need to create the state in the view model, and observe the state in the composable function - your code is a bit unclear for me so I'll just show you how I do it in my apps.
create the state in the view model:
class SomeViewModel() {
private val viewState = mutableStateOf(MyState())
// Expose as immutable so it won't be edited
fun getState(): State<MyState> = viewState
init {
val photos = someSource.load()
viewState.value = viewState.value.copy(mainPhotos = photos)
}
}
observe the state in the composable function:
#Composable
fun SomeDistanation() {
val viewModel: SomeViewModel = hiltViewModel()
val state: MyState by remember { viewModel.getState() }
SomeViewFun(state)
}
Now you'll get automatic recomposition in case the state changes.
Whenever I try to remove from the lazy column list I get arrayIndexOutOfBoundException
This is the array that
var productsList = remember { mutableStateListOf<Product>() }//I load products in this list
Whenever the user presses a certain button I do the following
productsList.remove(item)
I get array arrayIndexOutOfBoundException this is how I loop as well
itemsIndexed(productsList) { index, item ->
Anyway to avoid that error
Whole code for those interested:
fun MyProducts(navController: NavController,myProductsViewModel: MyProductsViewModel= viewModel()) {
var productsList = remember { mutableStateListOf<Product>() }
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
var currentImage = remember { mutableStateListOf<Int>() }
LaunchedEffect(key1 = Unit){
myProductsViewModel.getShop()
productsList.addAll(myProductsViewModel.productsList)
currentImage.addAll(List(productsList.size) {0})
}
var pickedImage: MutableState<String?> =remember { mutableStateOf("") }
BackHandler() {
navController.popBackStack()
}
LazyColumn(
Modifier
.fillMaxSize()
.padding(start = 16.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(8.dp)
,state = listState
) {
itemsIndexed(productsList) { index, item ->
.clickable {when(icon){
Icons.Default.Delete->{
scope.launch {
myProductsViewModel.removeProducts(item.product_id,item.shop_id,item)
productsList.remove(item)
}
}
Icons.Default.Edit->{
}
I am accessing the same list in the rest of the code but I don't think it is relevant to the problem
You have not provided a proper code example but it sounds like you are not updating the list and the lazy column properly so the item is removed from the list but the lazy column does not know about it.
From my point of view you are generally having some bad practice in your code. For example:
LaunchedEffect(key1 = Unit){
myProductsViewModel.getShop()
productsList.addAll(myProductsViewModel.productsList)
currentImage.addAll(List(productsList.size) {0})
}
This all looks like stuff that should be done in the viewmodel. Generally a composable is supposed to convert state to UI, that means, it shouldnt contain any business logic. The code snipped I just showed looks like something that can be done in the viewmodel.
Having something like this
var productsList = remember { mutableStateListOf<Product>() }
and then adding the elements with a LaunchedEffect is not how composables are supposed to be used.
First your myProductsViewModel.productsList should be a LiveData object thats holding your product list. Then you are supposed to do the following:
val productList by myProductsViewModel.productsList.observeAsState(emptyList())
Then you show it in your composable. If you want to change the list content, you should call a method for that on the viewmodel, which then updates the livedata object of your list accordingly.
I you hope I explained in clearly enough. Let me know if you have questions.
I am using Android Jetpack's Compose and have been trying to figure out how to save state for orientation changes.
My train of thought was making a class a ViewModel. As that generally worked when I would work with Android's traditional API.
I have used remember {} and mutableState {} to update the UI when information has been changed.
Please validate if my understanding is correct...
remember = Saves the variable and allows access via .value, this allows values to be cache. But its main use is to not reassign the variable on changes.
mutableState = Updates the variable when something is changed.
Many blog posts say to use #Model, however, the import gives errors when trying that method.
So, I added a : ViewModel()
However, I believe my remember {} is preventing this from working as intended?
Can I get a point in the right direction?
#Composable
fun DefaultFlashCard() {
val flashCards = remember { mutableStateOf(FlashCards())}
Spacer(modifier = Modifier.height(30.dp))
MaterialTheme {
val typography = MaterialTheme.typography
var question = remember { mutableStateOf(flashCards.value.currentFlashCards.question) }
Column(modifier = Modifier.padding(30.dp).then(Modifier.fillMaxWidth())
.then(Modifier.wrapContentSize(Alignment.Center))
.clip(shape = RoundedCornerShape(16.dp))) {
Box(modifier = Modifier.preferredSize(350.dp)
.border(width = 4.dp,
color = Gray,
shape = RoundedCornerShape(16.dp))
.clickable(
onClick = {
question.value = flashCards.value.currentFlashCards.answer })
.gravity(align = Alignment.CenterHorizontally),
shape = RoundedCornerShape(2.dp),
backgroundColor = DarkGray,
gravity = Alignment.Center) {
Text("${question.value}",
style = typography.h4, textAlign = TextAlign.Center, color = White
)
}
}
Column(modifier = Modifier.padding(16.dp),
horizontalGravity = Alignment.CenterHorizontally) {
Text("Flash Card application",
style = typography.h6,
color = Black)
Text("The following is a demonstration of using " +
"Android Compose to create a Flash Card",
style = typography.body2,
color = Black,
textAlign = TextAlign.Center)
Spacer(modifier = Modifier.height(30.dp))
Button(onClick = {
flashCards.value.incrementQuestion();
question.value = flashCards.value.currentFlashCards.question },
shape = RoundedCornerShape(10.dp),
content = { Text("Next Card") },
backgroundColor = Cyan)
}
}
}
data class Question(val question: String, val answer: String) {
}
class FlashCards: ViewModel() {
var flashCards = mutableStateOf( listOf(
Question("How many Bananas should go in a Smoothie?", "3 Bananas"),
Question("How many Eggs does it take to make an Omellete?", "8 Eggs"),
Question("How do you say Hello in Japenese?", "Konichiwa"),
Question("What is Korea's currency?", "Won")
))
var currentQuestion = 0
val currentFlashCards
get() = flashCards.value[currentQuestion]
fun incrementQuestion() {
if (currentQuestion + 1 >= flashCards.value.size) currentQuestion = 0 else currentQuestion++
}
}
There is another approach to handle config changes in Compose, it is rememberSaveable. As docs says:
While remember helps you retain state across recompositions, the state is not retained across configuration changes. For this, you must use rememberSaveable. rememberSaveable automatically saves any value that can be saved in a Bundle. For other values, you can pass in a custom saver object.
It seems that Mohammad's solution is more robust, but this one seems simpler.
UPDATE:
There are 2 built-in ways for persisting state in Compose:
remember: exists to save state in Composable functions between recompositions.
rememberSaveable: remember only save state across recompositions and doesn't handle configuration changes and process death, so to survive configuration changes and process death you should use remeberSaveable instead.
But there are some problems with rememberSaveable too:
Supports primitive types out of the box, but for more complex data, like data class, you must create a Saver to explain how to persist state into bundle,
rememberSaveable uses Bundle under the hood, so there is a limit of how much data you can persist in it, if data is too large you will face TransactionTooLarge exception.
with above said, below solutions are available:
setting android:configChangesin Manifest to avoid activity recreation in configuration changes. (not useful in process death, also doesn't save you from being recreated in Wallpaper changes in Android 12)
Using a combination of ViewModel + remeberSaveable + data persistance in storage
=======================================================
Old answer
Same as before, you can use Architecture Component ViewModel to survive configuration changes.
You should initialize your ViewModel in Activity/Fragment and then pass it to Composable functions.
class UserDetailFragment : Fragment() {
private val viewModel: UserDetailViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(context = requireContext()).apply {
setContent {
AppTheme {
UserDetailScreen(
viewModel = viewModel
)
}
}
}
}
}
Then your ViewModel should expose the ViewState by something like LiveData or Flow
UserDetailViewModel:
class UserDetailViewModel : ViewModel() {
private val _userData = MutableLiveData<UserDetailViewState>()
val userData: LiveData<UserDetailViewState> = _userData
// or
private val _state = MutableStateFlow<UserDetailViewState>()
val state: StateFlow<UserDetailViewState>
get() = _state
}
Now you can observe this state in your composable function:
#Composable
fun UserDetailScreen(
viewModel:UserDetailViewModel
) {
val state by viewModel.userData.observeAsState()
// or
val viewState by viewModel.state.collectAsState()
}
So, I have implemented a lazycolumnfor to work with a list of recipe elements, the thing is that it does not smooth scroll, if I just scroll fast it stutters till the last element appears and not smooth scroll.
Is this an error from my side or do I need to add something else?
data class Recipe(
#DrawableRes val imageResource: Int,
val title: String,
val ingredients: List<String>
)
val recipeList = listOf(
Recipe(R.drawable.header,"Cake1", listOf("Cheese","Sugar","water")),
Recipe(R.drawable.header,"Cake2", listOf("Cheese1","Sugar1","Vanilla")),
Recipe(R.drawable.header,"Cake3", listOf("Bread","Sugar2","Apple")))
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RecipeList(recipeList = recipeList)
}
}
}
#Composable
fun RecipeCard(recipe:Recipe){
val image = imageResource(R.drawable.header)
Surface(shape = RoundedCornerShape(8.dp),elevation = 8.dp,modifier = Modifier.padding(8.dp)) {
Column(modifier = Modifier.padding(16.dp)) {
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth().clip(shape = RoundedCornerShape(8.dp))
Image(asset = image,modifier = imageModifier,contentScale = ContentScale.Crop)
Spacer(modifier = Modifier.preferredHeight(16.dp))
Text(text = recipe.title,style = typography.h6)
for(ingredient in recipe.ingredients){
Text(text = ingredient,style = typography.body2)
}
}
}
}
#Composable
fun RecipeList(recipeList:List<Recipe>){
LazyColumnFor(items = recipeList) { item ->
RecipeCard(recipe = item)
}
}
#Preview
#Composable
fun RecipePreview(){
RecipeCard(recipeList[0])
}
Currently (version 1.0.0-alpha02) Jetpack Compose has 2 Composable functions for loading image resources:
imageResource(): this Composable function, load an image resource synchronously.
loadImageResource(): this function loads the image in a background thread, and once the loading finishes, recompose is scheduled and this function will return deferred image resource with LoadedResource or FailedResource
So your lazyColumn is not scrolling smoothly since you are loading images synchronously.
So you should either use loadImageResource() or a library named Accompanist by Chris Banes, which can fetch and display images from external sources, such as network, using the Coil image loading library.
UPDATE:
Using CoilImage :
First, add Accompanist Gradle dependency, then simply use CoilImage composable function:
CoilImage(data = R.drawable.header)
Using loadImageResource() :
val deferredImage = loadImageResource(
id = R.drawable.header,
)
val imageModifier = Modifier.preferredHeight(150.dp).fillMaxWidth()
.clip(shape = RoundedCornerShape(8.dp))
deferredImage.resource.resource?.let {
Image(
asset = it,
modifier = imageModifier
)
}
Note: I tried both ways in a LazyColumnFor, and although loadImageResource() performed better than imageResource() but still it didn't scroll smoothly.
So I highly recommend using CoilImage
Note 2: To use Glide or Picasso, check this repository by Vinay Gaba
On the other note, LazyColumn haven't been optimised for scrolling performance yet, but I've just tested on 1.0.0-beta07 release and can confirm it's way smoother than 1.0.0-beta06
Compose.UI 1.0.0-beta07 relevant change log:
LazyColumn/Row will now keep up to 2 previously visible items active (not disposed) even when they are scrolled out already. This allows the component to reuse the active subcompositions when we will need to compose a new item which improves the scrolling performance. (Ie5555)