Unity | Common pitfalls of reactive programming



A reactive approach feels at home for a client application. Often data isn't yet available and either waiting for it with loading or showing partial information is the solution.

I want to look at some of the hard-to-spot issues I've come across.


Example 1:


Here I'm using a Cell<T> class that combines data with reactors that allow changes in set data to be subscribed to.

    Bind(ServiceA.Parameter, par => button.interactable = par);
    Bind(ServiceB.Parameter, par => button.interactable = par);


Something doesn't feel right, doesn't it? You will probably write something like this:

    Bind(ServiceA.Parameter, UpdateButtonState);
    Bind(ServiceB.Parameter, UpdateButtonState);

// ------------

void UpdateButtonState()
{
    bool isA = ServiceA.Parameter;
    bool isB = ServiceB.Parameter;

    //Pick one:
    //button.interactable = isA && isB );
    //button.interactable = isA || isB );
    //button.interactable = isA ^  isB );
}

From the first implementation, it was not clear what is the intended logic (&& , || or ^) was. So now, using a single function for our logic we can know what the exact intent is.


Example 2:

    Bind(PlayerService.Health,            UpdatePlayerHealth);
    Bind(PlayerService.MaxHealthBoosters, UpdatePlayerHealth);
void UpdatePlayerHealth() 
{
   float hp = PlayerService.Health;
   float maxHp = PlayerService.GetMaxHealth();
   HealthText = hp + "/" + maxHp;
   HealthBar.FilledPercent = hp/maxHp;
}

The code may not trigger any red flags at first glance but the execution of it may yield an incorrect value of "100/0" with a good old fashion "Division by zero" on top.


if we are getting those values from the server then if they are processed in the following order...

// Player Service
Health.value = apiData.Health; // Max Health is still zero
_maxHealth = apiData.BaseMaxHealth;
MaxHealthBoosters.value = apiData.BoostersList;

...the abovementioned issues will rear their ugly heads.


I would usually go with a simple "Version" solution - the moment data is updated and processed we trigger the version for all the dependencies to get the fully baked cake of data.


There is still one problem though. What if we are dealing with a more expensive operation, not just a single health bar.

// -------------------------------
    Listen(ServiceA.ListFilter, UpdateList);
    Listen(ServiceA.ListElements, UpdateList);
    Listen(ServiceB.Currency, UpdateList);
    Bind(ServiceC.Version, UpdateList);
// -------------------------------
void UpdateList()
{
    // A more expensive calculation with Instantiation
}

In some cases, elements of the list can have a subscription to services to update themselves. But it is not always possible, especially if we have a more generic and reusable element.

The List update can execute multiple times per frame if Filter & Elements were changed at the same time.


My offered solution is SetDirty(); & LateUpdate();


int _dataVersion;
Int _viewVersion = -1;
void SetDirty () => _dataVersion++;

// -------------------------------
   Bind(ServiceA.ListFilter, SetDirty);
   Bind(ServiceA.ListElements, SetDirty);
   Bind(ServiceB.Currency, SetDirty);
   Bind(ServiceC.Version, SetDirty);
// -------------------------------


void LateUpdate()
{
   If (_viewVersion != _modelVersion)
   {
       _viewVersion = _modelVersion;
       UpdateList();
   }
}

Looks like a lot of code, but _viewVersion, _dataVersion, SetDirty(); , LateUpdate(); can be implemented in a base class leaving you with:


    Bind(ServiceA.ListFilter);   
    Bind(ServiceA.ListElements);
    Bind(ServiceB.Currency);
    Bind(ServiceC.Version);
// -------------------------------

protected override void OnDataChanged()
{
    // Refresh List
}