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())
}
}
Related
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()
}
I refer to the official Android documentation on using the Flow library in the recommended app architecture.
In the UserRepository class:
class UserRepository #Inject constructor(...) {
fun getUser(userId: String): Flow<User> {
refreshUser(userId)
// Returns a Flow object directly from the database.
return userDao.load(userId)
}
private suspend fun refreshUser(userId: String) {
...
}
...
}
I don't understand how refreshUser(), which is a suspending function, can be called in getUser(), which is a non-suspending function. Perhaps I'm missing something.
I'm trying to create something very similar to this class and, as expected, I get a compilation error stating that the suspending function can be called only in another suspending function. What is the minimal change required to make this work, such that in UserProfileViewModel, I can keep the LiveData<User> variable user as it already is:
val user = userRepository.getUser(userId).asLiveData()
You can't call a suspend function within non-suspend function, so the function getUser() has an error Suspend function 'refreshUser' should be called only from a coroutine or another suspend function. To make this error disappear add suspend keyword:
suspend fun getUser(userId: String): Flow<User> { ... }
To make your second code work you need to use liveData builder.
val user = liveData<User> {
emitSource(getUser(userId).asLiveData())
}
In liveData builder you can call suspend functions, in particular getUser(userId).
These two methods invoke the same use-case. In the first version, I hard-coded Dispatchers.IO, and things work as expected.
The second version (which I prefer) uses an injected dispatcher that defaults to the Dispatchers.IO type. It fails with the IllegalStateException described in the comments. Any ideas?
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsers: GetUsers,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
val liveData: MutableLiveData<List<User>> = MutableLiveData()
suspend fun getUsersByParamDispatcher(params: GetUsers.Params) {
// Successfully works as intended.
viewModelScope.launch(Dispatchers.IO) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
suspend fun getUsersByInjectDispatcher(params: GetUsers.Params) {
// IllegalStateException: Cannot access database on the main thread since it may potentially
// lock the UI for a long period of time.
// at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:494).
viewModelScope.launch(dispatcher) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
}
Logs confirm the exception and my curiosity is why are they different and how would I arrive at a working injected version.
Failing injected Dispatchers.IO:
>> coroutine.name: main
Working parameter Dispatchers.IO:
>> coroutine.name: DefaultDispatcher-worker-1
The dependencies are provided by #HiltViewModel and I expect dispatcher to respect its assigned default value. The Fragment creates this view model with the by viewModels() delegate.
It might be fine to hard-code the dispatcher. But with injection a blocking TestCoroutineDispatcher is easily passed during testing.
Maybe I'm overlooking something simple, or another way altogether.
// MainViewModelTest
#Before
fun setup() {
MockKAnnotations.init(this)
viewModel = MainViewModel(
getUsers,
coroutinesTestRule.testDispatcher
)
}
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}")
}
}
With the below code I'm getting the following error: "Suspend function 'getSomethingFromAPI' should be called only from a coroutine or another suspend function.", which is current. getSomethingFromAPI is indeed a suspend function of the ViewModel.
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(CallVM::class.java)
viewModel.applyLaunch {
this.getSomethingFromAPI()
}
}
fun <T: ViewModel> T.applyLaunch(block: T.() -> Unit)
= apply { viewModelScope.launch(Dispatchers.IO) { block() } }
As you can see though, in the applelaunch function I am executing getSomethingFromAPI inside of a coroutine (launch), but this information is lost. Is there any way to preserve it and keep T as ViewModel at the same time?
To be more specific, is it possible to have a shortcut function that implements two first lines of the below code?
viewModel.apply {
viewModelScope.launch(Dispatchers.IO) {
getSomethingFromAPI()
}
getSomethingFromAPI above sees both 'this' (ViewModel and coroutine).
I know it's not something important to have, but it might be good to know for creating DSL.
You are getting this error because you are trying to call a suspend function in a non-suspend lambda. Make lambda in applyLaunch suspend block: suspend T.() -> Unit