In this article by George Taskos, the author of the book, Xamarin Cross Platform Development Cookbook, we will discuss a cross-platform solution with Xamarin.Forms and MVVM architecture. Creating a cross-platform solution correctly requires a lot of things to be taken under consideration. In this article, we will quickly provide you with a starter MVVM architecture showing data retrieved over the network in a ListView control.
(For more resources related to this topic, see here.)
public interface IDataService
{
Task<IEnumerable<OrderModel>> GetOrdersAsync ();
}
[assembly: Xamarin.Forms.Dependency (typeof (DataService))]
namespace XamFormsMVVM{
public class DataService : IDataService {
protected const string BaseUrlAddress =
@"https://api.parse.com/1/classes";
protected virtual HttpClient GetHttpClient() {
HttpClient httpClient = new HttpClient(new
NativeMessageHandler());
httpClient.BaseAddress = new
Uri(BaseUrlAddress);
httpClient.DefaultRequestHeaders.Accept.Add(new
MediaTypeWithQualityHeaderValue
("application/json"));
return httpClient;
}
public async Task<IEnumerable<OrderModel>>
GetOrdersAsync ()
{
using (HttpClient client = GetHttpClient ())
{
HttpRequestMessage requestMessage = new
HttpRequestMessage(HttpMethod.Get,
client.BaseAddress + "/Order");
requestMessage.Headers.Add("X-Parse-
Application-Id",
"fwpMhK1Ot1hM9ZA4iVRj49VFz
DePwILBPjY7wVFy");
requestMessage.Headers.Add("X-Parse-REST-
API-Key", "egeLQVTC7IsQJGd8GtRj3ttJV
RECIZgFgR2uvmsr");
HttpResponseMessage response = await
client.SendAsync(requestMessage);
response.EnsureSuccessStatusCode ();
string ordersJson = await
response.Content.ReadAsStringAsync();
JObject jsonObj = JObject.Parse
(ordersJson);
JArray ordersResults = (JArray)jsonObj
["results"];
return JsonConvert.DeserializeObject
<List<OrderModel>>
(ordersResults.ToString ());
}
}
}
}
public interface IDataRepository {
Task<IEnumerable<OrderViewModel>> GetOrdersAsync ();
}
[assembly: Xamarin.Forms.Dependency (typeof (DataRepository))]
namespace XamFormsMVVM
{
public class DataRepository : IDataRepository
{
private IDataService DataService { get; set; }
public DataRepository () :
this(DependencyService.Get<IDataService> ())
{
}
public DataRepository (IDataService dataService)
{
DataService = dataService;
}
public async Task<IEnumerable<OrderViewModel>>
GetOrdersAsync ()
{
IEnumerable<OrderModel> orders = await
DataService.GetOrdersAsync ().ConfigureAwait
(false);
return orders.Select
(o => new OrderViewModel (o));
}
}
}
public class OrderViewModel : XLabs.Forms.Mvvm.ViewModel
{
string _orderNumber;
public string OrderNumber
{
get {
return _orderNumber;
}
set {
SetProperty (ref _orderNumber, value);
}
}
public OrderViewModel (OrderModel order)
{
OrderNumber = order.OrderNumber;
}
public override string ToString ()
{
return string.Format ("[{0}]", OrderNumber);
}
}
public class OrderListViewModel :
XLabs.Forms.Mvvm.ViewModel{
protected IDataRepository DataRepository { get; set; }
ObservableCollection<OrderViewModel> _orders;
public ObservableCollection<OrderViewModel> Orders
{
get {
return _orders;
}
set {
SetProperty (ref _orders, value);
}
}
public OrderListViewModel () :
this(DependencyService.Get<IDataRepository> ())
{
}
public OrderListViewModel (IDataRepository
dataRepository)
{
DataRepository = dataRepository;
DataRepository.GetOrdersAsync ().ContinueWith
(antecedent => {
if (antecedent.Status ==
TaskStatus.RanToCompletion) {
Orders = new
ObservableCollection<OrderViewModel>
(antecedent.Result);
}
}, TaskScheduler.
FromCurrentSynchronizationContext ());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
x_Class="XamFormsMVVM.OrderListView"
Title="Orders">
<ContentPage.Content>
<ListView ItemsSource="{Binding Orders}"/>
</ContentPage.Content>
</ContentPage>
public App()
{
if (!Resolver.IsSet) {
SetIoc ();
}
RegisterViews();
MainPage = new NavigationPage((Page)ViewFactory.
CreatePage<OrderListViewModel, OrderListView>());
}
private void SetIoc()
{
var resolverContainer = new SimpleContainer();
Resolver.SetResolver
(resolverContainer.GetResolver());
}
private void RegisterViews()
{
ViewFactory.Register<OrderListView,
OrderListViewModel>();
}
For Android:
For iOS:
A cross-platform solution should share as much logic and common operations as possible, such as retrieving and/or updating data in a local database or over the network, having your logic centralized, and coordinating components.
With Xamarin.Forms, you even have a cross-platform UI, but this shouldn't stop you from separating the concerns correctly; the more abstracted you are from the user interface and programming against interfaces, the easier it is to adapt to changes and remove or add components.
Starting with models and creating a DataService implementation class with its equivalent interface, IDataService retrieves raw JSON data over the network from the Parse API and converts it to a list of OrderModel, which are POCO classes with just one property. Every time you invoke the GetOrdersAsync method, you get the same 100 orders from the server.
Notice how we used the Dependency attribute declaration above the namespace to instruct DependencyService that we want to register this implementation class for the interface.
We took a step to improve the performance of the REST client API; although we do use the HTTPClient package, we pass a delegate handler, NativeMessageHandler, when constructing in the GetClient() method. This handler is part of the modernhttpclient NuGet package and it manages undercover to use a native REST API for each platform: NSURLSession in iOS and OkHttp in Android.
The IDataService interface is used by the DataRepository implementation, which acts as a simple intermediate repository layer converting the POCO OrderModel received from the server in OrderViewModel instances. Any model that is meant to be used on a view is a ViewModel, the view's model, and also, when retrieving and updating data, you don't carry business logic. Only data logic that is known should be included as data transfer objects.
Dependencies, such as in our case, where we have a dependency of IDataService for the DataRepository to work, should be clear to classes that will use the component, which is why we create a default empty constructor required from the XLabs ViewFactory class, but in reality, we always invoke the constructor that accepts an IDataService instance; this way, when we unit test this unit, we can pass our mock IDataService class and test the functionality of the methods. We are using the DependencyService class to register the implementation to its equivalent IDataRepository interface here as well.
OrderViewModel inherits XLabs.Forms.ViewModel; it is a simple ViewModel class with one property raising property change notifications and accepting an OrderModel instance as a dependency in the default constructor. We override the ToString() method too for a default string representation of the object, which simplifies the ListView control without requiring us, in our example, to use a custom cell with DataTemplate.
The second ViewModel in our architecture is the OrderListViewModel, which inherits XLabs.Forms.ViewModel too and has a dependency of IDataRepository, following the same pattern with a default constructor and a constructor with the dependency argument.
This ViewModel is responsible for retrieving a list of OrderViewModel and holding it to an ObservableCollection<OrderViewModel> instance that raises collection change notifications. In the constructor, we invoke the GetOrdersAsync() method and register an action delegate handler to be invoked on the main thread when the task has finished passing the orders received in a new ObservableCollection<OrderViewModel> instance set to the Orders property.
The view of this recipe is super simple: in XAML, we set the title property which is used in the navigation bar for each platform and we leverage the built-in data-binding mechanism of Xamarin.Forms to bind the Orders property in the ListView ItemsSource property. This is how we abstract the ViewModel from the view.
But we need to provide a BindingContext class to the view while still not coupling the ViewModel to the view, and Xamarin Forms Labs is a great framework for filling the gap. XLabs has a ViewFactory class; with this API, we can register the mapping between a view and a ViewModel, and the framework will take care of injecting our ViewModel into the BindingContext class of the view. When a page is required in our application, we use the ViewFactory.CreatePage class, which will construct and provide us with the desired instance.
Xamarin Forms Labs uses a dependency resolver internally; this has to be set up early in the application startup entry point, so it is handled in the App.cs constructor.
Run the iOS application in the simulator or device and in your preferred Android emulator or device; the result is the same with the equivalent native themes for each platform.
Xamarin.Forms is a great cross-platform UI framework that you can use to describe your user interface code declaratives in XAML, and it will be translated into the equivalent native views and pages with the ability of customizing each native application layer.
Xamarin.Forms and MVVM are made for each other; the pattern fits naturally into the design of native cross-platform mobile applications and abstracts the view from the data easy using the built-in data-binding mechanism.
Further resources on this subject: