Room insert lost when closing fragment and using ViewModelScope - android

I have followed the Android Room with a View tutorial but have changed it to a single-activity app with multiple fragments. I have a fragment for inserting records. The save button calls the viewmodel save method and then pops the backstack to return to the previous (list) fragment. Sometimes this works but often the insert does not occur. I assume that this is because once the fragment is destroyed, the ViewModelScope cancels any pending operations so if the insert has not already occured, it is lost.
Fragment:
private val wordViewModel: WordViewModel by viewModel
...
private fun saveAndClose() {
wordViewModel.save(word)
getSupportFragmentManager().popBackStack()
}
Viewmodel:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
Repository:
suspend fun insert(word: Word) {
wordDao.insert(word)
}
How do I fix this? Should I be using GlobalScope instead of ViewModelScope as I never want the insert to fail? If so, should this go in the fragment or the viewmodel?

One option is to add NonCancellable to the context for the insert:
fun save(word: Word) = viewModelScope.launch(Dispatchers.IO + NonCancellable) {
repository.insert(word)
}
The approach recommended and outlined in this post is to create your own application-level scope and run your non-cancellable operations in it.

Related

How should I go about implementing MVVM architecture pattern in my project?

I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!

What is the correct usage of Flow in Room?

I am using Room and I have written the Dao class as follows.
Dao
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
...etc
}
and this Flow is converted to LiveData through asLiveData() in ViewModel and used as follows.
ViewModel
#HiltViewModel
class MainViewModel #Inject constructor(
private val projectRepo: ProjectRepository
) : ViewModel() {
val allProjects = projectRepo.allProjects.asLiveData()
...
}
Activity
mainViewModel.allProjects.observe(this) { projects ->
adapter.submitList(projects)
...
}
When data change occurs, RecyclerView is automatically updated by the Observer. This is a normal example I know.
However, in my project data in Flow, what is the most correct way to get the data of the position selected from the list?
I have already written code that returns a value from data that has been converted to LiveData, but I think there may be better code than this solution.
private fun getProject(position: Int): Project {
return mainViewModel.allProjects.value[position]
}
Please give me suggestion
Room has in built support of flow.
#Dao
interface ProjectDao {
#Query("SELECT * FROM project")
fun getAllProjects(): Flow<List<Project>>
//lets say you are saving the project from any place one by one.
#Insert()
fun saveProject(project :Project)
}
if you call saveProject(project) from any place, your ui will be updated automatically. you don't have to make any unnecessary call to update your ui. the moment there is any change in project list, flow will update the ui with new dataset.
to get the data of particular position, you can get it from adapter list. no need to make a room call.

LiveData doesn't apdating its value after first call in kotlin

I'm trying to get the last id added from entity A to entity B to add to entity B by it , I fetched the id of the last element added to entity A like this :
in Dao :
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(addSpendEntity: AddSpendEntity) : Long
and in fun insert in repo i used mutableLiveData to save the last id inserted and get it to viewmodel then to observing it in fragment
in repo :
class AddSpendRepository(private var database: PersonalAccountingDateBase) {
private var id : Long = 0
private var mutableLiveData = MutableLiveData<Long>()
suspend fun insert(addSpendEntity: AddSpendEntity){
id = database.getAddSpendDao().insert(addSpendEntity)
Log.e("addspendrepository",id.toString())
mutableLiveData.postvalue(id)
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
...
}}
fun getMutableLiveData() : MutableLiveData<Long> = mutableLiveData
and in VM :
fun insertSpend(addSpendEntity: AddSpendEntity) = viewModelScope.launch(Dispatchers.IO) {
addSpendRepository.insert(addSpendEntity)
}
fun getMutableLiveData() : MutableLiveData<Long> = addSpendRepository.getMutableLiveData()
the observer in fragment i try to add to entity B When mutableLiveData is change :
private fun insert()
{
val totalMoney = binding.edtAddSpendSpendMoney.text.toString().toInt()
val notice = binding.edtAddSpendNotice.text.toString()
val date = binding.txtAddSpendDateText.text.toString()
val addSpendEntity = AddSpendEntity(totalMoney,notice,date)
addSpendViewModel.insertSpend(addSpendEntity)
addSpendViewModel.getMutableLiveData().observe(viewLifecycleOwner,
Observer {
Log.e("addspendfragment",it.toString())
if(it.toInt() != 0)
{
val dailyMovementEntity = DailyMovementEntity("make",totalMoney,notice,5,it.toInt())
addSpendViewModel.insertDailyMovement(dailyMovementEntity)
}
})
so the problem i faced is when to insert in the first time the value of mutable get null and the observer does'nt notice any thing then in the second time the observer notice the previos state of id and this condition continues as long as the application is running , when i close the app and do the same in the same way : The same problem is repeated as shown
enter image description here
You didn't show it in your code, so I'm just guessing, but here's a possible cause of your issue.
I'm guessing your ViewModel's insertSpend function is doing something like this:
fun insertSpend(addSpendEntity: AddSpendEntity) {
viewModelScope.launch(Dispatchers.IO) {
repository.insert(addSpendEntity)
}
}
The problem is, if you call MutableLiveData.value on a thread other than the main thread, then the change is not viewable until another loop of the main thread has occurred. You're not supposed to call .value on any thread besides the main thread. Then you get the proper value in your observer because observers are called on the next loop of the main thread.
Also, a suspend function should never require being called from a specific dispatcher, so you should not need to specify Dispatchers.IO when you launch your coroutine. More properly, your repository function should look like this, so it is safe to call it from anywhere. Any time a suspend function calls a function that requires a specific dispatcher, it is best to specify that dispatcher internally (I think of this as an extension of the single responsibility principle--outside functions shouldn't have to know what state to specify when calling another function if it can be avoided).
I would define it like this:
suspend fun insert(addSpendEntity: AddSpendEntity) = withContext(Dispatchers.Main) {
id = database.getAddSpendDao().insert(addSpendEntity) // it's safe to call this on main because it's a suspend function which by convention must not block
Log.e("addspendrepository",id.toString())
mutableLiveData.value = id
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
// ...
}
Just my opinion:
On the ViewModel side, in general, you should rarely ever be launching a coroutine on the ViewModel scope with a specific dispatcher. Android has a general convention of treating the main thread as the default, and it is full of functions that must be called on main for proper behavior. So it is clean to always leave that as your default and only use withContext(Dispatchers.IO) (or .Default) for the bits of your coroutine that need it. And you should never need those just to call suspend functions, because of the coroutine convention that suspend functions must never block. So you only need them when calling blocking code.

Android Room Pre-populated Data not visible first time

Freshly installing the app, the view model doesn't bind the data.
Closing the app and opening it again shows the data on the screen.
Is there any problem with the pre-population of data or is the use of coroutine is not correct?
If I use Flow in place of LiveData, it collects the data on the go and works completely fine, but its a bit slow as it is emitting data in the stream.
Also, for testing, The data didn't load either LiveData/Flow.
Tried adding the EspressoIdlingResource and IdlingResourcesForDataBinding as given here
Room creation
#Provides
#Singleton
fun provideAppDatabase(
#ApplicationContext context: Context,
callback: AppDatabaseCallback
): AppDatabase {
return Room
.databaseBuilder(context, AppDatabase::class.java, "database_name")
.addCallback(callback)
.build()
AppDatabaseCallback.kt
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
val data = computePrepopulateData(assets_file_name)
data.forEach { user ->
dao.get().insert(user)
}
}
}
Dao
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
#Query("SELECT * FROM $table_name")
suspend fun getAllUser(): List<User>
ViewModel
CoroutineScope(Dispatchers.IO).launch {
repository.getData().let {
listUser.postValue(it)
}
}
Attaching the data using BindingAdapter
app:list="#{viewModel.listUser}"
Your DAO returns suspend fun getAllUser(): List<User>, meaning it's a one time thing. So when the app starts the first time, the DB initialization is not complete, and you get an empty list because the DB is empty. Running the app the second time, the initialization is complete so you get the data.
How to fix it:
Switch getAllUser() to return a Flow:
// annotations omitted
fun getAllUser(): Flow<List<User>>
Switch insertUser to use a List
// annotations omitted
suspend fun insertUser(users: List<User>)
The reason for this change is reducing the number of times the Flow will emit. Every time the DB changes, the Flow will emit a new list. By inserting a List<User> instead of inserting a single User many times the (on the first run) Flow will emit twice (an empty list + the full list) compared to number of user times with a single insert.
Another way to solve this issue is to use a transaction + insert a single user.
I recommend you use viewModelScope inside the ViewModel to launch coroutines so it's properly canceled when the ViewModel is destroyed.

android room suspend insert success but does not return id

I'm trying to use room database with suspend keyword.
I can successfully insert a data into database. But after that, the insert method will not return anything, that means I can't do anything after insertion such as insert another data by the returned id.
here is my sample code:
#Dao
interface EventDao {
...
#Insert
suspend fun insert(event: Event): Long
...
}
class EventRepository(...) {
...
suspend fun insertEvent(event: Event, personId: Long) {
val id = eventDao.insertSync(event)
// !!! FREEZE - the code below here will never reach !!!
val attendee = EventAttendee(
personId = personId,
eventId = id
)
eventAttendeeDao.insert(attendee)
}
...
}
class EventEditingViewModel(...) : ViewModel() {
...
fun addEvent(event: Event) {
event.userId = userId
viewModelScope.launch {
eventRepository.insertEvent(event, friendId)
}
}
...
}
Actually, if I remove suspend keyword, I can get the return id properly. The freezing problem will only happen when I use suspend keyword in front of insertion method in Dao class.
Logcat doesn't show any logs of this, and the app doesn't crash, so I have no idea what happened.
My room version is 2.2.1. and I've tried 2.2.2 also.
Did I do the wrong way to use suspend function in room database?
I looked into coroutines library then found out that my insert method has been canceled cause by ViewModel deleted.
Because I built a DialogFragment and do the insertion after I click the PositiveButton of AlertDialog, so the viewModelScope has been clear before the insertion finished.
I think the solution may be the following ways:
use another scope such as GlobalScope.launch instead of viewModelScope.launch
dismiss the dialog after insertion.
return the data to another fragment, do the insertion in another page.
In this case, I think the best choice is the first choice. It's easier and would not cause any other lifecycles' problem.
I had the same problem as you, the easiest way to make this real-time update is to refresh the activity while you are still in the dialog box.
I added these lines of code AFTER I added the user in the database ( in the OK button of the dialog box):
finish();
overridePendingTransition(0, 0);
startActivity(getIntent());
overridePendingTransition(0, 0);
Hope it helps. It worked for me!

Categories

Resources