I have some trouble with Android data-binding.
I have a class like this:
class AppConfig private constructor() : BaseObservable() {
#Bindable
var title = ""
fun updateTitle(newTitle: String) {
title = newTitle
notifyPropertyChanged(BR.title)
}
......
}
When the app is in background, the app received an update push and function updateTitle is called. Then I turn to my app, I can see the title has changed. Then I push another update, the title doesn't change. Then I press the home button and bring the app to front again, the title is updated.
I have read the ViewDataBinding source code:
protected void requestRebind() {
if (mContainingBinding != null) {
mContainingBinding.requestRebind();
} else {
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (mLifecycleOwner != null) {
Lifecycle.State state = mLifecycleOwner.getLifecycle().getCurrentState();
if (!state.isAtLeast(Lifecycle.State.STARTED)) {
return; // wait until lifecycle owner is started
}
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
The condition !state.isAtLeast(Lifecycle.State.STARTED) failed at the first time, and variable mPendingRebind is set true. It seems that only when mRebindRunnable or mFrameCallback runs, variable mPendingRebind will be set false again. So the UI will never refresh.
I've seen this issue Data binding - XML not updating after onActivityResult. I try to use SingleLiveEvent, and I call updateObserver.call() in Activity's onResume. It doesn't work.
I've also tried to use reflect to set mPendingRebind false forcibly. It works but I think this is not a good way. What should I do?
Try this
var title = ""
#Bindable get() = title
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()
}
}
}
}
So basically, on the snackbar action button, I want to Retry API call if user click on Retry.
I have used core MVVM architecture with Flow. I even used Flow between Viewmodel and view as well. Please note that I was already using livedata between view and ViewModel, but now the requirement has been changed and I have to use Flow only. Also I'm not using and shared or state flow, that is not required.
Code:
Fragment:
private fun apiCall() {
viewModel.fetchUserReviewData()
}
private fun setObservers() {
lifecycleScope.launch {
viewModel.userReviewData?.collect {
LogUtils.d("Hello it: " + it.code)
setLoadingState(it.state)
when (it.status) {
Resource.Status.ERROR -> showErrorSnackBarLayout(-1, it.message, {
// Retry action button logic
viewModel.userReviewData = null
apiCall()
})
}
}
}
Viewmodel:
var userReviewData: Flow<Resource<ReviewResponse>>? = emptyFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
userReviewData = flow {
emit(Resource.loading(true))
repository.getUserReviewData().collect {
emit(it)
}
}
}
EDIT in ViewModel:
// var userReviewData = MutableStateFlow<Resource<ReviewResponse>>(Resource.loading(false))
var userReviewData = MutableSharedFlow<Resource<ReviewResponse>>()
fun fetchUserReviewData() {
viewModelScope.launch {
userReviewData.emit(Resource.loading(true))
repository.getUserReviewData().collect {
userReviewData.emit(it)
}
}
}
override fun onCreate() {}
}
EDIT in Activity:
private fun setObservers() {
lifecycleScope.launchWhenStarted {
viewModel.userReviewData.collect {
setLoadingState(it.state)
when (it.status) {
Resource.Status.SUCCESS ->
if (it.data != null) {
val reviewResponse: ReviewResponse = it.data
if (!AppUtils.isNull(reviewResponse)) {
setReviewData(reviewResponse.data)
}
}
Resource.Status.ERROR -> showErrorSnackBarLayout(it.code, it.message) {
viewModel.fetchUserReviewData()
}
}
}
}
}
Now, I have only single doubt, should I use state one or shared one? I saw Phillip Lackener video and understood the difference, but still thinking what to use!
The thing is we only support Portrait orientation, but what in future requirement comes? In that case I think I have to use state one so that it can survive configuration changes! Don't know what to do!
Because of the single responsibility principle, the ViewModel alone should be updating its flow to show the latest requested data, rather than having to cancel the ongoing request and resubscribe to a new one from the Fragment side.
Here is one way you could do it. Use a MutableSharedFlow for triggering fetch requests and flatMapLatest to restart the downstream flow on a new request.
A Channel could also be used as a trigger, but it's a little more concise with MutableSharedFlow.
//In ViewModel
private val fetchRequest = MutableSharedFlow<Unit>(replay = 1, BufferOverflow.DROP_OLDEST)
var userReviewData = fetchRequest.flatMapLatest {
flow {
emit(Resource.loading(true))
emitAll(repository.getUserReviewData())
}
}.shareIn(viewModelScope, SharingStarted.WhlieSubscribed(5000), 1)
fun fetchUserReviewData() {
LogUtils.d("Hello fetchUserReviewData: " + userReviewData)
fetchRequest.tryEmit(Unit)
}
Your existing Fragment code above should work with this, but you no longer need the ?. null-safe call since the flow is not nullable.
However, if the coroutine does anything to views, you should use viewLifecycle.lifecycleScope instead of just lifecycleScope.
I receive some user data from server at my app. One of the field which I receive has boolean data type and it changes due to user actions on server. According to value of this variable I have to show/hide a part of my layout. But I don't have enough time for it or I did it in wrong way :( So, first of all I send request at my singleton class and after getting response I save this json field to SharedPreferences:
if (!app_data!!.get("questionnaire").isJsonNull) {
context.getSharedPreferences("app_data", 0).edit().putBoolean("questionnaire", app_data.get("questionnaire").asBoolean).apply()
}
at this time I move to my fragment where I have to change visibility:
val bundle = Bundle()
val personalPage = PersonalPage()
if (sp!!.getBoolean("questionnaire", false)) {
bundle.putBoolean("questionnaire", true)
} else {
bundle.putBoolean("questionnaire", false)
}
personalPage.arguments = bundle
supportFragmentManager.beginTransaction().replace(R.id.contentContainerT, personalPage).addToBackStack(null).commit()
bottomNavigationView.selectedItemId = R.id.home_screen
and as you can see I send this variable via bundle but as I see it doesn't work good. And at target fragment I try to change visibility:
val bundle = arguments
val cont: RelativeLayout = rootView.findViewById(R.id.polls_container_view)
if (bundle != null) {
if (bundle.getBoolean("questionnaire")) {
if (sp.getBoolean("questionnaire", false) || cont.visibility == View.GONE) {
fragmentManager!!
.beginTransaction()
.detach(PersonalPage())
.attach(PersonalPage())
.commit()
}
cont.visibility = View.VISIBLE
val pollsBtn = rootView.findViewById<Button>(R.id.polls_button)
val pollsInfo = rootView.findViewById<Button>(R.id.polls_info)
pollsBtn.setOnClickListener {
activity!!.supportFragmentManager.beginTransaction().replace(R.id.contentContainerT, PollsScr()).addToBackStack(null).commit()
}
pollsInfo.setOnClickListener {
showPollsInfoMSG()
}
} else {
if (!sp.getBoolean("questionnaire", false) || cont.visibility == View.VISIBLE) {
fragmentManager!!
.beginTransaction()
.detach(PersonalPage())
.attach(PersonalPage())
.commit()
}
cont.visibility = View.GONE
}
}
But I see that when a user changes variable value from true to false and move to this fragment my view is still visible and I have to reload fragment for hiding view. And also when user receive true from server I also have to reload this fragment. I tried to get bool variable at onStart() fun but maybe I don't have enough time for it? Maybe this problem can be solved via broadcast receiver but I don't know how to do it :(
Kotlin channel stops being able to send events after putting app in background (don't keep activities enabled)
class UserRepositoryImpl(
private val userRequestDataSource: UserRequestDataSourceContract,
) : UserRepositoryContract {
private var loginToken: LoginTokenDecode? = null
private val authChannel by lazy { Channel<Boolean?>() }
override suspend fun requestLogin(userLoginBo: UserLoginRequestBo){
// isClosedForSend is true after putting app in background
if(!authChannel.isClosedForSend) {
authChannel.send(true)
}
}
Viewmodel
class UserViewModel : ViewModel {
init {
authChannelUc.invoke(scope = viewModelScope, onResult = ::authenticated)
}
...
}
Based on your comment that you are using viewModelScope; and the fact that you have "Do not keep activities in background" enabled on device - The Activity is killed going to background, which cancels the viewModelScope, which auto-closes the channel.
On the consumer side, use for:
for (token in authChannel) {
withContext(dispatcherForLaunch) {
onResult(isTokenValid(loginTokenDecode))
}
}
instead authChannel.consumerEach())
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)
})