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).
Related
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())
}
}
I am writing a unit test for my Datarepository layer which simply calls an interface.
I am using Kotlin, coroutines and MockK for unit testing.
In MockK, how can I verify that I have called apiServiceInterface.getDataFromApi() and has happened only once?
Should I put the code in runBlocking?
This is my code:
UnitTest
import com.example.breakingbad.api.ApiServiceInterface
import com.example.breakingbad.data.DataRepository
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import org.junit.Test
Repository
class DataRepositoryTest {
#MockK
private lateinit var apiServiceInterface: ApiServiceInterface
#InjectMockKs
private lateinit var dataRepository: DataRepository
#Test
fun getCharacters() {
val respose = dataRepository.getCharacters()
verify { apiServiceInterface.getDataFromApi() }
}
}
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCharacters(): Result<ArrayList<Character>> = kotlin.runCatching{
apiServiceInterface.getDataFromApi()
}
}
Interface
interface ApiServiceInterface {
#GET("api/characters")
suspend fun getDataFromApi(): ArrayList<Character>
}
I think you should prefer using runTest instead of runBlocking or runBlockingTest.To tell you in brief about the three.
runBlocking allows you to call suspend functions by blocking a new coroutine and it blocks the current thread until it is completed.
runBlockingTest will immediately execute the suspending function skipping past any delay and enter coroutine block immediately unlike runBlocking which will wait for the amount of the delay
Since kotlinx.coroutines 1.6.0 release, runBlockingTest is deprecated in favour of runTest due to these reasons listed in the migration guide.
runTest() will automatically skip calls to delay() and handle uncaught exceptions. Unlike runBlockingTest() , it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module.
I hope that answers your question of what to choose among these 3 to test your suspending function. You code would look like this -:
#Test
fun getCharacters() = runTest {
val response = dataRepository.getCharacters()
coVerify { apiServiceInterface.getDataFromApi() }
}
Also note that as David mentioned above, because getDataFromApi() is asynchronous/suspending function as well, you will have to use coVerify instead of verify to mock the same.
Yes, you should put the dataRepository.getCharacters() call in a runBlocking.
And the verify should be replaced for coVerify.
In the end, the test should look like this:
#Test
fun getCharacters() {
val respose = runBlocking { dataRepository.getCharacters() }
coVerify { apiServiceInterface.getDataFromApi() }
}
Also, since you want to verify it has happened only once, you need to call coVerify with the exactly parameter coVerify(exactly = 1)
I want to use reactive paradigm using Kotlin Flow in my Android project. I have an external callback-based API so my choice is using callbackFlow in my Repository class.
I've already read insightfully some proper docs with no help:
callbackFlow documentation
Callbacks and Kotlin Flows by Roman Elizarov
What I want to achieve:
Currently my Repository class looks like this (simplified code):
lateinit var callback: ApiCallback
fun someFlow() = callbackFlow<SomeModel> {
callback = object : ApiCallback {
override fun someApiMethod() {
offer(SomeModel())
}
}
awaitClose { Log.d("Suspending flow until methods aren't invoked") }
}
suspend fun someUnfortunateCallbackDependentCall() {
externalApiClient.externalMethod(callback)
}
Problem occurs when someUnfortunateCallbackDependentCall is invoked faster than collecting someFlow().
For now to avoid UninitializedPropertyAccessException I added some delays in my coroutines before invoking someUnfortunateCallbackDependentCall but it is kind of hack/code smell for me.
My first idea was to use by lazy instead of lateinit var as this is what I want - lazy initialization of callback object. However, I couldn't manage to code it altogether. I want to emit/offer/send some data from someApiMethod to make a data flow but going outside of callbackFlow would require ProducerScope that is in it. And on the other hand, someUnfortunateCallbackDependentCall is not Kotlin Flow-based at all (could be suspended using Coroutines API at best).
Is it possible to do? Maybe using some others Kotlin delegates? Any help would be appreciated.
To answer your question technically, you can of course intialise a callback lazyily or with lateinit, but you can't do this AND share the coroutine scope (one for the Flow and one for the suspend function) at the same time - you need to build some kind of synchronisation yourself.
Below I've made some assumptions about what you are trying to achieve, perhaps they are not perfect for you, but hopefully give some incite into how to improve.
Since it is a Repository that you are creating, I will first assume that you are looking to store SomeModel and allow the rest of your app to observe changes to it. If so, the easiest way to do this is with a MutableStateFlow property instead of a callbackFlow:
interface Repository {
val state: Flow<SomeModel>
suspend fun reload()
}
class RepositoryImpl(private val service: ApiService) : Repository {
override val state = MutableStateFlow(SomeModel())
override suspend fun reload() {
return suspendCoroutine { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
state.value = data
if (continuation.context.isActive)
continuation.resume(Unit)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
The downside to this solution is that you have to call reload() in order to actually make a call to your backend, collecting the Flow alone is not enough.
myrepository.state.collect {}
myrepository.reload()
Another solution, again depending on what exactly you are trying to achieve, is to provide two ways to call your backend:
interface Repository {
fun someFlow(): Flow<SomeModel>
suspend fun reload(): SomeModel
}
class RepositoryImpl(private val service: ApiService) : Repository {
override fun someFlow() = callbackFlow<SomeModel> {
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
offer(data)
}
})
awaitClose {
Log.d("TAG", "Callback Flow is closed")
}
}
override suspend fun reload(): SomeModel {
return suspendCoroutine<SomeModel> { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
if (continuation.context.isActive)
continuation.resume(data)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
Now you can either call reload() or someFlow() to retrieve SomeModel() and the Repository holds no "state".
Note that the reload() function is simply a 'coroutine' version of the callbackFlow idea.
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
I have developed 2 functions for the login.
The first "loginOne" works when I use the ViewModel scope.
The other one doesn't work when I use the LiveData scope.
Do you have an idea? I want to make "loginTwo" work.
API
interface LoginAPI {
#POST("login")
suspend fun getUser(#Body loginRequest: LoginRequest): User
}
Repository
class LoginRepository(private val loginAPI: LoginAPI) {
suspend fun getUser(loginRequest: LoginRequest) = loginAPI.getUser(loginRequest)
}
ViewModel
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private var user: LiveData<User>? = null
fun loginOne(username: String, password: String) {
viewModelScope.launch {
// i can enter here and get the user :)
val user = loginRepository.getUser(LoginRequest(username, password))
user
}
}
fun loginTwo(username: String, password: String) {
user = liveData(Dispatchers.IO) {
// i never enter inside.. why ?
val user = loginRepository.getUser(LoginRequest(username, password))
emit(user)
}
}
fun getUser(): LiveData<User>? = user
}
Fragment, my viewModel is injected with Koin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loginViewModel.getUser()?.observe(this, Observer { user ->
Log.d(LoginFragment::class.java.name, "User : $user ")
})
loginViewModel.loginOne("user","pcw123")
loginViewModel.loginTwo("user","pcw123")
}
Make sure that you created Scope in the right way. Also, that you are using appropriate Dispatchers to achieve wanted results.
You can additionally check if the call is being executed when you wanted to postValue.
Check if Job is still alive.
Check this thing.
Your emmit call looks suspicious.
When using LiveData, you might need to calculate values asynchronously. For example, you might want to retrieve a user's preferences and serve them to your UI. In these cases, you can use the liveData builder function to call a suspend function, serving the result as a LiveData object.
Each emit() call suspends the execution of the block until the LiveData value is set on the main thread.
In the example below, loadUser() is a suspend function declared elsewhere. Use the liveData builder function to call loadUser() asynchronously, and then use emit() to emit the result:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
EDIT: MutableLiveData for user variable - resolved the issue.
From the documentation:
The liveData building block serves as a structured concurrency
primitive between coroutines and LiveData. The code block starts
executing when LiveData becomes active and is automatically canceled
after a configurable timeout when the LiveData becomes inactive.
So, in your case, the 'user' liveData is already activated when you observing it from fragment. Because you called loginTwo() after liveData has been observed, the emit function will not triggered anymore. Try to call loginTwo() before observing liveData to get emit value from liveData ktx.