
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
}