Android viewmodel observing always - android

I am using KoinDI and I have a login screen. Here is my code -
My AppModule code which shows LoginViewModel DI definition -
private val viewModelModules = module {
viewModel { LoginViewModel(get()) }
}
My LoginFragment code -
private val viewModel: LoginViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.login_button?.setOnClickListener {
onLoginButtonPressed()
}
}
private fun onLoginButtonPressed() {
val email = view?.email_value?.text.toString()
val password = view?.password_value?.text.toString()
viewModel.onLoginPressed(email, password).observe(this, Observer {
if (it.userLoggedIn) {
//...
}
handleError(it.error)
})
}
The problem is when I click login and immediately put the app in background and API call fails (I fail it on purpose
for testing from the backend side) and when I bring the app in foreground I see
that the viewmodel continues to observe resulting in API call happening again and again until it succeeds. Why does it happen?
Why cannot my viewmodel observe only on login button click?

When you say viewModel.onLoginPressed.observe the activity/fragment will receive events when it is started or resumed state and when it is destroyed the observer will automatically be removed.
You seem to have a retry logic inside the viewModel that keep retrying.

Related

How to prevent data duplication caused by LiveData observation in Fragment?

I'm subscribed to an observable in my Fragment, the observable listens for some user input from three different sources.
The main issue is that once I navigate to another Fragment and return to the one with the subscription, the data is duplicated as the observable is handled twice.
What is the correct way to handle a situation like this?
I've migrated my application to a Single-Activity and before it, the subscription was made in the activity without any problem.
Here is my Fragment code:
#AndroidEntryPoint
class ProductsFragment : Fragment() {
#Inject
lateinit var sharedPreferences: SharedPreferences
private var _binding: FragmentProductsBinding? = null
private val binding get() = _binding!!
private val viewModel: ProductsViewModel by viewModels()
private val scanner: CodeReaderViewModel by activityViewModels()
private fun observeBarcode() {
scanner.barcode.observe(viewLifecycleOwner) { barcode ->
if (barcode.isNotEmpty()) {
if (binding.searchView.isIconified) {
addProduct(barcode) // here if the fragment is resumed from a backstack the data is duplicated.
}
if (!binding.searchView.isIconified) {
binding.searchView.setQuery(barcode, true)
}
}
}
}
private fun addProduct(barcode: String) {
if (barcode.isEmpty()) {
return
}
viewModel.insert(barcode)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.start(args.documentId)
if (args.documentType == "Etichette") {
binding.cvLabels.visibility = View.VISIBLE
}
initUI()
observe()
}
private fun observe() {
observeBarcode()
observeProducts()
observeLoading()
observeLast()
}
}
Unfortunately, LiveData is a terribly bad idea (the way it was designed), Google insisted till they kinda phased it out (but not really since it's still there) that "it's just a value holder"...
Anyway... not to rant too much, the solution you have to use can be:
Use The "SingleLiveEvent" (method is officially "deprecated now" but... you can read more about it here).
Follow the "official guidelines" and use a Flow instead, as described in the official guideline for handling UI Events.
Update: Using StateFlow
The way to collect the flow is, for e.g. in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // or RESUMED
viewModel.yourFlow.collectLatest { ... } // or collect { ... }
}
}
For that in your ViewModel you'd expose something like:
Warning: Pseudo-Code
// Imagine your state is represented in this sealed class
sealed class State {
object Idle: State
object Loading: State
data class Success(val name: String): State
data class Failure(val reason: String): State
}
// You need an initial state
private val _yourFlow = MutableStateFlow(State.Idle)
val yourFlow: StateFlow<State> = _yourFlow
Then you can emit using
_yourFlow.emit(State.Loading)
Every time you call
scanner.barcode.observe(viewLifecycleOwner){
}
You are creating a new anonymous observer. So every new call to observe will add another observer that will get onChanged callbacks. You could move this observer out to be a property. With this solution observe won't register new observers.
Try
class property
val observer = Observer<String> { onChanged() }
inside your method
scanner.barcode.observe(viewLifecycleOwner, observer)
Alternatively you could keep your observe code as is but move it to a Fragment's callback that only gets called once fex. onCreate(). onCreate gets called only once per fragment instance whereas onViewCreated gets called every time the fragment's view is created.

Is it safe to use SharedFlow for ViewModel driven navigation?

I implemented ViewModel driven navigation as shown in my code below. Basic idea is a Singleton class NavigationManager which is available to both, composables and the ViewModel, via dependency injection. The NavigationManager has a SharedFlow property named direction which can be changed from e.g. the ViewModel and is observed by the composables.
Now my question on this:
Is it safe to use a SharedFlow in this situation? As a SharedFlow is a hot flow and therefore can emit events while not being observed, is it possible that navigation events are lost? E.g. is it possible that a navigation event is emitted while the user rotates his phone and the NavigationManger.direction SharedFlow isn't observed for a short time (as the activity is recreated on rotation)?
// MainActivity.kt
// navigationManager.direction is observed here
// NavigationManager is a Singleton injected via dependency injection
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var navigationManager: NavigationManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyJetpackComposeTheme {
val navController = rememberNavController()
MyNavHost(navController)
LaunchedEffect(navigationManager.direction) {
navigationManager.direction.collect { direction ->
direction?.let {
Log.i("NavTest", "change route to: $direction")
navController.navigate(direction)
}
}
}
}
}
}
}
// The navigation manager. Instantiating it is done by the
// depdendency injection framework, not shown here for brevitiy
class NavigationManager(private val externalScope: CoroutineScope) {
private val _direction = MutableSharedFlow<String?>()
val direction : SharedFlow<String?> = _direction
fun navigate(direction: String) {
Log.d("NavTest", "navigating to $direction")
externalScope.launch {
_direction.emit(direction)
}
}
}
// triggering navigation from inside a ViewModel would be like this
// (navigationManger would be injected via dependency injection)
navigationManager.navigate("some_direction")

AutoClearedValue accessed from another thread after View is Destroyed

I am using AutoClearedValue class from this link and when view is destroyed, backing field becomes null and that is good but i have a thread(actually a kotlin coroutine) that after it is done, it accesses the value(which uses autoCleared) but if before it's Job is done i navigate to another fragment(view of this fragment is destroyed), then it tries to access the value, but since it is null i get an exception and therefore a crash.
what can i do about this?
also for which variables this autoCleared needs to be used? i use it for viewBinding and recyclerview adapters.
You have 2 option:
1- Cancelling all the running job(s) that may access to view after its destruction. override onDestroyView() to do it.
Also, you can launch the coroutines viewLifecycleOwner.lifecycleScope to canceling it self when view destroy.
viewLifecycleOwner.lifecycleScope.launch {
// do sth with view
}
2- (Preferred solution) Use Lifecycle aware components (e.g LiveData) between coroutines and view:
coroutines push the state or data in the live-data and you must observe it with viewLifeCycleOwner scope to update the view.
private val stateLiveData = MutableLiveData<String>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
stateLiveData.observe(viewLifecycleOwner) { value ->
binding.textView.text = value
}
}
private fun fetchSomething() {
lifecycleScope.launch {
delay(10_000)
stateLiveData.value = "Hello"
}
}

Use MutableStateFlow as Hot stream, Kotlin Android

I am migrating from LiveData to Flow and faced the following problem:
I have a flow in viewModel
class MyViewModel() : ViewModel() {
val state = MutableStateFlow<Boolean>(false)
}
class FirstFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchWhenCreated {
viewModel.loginPresenterState.startVerifyFragmentEvent.collectLatest {
Log.d("Nurs", "loginPresenterState $it")
if (it)
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
}
}
when this flow is triggered , My FirstFragment navigates to another fragment "B".
But when I press back button, the state triggers one more time, and instead of navigating to FirstFragment, I am coming back to "B". I suppose this behavior is because Flow is Cold. How to manage it be called only once?
Probably because the states remain same and when you came back it re-observes state and navigates. Check this article and use the EventWrapper that mentioned in the article. He used livedata but same logic applies for stateflow too. article

How to use android navigation without binding to UI in ViewModel (MVVM)?

I am using android navigation that was presented at Google I/O 2018 and it seems like I can use it by binding to some view or by using NavHost to get it from Fragment. But what I need is to navigate to another specific view from ViewModel from my first fragment depending on several conditions. For ViewModel, I extend AndroidViewModel, but I cannot understand how to do next. I cannot cast getApplication to Fragment/Activity and I can't use NavHostFragment. Also I cannot just bind navigation to onClickListener because the startFragment contains only one ImageView. How can I navigate from ViewModel?
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
showTour()
}else{
showCodeFragment()
Log.d(TAG, "Show Code Fragment")
}
}
}
private fun showCodeFragment(){
//??
}
private fun showTour(){
//??
}
}
My Fragment
class CaptionFragment : Fragment() {
private lateinit var viewModel: CaptionViewModel
private val navController by lazy { NavHostFragment.findNavController(this) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
viewModel = ViewModelProviders.of(this).get(CaptionViewModel::class.java)
return inflater.inflate(R.layout.fragment_caption, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.start()
}
}
I want to keep logic of navigation in ViewModel
How can I navigate from ViewModel?
The answer is please don't. ViewModel is designed to store and manage UI-related data.
New Answer
In my previous answers, I said that we shouldn't navigate from ViewModel, and the reason is because to navigate, ViewModel must have references to Activities/Fragments, which I believe (maybe not the best, but still I believe it) is never a good idea.
But, in recommended app architecture from Google, it mentions that we should drive UI from model. And after I think, what do they mean with this?
So I check a sample from "android-architecture", and I found some interesting way how Google did it.
Please check here: todo-mvvm-databinding
As it turns out, they indeed drive UI from model. But how?
They created an interface TasksNavigator that basically just a navigation interface.
Then in the TasksViewModel, they have this reference to TaskNavigator so they can drive UI without having reference to Activities / Fragments directly.
Finally, TasksActivity implemented TasksNavigator to provide detail on each navigation action, and then set navigator to TasksViewModel.
You can use an optional custom enum type and observe changes in your view:
enum class NavigationDestination {
SHOW_TOUR, SHOW_CODE_FRAGMENT
}
class CaptionViewModel(app: Application) : AndroidViewModel(app) {
private val dealerProfile = DealerProfile(getApplication())
val TAG = "REGDEB"
private val _destination = MutableLiveData<NavigationDestination?>(null)
val destination: LiveData<NavigationDestination?> get() = _destination
fun setDestinationToNull() {
_destination.value = null
}
fun start(){
if(dealerProfile.getOperatorId().isEmpty()){
if(dealerProfile.isFirstTimeLaunch()){
Log.d(TAG, "First Time Launch")
_destination.value = NavigationDestination.SHOW_TOUR
}else{
_destination.value = NavigationDestination.SHOW_CODE_FRAGMENT
Log.d(TAG, "Show Code Fragment")
}
}
}
}
And then in your view observe the viewModel destination variable:
viewModel.destination.observe(this, Observer { status ->
if (status != null) {
viewModel.setDestinationToNull()
status?.let {
when (status) {
NavigationDestination.SHOW_TOUR -> {
// Navigate to your fragment
}
NavigationDestination.SHOW_CODE_FRAGMENT -> {
// Navigate to your fragment
}
}
})
}
If you only have one destination you can just use a Boolean rather than the enum.
There are two ways I can recommend doing this.
Use LiveData to communicate and tell the fragment to navigate.
Create a class called Router and this can contain your navigation logic and reference to the fragment or navigation component. ViewModel can communicate with the router class to navigate.

Categories

Resources