Sharing display state between views in MVVM without relying on singletons - android

My app has several long processes that are user-initiated by different fragments and I'd like to display a progress dialog while they are being accomplished. I'd like this progress dialog to be displayed across several fragments so that bottom navigation is still enabled, so that the user still has a sense of agency, but they can't take any actually harmful actions otherwise. Most of my "main" fragments are blocked in this way, but others such as the settings and help fragments don't need to be.
My current solution is less than ideal. Actions are user-initiated in the fragment, but the activity becomes responsible for actually completing them, so that it can overlay a progress dialog over every fragment that it needs to. I'd much rather have the fragments be responsible for their own tasks.
The obvious separation of concerns is becoming pretty essential, as the size of the activity VM has become massive, and too much of the business logic is in that class (and is then delegated to the model appropriately).
e.g.
class MyFragment : Fragment() {
// MyActivity will implement this interface
interface NetworkProcess {
fun start()
}
// start() is called on a button click or something similar
}
class MyActivity : AppCompatActivity(), MyFragment.NetworkProcess {
override fun onCreate(savedInstanceBundle: Bundle?) {
// Observe state from VM layer
// Observer updates progress dialog
}
override fun start() {
// Pass action to VM layer
}
}
One solution I have tried is to have the interface only used for display state, but then if the fragment is navigated away from it is unable to call the interface and continue updating the dialog.
e.g.
class MyFragment : Fragment() {
// MyActivity will implement this interface
interface NetworkProcessDialog {
fun update(text: Int)
fun stop()
}
override fun onActivityCreated() {
// Observe state from viewmodel
// Set listener on a button send action to viewmodel to start process
}
}
class MyActivity : AppCompatActivity(), MyFragment.NetworkProcessDialog {
override fun updateDialog(text: Int) {
// Make dialog visible if not and show update
}
override fun stopDialog() {
// Make dialog invisible
}
}
Another solution I've thought of is to push the view state into the model layer. It seems though that this would necessitate a singleton (and a bunch concurrency protection work) so that the viewmodels can't start overwriting the state as it's in the process of accomplishing one of these long running tasks. This is currently avoided automatically since there is only ever one activity VM handling all the work.
So, is it possible to have this kind of separation of concerns without relying on singletons in the model layer?

I wouldn't have the Activity (nor the Fragments) own such responsibility. They are policyDelegates (name used by Google) for things you cannot get rid of (you NEED one or the other for your app to function). So they should only do what they know better:
Handle Lifecycle (can't get around this)
Inflate Views and display them
Save/Restore their states (when possible)
Interface between fragments (if needed)
Receive state updates and reflect that in the corresponding views
etc.
So what would I do?
Have a LongProcessesManager (name is up 2 you), that is capable of starting, stopping, and managing N number of "processes". It must also offer ways to indicate progress of said process.
Have a ProcessFragment that is capable of using said manager to initiate, and receive updates for a process (and it can act on said results, like update a progress bar).
If you need to display a GLOBAL progress, you can do so too, you activity can host more than 1 fragment, so there's nothing stopping you from putting a second fragment on screen that doesn't disappear while there's progress (like a music player for example) or whatever, I don't know your UI :)
Consider moving the management of said Manager to a Service that can become Foreground if that is a requirement (user hitting "Home" or moving away from your app to pick a phone call or check instagram while they wait for your LONG running processes).
Use DependencyInjection (Dagger, Koin, you pick) so you have a single instance of this manager, that gets injected in all the fragments (or Shared ViewModels if you want).
This is just a list of a few things I'd do out of the box if I were tasked with something like this.

Related

Why does Activity in Android restrict calling methods normally like other simple Classes?

I am asking for "Why", the reasoning behind this behavior.
The question is not a Duplicate of other "How to" type questions.
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fun mainFunction() {
// code
}
// AnotherClass.kt
class AnotherClass() {
fun anotherFunction() {
}
}
// SomeClass.kt
class SomeClass() {
init {
MainActivity().mainFunction() // unresolved reference ERROR
AnotherClass().anotherFunction() // Works Fine!
}
}
What is so different about Activity Class in Android? Why does Activity in Android restrict calling methods normally like other simple Classes?
Because activity it's an appcomponent managed by the OS. And methods like onCreate or onResume calling by OS. You just can decide what should be happened where this method will be called for operation system. For example:
you can release resources in the onStop or onDestroy methods.
That's because android app is working under Android OS(That have it's own rules).
For better understanding, I can recommend you to read the article:
Application Fundamentals
The Activity instance acts like a system plugin. The plugin defines various hooks for the system to call during various state of the activity. Hooks allow developer to customize the activity behavior. For example, when the system transitions an activity into the created state, it calls the onCreate hook of your plugin so that you can provide a view for the system to render inside the activity. The plugin itself does not have full control of the lifecycle of the activity, it just customizes it.
To simplify the customization, so that you can make certain assumption about the state and you can get the data you need easier, the system defines a contract. The contract defines the order of the hook invocation and the set of fields/properties which must be assigned/removed before/after calling each hook.
For the code snippet in the question, you have instantialize an activity instance, but you have not fulfilled the contract, e.g. providing an activity context, etc. If you try to call the methods inherited from the Activity class, it is likely to crash since the assumptions made by the implementation is no longer valid and it is trying to access some fields/properties which have not been properly initialized.

Can a coroutine scope leak an activity/fragment?

I need to save a EditText data into a file when to user exits the activity. I know I have plenty of options depending on how important saving that data for me. But for this particular case I'm ok that I can afford losing it so best-afford is fine.
We could maybe create a background service to decouple it entirely from the host activity. However,I was also thinking of creating a coroutine scope that might outlive the activity (i.e.: a singleton NonUIScope) and call the block like;
override fun onSaveInstanceState(bundle: ...) {
...
mInput = et.text.toString()
}
override fun onDestroy() {
NonUIScope.launch {
Downloader.saveData(mInput)
}
}
There might be slightly different implementations to achieve the same goal of course, for instance, using onSavedInstanceSate() instead of writing it into a file but my point is: if I use the ApplicationScope that I've created, could I potentially leak the activity (even there's no UI reference in the coroutine)?
As long as NonUIScope is not related in any way to your activity and also Downloader class doesn't hold an instance of your activity, it will not leak your activity.
E.g. if you use GlobalScope, the life time of the launched coroutine is limited only by the lifetime of the whole application, not by your activity.

Android - Under what circumstance / use case I would want to implement a custom LifecycleOwner

https://developer.android.com/topic/libraries/architecture/lifecycle#implementing-lco
The docs say that Fragments and Activities in Support Library 26.1.0 and later already implement the LifecycleOwner interface. This is greatly useful if we can use the LifecycleOwner of the activity or fragment to register LiveData objects or have it call our methods annotated with
#OnLifecycleEvent(Lifecycle.Event.ON_START)
#OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
etc
in our custom classes.
But we also have the option to implement a custom LifecycleOwner.
Under what circumstances does it make sense to have a custom LifecycleOwner, considering that it will complicate things because now we have to manually track the lifecycle events like:
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
etc
?
I think I can answer this question myself, the use case where having a custom LifecycleOwner makes sense to be used is if you have an atypical lifecycle meaning a different lifecycle than the one activities and fragments have.
I had to use fragments to load ads on the lockscreen of the phone, and that meant that the ad would have to be loaded when the phone's screen was off and the user would see the ad when he'd press the power on button of the phone and turn on the display.
This behavior is important: the ad has to be loaded in background (screen off) and when the user turns the screen on, the ad is there for the user to see it.
Performing fragment transaction when the screen is off meant that the transaction would happen after the onStop() of the activity is called and that is possible only using the commitAllowingStateLoss().
Also the data that was to be passed as arguments to the fragments had to be and could be updated when the screen was off as well, that means again when the activity is in the stopped state.
So I had to create a custom LifecycleOwner to be used with a LiveData object that would observe changes of the data that was supposed to be passed to the fragments.
This custom LifecycleOwner implementation would ignore the stopped state of the activity and that way the LiveData could react to changes of data even while the activity was in stopped state.

Cannot perform this action after onsaveinstancestate after using liveData for fragments transition

this one question has been bothering me for 6 months, it is like a missing peace.. So, I really like LiveData and use it a lot, perhaps too much. Basically, all our fragments adding and removing is managed by LiveData. I have done it for several reasons:
We need to remove fragments in some cases, after onPause has occurred (odd, but a must for our use case).
We have only a single activity with fragments.
I have created a specific navigationViewModel which is shared across all fragments and is created in activity.
I add, remove fragments in this manner:
//ViewModel
...
val addFragmentNr3 = SingleLiveEvent<Boolean>()
//Activity or some fragment calls this:
navigationViewModel.addFragmentNr3.value = true
Then I observe LiveData in Activity and handling transition:
​
navigationViewModel.addFragmentNr3.observe(this, Observer { response ->
if (response != null) {
if (response) {
router.addFragmentNr3(supportFragmentManager)
}
}
})
Then router handles it:
fun addFragmentNr3(supportFragmentManager: FragmentManager) {
val fragmentNr3 = FragmentNr3()
supportFragmentManager.beginTransaction().replace(R.id.root_layout, fragmentNr3, FRAGMENT_NR_3.commit()}
In my honest opinion this should definitely prevent from this crash:java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
However, it does occur in our crash analytics.. It occurs rarely after more complex logic (like updating livedata after onActivityResult), but it does occur...
My main question is: Isn't it is a case, that LiveData handles such scenarios and would emit results only when it safe to perform operations? If not, it means my logic is bad and this approach is complete failure.
P.S. I would like to use navigation library, but as I said we have to manually remove some fragments after user goes to background, or uses split mode and etc.
LiveData does not know whether an action is safe to perform or not.
onSaveInstanceState() is called sometime before onStop() for Android version below P. So there is a small chance that the observer gets notified after onSaveInstanceState() is called.
According to doc, it turned out that onSaveInstanceState() should mark the lifecycle as CREATED and observers are not supposed to be called after onSaveInstanceState().
Suggestion on how to fix it.
One way is to use Android Navigation component and let it handle all of the fragment transaction.
If this is not feasible--like op's case--I suggests just using .commitAllowingStateLoss().
fun addFragmentNr3(supportFragmentManager: FragmentManager) {
val fragmentNr3 = FragmentNr3()
supportFragmentManager.beginTransaction().replace(R.id.root_layout, fragmentNr3, FRAGMENT_NR_3
.commitAllowingStateLoss()}
Now, if you search on the internet there will be dozens of articles warning how using .commitAllowingStateLoss() is bad. I believe these claims are no longer applicable to modern Android development where view restoration does not rely on saved bundles. If you are building an Android application with view models, you hardly need to rely on the Android framework to do the saving. In a proper MVVM application, the view should be designed in a way that it can restore its complete state based on its view models, and view models only.

Android MVP and Analytics

How to apply Firebase Analytics(for example) on MVP app architecture? (I use Mosby to build MVP)
I want to track events of "opening screen", "do click action".
There is how I send "opening screen" event.
private const val ANALYTICS_SCREEN_NAME = "ask_password"
private const val ANALYTICS_ACTION_DONE = "done"
class AskPasswordPresenter : MyDiaryPresenter<AskPasswordView> {
#Inject
constructor(analytics: AnalyticsManager) : super(analytics) // AnalyticsManager is wrapper around Firebase Analytics API
override fun initialize() { // this method called when new ViewState created
super.initialize()
analytics.doScreenOpened(ANALYTICS_SCREEN_NAME)
}
fun done(password: String) { // called when user click on 'Done' button
...
analytics.doAction(ANALYTICS_SCREEN_NAME, ANALYTICS_ACTION_DONE)
}
}
doAction(...) called as it must. Okay.
initialize() called even when user navigates back to the screen from backstack. I want it to send event ONLY when user navigates to screen in "front direction". It also looks like a bad solution as initialize() method introduced for initializing Presenter when ViewState was created at the first time, not for logging analytics events.
It sounds like I must share Fragment's lifecycle to Presenter. Not good.
What can you recommend? Must I create another entity, like AnalyticsPresenter for each Fragment? How do you handle this case?
In my opinion Analytics belongs to the View layer and not Presenter layer.
So track it either directly in Fragment / Activity or (what I usually do) use one of the libraries like lightcycle
or CompositeAndroid to kind of plug in a "Analytics component" to your Activity / Fragment. By doing so your Fragment / Activity doesn't contain the code for Analytics but is rather decoupled into its own class (single responsibility).
I think analytics belong to presenter, but as i answered on similar question having analytics in View it's easier to jump on button/labels/... definitions and see where this button is located in UI and have better idea what to send for Category, Actio, Label and Value param of GAnalytics. I think i don't need to mention presenter mustn't have any android specific dependecies so you can't jump to button/labels/... definitions from presenter. Regards

Categories

Resources