Tuesday, January 28, 2020

One state, two state, previous state, new state

With the latest changes to PYDroid in 28, you'll probably notice that when renders occur, the render function is called only with a single State parameter. In fact, throughout the history of the PYDroid Arch library, there has never been an instance where the previous state was also passed through the render. PYDroid doesn't pass the previous state, PYDroid doesn't like the previous state.

Why not?

Ultimately, its a philosophical decision and not a technical one. Before we dive deeper, we should understand why the previousState is generally a wanted thing.

In many cases with MVI frameworks, the render function is responsible for drawing and updating the entire visual part of state. In many cases, the previousState and current state are used in this render function to avoid unneeded renders or re-renders. Sounds like a good thing, so why doesn't PYDroid like it?

I would argue that the point of MVI is to simply how to draw a screen - by giving the consumer a single parameter and saying "everything on screen must come from this state object". By including in the mix a previousState, the idea of rendering on to the screen is now a function where the output is a complex combination of two very specific inputs, instead of just a simple object. Adding previousState makes it just as tedious to track proper state as one would experience in a non MVI setup. It does have the nice benefit of avoiding un-needed drawing and rendering, but at what cost?

So then PYDroid doesn't use previousState, so therefore PYDroid can cause useless rerendering and is slow. Well - not quite. PYDroid debounces on states which are the same - if a state change ultimately results in the exact same state as the one before it, the render is never called. This is different from a framework like React, which will render each time setState is called as long as shouldComponentUpdate is true.

Then, how does PYDroid avoid the situation where a state change occurs, but only one component of the state has actually changed? For example, if your state holds an error Throwable and a loading boolean - how can you avoid re-rendering the view when only one of these changes but not the other? Well, PYDroid does nothing. It just sends the render call to the view layer.

But remember what our View layer is. We are not on the web, where the View is created and rendered as just a DOM - we are on Android. Android views largely handle these duplicate state issues on their own. Passing the same text string to a TextView will not cause a redraw. Passing a drawable to an ImageView will not reload it. Passing a new list to a RecyclerView adapter will be diffed via the submitList function, and only the individually changed ViewHolders will be re-rendered. Basically, PYDroid doesn't care - it delegates the responsibility of avoiding useless rendering to the View layer which does the rendering. In the event that you really need to manage previous states and avoid re-rendering, you can keep track of the specific components of state on your own.

Maybe in the future this may change if decide it is more trouble to not have previousState - but at least for the lifespan of 28, PYDroid will be a single state MVI framework.

========================
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
=========================

Wednesday, January 22, 2020

New droid, my droid, good droid, pydroid

PYDroid changes and incompatible APIs, name a more iconic duo.

The next version of PYDroid will be 20.8.0, which will be generally sane, but will require some API consumer changes to work.

The first big change is that the use of directly referencing Bundle objects in UiViewModels and UiViews is gone! Replacing it are two new abstractions, UiBundleReader and UiBundleWriter. I assume you can guess what they do. You can create a new reader or a writer from a Bundle using the respective Companion create method. They have a similar API to the normal bundle and should be simple to migrate to.

The perhaps larger change is an update to the rendering of UiViews. Gone is the notion of the UiSavedState class, as it was a class which held saved bundle data but was also stateful and sometimes returned data and sometimes did not depending on when the data was last consumed. This posed problems for nested render loops which relied on the same saved state. As a result, all render and onRender methods now only take a single parameter - state.

The init, inflate, teardown, and saveState functions on UiView which come from its interfaces are now final - please use hooks instead of overriding these if you extend from UiView directly. Any other consumer, like those from BaseUiView, PrefUiView, or just consumers of the IView interface should notice no changes.

As a result of these changes, some memory leaks have been plugged in the pyamsoft applications - I will try to get a release out soon. FridgeFriend is ever vigilent and still coming along.

Stay tuned.

========================
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
=========================

Monday, January 13, 2020

Suspending SharedPreferences

In an effort to future proof pyamsoft applications for what I assume will be a future announcement of deprecating SharedPreferences, I have updated PYDroid to access all preferences as suspending functions. This will avoid activity on the main thread as SharedPreferences are initialized and should improve read performance.

Initialization hooks on UiViewModels are now stored and replayed from a Set instead of a List, so ordering of these hooks is not guaranteed. While still guaranteed for UiView instances, you should not rely on this ordering as a strict API guarantee. Anything that requires ordering should run in the same block of a given hook.

FridgeFriend has received more improvements on its quest to finally - finally - FINALLY be released... and its not quite there yet but I will release it one day. Since a new Half Life is coming out I suppose my pool of excuses is slowly drying up. After all this time, I'm a bit anxious about releasing what is effectively still an MVP of an idea - but hey that's what you get with hobby time projects.

I've got more to talk about but its a nitty gritty nerd topic - so maybe another time.

Stay tuned.

========================
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
=========================

Monday, January 6, 2020

PYDroid Party

Happy new year, happy new PYDroid!

PYDroid 20.7.0 has been released over the holidays! This finishes the deprecation of the old override API for the PYDroid-Arch component, and brings the new hooks API as well as the ability to nest BaseUiView<S, V> objects within each other as long as the state and event types are the same!

This also brings a shiny new convenience API called the StateSaver

  private var stateSaver: StateSaver? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    ...
    stateSaver = createComponent(savedInstanceState, this, viewModel, view1, view2, view3) {
      ...
    }
  }

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


StateSaver helps to reduce the boilerplate of saving instance state on all your views and viewmodel by wrapping them all up into a single object and calling save state on everything all at once. By marking the object as nullable and creating it in the onCreate function, you can guarantee cases where onSaveInstanceState is called before construction (like in Fragments when you rely on onViewCreated) will never crash your application - as the StateSaver will only be available once the view is created.

Oh, and speaking of saving state, UiViewModel now has a doOnSaveState { state -> ... } hook which will let you hook directly into the Android state saving lifecycle. If you are correctly modelling state in your application using UiViewModel and UiViewStates, in many cases you can remove code out of your UiView save state hooks and into the view model hook. The UiViewModel init hooks also gained support for accessing the save state bundle in the doOnInit { savedInstanceState -> ... } hook.

BaseUiView<S,V> (one's which inflate and use an Android View or ViewGroup as the parent object) have gained support for nested UiViews. You can use the top level UiView layoutRoot as the container for nested BaseUiView objects - as long as the layoutRoot is a ViewGroup. This is a bit of an API hack - as we override the parent argument passed in with the constructor and use the containing UiView instead. In a major release like 21.X.X, this API will probably be re-worked to be less clunky - but I wanted to make nesting available without breaking existing UiView setups.

Each UiView and all of its nested UiViews will be subscribed to the same UiViewModel and will receive all events in parent -> child order. This includes inflate, teardown, and save state hooks. You can nest BaseUiView objects using the nest(vararg view: BaseUiView<S,V>) function from within a BaseUiView object - usually within the init block. Though you theoretically could dynamically nest views on the fly - it is generally recommended that you always nest views in the correct relations - and then dynamically show or hide the needed views on the fly. Save your sanity.

The PYDroid UI reference library gains some new function as well - all animators in the view animator utility package will now return the animator object instead of throwing it away. This will allow you to pause and cancel animations in progress if needed. The listener override API will now append your listener to the default animation listener instead of overwriting it entirely - this change should not be noticed for too many people - but be aware as it is a minor behavior change.

List level ViewHolder performance has been improved thanks to the new ViewBinder<S> API. The ViewBinder is a lightweight component-like API, which groups a lifecycle owner with an abitrary set of UiView<S> which all share the same UiViewState. Instead of responding to UiController events, the Binder will respond to UiView events - this is because the view holder itself is mainly a light weight display container. All events would generally be forwarded to the list component which will be a part of the controller. The ViewBinder has a single method, bind(state: S) which will fire a render on all of the held view components. This helps to simplify playing with the RecyclerView ViewHolder framework - layouts are inflated, click listeners are set, components are injected or constructed - and the the ViewBinder is created via the top level bindViews(vararg views UiView<S,V>) function inside of the onCreateViewHolder function. The ViewBinder is used in the onBindViewHolder function to bind a model mapped to UiState to the list item. Generally the lifecycle owner is the container lifecycle owner passed into to the onCreate for the view holder - this allows all items to be bound and unbound with the container state.

class ViewHolder(parentLifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) {

  private val viewBinder: ViewBinder<MyListItemState>

  init {
    ...
    viewBinder = bindViews(parentLifecycleOwner, view1, view2, view3) {
      ...
    }
  }

  fun bind(state: MyListItemState) {
    viewBinder.bind(state)
  }
}


You should notice these performance changes in the Open Source Licenses list page in pyamsoft applications.

Finally, though not documented, UiView initialization is now broken up into init(savedInstanceState: Bundle?) and inflate(savedInstanceState: Bundle?) steps. This is used for the new view model APIs, and breaks up the roiot view inflate step and the later inflation hooks into two seperate processes. If you, for some reason, do not use the standard UiViewModel component API, you will need to update your UiView classes to handle this view. It can be a no-op.

All of these big API changes are in prep for FridgeFriend to finally - finally ... finally release ........ someday soon! The holidays were a super lazy time for me - and as a result - I ate and drank and generally did zero work. So, continue to be patient as I continue to work hard and at some point I will deem the app good enough for a first version and that will be that.

But until then,

Stay tuned.

========================
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
=========================