Tuesday, June 2, 2020

How PYDroid Arch Seperates Concerns

The seperation of concerns is a useful thing for a programmer to use while working. Splitting up code into logical groupings which only have one kind of job can help a programmer isolate both their mental model of what they are working on, as well as the potential sources of bugs when the inevitable report comes rolling in. I want to talk about how and why PYDroid Arch breaks its MVI based pattern up into 3 arbitrary groupings, and how these groupings help to seperate the various concerns of the Android programmer.

I always approach Android application development with the mindset that what I am building is 3 seperate parts of a program, which come together in a hopefully cohesive way to form a nice Android application. In what I believe to be the order of importance, these parts are as follows

1. How the program performs as a behaving citizen of the device.
2. How the program produces value for the user via the data it presents.
3. How the program visually looks and "feels" as it provides value.

To start, here is what my mental model looks like for applications in PYDroid's MVI styled pattern:

[Controller] <==> [ViewModel] <==> [View]

Let's start with the Controller first, since the Controller is the component which is most responsible for adhering properly to the first part of application building.

Here is the standard PYDroid Arch entry point for an Activity:

override fun onCreate(savedInstanceState: Bundle?) {
  stateSaver = createComponent(
    savedInstanceState,
    lifecycleOwner, 
    viewModel, 
    view1,
    view2
  ) { 
      handleControllerEvent(it)
  }
}

override fun onSaveInstanceState(outState: Bundle) {
  stateSaver?.saveState(outState)
}

In PYDroid terms, this layer is the Controller. The Controller is the biggest beast on the Android platform, as it actually has two different jobs (which breaks SOLID sadly) - that is to both handle the platform lifecycle events, as well as decide WHAT is on screen WHERE. This means all the Android specific things, like Activity callbacks, permission requests, outside application intents, service binding is all handled at the Controller layer. It also saves application state - which is critically important! The Controller has a two way relationship with the ViewModel. It receives Controller events from the view model - things like show a dialog or change screens, push and pop fragments, and so on - and it can also call methods on the ViewModel itself - though you should try to do so sparingly. In general, try to only have the Controller talk back to the ViewModel in response to an Android lifecycle event, like a permission callback or a lifecycle callback.

Next is the ViewModel. This layer oversees WHICH data will be shown on screen. It interacts with the data model of the application, talking to databases or the network or device storage, and decides WHICH data should be used by the application. It consumes this data by either publishing a one-off event to the Controller layer which will respond appropriately, or by updating the state of the view. The ViewModel in PYDroid is very similar to the standard MVVM Android ViewModel - just instead of using one livedata per variable and observing each of these variables seperately in the View layer, you have one giant stream which sends snapshots of the entire state model to the View layer. This makes it much more difficult for different parts of the View to be rendered out of sync, as you will always render complete snapshots of state. The ViewModel is the busiest object in the pattern, as it is the bridge between the Android system, your application's data and the visuals of your application. It has a two way relationship with both the Controller and the View. It can publish events to the Controller, and be called from the Controller. It also publishes data to the View by means of updating its held state, and receives events from the View in response to things like button clicks or text inputs. It is able to save state just like the controller too!

Finally the View, which is the layer which handles HOW the data will appear on screen. The View is updated all at once via a render function, which passes a complete state object from the ViewModel. Any views that are bound to the ViewModel via the createComponent call will all receive the same state object from the ViewModel at the same time. This is the layer where you control how things look on screen - the colors, the animations, the inputs and the buttons and whatnot. This layer is two way bound to the ViewModel, it can publish events to the ViewModel in response to user interactions like clicking or typing, and receives updates from the ViewModel in the form of rendering state objects.

By establishing and enforcing three distinct logical and mental seperations when coding, you can help isolate issues and create more robust applications.

If you need to integrate with a system behavior, like the back button press or a permission request, it should be handled in your Controller. If your application does not restore properly on process death or after rotation, check the Controller. If Views are not appearing at the correct location on screen, check the Controller. If you aren't correctly handling the result of an implicit Intent, check the Controller. Anything Android will generally be in the Controller, which makes it easy to isolate where a problem is occurring.

Anything to do with data, like databases or networking or shared preferences or file access, should be handled by the ViewModel. Things like Daos or http clients should be handled in the ViewModel via a use case or an interactor. If anything goes wrong with the nitty gritty number crunching, you can check the ViewModel to see whats going wrong.

And the actual look of your application is handled by the View. Your app is placing buttons at the right location on screen, and displays the right numbers, but your button is the wrong color? Check the View. The text isn't updating or an animation is not smooth? Check the View. Seperating the View out allows you to fully focus on the mindset of how your application looks and feels, without having to worry about whether the app is hitting the right network endpoint or what happens if the user puts the app in the background in the middle of this flow.

--------------

As some closing notes, since PYDroid is a heavily opinionated library, here are some tips about where I think of certain Android components in this 3 part structure.

Activities and Fragments are Controllers. Nothing else is treated as a Controller. While a Service would fit the description, it doesn't display any visual and therefore does not fit into the PYDroid architecture pattern.

RecyclerView adapters are sort of in the middle. They kind of act like controllers because of their requirement to construct view holders and bind view holders, but I treat them as a required bridge between the View layer RecylerView the adapter is held in, and the View layer ViewHolders that the adapter creates.

class MyAdapter constructor(
  private val onEventCallback: (ViewEvent, Int) -> Unit
) : ListAdapter(ITEM_CALLBACK) {

  override fun onCreateViewHolder(poarent: ViewGroup, viewType: Int) : MyViewHolder {
    return MyViewHolder.create(parent, onEventCallback)
  }

  override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    holder.bindState(getItem(position))
  }

}

class MyViewHolder constructor(
  owner: LifecycleOwner,
  onEventCallback: (ViewEvent, Int) -> Unit
) : RecyclerView.ViewHolder(rootView) {

  private val binder: ViewBinder<ItemState>

  override fun onCreate(savedInstanceState: Bundle?) {
    binder = bindViews(
      owner, 
      view1,
      view2
    ) { 
        onEventCallback(it, adapterPosition)
    }
  }

  fun bindState(state: ItemState) {
    binder.bind(state)
  }
}

This way, a ViewHolder can still be part of the View layer, the adapter is just delegating to specific holders at specific positions, and the view event is still handled by a ViewModel which is bound at the RecyclerView level.

Both the createComponent and bindViews will clean up after themselves by using the passed in LifecycleOwner to decide when they are out of scope, meaning you do not need any onDestroy or onUnbindViewHolder code to release things like callbacks.

Finally, there is some disagreement over whether things like Dialogs showing should be modeled in the View state or at the Controller level. Let me be clear - I believe that a Dialog being shown should ideally be part of the View state on other platforms like React, but on Android because Dialogs nowadays are implemented via Fragments, this makes them Controllers - and thus are modeled by one-off Controller events from the ViewModel, instead of state updates to the View.

========================
Follow pyamsoft around the Web for updates and announcements about the newest applications!
Like what I do?

Send me an email at: pyam.soft@gmail.com
Or find me online at: https://pyamsoft.blogspot.com

Follow my Facebook Page
Check out my code on GitHub
=========================