In this post, I will talk about strategies for developing a cross-platform Xamarin application with a focus on code sharing, increasing testability, and reducing overall development and maintenance efforts.
The application architecture is itself problem specific, but there are certain design patterns that can guide the overall structure of the application. The ones I mostly work with are Model-View-Controller, Model-View-Presenter, and Model-View-ViewModel.
MVC should be adopted for small applications or proof of concept. Since Android and iOS both natively support MVC, it will mean fewer roadblocks and faster implementation.
MVVM reduces platform-specific code, and most of the logic is shared across platforms using PCLs. There are great MVVM frameworks out there that work really well with Xamarin, such as MVVM Cross, and the official Xamarin Forms.
MVVM Cross is a data-driven pattern, and the presentation logic is centralised. This means any upgrades to the library or to the system, can break custom navigation and screen transitions within the application. As Android and iOS platforms are constantly upgraded, with newer restrictions around security and permissions (background tasks/app permissions), and as the user interface (gestures/notifications / newer design guidelines) are greatly improved, support for these can be costly in both time and effort. That being said, it is still a very powerful platform, and most applications can be written with it, with great results.
Xamarin.Forms is a UI first framework and allow .Net developers to use their existing XAML skills for developing mobile applications. Check Data-first approach in Xamarin out to use Xamarin Forms in data-first approach. \However, this platform is still maturing and needs to reach a more stable stage. Even though native controls can now be used in XAML, but one loses the advantage of data binding, and the extra effort required to maintain proper navigation in the application, and catering for various screen types, outdoes the benefit for now.
MVP is another pattern that works really well with Xamarin cross-platform applications. It is a variant of the MVC pattern and allows full use of platform native capabilities, with great control over the UI of the application. It is the pattern of choice for native development and we will look more into it below.
MVP has 3 major components
Model: This is data that we want to show in our views. Apart from data classes, this carries responsibility for retrieving, storing, validating advanced datasets, and advanced functions such as syncing and conflict resolution, cache maintenance, offline capabilities.
View: This module is responsible for showing data to the user and responding to user gestures and actions. It should only be dependent upon the presenter and native platform capabilities for styling.
Presenter: Presenter is the layer below the view and is responsible for providing data the view can use. It relies on the model layer to retrieve data, and can also publish other events to UI, such as loading data state, timeout and error state.
The golden rule of mobile development is to keep the UI layer as dumb as possible. That means UI only needs to know about its data, view transitions and animations, and publishing interaction events, such as gestures, button clicks. As the mentioned view is completely dependent upon presenter, and presenter is dependent upon the view of human interaction. However, to promote unit testing for each of the modules, presenter and view need to be decoupled.
To achieve this, we can define a contract between the presenter and the view. For a login screen the view’s contract can be:
and the presenter contract can be
The presenter should be platform independent so that it can be shared across different mobile platforms. This means it does not respond to UI life-cycle methods unless explicitly told so.
The controllers in iOS and Activities in Android are never directly created using the constructor, which means that we cannot use dependency injection to provide a presenter to our view. In this scenario, I like to use an anti-pattern called ‘Service Locator’.
Service locator is considered anti-pattern because of the complexities of managing dependencies, and dependency’s child dependencies. This is highly error-prone, but in case of multi-threaded programs, where the start of the application can run many different initialisation threads, duplicate instance of a service can be created. This pattern works well in simple scenarios and this is exactly what we are trying to solve. A dependency service can be used to locate the presenter implementation inside the view.
If the scenario was anymore complex, it certainly means, that View has been assigned more work than it needs to perform and is against MVP principles. A sample of this is shown below
As shown above the AppDelegate in the application loads all the presenters in the application. When the view loads, it uses the service locator instance to retrieve its presenter.
By abstracting the presenter from the view gives us many advantages.
- One presenter will not work on all devices. Such as finger print functionality for login can only be used on devices that have the required hardware. In case inheritance can be used to create versions that can handle extra features while having no functionality in base presenters.
- By abstracting the creation of the presenter outside the view, allows to make our UI really passive, and thus we are able to test UI independent of any other module.
- This method allows to create presenters that can help automate UI testing, performing common testing scenarios automatically. This can also be used for automatic product demos.
The above implementation of MVP suffers from one disadvantage. It ignores the asynchronous nature of the mobile applications. Both Android and iOS are quite capable devices. Even a simple Notes application performs a lot of activities when loading. These can be logging in the user, loading the notes from the cache, syncing down any notes created on other device, syncing up any notes created during offline mode, resolving conflicts, showing progress to the user, restoring the state to what it was when the application was last closed. These activities can take from few milliseconds to few seconds. The longer the application takes to boot up, the higher the chance that the user will stop using the application, and will eventually remove it from the device.
Enterprise applications are much more complex. In a perfect world, they are developed using mobile-first approach. However, not all companies can follow this and instead of the application interacting with a unified set of APIs, it ends up interacting with multiple legacy systems, combine the result and then provide it to the UI layer. This means that the application load time can be really high especially if there is stale data on the device.
In such a scenario, instead of defining contracts between the presenter and view, a more reactive approach can be used. This allows us to build application with a user centric approach. This means we think of how the user is going to interact with application, and what does the user see while waiting for data to be available in the application. The code below shows this for a login screen.
The above code shows the asynchronous nature of the View, and it follows a push approach, reacting to events when raised by the presenter. This further decouple the bond between the view and the presenter allowing for easier testing, and also helps to identify real life scenarios within the app, such as data being delayed or no connectivity.
Thus, we see how MVP aims at building a very user centric app, with a very passive and light UI layer. Both iOS and Android platforms are competing to enhance user experience. In future, we can see apps that will react and adapt to user habits using machine learning. This also means that they will have to work in a stricter environment, like stricter guidelines for UI, background tasks, app permissions, reduced background tasks, and change in application notification patterns. MVP will definitely allow to handle these changes, without affecting user experience.