Coroutine being skipped when running background thread - android

So I am encountering a weird issue in my code where a coroutine is being completely skipped when I debug through my code and I am not sure why it is happening.
Basically I call this method(insertLabelBind) in my Fragment in the onViewCreated.
//FRAGMENT
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val addLabelButton: ImageView = view.findViewById(R.id.profile_edit_add_label_row_button)
val addQuestionButton: ImageView = view.findViewById(R.id.profile_edit_add_question_response_button)
val editBookImage: ImageView = view.findViewById(R.id.profile_image_edit)
navController = Navigation.findNavController(view)
bindBasicInfo(view)
bindLabels(view)
bindQandA(view)
addLabelButton.setOnClickListener{
insertLabelBind(view)
//add to label live data
}}
private fun insertLabelBind(view: View) = launch{
val word: EditText? = view.findViewById(R.id.profile_label_edit_add_row)
val labelTag = viewModel.createLabel(word?.text.toString())
labelTag?.observeOnce(viewLifecycleOwner, Observer { tag ->
if (tag == null) return#Observer
CoroutineScope(Main).launch {
setLabelTextInUI(tag, word)
}
})
}
Then in the Fragment's ViewModel this gets called and it keeps failing to retrieve for the label result
//VIEWMODEL
suspend fun createLabel(label: String): LiveData<LabelTag>? {
var labelResult: LiveData<LabelTag>? = null
var userLabelResult: LiveData<UserLabel>? = null
val labelTag = LabelTag(
0,
profileId,
label,
LocalDateTime.now().toString(),
LocalDateTime.now().toString(),
""
)
labelResult = coverRepository.createLabelTag(labelTag)
userLabelResult = coverRepository.getSingleUserLabel(profileId, labelResult?.value!!.id)
if (userLabelResult == null) {
val userLabel = UserLabel(
0,
profileId,
labelResult!!.value!!.id,
1,
label,
LocalDateTime.now().toString(),
LocalDateTime.now().toString(),
""
)
userLabelResult = coverRepository.createUserLabel(userLabel)
}
return if (userLabelResult != null) {
labelResult
} else
null
}
In the repository, this createLabelTag Method gets called
//REPOSITORY
override suspend fun createLabelTag(labelTag: LabelTag): LiveData<LabelTag>? {
return withContext(IO) {
val result = createLabelTagSource(labelTag)
when (result.status) {
Status.SUCCESS -> labelTagDao.getTagById(labelTag.id)
Status.LOADING -> null
Status.ERROR -> null
}
}
}
private suspend fun createLabelTagSource(labelTag: LabelTag): Resource<LabelTag> {
return labelTagDataSource.createLabelTag(
labelTag
)
}
This then calls the labelTagDataSource
//DATASOURCE
override suspend fun createLabelTag(labelTag: LabelTag): Resource<LabelTag> {
return try {
val fetchedLabelTag = responseHandler.handleSuccess(coverApiService
.createLabelTag(labelTag))
val list = listOf(fetchedLabelTag.data!!)
list.first().profileId = labelTag.profileId
_downloadedLabelTag.postValue(list)
fetchedLabelTag
} catch (e: NoConnectivityException) {
Log.e("Connectivity", "No internet connection.", e)
responseHandler.handleException(e)
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
The datasource triggers an observer in the repository for the downloadLabelTag and the data gets persisted into the labelTag room DB
//REPOSITORY
labelTagDataSource.apply {
downloadedLabelTag.observeForever { newLabelTag ->
persistFetchedLabelTag(newLabelTag)
}
}
private fun persistFetchedLabelTag(labelTags: List<LabelTag>) {
job = Job()
job?.let { repoJob ->
CoroutineScope(IO + repoJob).launch {
if(labelTags.size > 1)
labelTagDao.deleteAll()
for(result in labelTags){
labelTagDao.upsertTag(result)
}
repoJob.complete()
}
}
}
//DAO
interface LabelTagDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertTag(label: LabelTag)
#Query("SELECT * FROM label_tag WHERE label = :tag")
fun getLabelByTag(tag: String): LiveData<LabelTag>
#Query("SELECT * FROM label_tag WHERE id = :id")
fun getTagById(id: Long): LiveData<LabelTag>
#Query("SELECT * FROM label_tag WHERE profileId = :profileId")
fun getTagMultipleTags(profileId: Int): LiveData<List<LabelTag>>
#Query("DELETE FROM label_tag")
suspend fun deleteAll()
}
The issue is occurring the Repository.persistFetchedLabelTag and that CoroutineScope is being completely skipped. This results the ViewModel's labelResult to return null. Would like to know why this coroutine is being skipped. Any help will be grateful.

Related

why returned flow in room is collecting for each of my SELECT after any changes in database(INSERT,UPDATE,DELETE) MVVM

I am using flow to get data after any changes in my room database ,my app is note and it use SELECT by searching and filter by priority [High,Normal,Low]. and it has delete and update and insert too. I get flows from model and in viewModel by search and filter I Collect my data from flows and post it to my single liveData and in the last I get them in my Observer in the view
my problem is after in SELECT when I use UPDATE or INSERTT or DELETE , all of my old SELECT(search and filter) repeat in the moment. it means if I search 1000 once, and use filter to my notes 99 once; I will collect 1099 flows in the moment again and my observer in the my view will be bombing!!!
My Dao:
#Dao
interface NoteDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveNote(entity : NoteEntity)
#Delete
suspend fun deleteNote(note : NoteEntity)
#Update
suspend fun updateNote(note : NoteEntity)
#Query("SELECT * FROM $NOTE_TABLE")
fun getAllNotes() : Flow<MutableList<NoteEntity>>
#Query("SELECT * FROM $NOTE_TABLE WHERE id == :id")
fun getNote(id : Int) : Flow<NoteEntity>
#Query("SELECT * FROM $NOTE_TABLE WHERE priority == :priority")
fun fileNote(priority : String) : Flow<MutableList<NoteEntity>>
#Query("SELECT * FROM $NOTE_TABLE WHERE title LIKE '%' || :title || '%'")
fun searchNote(title : String) : Flow<MutableList<NoteEntity>>
}
My Repository:
class MainRepository #Inject constructor(private val dao : NoteDao) {
fun getAllNotes() = dao.getAllNotes()
fun getNotesByPriority(priority : String) = dao.fileNote(priority)
fun getNotesBySearch(title : String) = dao.searchNote(title)
suspend fun deleteNote(note: NoteEntity) = dao.deleteNote(note)
}
My ViewModel:
#HiltViewModel
class MainViewModel #Inject constructor(private val repository: MainRepository) : ViewModel() {
val notesData = object : MutableLiveData<DataStatus<MutableList<NoteEntity>>>(){
override fun postValue(value: DataStatus<MutableList<NoteEntity>>?) {
super.postValue(value)
Log.e("TAGJH", "postValue: ${value?.data?.size}")
}
}
fun getAllNotes() = viewModelScope.launch {
repository.getAllNotes().collect {
Log.e("TAGJH", "getAll: ${it.size}")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun getNoteByPriority(priority: String) = viewModelScope.launch {
repository.getNotesByPriority(priority).collect {
Log.e("TAGJH", "priority ${it.size }->$priority")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun getNoteBySearch(characters: String) = viewModelScope.launch {
repository.getNotesBySearch(characters).collect {
Log.e("TAGJH", "collect: ${it.size}")
notesData.postValue(DataStatus.success(it, it.isEmpty()))
}
}
fun deleteNote(note: NoteEntity) = viewModelScope.launch {
repository.deleteNote(note)
}
}
My View:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private var selectedItem: Int = 0
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding
#Inject
lateinit var noteAdapter: NoteAdapter
#Inject
lateinit var noteEntity: NoteEntity
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
binding?.apply {
setSupportActionBar(notesToolbar)
addNoteBtn.setOnClickListener {
NoteFragment().show(supportFragmentManager, NoteFragment().tag)
}
viewModel.notesData.observe(this#MainActivity) {
showEmpty(it.isEmpty)
noteAdapter.setData(it.data!!)
noteList.apply {
layoutManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
adapter = noteAdapter
}
}
viewModel.getAllNotes()
notesToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.actionFilter -> {
filterByProperty()
return#setOnMenuItemClickListener true
}
else -> return#setOnMenuItemClickListener false
}
}
deleteAndUpdateListener()
}
}
private fun deleteAndUpdateListener() =
noteAdapter.onItemClickListener { note, title ->
when (title) {
DELETE -> {
noteEntity.id = note.id
noteEntity.title = note.title
noteEntity.des = note.des
noteEntity.priority = note.priority
noteEntity.category = note.category
viewModel.deleteNote(noteEntity)
}
EDIT -> {
val noteFragment = NoteFragment()
val bundle = Bundle()
bundle.putInt(BUNDLE_ID, note.id)
noteFragment.arguments = bundle
noteFragment.show(supportFragmentManager, NoteFragment().tag)
}
}
}
private fun showEmpty(isShown: Boolean) {
binding?.apply {
if (isShown) {
emptyShowing.visibility = View.VISIBLE
noteList.visibility = View.GONE
} else {
emptyShowing.visibility = View.GONE
noteList.visibility = View.VISIBLE
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_toolbar, menu)
val search = menu.findItem(R.id.actionSearch)
val searchView = search.actionView as SearchView
searchView.queryHint = getString(R.string.search)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
viewModel.getNoteBySearch(newText)
Log.e("TAGJH", "searching")
return true
}
})
return super.onCreateOptionsMenu(menu)
}
private fun filterByProperty() {
val builder = AlertDialog.Builder(this)
val priories = arrayOf(ALL, HIGH, NORMAL, LOW)
builder.setSingleChoiceItems(priories, selectedItem) { dialog, item ->
when (item) {
0 -> {
viewModel.getAllNotes()
}
in 1..3 -> {
viewModel.getNoteByPriority(priories[item])
}
}
selectedItem = item
dialog.dismiss()
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
I want to see my note after any update or delete or insert is updating in my last SELECT from room database

Paging3 : Recycler View blinking and some items move in position after getting data from RemoteMediator

I am building a movies app which uses Paging3 to page from the network and from a local database at the same time using Remote Mediator.
which gets the data from TMDB api
and save them to room database.
But I am experiencing some blinking or flickers in the recycler view
when I scroll down some items change their position and jumps up and down.
here's a video of the issue : https://youtu.be/TzV9Mf85uzk
Working with paging source only for api or database works fine.
but when using remote mediator the blinking happens after inserting any page data from api to the database.
I don't know what is causing this hopefully I can find a solution.
Here's some of my code snippets:
RemoteMediator
class MovieRemoteMediator(
private val query: String ="",
private val repository: MovieRepository
) :
RemoteMediator<Int, Movie>() {
companion object {
private const val STARTING_PAGE_INDEX = 1
}
private val searchQuery = query.ifEmpty { "DEFAULT_QUERY" }
override suspend fun initialize(): InitializeAction {
// Require that remote REFRESH is launched on initial load and succeeds before launching
// remote PREPEND / APPEND.
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Movie>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> STARTING_PAGE_INDEX
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val remoteKey = getRemoteKeyForLastItem(state)
val nextPage = remoteKey?.nextPage
?: return MediatorResult.Success(endOfPaginationReached = remoteKey != null)
nextPage
}
}
val response = repository.getMoviesFromApi(page)
if (response is NetworkResult.Success) {
val movies = response.data.results ?: emptyList()
val nextPage: Int? =
if (response.data.page < response.data.totalPages) response.data.page + 1 else null
val remoteKeys: List<MovieRemoteKey> = movies.map { movie ->
MovieRemoteKey(searchQuery, movie.id, nextPage)
}
repository.insertAndDeleteMoviesAndRemoteKeysToDB(
searchQuery,
movies,
remoteKeys,
loadType
)
return MediatorResult.Success(
endOfPaginationReached = nextPage == null
)
} else {
val error = (response as NetworkResult.Error).errorMessage
return MediatorResult.Error(Exception(error))
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Movie>): MovieRemoteKey? {
return state.pages.lastOrNull() { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { movie ->
repository.getMovieRemoteKey(movie.id.toInt(), searchQuery)
}
}
}
Repository
class MovieRepository #Inject constructor(
private val apiClient: ApiService,
private val movieDao: MovieDao,
private val movieRemoteKeyDao: MovieRemoteKeyDao
) {
companion object {
private const val PAGE_SIZE =20
val config = PagingConfig(pageSize = PAGE_SIZE,
enablePlaceholders = false)
}
#OptIn(ExperimentalPagingApi::class)
fun getPagingMovies() = Pager(config,
remoteMediator = MovieRemoteMediator(repository = this)
) {
getPagedMoviesFromDB(SortType.DEFAULT, "")
}.flow
suspend fun insertAndDeleteMoviesAndRemoteKeysToDB(
query: String,
movies: List<Movie>,
remoteKeys: List<MovieRemoteKey>,
loadType: LoadType
)= withContext(Dispatchers.IO) {
movieRemoteKeyDao.insertAndDeleteMoviesAndRemoteKeys(query,movies, remoteKeys, loadType)
}
suspend fun getMovieRemoteKey(itemId:Int,query:String):MovieRemoteKey? {
return movieRemoteKeyDao.getRemoteKey(itemId,query)
}
MovieDao
fun getSortedMovies(sortType: SortType, searchQuery: String) : Flow<List<Movie>> =
when(sortType){
SortType.ASC -> getSortedMoviesASC(searchQuery)
SortType.DESC -> getSortedMoviesDESC(searchQuery)
SortType.DEFAULT -> getMovies()
}
fun getPagedMovies(sortType: SortType, searchQuery: String) : PagingSource<Int,Movie> =
when(sortType){
SortType.ASC -> getPagedSortedMoviesASC(searchQuery)
SortType.DESC -> getPagedSortedMoviesDESC(searchQuery)
SortType.DEFAULT -> getDefaultPagedMovies(searchQuery.ifEmpty { "DEFAULT_QUERY" })
}
#Query("SELECT * FROM movies ORDER BY popularity DESC")
fun getMovies(): Flow<List<Movie>>
#Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
" OR originalTitle LIKE :search" +
" ORDER BY title ASC")
fun getSortedMoviesASC(search:String): Flow<List<Movie>>
#Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
" OR originalTitle LIKE :search" +
" ORDER BY title DESC")
fun getSortedMoviesDESC(search:String): Flow<List<Movie>>
#Transaction
#Query("SELECT * FROM movies" +
" INNER JOIN movie_remote_key_table on movies.id = movie_remote_key_table.movieId" +
" WHERE searchQuery = :search" +
" ORDER BY movie_remote_key_table.id")
fun getDefaultPagedMovies(search:String): PagingSource<Int,Movie>
#Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
" OR originalTitle LIKE :search" +
" ORDER BY title ASC")
fun getPagedSortedMoviesASC(search:String): PagingSource<Int,Movie>
#Query("SELECT * FROM movies WHERE title LIKE '%' || :search || '%'" +
" OR originalTitle LIKE :search" +
" ORDER BY title DESC")
fun getPagedSortedMoviesDESC(search:String): PagingSource<Int,Movie>
#Query("SELECT * FROM movies WHERE id = :id")
fun getMovieById(id: Int): Flow<Movie>
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertMovie(movie: Movie)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertMovies(movies: List<Movie>)
#Query("DELETE FROM movies")
fun deleteAllMovies()
#Query("DELETE FROM movies WHERE id = :id")
fun deleteMovieById(id: Int)
}
RemoteKeyDao
#Dao
interface MovieRemoteKeyDao {
#Query("SELECT * FROM movie_remote_key_table WHERE movieId = :movieId AND searchQuery = :query LIMIT 1")
suspend fun getRemoteKey(movieId: Int, query: String): MovieRemoteKey?
#Query("DELETE FROM movie_remote_key_table WHERE searchQuery = :query")
suspend fun deleteRemoteKeys(query: String)
#Transaction
#Query("DELETE FROM movies WHERE id IN ( SELECT movieId FROM movie_remote_key_table WHERE searchQuery = :query)")
suspend fun deleteMoviesByRemoteKeys(query: String)
#Transaction
suspend fun insertAndDeleteMoviesAndRemoteKeys(
query: String,
movies: List<Movie>,
remoteKeys: List<MovieRemoteKey>,
loadType: LoadType
) {
if (loadType == LoadType.REFRESH) {
Timber.d("REMOTE SOURCE DELETING:")
deleteMoviesByRemoteKeys(query)
deleteRemoteKeys(query)
}
Timber.d("REMOTE SOURCE INSERTING ${movies.size} Movies and ${remoteKeys.size} RemoteKeys :")
insertMovies(movies)
insertRemoteKey(remoteKeys)
}
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRemoteKey(movieRemoteKey: List<MovieRemoteKey>)
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertMovies(movies: List<Movie>)
}
MoviesViewModel
#HiltViewModel
class MoviesViewModel #Inject constructor(
private val repository: MovieRepository, private val preferenceManger: PreferenceManger
) : ViewModel() {
private val searchFlow = MutableStateFlow("")
private val sortFlow = preferenceManger.preferencesFlow
val movies = repository.getPagingMovies().cachedIn(viewModelScope)
//
// val movies: StateFlow<Resource<List<Movie>>> = sortFlow.combine(searchFlow) { sort, search ->
// Pair(sort, search)
// } //For having timeouts for search query so not overload the server
// .debounce(600)
// .distinctUntilChanged()
// .flatMapLatest { (sort, search) ->
// repository.getMovies(sort, search)
// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Resource.Loading())
//
fun setSearchQuery(query: String) {
searchFlow.value = query
}
fun saveSortType(type: SortType) {
viewModelScope.launch {
preferenceManger.saveSortType(type)
}
}
private val _currentMovie = MutableLiveData<Movie?>()
val currentMovie: LiveData<Movie?>
get() = _currentMovie
fun setMovie(movie: Movie?) {
_currentMovie.value = movie
}
}
MovieFragment
#AndroidEntryPoint
class MoviesFragment : Fragment(), MovieClickListener {
private lateinit var moviesBinding: FragmentMoviesBinding
private lateinit var pagingMovieAdapter: PagingMovieAdapter
private val viewModel: MoviesViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
moviesBinding = FragmentMoviesBinding.inflate(inflater, container, false)
return moviesBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUI()
getMovies()
}
private fun setupUI() {
pagingMovieAdapter = PagingMovieAdapter(this)
moviesBinding.moviesRv.layoutManager = GridLayoutManager(context, 3)
moviesBinding.moviesRv.adapter = pagingMovieAdapter
moviesBinding.moviesRv.setHasFixedSize(true)
setHasOptionsMenu(true)
}
private fun getMovies() {
// repeatOnLifeCycle(pagingMovieAdapter.loadStateFlow) { loadStates ->
// val state = loadStates.refresh
// moviesBinding.loadingView.isVisible = state is LoadState.Loading
//
// if (state is LoadState.Error) {
// val errorMsg = state.error.message
// Toast.makeText(context, errorMsg, Toast.LENGTH_LONG).show()
// }
//
// }
lifecycleScope.launchWhenCreated{
viewModel.movies.collectLatest { pagingMovieAdapter.submitData(it) }
}
// repeatOnLifeCycle(viewModel.movies,pagingMovieAdapter::submitData)
// //scroll to top after updating the adapter
// repeatOnLifeCycle(pagingMovieAdapter.loadStateFlow
// .distinctUntilChangedBy { it.refresh }
// .filter { it.refresh is LoadState.NotLoading }
// ) {
// moviesBinding.moviesRv.scrollToPosition(0)
// }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
val searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.onQueryTextChanged() { query ->
viewModel.setSearchQuery(query)
}
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_sort_asc -> {
viewModel.saveSortType(SortType.ASC)
true
}
R.id.action_sort_desc -> {
viewModel.saveSortType(SortType.DESC)
true
}
R.id.action_sort_default -> {
viewModel.saveSortType(SortType.DEFAULT)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onMovieClickListener(movie: Movie?) {
Toast.makeText(context, movie?.title, Toast.LENGTH_SHORT).show()
viewModel.setMovie(movie)
movie?.id?.let {
val action = MoviesFragmentDirections.actionMoviesFragmentToDetailsFragment2(it)
findNavController().navigate(action)
}
}
}
PagingMovieAdapter
class PagingMovieAdapter(private val movieClickListener: MovieClickListener)
: PagingDataAdapter<Movie, PagingMovieAdapter.PagingMovieViewHolder>(diffUtil) {
companion object{
val diffUtil = object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem == newItem
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagingMovieViewHolder {
return PagingMovieViewHolder.from(parent,movieClickListener)
}
override fun onBindViewHolder(holder: PagingMovieViewHolder, position: Int) {
val movie = getItem(position)
holder.bind(movie)
}
class PagingMovieViewHolder(private val movieBinding: ItemMovieBinding,private val movieClickListener: MovieClickListener) :
RecyclerView.ViewHolder(movieBinding.root) , View.OnClickListener{
init {
movieBinding.root.setOnClickListener(this)
}
fun bind(movie: Movie?) {
movie.let { movieBinding.movie = movie }
}
companion object {
fun from(parent: ViewGroup, movieClickListener: MovieClickListener): PagingMovieViewHolder {
val inflater = LayoutInflater.from(parent.context)
val movieBinding = ItemMovieBinding.inflate(inflater, parent, false)
return PagingMovieViewHolder(movieBinding,movieClickListener)
}
}
override fun onClick(p0: View?) {
val movie = movieBinding.movie
movieClickListener.onMovieClickListener(movie)
}
}
}
Thanks.
For anyone that might face this problem,
The blinking was because my diffUtil callback returning false on areContentTheSame because I had a long array parameter on my data model class
and kotlin data class equals method compare arrays based on their reference not the value so I had to manually override equals method.
and for items moving in their position, it was because I was disabling placeholders
on the paging config
which made the paging library to return a wrong offset after updating the database
so making enablePlaceholders = false solves the issue.
also of the order of data coming from the api not the same of the data coming from the database might cause this issue.
Thanks

Android Espresso Testing: java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()

I am almost new to android testing and following the official docs and Udacity course for learning purposes.
Coming to the issue I want to check when the task is completed or incompleted to be displayed properly or not, for this I wrote a few tests. Here I got the exception that toast can not be displayed on a thread that has not called Looper.prepare.
When I comment out the toast msg live data updating line of code then all tests work fine and pass successfully. I am new to android testing and searched out a lot but did not get any info to solve this issue. Any help would be much appreciated. A little bit of explanation will be much more helpful if provided.
Below is my test class source code along with ViewModel, FakeRepository, and fragment source code.
Test Class.
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var tasksRepository: FakeTasksRepository
#Before
fun setUp() {
tasksRepository = FakeTasksRepository()
ServiceLocator.taskRepositories = tasksRepository
}
#Test
fun addNewTask_addNewTaskToDatabase() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "1", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val task = tasksRepository.getTask(newTask.id)
assertEquals(newTask.id,(task as Result.Success).data.id)
}
#Test
fun activeTaskDetails_DisplayedInUi() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isNotChecked()))
}
#Test
fun completedTaskDetails_DisplayedInUI() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",true)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer <TaskDetailFragment>(bundle,R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isChecked()))
}
#After
fun tearDown() = mainCoroutineRule.runBlockingTest {
ServiceLocator.resetRepository()
}
}
FakeRepository class.
class FakeTasksRepository: TasksRepository {
var tasksServiceData: LinkedHashMap<String,Task> = LinkedHashMap()
private val observableTasks: MutableLiveData<Result<List<Task>>> = MutableLiveData()
private var shouldReturnError: Boolean = false
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { fetchAllToDoTasks() }
return observableTasks.map { tasks ->
when(tasks) {
is Result.Loading -> Result.Loading
is Result.Error -> Result.Error(tasks.exception)
is Result.Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return#map Result.Error(Exception("Not found"))
Result.Success(task)
}
}
}
}
override suspend fun completeTask(id: String) {
tasksServiceData[id]?.completed = true
}
override suspend fun completeTask(task: Task) {
val compTask = task.copy(completed = true)
tasksServiceData[task.id] = compTask
fetchAllToDoTasks()
}
override suspend fun activateTask(id: String) {
tasksServiceData[id]?.completed = false
}
override suspend fun activateTask(task: Task) {
val activeTask = task.copy(completed = false)
tasksServiceData[task.id] = activeTask
fetchAllToDoTasks()
}
override suspend fun getTask(taskId: String): Result<Task> {
if (shouldReturnError) return Result.Error(Exception("Test Exception"))
tasksServiceData[taskId]?.let {
return Result.Success(it)
}
return Result.Error(Exception("Could not find task"))
}
override suspend fun getTasks(): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun clearAllCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.completed
} as LinkedHashMap<String, Task>
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
fetchAllToDoTasks()
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
fetchAllToDoTasks()
}
override suspend fun fetchAllToDoTasks(): Result<List<Task>> {
if(shouldReturnError) {
return Result.Error(Exception("Could not find task"))
}
val tasks = Result.Success(tasksServiceData.values.toList())
observableTasks.value = tasks
return tasks
}
override suspend fun updateLocalDataStore(list: List<Task>) {
TODO("Not yet implemented")
}
fun addTasks(vararg tasks: Task) {
tasks.forEach {
tasksServiceData[it.id] = it
}
runBlocking {
fetchAllToDoTasks()
}
}
}
Fragment class.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadTaskById(args.taskId)
setUpToast(this,viewModel.toastText)
viewModel.editTaskEvent.observe(viewLifecycleOwner, {
it?.let {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToAddEditFragment(
args.taskId,
resources.getString(R.string.edit_task)
)
findNavController().navigate(action)
}
})
binding.editTaskFab.setOnClickListener {
viewModel.editTask()
}
}
ViewModel class.
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val TAG = "TaskDetailViewModel"
private val _taskId: MutableLiveData<String> = MutableLiveData()
private val _task = _taskId.switchMap {
tasksRepository.observeTask(it).map { res ->
Log.d("Test","res with value ${res.toString()}")
isolateTask(res)
}
}
val task: LiveData<Task?> = _task
private val _toastText = MutableLiveData<Int?>()
val toastText: LiveData<Int?> = _toastText
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _editTaskEvent = MutableLiveData<Unit?>(null)
val editTaskEvent: LiveData<Unit?> = _editTaskEvent
fun loadTaskById(taskId: String) {
if(dataLoading.value == true || _taskId.value == taskId) return
_taskId.value = taskId
Log.d("Test","loading task with id $taskId")
}
fun editTask(){
_editTaskEvent.value = Unit
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = _task.value ?: return#launch
if(completed) {
tasksRepository.completeTask(task.id)
_toastText.value = R.string.task_marked_complete
}
else {
tasksRepository.activateTask(task.id)
_toastText.value = R.string.task_marked_active
}
}
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error
null
}
}
#Suppress("UNCHECKED_CAST")
class TasksDetailViewModelFactory(
private val tasksRepository: TasksRepository
): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return (TaskDetailViewModel(
tasksRepository
) as T)
}
}
}
In this method in ViewModel when I comment out the below line of code all tests passed.
_toastText.value = R.string.loading_tasks_error
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error // Comment out this line then all test passed.
null
}
}

Paging 3 Library calls the load method recursively with LoadType.APPEND

I am trying to display data from IconFinder API. It seems to be ItemKeyedDataSource for me and I used Paging3 to display the data as it's mentioned in the official docs.
Here is code, please check if there're any issues with the implementation I have done and where is the mistake.
IconSetsRemoteMediator
#OptIn(ExperimentalPagingApi::class)
class IconSetsRemoteMediator(
private val query: String?,
private val database: IconsFinderDatabase,
private val networkService: IconFinderAPIService
) : RemoteMediator<Int, IconSetsEntry>() {
private val TAG: String? = IconSetsRemoteMediator::class.simpleName
private val iconSetsDao = database.iconSetsDao
private val remoteKeysDao = database.remoteKeysDao
override suspend fun initialize(): InitializeAction {
// Load fresh data when ever the app is open new
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, IconSetsEntry>
): MediatorResult {
val iconSetID = when (loadType) {
LoadType.REFRESH -> {
null
}
LoadType.PREPEND -> {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
LoadType.APPEND -> {
Log.d(TAG, "LoadType.APPEND")
val lastItem = state.lastItemOrNull()
if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
// Get the last item from the icon-sets list and return its ID
lastItem.iconset_id
}
}
try {
// Suspending network load via Retrofit.
val response = networkService.getAllPublicIconSets(after = iconSetID)
val iconSets = response.iconsets
val endOfPaginationReached = iconSets == null || iconSets.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
// Delete the data in the database
iconSetsDao.deleteAllIconSets()
//remoteKeysDao.deleteRemoteKeys()
}
Log.d(TAG, "iconSets = ${iconSets?.size}")
Log.d(TAG, "endOfPaginationReached = $endOfPaginationReached")
Log.d(TAG, "state.anchorPosition = ${state.anchorPosition}")
Log.d(TAG, "state.pages = ${state.pages.size}")
val time = System.currentTimeMillis()
/*val remoteKeys = iconSets!!.map {
RemoteKeysEntry(it.iconset_id, time)
}*/
// Insert new IconSets data into database, which invalidates the current PagingData,
// allowing Paging to present the updates in the DB.
val data = iconSets!!.mapAsIconSetsEntry()
iconSetsDao.insertAllIconSets(data)
// Insert the remote key values which set the time at which the data is
// getting updated in the DB
//remoteKeysDao.insertRemoteKeys(remoteKeys)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
}
IconFinderRepository
class IconFinderRepository(
private val service: IconFinderAPIService,
private val database: IconsFinderDatabase
) {
private val TAG: String? = IconFinderRepository::class.simpleName
fun getPublicIconSets(): Flow<PagingData<IconSetsEntry>> {
Log.d(TAG, "New Icon Sets query")
val pagingSourceFactory = { database.iconSetsDao.getIconSets() }
#OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(pageSize = NUMBER_OF_ITEMS_TO_FETCH, enablePlaceholders = false),
remoteMediator = IconSetsRemoteMediator(
query = null,
database,
service
),
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
const val NUMBER_OF_ITEMS_TO_FETCH = 20
}
}
IconSetViewHolder
class IconSetViewHolder private constructor(val binding: RecyclerItemIconSetBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(iconSetsEntry: IconSetsEntry?) {
if (iconSetsEntry == null) {
//Show the Loading UI
} else {
binding.model = iconSetsEntry
binding.executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): IconSetViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RecyclerItemIconSetBinding.inflate(layoutInflater, parent, false)
return IconSetViewHolder(binding)
}
}
}
IconSetAdapter
class IconSetAdapter : PagingDataAdapter<UiModel.IconSetDataItem, ViewHolder>(UI_MODEL_COMPARATOR) {
companion object {
private val UI_MODEL_COMPARATOR =
object : DiffUtil.ItemCallback<UiModel.IconSetDataItem>() {
override fun areContentsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean {
return oldItem.iconSetsEntry.iconset_id == newItem.iconSetsEntry.iconset_id
}
override fun areItemsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean =
oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (viewType == R.layout.recycler_item_icon_set) {
IconSetViewHolder.from(parent)
} else {
IconSetViewHolder.from(parent)
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is UiModel.IconSetDataItem -> R.layout.recycler_item_icon_set
null -> throw UnsupportedOperationException("Unknown view")
else -> throw UnsupportedOperationException("Unknown view")
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
when (uiModel) {
is UiModel.IconSetDataItem -> (holder as IconSetViewHolder).bind(uiModel.iconSetsEntry)
}
}
}
}
HomeFragmentViewModel
class HomeFragmentViewModel(application: Application) : AndroidViewModel(application) {
private val TAG: String? = HomeFragmentViewModel::class.simpleName
private val repository: IconFinderRepository = IconFinderRepository(
IconFinderAPIService.create(),
IconsFinderDatabase.getInstance(application)
)
private var iconSetsQueryResult: Flow<PagingData<UiModel.IconSetDataItem>>? = null
fun iconSetsQuery(): Flow<PagingData<UiModel.IconSetDataItem>> {
val newResult: Flow<PagingData<UiModel.IconSetDataItem>> = repository.getPublicIconSets()
.map { pagingData -> pagingData.map { UiModel.IconSetDataItem(it) } }
.cachedIn(viewModelScope)
iconSetsQueryResult = newResult
return newResult
}
/**
* Factory for constructing HomeFragmentViewModel
*/
class Factory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(HomeFragmentViewModel::class.java)) {
return HomeFragmentViewModel(application) as T
}
throw IllegalArgumentException("Unable to construct ViewModel")
}
}
}
sealed class UiModel {
data class IconSetDataItem(val iconSetsEntry: IconSetsEntry) : UiModel()
}
IconSetFragment: This is one of the fragments implemented as part of ViewPager. Its parent is a Fragment in an Activity.
class IconSetFragment : Fragment() {
private val TAG: String = IconSetFragment::class.java.simpleName
/**
* Declaring the UI Components
*/
private lateinit var binding: FragmentIconSetBinding
private val viewModel: HomeFragmentViewModel by viewModels()
private val adapter = IconSetAdapter()
private var job: Job? = null
companion object {
fun newInstance(): IconSetFragment {
return IconSetFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Get a reference to the binding object
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_icon_set, container, false)
Log.d(TAG, "onCreateView")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter()
job?.cancel()
job = viewLifecycleOwner.lifecycleScope.launch {
viewModel.iconSetsQuery().collectLatest {
adapter.submitData(it)
Log.d(TAG, "collectLatest $it")
}
}
}
private fun initAdapter() {
binding.rvIconSetList.adapter = adapter
/*.withLoadStateHeaderAndFooter(
header = LoadStateAdapter(), // { adapter.retry() },
footer = LoadStateAdapter { adapter.retry() }
)*/
}
}
IconSetsDao
#Dao
interface IconSetsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllIconSets(iconSets: List<IconSetsEntry>)
#Query("SELECT * FROM icon_sets_table")
fun getIconSets(): PagingSource<Int, IconSetsEntry>
#Query("DELETE FROM icon_sets_table")
suspend fun deleteAllIconSets()
}
This is the Logcat screenshot, the load() method is being invoked without any scrolling action.
I have the similar issue, seems the recursive loading issue is fixed by setting the recyclerView.setHasFixedSize(true)

Room returning Null value while using Live Data but returns Proper value when its not wrapped with Livedata

I am using the following DAO
#Dao
interface GroceryListDao {
#Insert
fun insert(list: GroceryList)
#Update
fun update(list: GroceryList)
#Query("Delete from grocery_list_table")
fun clear()
#Query ("Select * from grocery_list_table")
fun getAllItems(): LiveData<List<GroceryList>>
#Query ("Select * from grocery_list_table where itemId = :item")
fun getItem(item: Long): GroceryList?
#Query ("Select * from grocery_list_table where item_status = :status")
fun getItemsBasedOnStatus(status: Int): List<GroceryList>
}
And my database has 3 columns groceryId(Long - autogenerated), groceryName(String) and groceryStatus(Int - 1/0).
When I am using getItemsBasedOnStatus(status: Int) without using LiveData I am able to retrieve the data. But when it is wrapped with LiveData I am getting null.
The other issue is when I get a list of items from a database without wrapping with LiveData and assigning to MutableLiveData in ViewModel, then the assigned MutableLiveData is displaying null values. I see lot of questions on this but no answer.
Adding code for my viewModel and Fragment
ViewModel
class GroceryListViewModel(
val database: GroceryListDao,
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
var grocerylistItems = database.getAllItems()
var groceryItem = MutableLiveData<GroceryList>()
var groceryitems = MutableLiveData<List<GroceryList>>()
init {
getAllItemsFromDatabase()
}
fun insertIntoDatabase(item: GroceryList) {
uiScope.launch {
insert(item)
}
}
private suspend fun insert(item: GroceryList) {
withContext(Dispatchers.IO) {
database.insert(item)
}
}
fun updateGrocerylist(itemId: Long, status: Int) {
uiScope.launch {
groceryItem.value = getItem(itemId)
groceryItem.value?.itemStatus = status
groceryItem.value?.let { update(it) }
}
}
private suspend fun update(item: GroceryList) {
withContext(Dispatchers.IO) {
database.update(item)
}
}
private suspend fun getItem(itemId: Long): GroceryList? {
return withContext(Dispatchers.IO) {
var item = database.getItem(itemId)
item
}
}
fun getAllItemsFromDatabase() {
uiScope.launch {
getAllItems()
}
}
private suspend fun getAllItems() {
withContext(Dispatchers.IO) {
grocerylistItems = database.getAllItems()
}
}
fun getPendingItemsFromDatabase(status: Int) {
uiScope.launch {
getPendingItems(status)
}
}
private suspend fun getPendingItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Pending Items:" + items.size)
}
}
fun getDoneItemsFromDatabase(status: Int) {
uiScope.launch {
getDoneItems(status)
}
}
private suspend fun getDoneItems(status: Int) {
withContext(Dispatchers.IO) {
val items = database.getItemsBasedOnStatus(status)
groceryitems.postValue(items)
Log.i("Grocery List", "Done Items:" + items.size)
}
}
fun clearAllItemsFromDatabase() {
uiScope.launch {
clearItems()
}
}
private suspend fun clearItems() {
withContext(Dispatchers.IO) {
database.clear()
getAllItemsFromDatabase()
}
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Fragment
class GroceryLIstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = FragmentGroceryLIstBinding.inflate(inflater,container,false)
val application = requireNotNull(this.activity).application
val dataSource = GroceryDatabase.getInstance(application)?.groceryListDatabaseDao
val viewModelFactory = dataSource?.let { GroceryListViewModelFactory(it, application) }
val viewModel = viewModelFactory?.let {
ViewModelProvider(
this,
it
).get(GroceryListViewModel::class.java)
}
viewModel?.grocerylistItems?.observe(this , Observer {
binding.grocerylistView.removeAllViews() // is it correct ?
for (item in it){
Log.i("Grocery List","Grocery Id=" + item.itemId+" ,Grocery Name=" + item.itemName +", Grocery status="+item.itemStatus)
addItemToScrollbar(item, binding, viewModel)
}
})
binding.addGrocery.setOnClickListener {
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view?.windowToken, 0)
val item = binding.groceryitemField.text.toString()
if (!item.isNullOrBlank()) {
val newItem = GroceryList(itemName = item)
viewModel?.insertIntoDatabase(newItem)
if (viewModel != null) {
addItemToScrollbar(newItem, binding,viewModel)
}
binding.groceryitemField.text.clear()
}
}
binding.doneCheckbox.setOnClickListener {
if (binding.doneCheckbox.isChecked)
viewModel?.getDoneItemsFromDatabase(1)
else
viewModel?.getAllItemsFromDatabase()
}
binding.pendingCheckbox.setOnClickListener {
if (binding.pendingCheckbox.isChecked) {
viewModel?.getPendingItemsFromDatabase(0)
}
else
viewModel?.getAllItemsFromDatabase()
}
binding.clearGrocery.setOnClickListener {
viewModel?.clearAllItemsFromDatabase()
binding.grocerylistView.removeAllViews()
}
return binding.root
}
private fun addItemToScrollbar(
item: GroceryList,
binding: FragmentGroceryLIstBinding,
viewModel: GroceryListViewModel
) {
val itemBox = AppCompatCheckBox(context)
itemBox.text = item.itemName
itemBox.isChecked = item.itemStatus == 1
binding.grocerylistView.addView(itemBox)
itemBox.setOnClickListener {
val itemstatus: Int = if (itemBox.isChecked)
1
else {
0
}
viewModel?.updateGrocerylist(item.itemId,itemstatus)
}
}
}
Any help would be appreciated.
This most likely the same issue as here (read the linked answer). Due to way asynchronous way LiveData is working, it will return null when you call it. LiveData is meant to be used in conjunction with Observers, that will be triggered once changes to observed subject occur.
An Observer can look like this
database.getItemsBasedOnStatus(status).observe(viewLifecycleOwner, Observer { groceryList->
// Do cool grocery stuff here
})
If you want to retrieve your data inside your ViewModel you do not have a viewLifecycleOwner, you can then use "observeForever()", but then you have to remove the Observer explicitly, see this answer.
Same issue and answer also in this post

Categories

Resources