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 allows for changes in set data to be subscribed to.

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


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 Exception" 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. Data is updated first, informing subscriptions about any changes. Then, all the Views react to those changes.


For example. 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
}

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
}