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 :(
Related
I have some custom DialogFragments in my Android application. I would prevent the user from opening multiple dialogs, and I would also avoid disabling the button which shows the dialog once it's pressed or using a variable or it.
So I was trying to make an extension for the fragment which checks for shown fragments via their tags like this:
fun Fragment.isFragmentVisible(tag: String): Boolean {
val fragment = childFragmentManager.findFragmentByTag(tag)
if (fragment != null && fragment.isVisible) {
return true
}
return false
}
And before showing the dialog to check it like this:
binding.destination.editText?.setOnClickListener {
if (!isFragmentVisible(ShopsDialog.TAG)) {
ShopsDialog.newInstance(this, "Destinazione") { bundle ->
val shopId = bundle.getString("shopId")
viewModel.setDestination(shopId)
}
}
}
While the newInstance is:
fun newInstance(fragment: Fragment, title: String, bundle: (Bundle) -> Unit) = ShopsDialog().apply {
arguments = Bundle().apply {
putString(ARG_TITLE, title)
}
fragment.setFragmentResultListener(KEY_CALLBACK_BUNDLE) { requestKey: String, bundle: Bundle ->
if (requestKey == KEY_CALLBACK_BUNDLE) {
bundle(bundle)
}
}
show(fragment.childFragmentManager, TAG)
}
But this seems not to work, I've tried even to use requireActivity().supportFragmentManager instead childFragmentManager but still nothing...
Before showing bottomsheetdialogfragment , i think you can check it like this
if(childFragmentManager.findFragmentByTag(ShopsDialog::class.java.simpleName) == null){
// show your bottom sheet dialog fragment
}
If you are going to show bottomSheetDialogFragment in Fragment then use childFragmentManager and if it is activity then use supportFragmentManager
In my MainActivity I have BottomNavigation. My activity is connected with MainViewModel. When app starts I fetch data from firebase. Until the data is downloaded, app displays ProgressBar and BottomNavigation is hide (view.visibility = GONE). When data has been downloaded I hide ProgressBar and show BottomNavigation with the app's content. It works great.
In another part of the app user can open gallery and choose photo. The problem is when activity with photo to choose has been closed, MutableStateFlow is triggered and bottomNavigation displays again but it should be hide in that specific part(fragment) of the app.
Why my MutableStateFlow is triggered although I don't send to it anything when user come back from gallery activity?
MainActivity (onStart):
private val mainSharedViewModel by viewModel<MainSharedViewModel>()
override fun onStart() {
super.onStart()
lifecycle.addObserver(mainSharedViewModel)
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val navHostFragment: FragmentContainerView = findViewById(R.id.bottomNavHostFragment)
bottomNavController = navHostFragment.findNavController()
bottomNavigationView.apply {
visibility = View.GONE
setupWithNavController(navHostFragment.findNavController())
}
//the fragment from wchich I open GalleryActivity is hide (else ->)
navHostFragment.findNavController().addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.mainFragment,
R.id.profileFragment,
R.id.homeFragment -> bottomNavigationView.visibility = View.VISIBLE
else -> bottomNavigationView.visibility = View.GONE
}
}
mainSharedViewModel.viewModelScope.launch {
mainSharedViewModel.state.userDataLoadingState.collect {
if (it == UserDataLoading.LOADED) {
bottomNavigationView.visibility = View.VISIBLE
} else {
bottomNavigationView.visibility = View.GONE
}
}
}
}
ViewModel:
class MainSharedViewState {
val userDataLoadingState = MutableStateFlow(UserDataLoading.LOADING) }
enum class UserDataLoading {
LOADING, UNKNOWN_ERROR, NO_CONNECTION_ERROR, LOADED }
When you come back from the gallery, the stateflow value is still set as Loaded, as the Viewmodel has not been cleared (and the activity was set to Stopped, not destroyed. It is still in the back stack.) This is why the bottomNavigationView is visible when you come back.
Although your architecture/solution is not how I would have done it, in your circumstances I guess you could change the value of the MutableStateFlow when the activity's onStop is called. Either that or use a MutableSharedFlow instead with a replayCount of 0 so that there is no value collected (although then, the bottomNavigationView will still be set as Visible if it is visible by default in XML.)
SOLVED:
I've created
val userDataLoadingState = MutableSharedFlow<UserDataLoading>(replay = 0)
and when my ViewModel is created I set
state.userDataLoadingState.emit(UserDataLoading.LOADING)
and I collect data in Activity
lifecycleScope.launch {
mainSharedViewModel.state.userDataLoadingState.collect {
if (it == UserDataLoading.LOADED) {
bottomNavigationView.visibility = View.VISIBLE
} else {
bottomNavigationView.visibility = View.GONE
}
}
}
Now it works great. I don't know why it didn't work before.
I have a SignUpFragment in which Firebase is used for signing up and a livedata observer is there.
SignUpFragment
viewModel.userMediatorLiveData.observe(this, Observer {
Timber.i("Screen", this.javaClass.simpleName)
if (it.status != null && it.message != null) {
btn_sign_up.showSnack(it.message)
if (it.status) {
PreferenceHelper.userPassword = tie_password.getTrimmedText()
returnToLoginScreen()
}
}
})
When a user successfully sign up, I'm navigating him to the Login Screen, but in login screen there's also a livedata observer which uses the same variable.
LoginFragment
// this observer is used also for listening to Firebase Login
viewModel.userMediatorLiveData.observe(this, Observer {
Timber.i("Screen", this.javaClass.simpleName)
if (it.status != null && it.message != null) {
btn_login.showSnack(it.message)
if (it.status) {
PreferenceHelper.userPassword = tie_password.getTrimmedText()
context?.startActivity(HomeActivity::class.java)
requireActivity().finish()
}
}
})
Here you can see that the condition will be true for both cases and Login screen switches to HomeScreen.
How can I handle such a situation?
Please note that I'm sharing viewmodel using
private val viewModel: AuthViewModel by activityViewModels()
Also replaced this with viewLifecycleOwner but still no hope.
Okay, so continuing our discussion here, you're sharing ViewModel between fragments, so when you're observing the particular live data in the fragment, you're not reinitializing it instead you're using the same instance. So there can be 2 ways you can handle this. One: Stop sharing your ViewModel, or remove your observer as soon as you change the flag.
this is the 2nd way:
val mUserData = viewModel.userMediatorLiveData
val mUserObserver = Observer {
Timber.i("Screen", this.javaClass.simpleName)
if (it.status != null && it.message != null) {
btn_login.showSnack(it.message)
if (it.status) {
PreferenceHelper.userPassword = tie_password.getTrimmedText()
context?.startActivity(HomeActivity::class.java)
requireActivity().finish()
//notice this
mUserData.removeObserver(mUserObserver)
}
}
}
mUserData.observe(viewLifecycleOwner, mUserObserver)
You might have to define your mUserObserver first(outside the function).
Try this and let me know
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
This question already has answers here:
MVVM pattern and startActivity
(3 answers)
Closed 5 years ago.
Hi I have one activity LoginActivity.kt and LoginViewModel. I am calling the login API in the login method of LoginViewModel. On success of it, I want to start home activity. What is the correct way to do it in MVVM approach ?
LoginViewModel.kt
class LoginViewModel : BaseViewModel<LoginNavigator>(), AnkoLogger {
val emailField = ObservableField<String>()
private val email: String
get() = emailField.get()
val passwordField = ObservableField<String>()
private val password: String
get() = passwordField.get()
val progressVisibility: ObservableInt = ObservableInt(View.GONE)
#Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
fun login(view: View) {
// here I am calling API and on success
}
/**
* Validate email and password. It checks email and password is empty or not
* and validate email address is correct or not
* #param email email address for login
* #param password password for login
* #return true if email and password pass all conditions else false
*/
private fun isEmailAndPasswordValid(email: String, password: String): Boolean {
if (email.isEmpty()) return false
if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) return false
if (password.isEmpty()) return false
return true
}
}
LoginActivity.kt
class LoginActivity : BaseActivity(), LoginNavigator {
#Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
performDependencyInjection()
super.onCreate(savedInstanceState)
val activityLoginBinding: ActivityLoginBinding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
activityLoginBinding.loginViewModel = loginViewModel
loginViewModel.mNavigator = this
}
Let say a simple scenario using your login idea, user login fail and the app need to make a simple Toast or SnackBar to show the related information such as "Your username and password is incorrect". The code you need is
Toast (Required Context)
Toast.makeText(context, text, duration).show();
Snackbar (Required View)
Snackbar.make(findViewById(R.id.myCoordinatorLayout),
R.string.email_archived, Snackbar.LENGTH_SHORT);
If you want to use it inside your ViewModel (I am not familiar with Kotlin)
#Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
void function login(final View view) {
// here I am calling API and on success
repo.login(result -> {
if(result.statusCode == 401)
Toast.makeText(view.getContext(), "Login failed...", duration).show();
});
}
You are going to find the reference of the activity in the reverse way, which make more complex code and hard to maintain since every time you need to get the reference of the activity or context to do something related to the view or activity in the view model instead of the Activity
From the google sample, you can see doSearch() function is called when the input is done. And after the search result fetched , the binding will put the latest result back to this observer , and now it is the activity job to update the result in the adapter.
private void initSearchInputListener() {
binding.get().input.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
doSearch(v);
return true;
}
return false;
});
binding.get().input.setOnKeyListener((v, keyCode, event) -> {
if ((event.getAction() == KeyEvent.ACTION_DOWN)
&& (keyCode == KeyEvent.KEYCODE_ENTER)) {
doSearch(v);
return true;
}
return false;
});
}
private void doSearch(View v) {
String query = binding.get().input.getText().toString();
// Dismiss keyboard
dismissKeyboard(v.getWindowToken());
binding.get().setQuery(query);
searchViewModel.setQuery(query);
}
private void initRecyclerView() {
binding.get().repoList.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
LinearLayoutManager layoutManager = (LinearLayoutManager)
recyclerView.getLayoutManager();
int lastPosition = layoutManager
.findLastVisibleItemPosition();
if (lastPosition == adapter.get().getItemCount() - 1) {
searchViewModel.loadNextPage();
}
}
});
searchViewModel.getResults().observe(this, result -> {
binding.get().setSearchResource(result);
binding.get().setResultCount((result == null || result.data == null)
? 0 : result.data.size());
adapter.get().replace(result == null ? null : result.data);
binding.get().executePendingBindings();
});
searchViewModel.getLoadMoreStatus().observe(this, loadingMore -> {
if (loadingMore == null) {
binding.get().setLoadingMore(false);
} else {
binding.get().setLoadingMore(loadingMore.isRunning());
String error = loadingMore.getErrorMessageIfNotHandled();
if (error != null) {
Snackbar.make(binding.get().loadMoreBar, error, Snackbar.LENGTH_LONG).show();
}
}
binding.get().executePendingBindings();
});
}
Also, according to the answer from #Emanuel S, you will see his argument
WeakReference to a NavigationController which holds the Context of the
Activity. This is a common used pattern for handling context-bound
stuff inside a ViewModel.
I highly decline this for several reasons. First: that usually means
that you have to keep a reference to your NavigationController which
fixes the context leak, but doesnt solve the architecture at all.
The best way (in my oppinion) is using LiveData which is lifecycle
aware and can do all the wanted stuff.
Another question you may think about it if you implement ui action inside the viewmodel , if you get a NullPointerException in your view or context or do some enhancement about it, which class you will find first ? ViewModel or Activity ? Since the first one you hold the UI action , the second one you hold the data binding. Both may be possible in the troubleshoot.