FutureShocked
FutureShocked

Reputation: 888

Sharing display state between views in MVVM without relying on singletons

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?

Upvotes: 0

Views: 60

Answers (1)

Martin Marconcini
Martin Marconcini

Reputation: 27236

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.

Upvotes: 1

Related Questions