how to delete room database when app closed? - android

i tried to call ViewModel to delete table when activity Destroy but it not work, when i call ViewModel when activity stop i get a error.
DAO interface
#Dao
interface NoteDAO {
#Query("select * from note_table")
fun getAllNote() : LiveData<List<Note>>
#Query("DELETE FROM note_table")
fun deleteAllData()
}
Repository
class NoteRepository(app:Application) {
private val noteDAO : NoteDAO
init {
val noteDatabase: NoteDatabase = NoteDatabase.getInstance(app)
noteDAO = noteDatabase.getNoteDao()
}
fun getAllNote():LiveData<List<Note>> = noteDAO.getAllNote()
fun deleteAllData() = noteDAO.deleteAllData()
}
Viewmodel
class NoteViewModel(app: Application): ViewModel() {
private val noteRepository: NoteRepository = NoteRepository(app)
fun deleteAllData() = noteRepository.deleteAllData()
fun getAllNote() : LiveData<List<Note>> = noteRepository.getAllNote()
class NoteViewModelFactory(private val application: Application) : ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(NoteViewModel::class.java)){
return NoteViewModel(application) as T
}
throw IllegalArgumentException("Unable construct viewmodel")
}
}
}
when i call in onStop method
override fun onStop() {
super.onStop()
noteViewModel.deleteAllData()
}
i get the error
java.lang.RuntimeException: Unable to stop activity {com.example.database/com.example.database.MainActivity}: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
at android.app.ActivityThread.callActivityOnStop(ActivityThread.java:4624)
at android.app.ActivityThread.performStopActivityInner(ActivityThread.java:4594)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:4669)
at android.app.servertransaction.StopActivityItem.execute(StopActivityItem.java:41)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
please help me and have a nice day,everyone!

deleteDatabase(AppDatabase.DATABASE_NAME);
Or
new Thread(() -> AppDatabase.getInstance(NavigationActivity.this).clearAllTables()).start(); //clear all rows from database

if you want to perform database operation on main thread then add this line while building database object
.allowMainThreadQueries()
Full Sample
Room.databaseBuilder(appContext,LocalDatabase::class.java,"app.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
But It's not good practice to perform a database operation on the main thread.
You must use coroutines to perform database operation

I would do it in the onStop lifecycle function, calling the viewmodel, and in the viewmodel calling a coroutine executed in a background thread.
Activity/Fragment:
override fun onStop() {
viewModel.wipeData()
}
ViewModel:
class ViewModel #Inject constructor (wipeDataUseCase: WipeDataUseCase){
fun wipeData() {
viewModelScope.launch(IO) {
when (wipeDataUseCase()){
true -> {
//do something
}
false -> {
// do something
}
}
}
}
}
Use Case:
class WipeDataUseCase #Inject constructor (roomDataBase: RoomDataBase){
suspend operator fun invoke() = roomDataBase.dao().wipeData()
}
I would've use the coroutine mentioned above, launching the process in the background calling its respective Use Case.
As it is meant to be on the moment that the app is over there is no much thing to play with the callback of the wiping data operation.

To delete your database, you can use
deleteDatabase(AppDatabase.DATABASE_NAME);
or
new Thread(() -> AppDatabase.getInstance(NavigationActivity.this).clearAllTables()).start();
//clear all rows from database

You are running a database query on Main Thread which could potentially block UI. You should use coroutines to execute your queries on the background thread.
Make your deleteAllData method suspend
#Query("DELETE FROM note_table")
suspend fun deleteAllData()
Also, make deleteAllData method suspend in NoteRespository
suspend fun deleteAllData() = noteDAO.deleteAllData()
In your NoteViewModel class, call this method in viewModelScope
fun deleteAllData() = viewModelScope.launch {
noteRepository.deleteAllData()
}

Related

"No virtual method ~~" error occurs if the suspend modifier is removed

I am writing code to fetch data from Room.
as shown in the image below, the IDE indicates that suspend is a useless modifier.
So, if I remove suspend, the following error occurs.
E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.example.lightweight, PID: 6483
java.lang.NoSuchMethodError: No virtual method getWorkoutList(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; in class Lcom/example/lightweight/data/repository/WorkoutListRepository; or its super classes (declaration of 'com.example.lightweight.data.repository.WorkoutListRepository' appears in /data/data/com.example.lightweight/code_cache/.overlay/base.apk/classes13.dex)
at com.example.lightweight.presentation.viewmodel.WorkoutListViewModel$getList$1.invokeSuspend(WorkoutListViewModel.kt:23)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}#6975010, Dispatchers.IO]
I/Process: Sending signal. PID: 6483 SIG: 9
However, if I leave suspend as is, it runs normally. For what reason?
DAO
#Dao
interface WorkoutDao {
#Query("SELECT * From WorkoutListTest")
fun getWorkoutList() : List<WorkoutListTest>
}
ViewModel
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
private var _list = MutableLiveData<List<String>>()
val list: LiveData<List<String>>
get() = _list
fun getList() {
viewModelScope.launch(Dispatchers.IO) {
repository.getWorkoutList()
}
}
}
Repository
class WorkoutListRepository(private val dao: WorkoutDao) {
fun getWorkoutList() : List<WorkoutListTest> {
val list: List<WorkoutListTest> = dao.getWorkoutList()
return list
}
}
The part that uses ViewModel in Fragment.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vm.getList()
vm.list.observe(viewLifecycleOwner) { list ->
}
}
It's because your DAO function is not marked suspend. Since the DAO function is a regular synchronous, blocking function, calling through to it in your suspend function doesn't take advantage of the suspend modifier. Incidentally, this would also mean your suspend function is blocking, which breaks convention and would cause your coroutines to block threads incorrectly. But the Kotlin compiler cannot detect that the DAO function is blocking, so the warning message doesn't mention that.
You need to mark your DAO function suspend:
#Dao
interface WorkoutDao {
#Query("SELECT * From WorkoutListTest")
suspend fun getWorkoutList() : List<WorkoutListTest>
}
You said that leaving suspend, it ran normally. This is because you launched your coroutine using Dispatchers.IO. But you shouldn't have to use Dispatchers.IO unless you're calling a blocking function. By convention, suspend functions never block. So if you were following conventions, you would not use Dispatchers.IO to call a suspend function, but then your incorrectly blocking suspend function would hang the main thread.
Side note, there's a much easier way to fetch an item from a suspend function one time to fill in a LiveData. Your ViewModel class could be changed to the following and it would have the exact same behavior:
class WorkoutListViewModel(
private val repository: WorkoutListRepository
) : ViewModel() {
val list: LiveData<List<String>> = liveData {
emit(repository.getWorkoutList())
}
}

How to return value in coroutine scope?

Is it possible to to return value in Coroutine Scope without run blocking?
For now my code in repository looks like this:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? {
runBlocking {
return#runBlocking
CoroutineScope(Dispatchers.Main).launch {
getWorkItemByIdUseCase.build(workItemId)
}
}
return null
}
this is my useCase
class GetWorkItemByIdUseCase(private val workItemDao: WorkItemDao) :
BaseUseCase<Int, WorkItemRoom>() {
override suspend fun create(id: Int): WorkItemRoom {
return workItemDao.getWorkItemById(id)
}
}
baseUseCase
abstract class BaseUseCase<P, R> {
protected abstract suspend fun create(params: P): R
open suspend fun build(params: P): R = create(params)
}
Dao
#Dao
abstract class WorkItemDao {
#Query("SELECT * FROM workitem WHERE id=:id")
abstract suspend fun getWorkItemById(id: Int): WorkItemRoom
}
... but certainly I know it is not a proper solution. How would you achieve this? In viewmodels' or fragments I can directly use lifecycleScope`, but what in other cases, where the must is to call useCase directly from method below. Is it efficient to call Dispatchers.Main all the time?
CoroutineScope(Dispatchers.Main).launch { }
You could just pass the value from a coroutine to liveData and then use an observer
private val observableMutableLiveData = MutableLiveData<Type>()
val observableLiveData: LiveData<Type> = observableMutableLiveData
and later in a coroutine:
CoroutineScope(Dispatchers.Main).launch {
observableMutableLiveData.postValue(value)
}
and then in an activity:
viewModel.observableLiveData.observe(this) { Type ->
Log.i("result", Type)
}
It doesn't make sense to use runBlocking in a suspend function. (It hardly ever makes sense to use it at all, except as a bridge between coroutines and non-coroutine code in a project that is partially converted to using coroutines but still needs to support legacy code or libraries.)
You should just call the function you need.
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? { //may not need nullable return value
return getWorkItemByIdUseCase.build(workItemId)
}
If you need to specify a dispatcher, use withContext:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? = withContext(Dispatchers.Main) {
getWorkItemByIdUseCase.build(workItemId)
}
However, if build is a suspend function, there's no need to specify a Dispatcher when calling it. Suspend functions are responsible for internally calling their functions on appropriate threads/dispatchers.
If you need a coroutine scope inside a coroutine or suspend function, use the lowercase coroutineScope function, which creates a scope that will be automatically cancelled if the coroutine is cancelled. This example doesn't make much sense, because normally you don't need a new scope unless you are running parallel jobs inside it:
suspend fun getWorkItem(workItemId: Int): WorkItemRoom? = coroutineScope(Dispatchers.Main) {
getWorkItemByIdUseCase.build(workItemId)
}
Have you ever listened about a lambda?
It looks like call: (MyResult) -> Unit
I use it from time to time like
fun someToDo(call: (MyResult) -> Unit) {
scope.launch(Dispatchers.IO) {
val result = getFromSomeWere()
launch(Dispatchers.Main){call(result)}
}
}

Why is it trying to access the database on the main thread?

I am getting this error while trying to display Room data in a LazyColumn in my project.
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
I don't know why it is trying to access the database since I'm getting it with a ViewModelScope. Bellow is my MainActivity code and the ViewModel
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val users = viewModel.userList.value
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(users){data->
MyCard(data)
}
#HiltViewModel
class UserViewModel #Inject constructor(
private val repository: MainRepository
) : ViewModel() {
val userList: MutableState<List<User>> = mutableStateOf(listOf())
init {
viewModelScope.launch {
try {
val result: List<User> = repository.getAllUsers()
userList.value = result
} catch (e: Exception) {
Log.e("SSS", "${e.message.toString()}; ${e.stackTrace}")
}
}
}
Assuming you are following the pattern of your repository passing through functions from the DAO, you should mark this function in your DAO as a suspend function. This will cause it to automatically use a background thread. Then mark your corresponding repository function suspend so it can call the other suspend function.
Then in your coroutine, since getAllUsers() is a proper suspend function that internally handles proper use of background threads, there is nothing more you need to change.
The reason it gives you the warning is that by default, the viewModelScope runs on the main thread. It is up to you to wrap blocking calls in withContext(Dispatchers.IO) to run them in a background thread. But if you use suspend functions from the DAO, you don't have to worry about that because the function isn't blocking.
When launch { ... } is used without parameters, it inherits the
context (and thus dispatcher) from the CoroutineScope it is being
launched from.
Use IO thread to execute your code
viewModelScope.launch(Dispatchers.IO) {
try {
val result: List<User> = repository.getAllUsers()
userList.postValue(result)
} catch (e: Exception) {
Log.e("SSS", "${e.message.toString()}; ${e.stackTrace}")
}
}

Why do I get 'Cannot access database on the main thread since it may potentially lock the UI for a long period of time.' error in Android Studio?

I get the error "java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time." when I run Code A, why?
The Code B can work well when I replace with fun add(aMVoice: MVoice)=viewModelScope.launch (Dispatchers.IO){}, why?
The Code C can work well when I replace with suspend fun add(aMVoice: MVoice), why?
And more, which one is better between Code B and Code C?
Code A
#Dao
interface DBVoiceDao{
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun add(aMVoice: MVoice)
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun add(aMVoice: MVoice){
mDBVoiceDao.add(aMVoice)
}
}
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
fun add(aMVoice: MVoice)=viewModelScope.launch{
mDBVoiceRepository.add(aMVoice)
}
}
class FragmentHome : Fragment() {
private val mHomeViewModel by lazy {...}
...
btnInsert.setOnClickListener {
val aMVoice = MVoice()
mHomeViewModel.add(aMVoice)
}
...
}
Code B
#Dao
interface DBVoiceDao{
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun add(aMVoice: MVoice)
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun add(aMVoice: MVoice){
mDBVoiceDao.add(aMVoice)
}
}
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
fun add(aMVoice: MVoice)=viewModelScope.launch (Dispatchers.IO){ //I add Dispatchers.IO
mDBVoiceRepository.add(aMVoice)
}
}
class FragmentHome : Fragment() {
private val mHomeViewModel by lazy {...}
...
btnInsert.setOnClickListener {
val aMVoice = MVoice()
mHomeViewModel.add(aMVoice)
}
...
}
Code C
#Dao
interface DBVoiceDao{
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun add(aMVoice: MVoice) //I add suspend
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun add(aMVoice: MVoice){
mDBVoiceDao.add(aMVoice)
}
}
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
fun add(aMVoice: MVoice)=viewModelScope.launch {
mDBVoiceRepository.add(aMVoice)
}
}
class FragmentHome : Fragment() {
private val mHomeViewModel by lazy {...}
...
btnInsert.setOnClickListener {
val aMVoice = MVoice()
mHomeViewModel.add(aMVoice)
}
...
}
}
Option A uses viewModelScope.launch. The default dispatcher for viewModelScope is Dispatchers.Main.immediate as per the documentation. As add isn't a suspending method, it runs on that dispatcher directly - i.e., it runs on the main thread.
Option B uses viewModelScope.launch(Dispatchers.IO) which means the code runs on the IO dispatcher. As this isn't the main thread, it succeeds.
Option C makes add a suspending function. As per the Async queries with Kotlin coroutines guide, this automatically moves the database access off of the main thread for you, no matter what dispatcher you are using. Option C is always the right technique to use when using Room + Coroutines
Room doesn't support database access on the main thread unless you've called allowMainThreadQueries() on the builder because it might lock the UI for a long period of time. Asynchronous queries—queries that return instances of LiveData or Flowable—are exempt from this rule because they asynchronously run the query on a background thread when needed.

Is it possible to spy on suspend Android Room DAO functions with MockK

I am investigation the MockK library with my Android JUnit tests
testImplementation "io.mockk:mockk:1.10.0"
I have an issue when attempting to spyk on suspend functions
heres my Junit test
#ExperimentalCoroutinesApi
#FlowPreview
#RunWith(AndroidJUnit4::class)
class BackOffCriteriaDaoTest : BaseTest() {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: BackoffCriteriaDAO
#Test
fun backOffCriteria() = runBlocking {
dao = spyk(myRoomDatabase.backoffCriteriaDAO())
assertNotNull(dao.getBackoffCriteria())
assertEquals(backOffCriteriaDO, dao.getBackoffCriteria())
dao.delete()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
}
This test throws an java.lang.AssertionError at dao.reset() as follows:-
java.lang.AssertionError: Verification failed: call 2 of 2: BackoffCriteriaDAO_Impl(#2).reset(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/reset(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
My dao reset() method resembles this:-
#Transaction
suspend fun reset() {
delete()
insert(BackoffCriteriaDO(THE_BACKOFF_CRITERIA_ID, BACKOFF_CRITERIA_MILLISECOND_DELAY, BACKOFF_CRITERIA_MAX_RETRY_COUNT))
}
Why am I seeing this java.lang.AssertionError?
How do I coVerify that suspend functions have been called?
UPDATE
I believe the issue is caused by the fact I am using Room database.
My dao interface method reset() is implemented by room generated code as
#Override
public Object reset(final Continuation<? super Unit> p0) {
return RoomDatabaseKt.withTransaction(__db, new Function1<Continuation<? super Unit>, Object>() {
#Override
public Object invoke(Continuation<? super Unit> __cont) {
return BackoffCriteriaDAO.DefaultImpls.reset(BackoffCriteriaDAO_Impl.this, __cont);
}
}, p0);
}
which means the coVerify{} is matching this function and not my interface version.
Is it possible to match this generated version of public Object reset(final Continuation<? super Unit> p0)?
Is this a more basic issue with mockk that it cannot mockk java classes?
Or Java implementations of Kotlin interfaces?
UPDATE 2
When my Room DAO functions are not suspend then Mockk works as required
using these dummy functions in my DAO:-
#Transaction
fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
My test passes
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
verify { dao.experiment() }
}
When I change my dummy functions as follows the test still passes
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
However when I change my dummy functions as follows the test throws an exception
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
suspend fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
The failing tests resembles this:-
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
coVerify { dao.experiment() }
}
The exception is
java.lang.AssertionError: Verification failed: call 1 of 1: BackoffCriteriaDAO_Impl(#2).experiment(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/experiment(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
There might be nothing wrong with spy but the asynchronous nature of the transaction function you are invoking.
To test with suspending functions with scope you might need to use
launch builder and advance time until idle, or for a time period for testing the progress, like it's done with RxJava counter parts.
I had the same issue with MockWebServer, here you can check out the question.
launch {
dao.delete()
}
advanceUntilIdle()
And use Coroutine rule with tests to have same scope for each operation.
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
#Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
You can use rule as below
testCoroutineRule.runBlockingTest {
dao.delete()
advanceUntilIdle()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
Also you can try putting dao.delete() in launch. In some tests it did not work without launch while some other work without it and even some of them are flakky with everything i tried. There are some issues with coroutines-test to be solved.
here you can check how it's done and there are some issues with test-coroutines, you can check out my other question here.
I created a playground to test coroutines, it might helpful and you can test out the issues with coroutines, and another one with mockK and coroutines tests.

Categories

Resources