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

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
}
}

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

Unable to test LiveData inside ViewModel

So, I have this ViewModel class
#HiltViewModel
class HomeViewModel #Inject constructor(private val starWarsRepository: StarWarsRepository) : ViewModel() {
/**
* Two-Way binding variable
*/
val searchQuery: MutableLiveData<String> = MutableLiveData()
val characters = searchQuery.switchMap { query ->
if (query.isNotBlank() && query.isNotEmpty()) {
characterPager.liveData.cachedIn(viewModelScope)
} else {
MutableLiveData()
}
}
fun retrySearch() {
searchQuery.postValue(searchQuery.value)
}
private val characterPager by lazy {
val searchPagingConfig = PagingConfig(pageSize = 20, maxSize = 100, enablePlaceholders = false)
Pager(config = searchPagingConfig) {
CharacterSearchPagingSource(starWarsRepository, searchQuery.value ?: String())
}
}
}
and here it's counter TestingClass
#ExperimentalCoroutinesApi
#RunWith(JUnit4::class)
class HomeViewModelTest {
#get:Rule
val instantTaskExecutionRule = InstantTaskExecutorRule()
private lateinit var homeViewModel: HomeViewModel
private val starWarsAPI = mock(StarWarsAPI::class.java)
private val testDispatcher = UnconfinedTestDispatcher()
#Before
fun setup() {
Dispatchers.setMain(testDispatcher)
homeViewModel = HomeViewModel(StarWarsRepositoryImpl(starWarsAPI, testDispatcher))
}
#AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
#Test
fun `live data should emit success with result when searching for a character`() = runTest {
val character = CharacterEntity("", "", "", "172", "", listOf(), listOf())
`when`(starWarsAPI.searchCharacters("a", null)).thenReturn(CharacterSearchResultEntity(listOf(character, character), null, null))
homeViewModel.searchQuery.value = "a"
val result = homeViewModel.characters.getOrAwaitValue()
assertEquals(PagingSource.LoadResult.Page(listOf(), null, null), result)
}
}
Here is the repository
class StarWarsRepositoryImpl(private val starWarsAPI: StarWarsAPI, private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO) : StarWarsRepository {
override suspend fun searchCharacters(query: String, page: Int?) = withContext(coroutineDispatcher) {
starWarsAPI.searchCharacters(query, page)
}
}
I'm using this extension function from a google sample.
fun <T> LiveData<T>.getOrAwaitValue(time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS, afterObserve: () -> Unit = {}): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this#getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
#Suppress("UNCHECKED_CAST")
return data as T
}
The problem is I'm getting an exception from the getOrAwaitValue that LiveData value was never set.?

how to test android compose mutableStateOf like flow Turbine

I following the new google guide and end with this to represent the state in viewmodel like tis
var uiState: AccountSettingUiState by
mutableStateOf(AccountSettingUiState.Initial)
private set
then I have this function
fun resetPasswordUseCase(context: Context) {
resetPasswordUseCase.execute(context)
.subscribeOn(rxSchedulers.io)
.doOnSubscribe {
uiState = AccountSettingUiState.Loading
}
.observeOn(rxSchedulers.ui)
.subscribe {
uiState = AccountSettingUiState.Result
}
}
I want to test this function by assert emitting loading then result but how
I can capture the values
I can test only final state
ViewModel
#HiltViewModel
class SearchViewModel #Inject constructor(
private val searchItemUseCase: SearchItemUseCase,
private val searchUIMapper: SearchUIMapper
) : ViewModel() {
var search by mutableStateOf<StateUI<SearchUI>>(StateUI.Init())
fun searchItem(query: String) {
viewModelScope.launch {
search = StateUI.Loading()
searchItemUseCase.execute(query)
.catch { error ->
search = StateUI.Error(error)
}
.map {
searchUIMapper.map(it)
}.collect {
search = if (it.results.isEmpty()) {
StateUI.Error(EmptySearchException())
} else {
StateUI.Success(it)
}
}
}
}
}
Test
#ExperimentalCoroutinesApi
#ExperimentalTime
class SearchViewModelTest {
private lateinit var searchViewModel: SearchViewModel
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#get:Rule
var coroutineRule = TestCoroutineRule()
#MockK(relaxed = true)
private lateinit var searchItemUseCase: SearchItemUseCase
private val searchUIMapper by lazy {
SearchUIMapper(ItemUIMapper())
}
#Before
fun setup() {
MockKAnnotations.init(this)
searchViewModel = SearchViewModel(
searchItemUseCase,
searchUIMapper
)
}
#Test
fun success() = coroutineRule.runBlockingTest {
val search = getSearchEntity(itemEntityList)
//Given
coEvery { searchItemUseCase.execute("") } returns flow {
emit(search)
}
searchViewModel.searchItem("")
assertEquals(StateUI.Success(searchUIMapper.map(search)), searchViewModel.search)
}
#Test
fun emptySearch() = coroutineRule.runBlockingTest {
val search = getSearchEntity(emptyList())
//Given
coEvery { searchItemUseCase.execute("") } returns flow {
emit(search)
}
//When
searchViewModel.searchItem("")
//Verify
assertTrue((searchViewModel.search as StateUI.Error<SearchUI>).data is EmptySearchException)
}
#Test
fun error() = coroutineRule.runBlockingTest {
val error = NoConnectivityException()
//Given
coEvery { searchItemUseCase.execute("") } returns flow {
throw error
}
//When
searchViewModel.searchItem("")
assertEquals(StateUI.Error<SearchUI>(error), searchViewModel.search)
}
}

Coroutine being skipped when running background thread

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.

Kotlin coroutine scope & job cancellation in non-lifecycle classes

How to use new Kotlin v1.3 coroutines in classes which do not have lifecycles, like repositories?
I have a class where I check if the cache is expired and then decide whether I fetch the data from the remote API or local database. I need to start the launch and async from there. But then how do I cancel the job?
Example code:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val expirationInterval = 60 * 10 * 1000L /* 10 mins */
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun getNotes(): LiveData<List<Note>> {
if (isOnline() && isCacheExpired()) {
remote.getNotes(object : GetNotesCallback {
override fun onGetNotes(data: List<Note>?) {
data?.let {
launch {
cache.saveAllNotes(it)
cache.setLastCacheTime(System.currentTimeMillis())
}
}
}
})
}
return cache.getNotes()
}
override fun addNote(note: Note) {
if (isOnline()) {
remote.createNote(note, object : CreateNoteCallback {
override fun onCreateNote(note: Note?) {
note?.let { launch { cache.addNote(it) } }
}
})
} else {
launch { cache.addNote(note) }
}
}
override fun getSingleNote(id: Int): LiveData<Note> {
if (isOnline()) {
val liveData: MutableLiveData<Note> = MutableLiveData()
remote.getNote(id, object : GetSingleNoteCallback {
override fun onGetSingleNote(note: Note?) {
note?.let {
liveData.value = it
}
}
})
return liveData
}
return cache.getSingleNote(id)
}
override fun editNote(note: Note) {
if (isOnline()) {
remote.updateNote(note, object : UpdateNoteCallback {
override fun onUpdateNote(note: Note?) {
note?.let { launch { cache.editNote(note) } }
}
})
} else {
cache.editNote(note)
}
}
override fun delete(note: Note) {
if (isOnline()) {
remote.deleteNote(note.id!!, object : DeleteNoteCallback {
override fun onDeleteNote(noteId: Int?) {
noteId?.let { launch { cache.delete(note) } }
}
})
} else {
cache.delete(note)
}
}
private fun isCacheExpired(): Boolean {
var delta = 0L
runBlocking(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
val lastCacheTime = async { cache.getLastCacheTime() }
delta = currentTime - lastCacheTime.await()
}
Timber.d("delta: $delta")
return delta > expirationInterval
}
private fun isOnline(): Boolean {
val runtime = Runtime.getRuntime()
try {
val ipProcess = runtime.exec("/system/bin/ping -c 1 8.8.8.8")
val exitValue = ipProcess.waitFor()
return exitValue == 0
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
return false
}
}
You can create some cancelation method in the repository and call it from class which has lifecycle (Activity, Presenter or ViewModel), for example:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
fun cancel() {
job.cancel()
}
//...
}
class SomePresenter(val repo: NotesRepository) {
fun detachView() {
repo.cancel()
}
}
Or move launching coroutine to some class with lifecycle.

Categories

Resources