Intro
This is part 3 of my series on using the Uno Platform to write a cross-platform app, able to target both single and dual-screen devices. In this post I cover the architecture of the COduo app with an aim to providing an understanding of how it's primary components interoperate to provide a robust and testable experience across multiple platforms and screen configurations.
For an introduction to COduo or to find further posts in this series, please use the links below:
- Part 1 - Background
- Part 2 - Infrastructure
- Part 3 - Client Architecture
- Part 4 - Using the TwoPaneView
- Part 5 - Implementing the interactive UK Map
- Part 6 - Charts on the Uno Platform
- Part 7 - Windows, Win10X and releasing to the Microsoft Store
- Part 8 - Android and releasing to the Google Play Store
- Part 9 - iOS and releasing to the Apple App Store
Architecture
In part 2 I presented the following diagram and discussed the service-side infrastructure components.
This post will be focussing on the architectural components of the app. Again, this isn't specifically about how the Uno Platform was used to implement the app so I will endeavour to keep these discussions at a high level. However, in order to understand how the app functions, I think it's important to understand it's various components and interactions.
To do this we should first outline some of the conventions and libraries used within COduo in order to facilitate further discussion around the actual implementation.
Conventions & Libraries
Fluent Namespacing
COduo employs "Fluent Namespacing", an introduction to which can be found in my blog post here. To summarise, Fluent Namespacing promotes the practise of grouping classes by functional domain, not functional pattern.
For example, the Application State Machine is a class named Machine
in the Application.State
namespace; therefore having a full-name of Application.State.Machine
. This is in contrast to a conventional grouping of classes by functional pattern where - for example - there would typically be a class named ApplicationStateMachine
in the StateMachines
namespace.
While this might initially take some getting used to, as you examine the source code for COduo you should hopefully see how this approach simplifies class names, eases navigation and promotes good practices.
Reactive Extensions, MVVM & MVx.Observable
The Reactive Extensions (Rx) library is used throughout COduo to implement many different types of component from State Machines to the Event Bus. One area where Rx shines particularly brightly however is as a means to write functional, declarative and reactive user interfaces.
COduo does just this by implementing MVVM style ViewModels as collections of Reactive Behaviours via the MVx.Observable library.
If you are unfamiliar with Rx I would certainly suggest taking the time to learn about it. Not only will you understand more of how COduo hangs together but, once you "get" it, I almost guarantee you'll start to see programming problems in a different light. Lee Campbell has a great introduction to Rx on his aptly named website "IntroToRx.com".
Implementation
99% Shared
As can be seen from the diagram above, while COduo comprises many "head" projects (i.e. UWP, Android, iOS, etc), all application code - except for a very small "Platform Services" layer - is shared across all platforms. This includes all state and application lifetime management, navigation and data access and View/ViewModel implementations. I believe this is quite an achievement and speaks volumes about the potential for the Uno Platform to lower TCO when implementing and maintaining a cross-platform solution.
The "Platform Services" layer comprises a couple of interface implementations in each head project which provides platform specific functionality. For example, the use of Reactive Extensions requires IScheduler implementations for correctly marshalling events to and from the platform's "UI thread". The implementation of (and access to) the correct IScheduler implementation is different for each platform so each head project contains an implementation of the Platform.ISchedulers
interface. Shown below is the Platform.ISchedulers
implementation for Android:
public class Schedulers : ISchedulers
{
private static readonly Lazy<IScheduler> DispatchScheduler = new Lazy<IScheduler>(() => new SynchronizationContextScheduler(SynchronizationContext.Current));
public IScheduler Default => Scheduler.Default;
public IScheduler Dispatcher => DispatchScheduler.Value;
}
Views & View Models
As mentioned above, COduo employs the MVVM pattern to separate GUI and business logic. Each View uses data-binding to declaratively bind information provided by the ViewModel to the various controls presented in the UI. The ViewModel uses Reactive Behaviours and MVx.Observable properties to react to user interactions and changes in application state.
The example below shows how current value for "Tonnes Of CO2 per hour" is implemented in the Home.ViewModel
:
public class ViewModel : IViewModel, INotifyPropertyChanged
{
private readonly Data.IProvider _dataProvider;
private readonly Platform.ISchedulers _schedulers;
...
private readonly MVx.Observable.Property<int> _selectedRegion;
private readonly MVx.Observable.Property<Common.Period> _currentPeriod;
private readonly MVx.Observable.Property<double> _tonnesOfCO2PerHour;
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel(Data.IProvider dataProvider, Platform.ISchedulers schedulers)
{
_dataProvider = dataProvider;
_schedulers = schedulers;
...
_currentPeriod = new MVx.Observable.Property<Common.Period>(nameof(CurrentPeriod), args => PropertyChanged?.Invoke(this, args));
_selectedRegion = new MVx.Observable.Property<int>(0, nameof(SelectedRegion), args => PropertyChanged?.Invoke(this, args));
_tonnesOfCO2PerHour = new MVx.Observable.Property<double>(nameof(TonnesOfCO2PerHour), args => PropertyChanged?.Invoke(this, args));
...
}
private IDisposable ShouldRefreshTonnesOfCO2PerHourWhenPeriodOrSelectedRegionChanges()
{
return Observable
// When the current value of either `_currentPeriod` or `_selectedRegion` changes ...
.CombineLatest(_currentPeriod, _selectedRegion, (period, regionId) => period?.Regions
// ... retreive the data for the selected region from the current period ...
.Where(region => region.RegionId == regionId)
// ... and use this data to calculate Tonnes Of CO2 Per Hour
.Select(region => (region.Estimated.TotalMW * MegaWattsToKiloWatts * region.Estimated.GramsOfCO2PerkWh) / GramsInAMetricTonne ?? 0.0)
// ... returning the first value or 0
.FirstOrDefault() ?? 0.0)
// ... then move onto the UI thread
.ObserveOn(_schedulers.Dispatcher)
// ... and update the _tonnesOfCO2PerHour value with the value
// calculated above causing the PropertyChanged event to be
// raised for the `TonnesOfCO2PerHour` property
.Subscribe(_tonnesOfCO2PerHour);
}
public IDisposable Activate()
{
return new CompositeDisposable(
...
ShouldRefreshTonnesOfCO2PerHourWhenPeriodOrSelectedRegionChanges()
...
);
}
...
public Common.Period CurrentPeriod
{
get { return _currentPeriod.Get(); }
}
public double TonnesOfCO2PerHour
{
get { return _tonnesOfCO2PerHour.Get(); }
}
public int SelectedRegion
{
get { return _selectedRegion.Get(); }
set { _selectedRegion.Set(value); }
}
...
}
As you can see, all source data and logic for implementing this behaviour is wrapped into a single, appropriately named method called 'ShouldRefreshTonnesOfCO2PerHourWhenPeriodOrSelectedRegionChanges'. While the code in this method should be comprehensible to anyone fluent with LINQ extension-method syntax, it has been annotated for clarity.
This pattern is repeated for each behaviour the ViewModel is required to implement.
Application & Navigation State
Similar to how we employ MVVM to separate view and business logic, I find it beneficial to separate view and application/navigation logic which all too often are conflated together. Doing this brings benefits similar to the adoption of MVVM in the view layer (i.e. simplified logic, enhanced testability, etc) to the application layer.
As such, application state and navigation state are managed by a dedicated state machines. These are implemented as Reactive State Machines and designed to mirror lifetime and navigation states in the app. Stateful application and navigation data is passed between states via a mutable Application.Aggregate.Root
.
These approaches allow the app to elegantly manage lifetime events such as the app being suspended / resumed and to transparently restore navigation state and data.
Here is COduo's current state diagram:
Communication
All communication between disparate app components (for example between the state-machine and a view model) occurs via events published through an Event Bus. This promotes decoupling by ensuring that the component that raises an event requires no knowledge of a component which might consume the event, and vice versa.
Data
Data for the application is retrieved and deserialized by the Data.Provider
. The Data.Provider
sets up an Rx subscription to acquire new data every 15 minutes or whenever a Data.Requested
event is received from the event bus. This data is exposed to the rest of the application as an IObservable<>
which has been designed to immediately return the current value whenever a new consumer subscribes.
The Data.Provider
starts fetching data when the Activate
method is called and will continue to fetch data - regardless of whether there currently exists any subscribers - until the IDisposable
result of the Activate
method is disposed. This ensures data is immediately available to ViewModels when they need it (i.e. after navigation) and allows data acquisition to be correctly managed through Suspend/Resume transitions.
Source Code
You can find the source code for COduo in my Github repository. Should you like or use it, please take the time to "star" the repository; it's a small gesture which really fuels developers's enthusiasm for projects such as these.
Part 4
Now we understand how the application hangs together, in Part 4 I will detail how to setup an Uno Platform solution such that you're able to use a TwoPaneView
control and how the TwoPaneView control is used within COduo.
Finally
I hope you enjoy this series and that it goes some way to demonstrating the massive potential presented by the Uno Platform for delivering cross-platform experiences without having to invest in additional staff training nor bifurcating your development efforts.
If you or your company are interested in building apps that can leverage the dual screen capabilities of new devices such as the Surface Duo and Surface Neo, or are keen to understand how a single code-base can deliver apps to every platform from mobile phones to web sites, then please feel free to drop me a line using any of the links below or from my about page. I am actively seeking new clients in this space and would be happy to discuss any ideas you have or projects you're planning.