No code, just a theoretical question.
The scenario is this
Room database with one table which houses the fields necessary.
Flow is setup to watch for database changes and report back to the UI.
So. sitting on page
/user/5
There is a delete button. Click the delete button the event passes up to the viewmodel and the viewmodel reacts.
UIDisplayUserDetails.kt
#composable
Button {
onclick:
event -> deleteClicked
navigation("userlist")
==================
viewmodel.kt
event deleteclicked {
repository.deleteUser(5)
}
=================
The design flaw is as follows.
The user deleted from the Room database works great. But that change in the Room database causes the Flow to recognize a change in the table in the Room database. That in turn sends the changes down stream. Which in turn causes a recompose.
The recompose causes a recompose on /user/5 which no longer exists in the database. Then things break.
What would be the correct method to delete a record from the Room database without causing a recompose. or the correct way to think about how to do this.
thanks
Tried to delete a record from the Room database, which works, but that causes a recompose. I either don't want the recompose, or want a new way to think about this design pattern
If you set things up like below then it should recompose UsersRow() from the state of users changing which would then leave out user 5 entirely when it is deleted from Room. This would avoid a crash I beleve.
#Composable
fun UsersRow(context: Context, viewModel: UserViewModel) {
val users by viewModel.getUsers().observeAsState() // Observing livedata/flow
Row {
users.forEach { user ->
UserCard(user)
}
}
}
List docs
State and Jetpack Compose
Related
Say that, I'm building a custom compose layout and populating that list as below
val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
list.forEach { item ->
key(item){
listItemCompose( data = item,
onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
}
}
This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following
{index1,index2 -> list[index1].propertyName = true}
Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.
You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.
The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.
Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.
This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.
you can recreate an entire composition using this
val text = remember { mutableStateOf("foo") }
key(text.value) {
YourComposableFun(
onClick = {
text.value = "bar"
}
) {
}
}
call this
currentComposer.composition.recompose()
So currently I have a Dao with a function that emits a Flow<>
#Query("SELECT * FROM ${Constants.Redacted}")
fun loadAllContacts(): Flow<List<Redacted>>
I am calling this from a repository like so
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
I am injecting the repository into the viewModel's constructor, and then at the top of my viewModel I have a val like so
val contacts: LiveData<List<Redacted>> = contactRepository.loadAllContacts.asLiveData()
Which is being observed in my Activity like so
viewModel.contacts.observe(this) { contacts ->
viewModel.onContactsChange(contacts)
}
My thinking is that the Flow is converted to a LiveData, and then I can observe this LiveData from my activity and kick off this function to actually update the viewModel upon the data being updated.
For now onContactsChange just looks like
fun onContactsChange(list: List<Redacted>) {
Timber.i("VIEW UPDATE")
}
The problem is that I only see this Timber log upon opening the activity, and never again. I verified that data IS going into my database, and I verified that an insert occurred successfully while the activity & viewModel are open. But I never see the log from onContactsChange again. When I close the activity, and reopen it, I do see my new data, so that is another reason I know my insert is working correctly.
I would like to add that I am using a single instance (singleton) of my repository, and I think I can verify this by the fact that I can see my data at all, at least when the view is first made.
Figured it out:
Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process.
If your app runs in multiple processes, include enableMultiInstanceInvalidation() in your database builder invocation. That way, when you have an instance of AppDatabase in each process, you can invalidate the shared database file in one process, and this invalidation automatically propagates to the instances of AppDatabase within other processes.
It's a little bit hard to follow your question, but I think I see the overall problem with your Flow object not updating the way you want it too.
Following this quick tutorial, it seems that first you should declare your Flow object inside your Repository the same way you're already doing
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
and have your VM 'subscribe' to it by using the collect coroutine which would then allow you to dump all this data into a MutableLiveData State
data class YourState(..)
val state = MutableLiveData<YourState>()
init {
contactRepository.loadAllContacts().collect {
if (it.isNotEmpty()) {
state.postValue(YourState(
...
)
}
}
}
that your Activity/Fragment could then observe for changes
viewModel.state.observe(.. { state ->
// DO SOMETHING
})
P.S. The tutorial also mentions that because of how Dao's work, you might be getting updates for even the slightest of changes, but that you can use the distinctUntilChanged() Flow extension function to get more specific results.
I have a database table which stores some records. I have been able to correctly populate a RecyclerView in a Fragment, following tutorials like this one and similar ones found via search engine.
What I want to do next is to tie an "Edit record {id}" fragment that is tied to the RecyclerView. In other words, if I click on an item in the Recycler view, another fragment(or activity) should open, load the data for record[id] from the database and then allow me to save and update the record if needed.
The point where I am stuck is retrieving the single record from the database, because I systematically end up with either (1) calling the query inside the main thread, which Room prevents me from doing, or (2) getting some random null pointer.
I have seen solutions even here on stackoverflow, but I can't make sense on how to integrate them in my case.
What I can't make sense of is how to make the async call (whether with threads/coroutines), store the result in a variable, and use it to populate the fields in the Edit fragment.
Internet search have been very disappointing, for all I find are (duplicate) tutorials that are either incomplete, irrelevant or obsolete.
Good pointers are welcome. I would prefer not to use third party libraries to do this, unless someone can explain to me the advantages in doing so.
Sorry for the long post: I haven't added code because there would be too many pieces to show and you would probably know anyway. I will answer any questions however to help out.
Also, I am new to Kotlin/Android, and I am trying to tame this beast :-)
Its hard to say anything specific without any code, but the correct way to do it would be
Retrieve all records from Room
Load them in your recycler view, so recycler adapter will have a list of all your records
setup on click listener in your recycler adapter to open the next activity or fragment
pass the primary key (as in room) of clicked item to the next activity or fragment
In your next activity retrieve a record from room using the primary key
bind the retrieved record to UI
If your recycler view and adapter are correctly setup then you should have following in your adapter
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
// dataList contains all your records as retrieved from room
// and loaded in your recycler view
setListeners(dataList[position], holder)
holder.bind(dataList[position])
}
private fun setListeners(selectedRecord: YourRecordTypeInRoom, viewHolder: YourViewHolder){
viewHolder.itemView.setOnClickListener {
var intent = Intent(viewHolder.itemView.context, NextActivity::class.java)
// pass primary key to next activity
intent.putExtra("primaryKey", selectedRecord.primaryKey)
viewHolder.itemView.context.startActivity(intent)
}
}
Now to retrieve your single record you should have something as follows in your dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): YourRecordType
Edit:
You can also modify the return type of above function to be a LiveData object, which will allow you to observe it in your activity in an async manner. with live data your code would look some thing as follows.
In Dao
#Query("Select * FROM your_table where primaryKey = :primaryKey")
fun findByPrimaryKey(primaryKey: PrimaryKeyType): LiveData<YourRecordType>
In your view model
fun getRecordByPrimaryKey(primaryKey: PrimaryKeyType) = yourDao.findByPrimaryKey(primaryKey)
and in your activity or fragment
viewModel.getRecordByPrimaryKey(primaryKey).observe(this, Observer{
// Bind your record on UI
})
1) Return fun someFunction():LiveData<Model> in Room class, (you should be able to call it from Main thread). After getting value once, you can stop observing, since you want only single value
2) You can use Kotlin Coroutines, this way you return suspend fun someFunction():Model. You can only call this function from another Coroutine, so it will be something like:
class ViewModel{
fun normalFunction(){
viewModelScope.launch{
val result = room.someFunction()
// tell View that you have result (View observes result using LiveData)
}
}
}
I have an Android app with a Room database which consumes REST API.
Room is acting as single source of truth, i.e. I am updating UI when API result is saved in the Room.
In one of my screens, I need to show a filtered list (with the latest updates from API), for example, List of movies filtered by author.
When the user changes author filter, the list needs to be updated, but also, the list needs to be updated when movies change in the backend as a result of an API call (stored in the db).
Second I can achieve with LiveData> object that is created from Room call, it will dispatch changes from Room db.
But, how do I incorporate changes activated from user (by switching filter) over same source (filtered list of movies)?
For anyone else, it's actually quite simple with MediatorLiveData.
val selectedItem = MediatorLiveData<Voyage>()
var voyages: LiveData<Resource<List<Voyage>>>
var voyageFilter = MutableLiveData<VoyageFilter>()
selectedItem.addSource(voyageFilter) { filter ->
//do something
}
selectedItem.addSource(voyages) { listResource ->
//do something
}
I have two activities. First activity shows list of notes. Notes themselves are lists.
I use Android Architecture Components: ViewModel, LiveData; with Repository, Room, Dao, etc.
So, I make a method getAllNotes() in Dao, Repository and ViewModel like in google sample apps. In onCreate method of first activity I call observe and set adapter's content of a RecyclerView. And it works fine - it shows the list with Note titles.
Like that:
override fun onCreate(savedInstanceState: Bundle?) {
//some code
viewModel = obtainViewModel()
viewModel.getAllNotes().observe(this, Observer<List<Notes>> { notes ->
recView.setNote(notes)
}
}
Then I have a button that starts new Activity to create new Note. That note contains list of Lines which for now contains only string and foreign key.
data class Line {
var id: Long? = null
var note_id: Long? = null
var payload: String? = null
}
Note and Line are one-to-many relation and they are connected by id of Note and foreign key note_id in Line.
(I don't write here all of the code, it works, trust me)
The problem is, that to insert Lines in database I firstly need to insert the parent Note and I do that. And it works almost OK too. But the liveData of the getAllNotes() from the first Activity gets notified by this insertion. And if the user, as a result, decides to delete all the lines and go back to the first activity even if I delete temporary Note entity from the database the list on the first Activity shows it for a moment because it gets deleted in a background with a small delay.
What I see as a solution:
1) Unsubscribe observers from livedata. I tried to do it in onStop method, but it gets called after the onCreate method of the second activity where the entity is being created, so the livedata already gets notified and observers are removed after temporary Note passed into the list.
2) Not use Room/SQLite as cache. Since this Note and Lines are not guaranteed to stay then and shouldn't be shown or inserted into a table. So, I can keep it all in properties of viewModel (i.e. in memory). But I see a lot of overhead work to save these entities through screen rotation, minimizing the app and all that stuff with saving state and restoring it.
3) Create two additional entities like CachedNote and CachedLine and corresponding tables, to work with it until I decide to persist the work, insert it into original tables and show it.
4) Add property to the Note entity like "visible" and add this parameter to Query, to make entity note shown, until I decide to persist the work. But there could be a lot of "updateNoteWithLines" every where.
What should I do? I didn't google anything useful.
I know it's like "What's the best way question", forgive me.
You can try to call the observe in onResume and then call removeObserver in onPause, that way the Activity will not be updated, please look at the example here.