How to Use Realm Effectively in a Xamarin.Forms App
Rate this article
Taking care of persistence while developing a mobile application is fundamental nowadays. Even though mobile connection bandwidth, as well as coverage, has been steadily increasing over time, applications still are expected to work offline and in a limited connectivity environment.
This becomes even more cumbersome when working on applications that require a steady stream of data with the service in order to work effectively, such as collaborative applications.
Caching data coming from a service is difficult, but Realm can ease the burden by providing a very natural way of storing and accessing data. This in turn will make the application more responsive and allow the end user to work seamlessly regardless of the connection status.
The aim of this article is to show how to use Realm effectively, particularly in a Xamarin.Forms app. We will take a look at SharedGroceries, an app to share grocery lists with friends and family, backed by a REST API. With this application, we wanted to provide an example that would be simple but also somehow complete, in order to cover different common use cases. The code for the application can be found in the repository here.
Before proceeding, please note that this is not an introductory article to Realm or Xamarin.Forms, so we expect you to have some familiarity with both. If you want to get an introduction to Realm, you can take a look at the documentation for the Realm .NET SDK. The official documentation for Xamarin.Forms and MVVM are valuable resources to learn about these topics.
Build better mobile apps with Atlas Device Sync: Atlas Device Sync is a fully-managed mobile backend-as-a-service. Leverage out-of-the-box infrastructure, data synchronization capabilities, built-in network handling, and much more to quickly launch enterprise-grade mobile apps. Get started now by build: Deploy Sample for Free!
In this section, we are going to discuss the difference between the architecture of an application backed by a classic SQL database and the architecture of an application that uses Realm.
In an app backed by a classic SQL database, the structure of the application will be similar to the one shown in the diagram, where the arrows represent the dependency between different components of the application. The view model requests data from a repository that can retrieve it both from a remote data source (like a web service) when online and from a local database, depending on the situation. The repository also takes care of keeping the local database up to date with all the data retrieved from the web service.
This approach presents some issues:
- Combining data coming from both the remote data source and the local one is difficult. For example, when opening a view in an application for the first time, it's quite common to show locally cached data while the data coming from a web service is being fetched. In this case, it's not easy to synchronize the retrieval, as well as merge the data coming from both sources to present in the view.
- The data coming from the local source is static. The objects that are retrieved from the database are generally POCOs (plain old class object) and as such, they do not reflect the current state of the data present in the cache. For example, in order to keep the data shown to the user as fresh as possible, there could be a synchronization process in the background that is continuously retrieving data from the web service and inserting it into the database. It's quite complex to make this data available to the final user of the application, though, as with a classic SQL database we can get fresh data only with a new query, and this needs to be done manually, further increasing the need to coordinate different components of the application.
- Pagination is hard. Objects are fully loaded from the database upon retrieval, and this can cause performance issues when working with big datasets. In this case, pagination could be required to keep the application performant, but this is not easy to implement.
When working with Realm, instead, the structure of the application should be similar to the one in the diagram above.
In this approach, the realm is directly accessed from the view model, and not hidden behind a repository like before. When information is retrieved from the web service, it is inserted into the database, and the view model can update the UI thanks to notifications coming from the realm. In our architecture, we have decided to call DataService the entity responsible for the flow of the data in the application.
There are several advantages to this approach:
- Single source of truth removes conflicts. Because data is coming only from the realm, then there are no issues with merging and synchronizing data coming from multiple data sources on the UI. For example, when opening a view in an application for the first time, data coming from the realm is shown straight away. In the meantime, data from the web service is retrieved and inserted into the realm. This will trigger a notification in the view model that will update the UI accordingly.
- Objects and collections are live. This means that the data coming from the realm is always the latest available locally. There is no need to query again the database to get the latest version of the data as with an SQL database.
- Objects and collections are lazily loaded. This means that there is no need to worry about pagination, even when working with huge datasets.
- Bindings. Realm works out of the box with data bindings in Xamarin.Forms, greatly simplifying the use of the MVVM pattern.
As you can see in the diagram, the line between the view model and the DataService is dashed, to indicate that is optional. Due to the fact that the view model is showing only data coming from the realm, it does not actually need to have a dependency on the DataService, and the retrieval of data coming from the web service can happen independently. For example, the DataService could continuously request data to the web service to keep the data fresh, regardless of what is being shown to the user at a specific time. This continuous request approach can also be used a SQL database solution, but that would require additional synchronization and queries, as the data coming from the database is static. Sometimes, though, data needs to be exchanged with the web service in consequence of specific actions from the user—for example with pull-to-refresh—and in this case, the view model needs to depend on the DataService.
In this section, we are going to introduce our example application and how to run it.
SharedGroceries is a simple collaborative app that allows you to share grocery lists with friends and family, backed by a REST API. We have decided to use REST as it is quite a common choice and allowed us to create a service easily. We are not going to focus too much on the REST API service, as it is outside of the scope of this article.
Let's take a look at the application now. The screenshots here are taken from the iOS version of the application only, for simplicity:
- (a) The first page of the application is the login page, where the user can input their username and password to login.
- (b) After login, the user is presented with the shopping lists they are currently sharing. Additionally, the user can add a new list here.
- (c) When clicking on a row, it goes to the shopping list page that shows the content of such list. From here, the user can add and remove items, rename them, and check/uncheck them when they have been bought.
To run the app, you first need to run the web service with the REST API. In order to do so, open the
SharedGroceriesWebService
project, and run it. This should start the web service on http://localhost:5000
by default. After that, you can simply run the SharedGroceries
project that contains the code for the Xamarin.Forms application. The app is already configured to connect to the web service at the default address.For simplicity, we do not cover the case of registering users, and they are all created already on the web service. In particular, there are three predefined users—
alice
, bob
, and charlie
, all with password set to 1234
—that can be used to access the app. A couple of shopping lists are also already created in the service to make it easier to test the application.In this section, we are going to go into detail about the structure of the app and how to use Realm effectively. The structure follows the architecture that was described in the architecture section.
If we start from the lower part of the architecture schema, we have the
RestAPI
namespace that contains the code responsible for the communication with the web service. In particular, the RestAPIClient
is making HTTP requests to the SharedGroceriesWebService
. The data is exchanged in the form of DTOs (Data Transfer Objects), simple objects used for the serialization and deserialization of data over the network. In this simple app, we could avoid using DTOs, and direcly use our Realm model objects, but it's always a good idea to use specific objects just for the data transfer, as this allows us to have independence between the local persistence model and the service model. With this separation, we don't necessarily need to change our local model in case the service model changes.Here you have the example of one of the DTOs in the app:
1 public class UserInfoDTO 2 { 3 public Guid Id { get; set; } 4 public string Name { get; set; } 5 6 public UserInfo ToModel() 7 { 8 return new UserInfo 9 { 10 Id = Id, 11 Name = Name, 12 }; 13 } 14 15 public static UserInfoDTO FromModel(UserInfo user) 16 { 17 return new UserInfoDTO 18 { 19 Id = user.Id, 20 Name = user.Name, 21 }; 22 } 23 }
UserInfoDTO
is just a container used for the serialization/deserialization of data transmitted in the API calls, and contains methods for converting to and from the local model (in this case, the UserInfo
class).RealmService
is responsible for providing a reference to a realm:1 public static class RealmService 2 { 3 public static Realm GetRealm() => Realm.GetInstance(); 4 }
The class is quite simple at the moment, as we are using the default configuration for the realm. Having a separate class becomes more useful, though, when we have a more complicated configuration for the realm, and we want avoid having code duplication.
Please note that the
GetRealm
method is creating a new realm instance when it is called. Because realm instances need to be used on the same thread where they have been created, this method can be used from everywhere in our code, without the need to worry about threading issues.
It's also important to dispose of realm instances when they are not needed anymore, especially on background threads.The
DataService
class is responsible for managing the flow of data in the application. When needed, the class requests data from the RestAPIClient
, and then persists it in the realm. A typical method in this class would look like this:1 public static async Task RetrieveUsers() 2 { 3 try 4 { 5 //Retrieve data from the API 6 var users = await RestAPIClient.GetAllUsers(); 7 8 //Persist data in Realm 9 using var realm = RealmService.GetRealm(); 10 realm.Write(() => 11 { 12 realm.Add(users.Select(u => u.ToModel()), update: true); 13 }); 14 } 15 catch (HttpRequestException) //Offline/Service is not reachable 16 { 17 } 18 }
The
RetrieveUsers
method is first retrieving the list of users (in the form of DTOs) from the Rest API, and then inserting them into the realm, after a conversion from DTOs to model objects. Here you can see the use of the using
declaration to dispose of the realm at the end of the try block.The definition of the model for Realm is generally straightforward, as it is possible to use a simple C# class as a model with very little modifications. In the following snippet, you can see the three model classes that we are using in SharedGroceries:
1 public class UserInfo : RealmObject 2 { 3 [ ]4 public Guid Id { get; set; } 5 public string Name { get; set; } 6 } 7 8 public class GroceryItem : EmbeddedObject 9 { 10 public string Name { get; set; } 11 public bool Purchased { get; set; } 12 } 13 14 public class ShoppingList : RealmObject 15 { 16 [ ]17 public Guid Id { get; set; } = Guid.NewGuid(); 18 public string Name { get; set; } 19 public ISet<UserInfo> Owners { get; } 20 public IList<GroceryItem> Items { get; } 21 }
The models are pretty simple, and strictly resemble the DTO objects that are retrieved from the web service. One of the few caveats when writing Realm model classes is to remember that collections (lists, sets, and dictionaries) need to be declared with a getter only property and the correspondent interface type (
IList
, ISet
, IDictionary
), as it is happening with ShoppingList
.Another thing to notice here is that
GroceryItem
is defined as an EmbeddedObject
, to indicate that it cannot exist as an independent Realm object (and thus it cannot have a PrimaryKey
), and has the same lifecycle of the ShoppingList
that contains it. This implies that GroceryItem
s get deleted when the parent ShoppingList
is deleted.We will now go through the two main view models in the app, and discuss the most important points. We are going to skip
LoginViewModel
, as it is not particularly interesting.ShoppingListsCollectionViewModel
is the view model backing ShoppingListsCollectionPage
, the main page of the application, that shows the list of shopping lists for the current user. Let's take a look look at the main elements:1 public class ShoppingListsCollectionViewModel : BaseViewModel 2 { 3 private readonly Realm realm; 4 private bool loaded; 5 6 public ICommand AddListCommand { get; } 7 public ICommand OpenListCommand { get; } 8 9 public IEnumerable<ShoppingList> Lists { get; } 10 11 public ShoppingList SelectedList 12 { 13 get => null; 14 set 15 { 16 OpenListCommand.Execute(value); 17 OnPropertyChanged(); 18 } 19 } 20 21 public ShoppingListsCollectionViewModel() 22 { 23 //1 24 realm = RealmService.GetRealm(); 25 Lists = realm.All<ShoppingList>(); 26 27 AddListCommand = new AsyncCommand(AddList); 28 OpenListCommand = new AsyncCommand<ShoppingList>(OpenList); 29 } 30 31 internal override async void OnAppearing() 32 { 33 base.OnAppearing(); 34 35 IDisposable loadingIndicator = null; 36 37 try 38 { 39 //2 40 if (!loaded) 41 { 42 //Page is appearing for the first time, sync with service 43 //and retrieve users and shopping lists 44 loaded = true; 45 loadingIndicator = DialogService.ShowLoading(); 46 await DataService.TrySync(); 47 await DataService.RetrieveUsers(); 48 await DataService.RetrieveShoppingLists(); 49 } 50 else 51 { 52 DataService.FinishEditing(); 53 } 54 } 55 catch 56 { 57 await DialogService.ShowAlert("Error", "Error while loading the page"); 58 } 59 finally 60 { 61 loadingIndicator?.Dispose(); 62 } 63 } 64 65 //3 66 private async Task AddList() 67 { 68 var newList = new ShoppingList(); 69 newList.Owners.Add(DataService.CurrentUser); 70 realm.Write(() => 71 { 72 return realm.Add(newList, true); 73 }); 74 75 await OpenList(newList); 76 } 77 78 private async Task OpenList(ShoppingList list) 79 { 80 DataService.StartEditing(list.Id); 81 await NavigationService.NavigateTo(new ShoppingListViewModel(list)); 82 } 83 }
In the constructor of the view model (1), we are initializing
realm
and also Lists
. That is a queryable collection of ShoppingList
elements, representing all the shopping lists of the user. Lists
is defined as a public property with a getter, and this allows to bind it to the UI, as we can see in ShoppingListsCollectionPage.xaml
:1 <ContentPage.Content> 2 <!--A--> 3 <ListView ItemsSource="{Binding Lists}" 4 SelectionMode="Single" 5 SelectedItem="{Binding SelectedList, Mode=TwoWay}"> 6 <ListView.ItemTemplate> 7 <DataTemplate> 8 <!--B--> 9 <TextCell Text="{Binding Name}" /> 10 </DataTemplate> 11 </ListView.ItemTemplate> 12 </ListView> 13 </ContentPage.Content>
The content of the page is a
ListView
whose ItemsSource
is bound to Lists
(A). This means that the rows of the ListView
are actually bound to the elements of Lists
(that is, a collection of ShoppingList
). A little bit down, we can see that each of the rows of the ListView
is a TextCell
whose text is bound to the variable Name
of ShoppingList
(B). Together, this means that this page will show a row for each of the shopping lists, with the name of list in the row.An important thing to know is that, behind the curtains, Realm collections (like
Lists
, in this case) implement INotifyCollectionChanged
, and that Realm objects implement INotifyPropertyChanged
. This means that the UI will get automatically updated whenever there is a change in the collection (for example, by adding or removing elements), as well as whenever there is a change in an object (if a property changes). This greatly simplifies using the MVVM pattern, as implementing those interfaces manually is a tedious and error-prone process.Coming back to
ShoppingListsCollectionViewModel
, in OnAppearing
, we can see how the Realm collection is actually populated. If the page has not been loaded before (2), we call the methods DataService.RetrieveUsers
and DataService.RetrieveShoppingLists
, that retrieve the list of users and shopping lists from the service and insert them into the realm. Due to the fact that Realm collections are live, Lists
will notify the UI that its contents have changed, and the list on the screen will get populated automatically.
Note that there are also some more interesting elements here that are related to the synchronization of local data with the web service, but we will discuss them later.Finally, we have the
AddList
and OpenList
methods (3) that are invoked, respectively, when the Add button is clicked or when a list is clicked. The OpenList
method just passes the clicked list
to the ShoppingListViewModel
, while AddList
first creates a new empty list, adds the current user in the list of owners, adds it to the realm, and then opens the list.ShoppingListViewModel
is the view model backing ShoppingListPage
, the page that shows the content of a certain list and allows us to modify it:1 public class ShoppingListViewModel : BaseViewModel 2 { 3 private readonly Realm realm; 4 5 public ShoppingList ShoppingList { get; } 6 public IEnumerable<GroceryItem> CheckedItems { get; } 7 public IEnumerable<GroceryItem> UncheckedItems { get; } 8 9 public ICommand DeleteItemCommand { get; } 10 public ICommand AddItemCommand { get; } 11 public ICommand DeleteCommand { get; } 12 13 public ShoppingListViewModel(ShoppingList list) 14 { 15 realm = RealmService.GetRealm(); 16 17 ShoppingList = list; 18 19 //1 20 CheckedItems = ShoppingList.Items.AsRealmQueryable().Where(i => i.Purchased); 21 UncheckedItems = ShoppingList.Items.AsRealmQueryable().Where(i => !i.Purchased); 22 23 DeleteItemCommand = new Command<GroceryItem>(DeleteItem); 24 AddItemCommand = new Command(AddItem); 25 DeleteCommand = new AsyncCommand(Delete); 26 } 27 28 //2 29 private void AddItem() 30 { 31 realm.Write(() => 32 { 33 ShoppingList.Items.Add(new GroceryItem()); 34 }); 35 } 36 37 private void DeleteItem(GroceryItem item) 38 { 39 realm.Write(() => 40 { 41 ShoppingList.Items.Remove(item); 42 }); 43 } 44 45 private async Task Delete() 46 { 47 var confirmDelete = await DialogService.ShowConfirm("Deletion", 48 "Are you sure you want to delete the shopping list?"); 49 50 if (!confirmDelete) 51 { 52 return; 53 } 54 55 var listId = ShoppingList.Id; 56 realm.Write(() => 57 { 58 realm.Remove(ShoppingList); 59 }); 60 61 await NavigationService.GoBack(); 62 } 63 }
As we will see in a second, the page is binding to two different collections,
CheckedItems
and UncheckedItems
, that represent, respectively, the list of items that have been checked (purchased) and those that haven't been. In order to obtain those, AsRealmQueryable
is called on ShoppingList.Items
, to convert the IList
to a Realm-backed query, that can be queried with LINQ.The xaml code for the page can be found in
ShoppingListPage.xaml
. Here is the main content:1 <ContentPage.Content> 2 <ScrollView> 3 <!--A--> 4 <StackLayout Orientation="Vertical" Margin="20" Spacing="10"> 5 <!--B--> 6 <Editor Text="{Binding ShoppingList.Name}" HorizontalOptions="Fill" 7 Placeholder="Shopping List Name"/> 8 <!--C--> 9 <StackLayout BindableLayout.ItemsSource="{Binding UncheckedItems}"> 10 <BindableLayout.ItemTemplate> 11 <DataTemplate> 12 <!--G--> 13 <StackLayout Orientation="Horizontal"> 14 <!--H--> 15 <CheckBox IsChecked="{Binding Purchased}" VerticalOptions="Center" /> 16 <!--I--> 17 <Entry Text="{Binding Name}" 18 Keyboard="Email" 19 HorizontalOptions="FillAndExpand" 20 VerticalOptions="Center"/> 21 <!--J--> 22 <Button Text="X" FontSize="Title" 23 Command="{Binding Path=BindingContext.DeleteItemCommand, Source={x:Reference page}}" 24 CommandParameter="{Binding .}"/> 25 </StackLayout> 26 </DataTemplate> 27 </BindableLayout.ItemTemplate> 28 </StackLayout> 29 <!--D--> 30 <Button Text="+ Add Item" Command="{Binding AddItemCommand}"/> 31 <!--E--> 32 <BoxView Color="Gray" HeightRequest="1"/> 33 <Label Text="{Binding CheckedItems.Count, StringFormat='{0} ticked'}"/> 34 <!--F--> 35 <StackLayout BindableLayout.ItemsSource="{Binding CheckedItems}"> 36 <BindableLayout.ItemTemplate> 37 <DataTemplate> 38 <StackLayout Orientation="Horizontal"> 39 <CheckBox IsChecked="{Binding Purchased}" VerticalOptions="Center" /> 40 <Label Text="{Binding Name}" TextDecorations="Strikethrough" 41 HorizontalOptions="FillAndExpand" 42 VerticalOptions="Center"/> 43 <Button Text="X" FontSize="Title" 44 Command="{Binding Path=BindingContext.DeleteItemCommand, Source={x:Reference page}}" 45 CommandParameter="{Binding .}"/> 46 </StackLayout> 47 </DataTemplate> 48 </BindableLayout.ItemTemplate> 49 </StackLayout> 50 </StackLayout> 51 </ScrollView> 52 </ContentPage.Content>
This page is composed by an external
StackLayout
(A) that contains:- (B) An
Editor
whoseText
is bound toShoppingList.Name
. This allows the user to read and eventually modify the name of the list. - (C) A bindable
StackLayout
that is bound toUncheckedItems
. This is the list of items that need to be purchased. Each of the rows of theStackLayout
are bound to an element ofUncheckedItems
, and thus to aGroceryItem
. - (D) A
Button
that allows us to add new elements to the list. - (E) A separator (the
BoxView
) and aLabel
that describe how many elements of the list have been ticked, thanks to the binding toCheckedItems.Count
. - (F ) A bindable
StackLayout
that is bound toCheckedItems
. This is the list of items that have been already purchased. Each of the rows of theStackLayout
are bound to an element ofCheckedItems
, and thus to aGroceryItem
.
If we focus our attention on on the
DataTemplate
of the first bindable StackLayout
, we can see that each row is composed by three elements:- (H) A
Checkbox
that is bound toPurchased
ofGroceryItem
. This allows us to check and uncheck items. - (I) An
Entry
that is bound toName
ofGroceryItem
. This allows us to change the name of the items. - (J) A
Button
that, when clicked, executed theDeleteItemCommand
command on the view model, withGroceryItem
as argument. This allows us to delete an item.
Please note that for simplicity, we have decided to use a bindable
StackLayout
to display the items of the shopping list. In a production application, it could be necessary to use a view that supports virtualization, such as a ListView
or CollectionView
, depending on the expected amount of elements in the collection.An interesting thing to notice is that all the bindings are actually two-ways, so they go both from the view model to the page and from the page to the view model. This, for example, allows the user to modify the name of a shopping list, as well as check and uncheck items. The view elements are bound directly to Realm objects and collections (
ShoppingList
, UncheckedItems
, and CheckedItems
), and so all these changes are automatically persisted in the realm.To make a more complete example about what is happening, let us focus on checking/unchecking items. When the user checks an item, the property
Purchased
of a GroceryItem
is set to true, thanks to the bindings. This means that this item is no more part of UncheckedItems
(defined as the collection of GroceryItem
with Purchased
set to false in the query (1)), and thus it will disappear from the top list. Now the item will be part of CheckedItems
(defined as the collection of GroceryItem
with Purchased
set to true in the query (1)), and as such it will appear in the bottom list. Given that the number of elements in CheckedItems
has changed, the text in Label
(E) will be also updated.Coming back to the view model, we then have the
AddItem
, DeleteItem
, and Delete
methods (2) that are invoked, respectively, when an item is added, when an item is removed, and when the whole list needs to be removed. The methods are pretty straightforward, and at their core just execute a write transaction modifying or deleting ShoppingList
.In this section, we are going to discuss how shopping list editing is done in the app, and how to synchronize it back to the service.
In a mobile application, there are generally two different ways of approaching editing:
- Save button. The user modifies what they need in the application, and then presses a save button to persist their changes when satisfied.
- Continuous save. The changes by the user are continually saved by the application, so there is no need for an explicit save button.
Generally, the second choice is more common in modern applications, and for this reason, it is also the approach that we decided to use in our example.
The main editing in
SharedGroceries
happens in the ShoppingListPage
, where the user can modify or delete shopping lists. As we discussed before, all the changes that are done by the user are automatically persisted in the realm thanks to the two-way bindings, and so the next step is to synchronize those changes back to the web service. Even though the changes are saved as they happen, we decided to synchronize those to the service only after the user is finished with modifying a certain list, and went away from the ShoppingListPage
. This allows us to send the whole updated list to the service, instead of a series of individual updates. This is a choice that we made to keep the application simple, but obviously, the requirements could be different in another case.In order to implement the synchronization mechanism we have discussed, we needed to keep track of which shopping list was being edited at a certain time and which shopping lists have already been edited (and so can be sent to the web service). This is implemented in the following methods from the
DataService
class:1 public static void StartEditing(Guid listId) 2 { 3 PreferencesManager.SetEditingListId(listId); 4 } 5 6 public static void FinishEditing() 7 { 8 var editingListId = PreferencesManager.GetEditingListId(); 9 10 if (editingListId == null) 11 { 12 return; 13 } 14 15 //1 16 PreferencesManager.RemoveEditingListId(); 17 //2 18 PreferencesManager.AddReadyForSyncListId(editingListId.Value); 19 20 //3 21 Task.Run(TrySync); 22 } 23 24 public static async Task TrySync() 25 { 26 //4 27 var readyForSyncListsId = PreferencesManager.GetReadyForSyncListsId(); 28 29 //5 30 var editingListId = PreferencesManager.GetEditingListId(); 31 32 foreach (var readyForSyncListId in readyForSyncListsId) 33 { 34 //6 35 if (readyForSyncListId == editingListId) //The list is still being edited 36 { 37 continue; 38 } 39 40 //7 41 var updateSuccessful = await UpdateShoppingList(readyForSyncListId); 42 if (updateSuccessful) 43 { 44 //8 45 PreferencesManager.RemoveReadyForSyncListId(readyForSyncListId); 46 } 47 } 48 }
The method
StartEditing
is called when opening a list in ShoppingListsCollectionViewModel
:1 private async Task OpenList(ShoppingList list) 2 { 3 DataService.StartEditing(list.Id); 4 await NavigationService.NavigateTo(new ShoppingListViewModel(list)); 5 }
This method persists to disk the
Id
of the list that is being currently edited.The method
FinishEditing
is called in OnAppearing
in ShoppingListsCollectionViewModel
:1 internal override async void OnAppearing() 2 { 3 base.OnAppearing(); 4 5 if (!loaded) 6 { 7 .... 8 await DataService.TrySync(); 9 .... 10 } 11 else 12 { 13 DataService.FinishEditing(); 14 } 15 } 16 17 }
This method is called when
ShoppingListsCollectionPage
appears on screen, and so the user possibly went back from the ShoppingListsPage
after finishing editing. This method removes the identifier of the shopping list that is currently being edited (if it exists)(1), and adds it to the collection of identifiers for lists that are ready to be synced (2). Finally, it calls the method TrySync
(3) in another thread.Finally, the method
TrySync
is called both in DataService.FinishEditing
and in ShoppingListsCollectionViewModel.OnAppearing
, as we have seen before. This method takes care of synchronizing all the local changes back to the web service:- It first retrieves the ids of the lists that are ready to be synced (4), and then the id of the (eventual) list being edited at the moment (5).
- Then, for each of the identifiers of the lists ready to be synced (
readyForSyncListsId
), if the list is being edited right now (6), it just skips this iteration of the loop. Otherwise, it updates the shopping list on the service (7). - Finally, if the update was successful, it removes the identifier from the collection of lists that have been edited (8).
This method is called also in
OnAppearing
of ShoppingListsCollectionViewModel
if this is the first time the corresponding page is loaded. We do so as we need to be sure to synchronize data back to the service when the application starts, in case there have been connection issues previously.Overall, this is probably a very simplified approach to synchronization, as we did not consider several problems that need to be addressed in a production application:
- What happens if the service is not reachable? What is our retry policy?
- How do we resolve conflicts on the service when data is being modified by multiple users?
- How do we respect consistency of the data? How do we make sure that the changes coming from the web service are not overriding the local changes?
Those are only part of the possible issues that can arise when working with synchronization, especially in a collaborative applications like ours.
In this article, we have shown how Realm can be used effectively in a Xamarin.Forms app, thanks to notifications, bindings, and live objects.
The use of Realm as the source of truth for the application greatly simplified the architecture of SharedGroceries and the automatic bindings, together with notifications, also streamlined the implementation of the MVVM pattern.
Nevertheless, synchronization in a collaborative app such as SharedGroceries is still hard. In our example, we have covered only part of the possible synchronization issues that can arise, but you can already see the amount of effort necessary to ensure that everything stays in sync between the mobile application and the web service.
In a following article, we are going to see how we can use Realm Sync to greatly simplify the architecture of the application and resolve our synchronization issues.