I have a login form. I use StateFlow to send LoginResult (after call API) from ViewModel to Activity. In the Activity, I will show an error dialog if login failed.
It works well for the first time but from the second time I login failed, the error dialog won't show again. I tested both .value and .emit on StateFlow
private val _loginResult = MutableStateFlow(LoginResult())
val loginResult: StateFlow<LoginResult> = _loginResult
fun login(email: String, password: String) {
viewModelScope.launch {
when (val result = loginRepository.login(email, password)) {
is Result.Fail-> {
_loginResult.value = LoginResult(error = "Login failed")
// _loginResult.emit(LoginResult(error = "Login failed")) same issue
}
...
}
}
}
For this case, I use SharedFlow so my Activity still able to collect the new value even it same the last value
private val _loginResult = MutableSharedFlow<LoginResult>()
val loginResult: SharedFlow<LoginResult> = _loginResult
...
_loginResult.emit(LoginResult(error = "Login failed"))
Another possible solution but it's not good is change my current data class LoginResult(...) to class LoginResult. Then every new instance of LoginResult will different
That's by default. What you could do is to emit one value when the login is in progress, before emitting the result. That way you would have a sequence like:
in_progress -> error -> in_progress -> error
Apps usually display a spinner or something when the in_progress or loading or whatever is emitted.
Related
I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.
As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).
Now the problem is that :
Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.
Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.
Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.
My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.
Can anyone guide me how to solve this. Thanks
Some code that might be needed:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
checkNumber()
}
Function which checks the number :
private fun checkNumber() {
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it!!.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
Timber.d("${it.message}")
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
binding.etMobileNumber.setText("")
}
})
}
ViewModel code:
private val _response = MutableLiveData<AuthResponse>()
val response: LiveData<AuthResponse>
get() = _response
fun authDriver(number: Long) = viewModelScope.launch {
Timber.d("Number: $number")
myRepo.authDriver(number).let {
_response.postValue(it)
}
}
P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.
I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.
sealed interface AuthState {
object NotYetRequested: AuthState
object AwaitingResponse: AuthState
class ResponseReceived(val response: AuthResponse): AuthState {
var isHandled = false
private set
fun markHandled() {
isHandled = true
}
}
}
// In ViewModel:
private val _authState = MutableLiveData<AuthState>().also {
it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState
fun requestAuthentication() = viewModelScope.launch {
_authState.value = AuthState.AwaitingResponse
val response = myRepo.authenticate()
_authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:
viewModel.authState.observe(viewLifecycleOwner) { authState ->
when (authState) {
AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
is AuthStateResponseReceived -> when {
authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
authState.response.isSuccessful -> {
goToNextScreen()
authState.markHandled()
}
else -> {
showErrorToast()
ShowUiRequestingAuthentication()
authState.markHandled()
}
}
}
}
I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.
Okay so I've been using StateFlow with Room database for a while. Now I have one common case. At the start of my app I have a logic that if ROOM database is empty I should show an EmptyContent(), otherwise I will show the ListContent() from ROOM database.
Now every time I launch the app, I'm always getting that EmptyContent() shown for a HALF a second maybe, and then the ListContent() is displayed. After that when I'm using the app everything works normal. But at that app launch time, while ROOM database is working I guess, that EmptyContent() is shown for just a small amount of period (Because my StateFlow default value is an empty list), and after that the actual LIST from Database is displayed.
Now I have one solution for that, to just use delay() function inside a Coroutine, to wait for example 200MS and then trigger the function for reading the DATABASE, because those 200MS are enough for ROOM database to actually get the value and update my STATE FLOW variable with the actual data instead of using that StateFlow default value for a half second at the beginning.
Is that a good solution, I must ask? Because I'm using coroutine, the thread is not blocked, and I'm just waiting until ROOM database updates my STATE FLOW variable the second time.
#Composable
fun displayContent(
tasks: List<ToDoTask>,
ListContent: #Composable () -> Unit
) {
val scope = rememberCoroutineScope()
var counter by remember { mutableStateOf(0)}
LaunchedEffect(Unit){
scope.launch {
delay(200)
counter = 1
}
}
if(counter == 1){
if (tasks.isNotEmpty()) {
ListContent()
} else {
EmptyContent()
}
}
}
My suggestion would be map your expected states.
For instance:
sealed class RequestState<out T> {
object Idle : RequestState<Nothing>()
object Loading : RequestState<Nothing>()
data class Success<T>(val data: T) : RequestState<T>()
data class Error(
val t: Throwable,
var consumed: Boolean = false
) : RequestState<Nothing>()
}
And your function would be something like:
#Composable
fun YourScreen() {
val requestState = viewModel.screenData.collectAsState()
when (requestState) {
is Idle ->
// This is the default state, do nothing
is Loading ->
// Display some progress indicator
is Success ->
YourListScreen(requestState.data) // Show the list
is Error ->
// Display an error.
}
LaunchedEffect(Unit) {
viewModel.loadData()
}
}
Of course, in your view model you must emit these values properly...
class YourView: ViewModel() {
private val _screenData =
MutableStateFlow<RequestState<List<ToDoTask>>>(RequestState.Idle)
val screenDatae: StateFlow<RequestState<List<ToDoTask>>> = _screenData
fun loadData() {
_screenData.value = Loading
try {
// load the data from database
_screenData.value = Success(yourLoadedData)
} catch (e: Exception) {
_screenData.value = Error(e)
}
}
}
I am struggling to understand how to handle emitted liveData. I have written four different examples of liveData here,
class MainViewModel : ViewModel() {
val viewModelValue = MyRepo.liveValue
fun viewModelGetNextValue(){
MyRepo.getNextValue()
}
val viewModelSquareValue = MyRepo.squareLiveValue
fun viewModelGetSquareValue(x:Int){
MyRepo.getSquareValue(x)
}
val viewModelEmitValue = MyRepo.emitLiveValue
lateinit var viewModelEmitFunctionValue:LiveData<String>
fun viewModelEmitLiveFunction(x:Int){
viewModelEmitFunctionValue = MyRepo.emitLiveFunction(x)
}
}
object MyRepo{
var value = 1
val liveValue = MutableLiveData<Int>()
fun getNextValue(){
liveValue.postValue(++value)
}
val squareLiveValue = MutableLiveData<Int>()
fun getSquareValue(x:Int){
squareLiveValue.postValue(x*x)
}
val emitLiveValue = liveData {
emit("First Emit")
delay(2000)
emit("second value")
}
fun emitLiveFunction(x:Int) = liveData {
emit("value: $x")
delay(2000)
emit("square: ${x*x}")
}
}
And part of the Fragment code is,
viewModel.viewModelValue.observe(viewLifecycleOwner, Observer {
Toast.makeText(activity, "$it", Toast.LENGTH_SHORT).show()
})
viewModel.viewModelSquareValue.observe(viewLifecycleOwner, Observer {
Toast.makeText(activity, "$it", Toast.LENGTH_SHORT).show()
viewModel.viewModelSquareValue.removeObservers(viewLifecycleOwner)
})
viewModel.viewModelEmitValue.observe(viewLifecycleOwner, Observer {
Toast.makeText(activity, it, Toast.LENGTH_SHORT).show()
})
button1.setOnClickListener { viewModel.viewModelGetNextValue() }
button2.setOnClickListener { viewModel.viewModelGetSquareValue(++x) }
button3.setOnClickListener {
viewModel.viewModelEmitLiveFunction(++x)
viewModel.viewModelEmitFunctionValue.observe(viewLifecycleOwner, Observer {
Toast.makeText(activity, it, Toast.LENGTH_SHORT).show()
})
}
First Two examples of LiveData (viewModelValue and viewModelSquareValue) is easy to observe. and can be invoked with the button's click listener. The third livedata viewModelEmitValue where I have used emit automatically shows the value.
What do I have to do if I want those values after a button being
clicked? Do I just have to write the observer code within the click
listener?
The last liveData viewModelEmitFunctionValue is working. But is it
the only way (using lateinit var) to get the value if I want to get it after I click a
button?
The last liveData viewModelEmitFunctionValue is working. But is it the only way (using lateinit var) to get the value if I want to get it after I click a button?
In this way you are creating observers for each button click, adding an additional Toast every other click. Good news is that you are creating LiveData instance as well with every click so previous observers cleaned up. But it's a bit of a messy approach.
val emitLiveValue = liveData { is not a lazy way to declare a LiveData, so once you Repo is initialized it starts executing code inside liveData{}
In case of fun emitLiveFunction(x:Int) = liveData { you are creating LiveData only at the moment of calling the function, so that's why it works well.
My suggestion is to store x value in live data and calculate emitLiveFunction on each change of it. You can achieve it using Transformations.switchMap
class MainViewModel : ViewModel() {
...
private val x = MutableLiveData<Int>()
val functionResult = x.switchMap { MyRepo.emitLiveFunction(it) }
fun viewModelEmitLiveFunction(x:Int) {
this.x.postValue(x)
}
}
Now you can add an observer to functionResult right after activity created and calling viewModelEmitLiveFunction(x) on button 3 click you will initiate repo function execution with new value x
I'm making a system of "sessions" where the user can launch, finish and view his session.
The user go through a first fragment to create his session and then go into a fragment "in session".
If he return to the main menu before finishing his session, I want him to go directly to "in session" without going through the "new session" fragment.
All session data are stored into a local database and I use Kotlin coroutines to fetch data from the db (see code example below)
It's my first time using coroutine, and I will admit it's a bit fuzzy
for me, all the help is welcome
The problem is that when the user press the bouton to navigate, the coroutine finish after the verification to see if there is a current session, that lead to verify a null object or the previous session of the current session, and so navigate to a the "new session" fragment
What I need is a way to wait for the coroutine to finish and then
handle the button click
All the code wrote here is contain inside inside the viewModel.
This is how I setup the Job/Scope
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
And this is how I launch the coroutine:
private fun initializeLastSession() {
uiScope.launch {
lastSession.value = getLastSessionFromDatabase()
}
}
private suspend fun getLastSessionFromDatabase(): Session? {
return withContext(Dispatchers.IO) {
var session = database.getLastSession()
session
}
}
The verification is made inside this function
fun isSessionActive(): Boolean {
//Simplified
if (lastSession.value = null) {
return false
} else if (...) {
return true
} else {
return false
}
This last function "isSessionActive" is called from an if statement from the fragment itlsef, when the user press the navigation button.
If it's true then it navigate to "InSession", else in "newSession"
I've seen multiple way of waiting for a coroutine to finish but none match the way I launch it, and even less have a solution that has worked for me.
Would you allow me with a simple example unrelated to your code? But strongly related to the problem:
uiScope.launch{
withContext(Dispatchers.IO){
val dataFromDatabase = getSomeDataFromDatabase()
if (dataFromDatabase.notEmpty()){ //or something
withContext(Dispatchers.Main){
//send data to fragment here :)
}
}
}
}
EDIT:
Since you stated you are in the ViewModel, you don't need to return any value, you need to observe that changed value:
//on top of your ViewModel class:
val yourVariableName: MutableLiveData<Boolean> = MutableLiveData()
//than in your method:
uiScope.launch{
withContext(Dispatchers.IO){
val dataFromDatabase = getSomeDataFromDatabase()
if (dataFromDatabase.notEmpty()){ //or something
withContext(Dispatchers.Main){
if (lastSession.value = null) {
yourVariableName.value = false
} else if (...) {
yourVariableName.value = true
} else {
yourVariableName.value = false
}
}
}
}
}
And than in your fragment:
//after you have successfully instantiated the `ViewModel`:
mViewModel.yourVariableName.observe(this , Observer{ valueYouAreObserving->
// and here you have the value true ore false
Log.d("Tag", $valueYouAreObserving)
})