My company's (iOS and Android) apps (and the corresponding backend servers) are reaching a complexity level where making changes is dominated by concerns about side-effects and flow of information. Early on in the app lifecycle, it was a simpler back-and-forth between one user and their data persisted on the server, with less interaction between different users, and few ways of using the data. Now we have to deal with simultaneous changes on multiple devices, the possibility that information is out-of-sync or not available yet.
Our main app has outgrown its primitive initial design. It has migrated from
Android Java to
Xamarin native (iOS and Android) to
Xamarin Forms - but the bulk of the code base is still organized the way it was in the earliest implementation. And new apps I'd like to start on a more solid, thought-through foundation.
Also planning an overhaul of communication between server and client devices.
Plus we have
WinForms desktop apps, that we'd like to re-write for both Windows tablets (
Even within a single device, we are encountering problems due to multiple threads. And the need to thoroughly distinguish between what code runs on UI thread, what shouldn't, and what might be needed on either UI thread or background thread.
Originally I assumed MVVM and data binding made this much easier: that background threads could modify a bound property at any time, and that would eventually propagate to the UI. In my experience, this is not true in Xamarin Forms (Sometimes the display updates; sometimes it doesn't. I didn't find any pattern, so I now avoid touching bound variables except from UI thread). In fact, even on the UI thread, updates can fail to happen. Specifically, make a change to a bound property during
OnAppearing - what shows on the page is (sometimes? usually? depends on the layout class?) the state just before OnAppearing was called - the change you've just made won't appear. (I'm not referring to "one-way" vs"two-way" binding; the exact same change made after the page is drawn always works.) There are also subtleties in data binding when making custom controls: situations where the value set in a constructor somehow overrides the value set in a style, even though a style "logically should" always apply after construction. Situations where its difficult to correctly propagate back-and-forth programmatic changes between the custom control's BindableProperty and the ViewModel's Property. [Eventually I'll solve this by debugging exactly what X-Forms source code is doing.] Declarative programming and data binding are wonderful when they work, but debugging problems is sometimes excessively painful.
Given difficulties in these three areas (distributed computing, UI vs background threads, dynamic data binding), I am seriously wondering whether its worth switching to a distributed computing paradigm, even at a fine grain within an app running on a single device. For example, a distributed computing paradigm can simplify how data is managed throughout the code - currently we have to "know" whether we have "current enough" information locally, and manage the syncing of local changes with remote changes. At first this was just a few areas of code that had to deal with that; but the "concern" has been spreading through a lot of code, complicating what "should" be simple algorithms.
Won't directly address the third area (dynamic data binding), but I believe the resultant code refactoring will make that easier to "get right". And completely rebuilding our synchronization on top of
async + await will help most of the "UI vs background" issues we have. (Early experiments show that we will end up marking a lot of methods
async, leading to corresponding code changes in many call sites. But that's probably inevitable, our early synchronization techniques evolved haphazardly.)
So far the two candidate technologies that look most appropriate - most likely to be capable of great performance even at a fine grain level - are Microsoft Research's Orleans / Introducing Orleans 3.0 and Akka.NET / What is Akka.NET.
I've read some about both technologies. And found this Comparison of Orleans and Akka Actors to be quite useful.
Has anyone here used either Orleans or Akka?
Any comments on whether one of those seems a "better fit" to Xamarin Forms?
Insights into performance, if use at a fine grain level? What I mean by that, is it isn't just communication with the server. Or even just persisting data from page to page in the app. What happens if we change our main data classes to become actors, and there are tens of thousands of actors running inside one app? I think most of the time, most actors, won't be executing any code (or little code) - its just that they are there, so that a change from one place (either on the device, or pushed from server or from another device) naturally propagates throughout the network of actors.
Battery life: On modern phones, perhaps even more important than "will this run fast enough" is "what's the power/battery cost of an asynchronous message call vs a synchronous method call"? for example, if there are 10 K [local inside app] messages / second average, how quickly will the battery drain? [Yes I know; ultimately I have to write a proof of concept app, and start measuring on specific devices. But there is a substantial chance we'll decide not to go down this road - at least not yet - so I'm looking for any ballpark info to help decide whether to invest further evaluation time now.]
(Not talking about messages to from server; these are messages between actors running within the app; the question is whether it is practical to push this distributed technique down deeply into the app.)
If we decide to restrict such a distributed technology to the highest level - communication between devices - then we might still want an asynchronous streaming mechanism within the app. In C# 8 that would be Async streams. An alternative is ReactiveX. The key concept is described well there:
Observables: similar to
Iterables, but for non-blocking use ("push" rather than "pull"). If we use
Akka, then we would use its Reactive Streams.
Therefore, given C# 8, the path of least resistance is for us to:
1. Rebuild all our synchronization on top of
async + await.
Orleans for inter-device communication. Or not: if that is all we are using it for, might go a completely different direction.
Those 3 steps each seem well worth it in our case. But on that path, probably won't attempt to use
actor pattern more deeply inside the app. My gut feeling right now is that if one wants to heavily use actors (
grains in Orleans),
Akka is more full-featured. Or will be, once Akka Typed is finalized, and ported to Akka.NET. For me,
*Static* type safety is a core requirement; don't want to write my own generic message wrappers, and then have to replace them later.
Importantly, extracted from the comparison linked above [which is a much better "neutral" POV than the items I've selected here; I'm introducing my bias on which trade-offs I prefer; that comparison is very good at not saying either is good or bad; merely clarifying the design decisions]:
Some Pros of Akka:
- [Orleans] Grains are not Actors according to Carl Hewitt’s definition since they lack the ability to create other Actors dynamically. (Not to overstate that; Orleans does have
Grain Activations, which presumably can manage such creation.)
- Akka ... does guarantee message ordering between each sender–recipient pair.
- Akka does not copy messages unless sending over a remote network link, trusting the user to not use mutable objects in Actor messages. [I expect less overhead for local messages, though it does add a coding/testing burden to guarantee non-mutated.]
- Akka considers exceptions emanating from the Actor as failures and sends them to the supervisor to be handled. See What problems does actor model solve / Actors handle error situations gracefully. This is not the same as propagating errors as exceptions to the client.
Some Pros of Orleans:
- The guiding question for Orleans is “what is the default behavior that is most natural and easy for non-experts?”
- Orleans Grains are deployed on silos that can span multiple cluster nodes and the user has no direct influence on their precise placement or load-dependent movement. This means that making use of elastically provisioned resources is fully automatic and built into the model.
- Orleans aims for convenience and uses syntax that is familiar to OOP practitioners, Akka makes messaging very explicit and requires the user to define message classes...
Promises: Orleans relies upon the async–await language feature to formulate continuations that are invoked asynchronously. This means that the user need only ensure that methods return Promises and can then write code as if it was synchronous. [Orleans builds on async/await; message passing looks like a method call. Succinct; familiar].