Como usar o Realm de forma eficaz em um aplicativo Xamarin.Forms
Avalie esse Artigo
Atualmente, é fundamental cuidar da persistência ao desenvolver um aplicativo móvel. Embora a largura de banda da conexão móvel, bem como a cobertura, tenha aumentado constantemente ao longo do tempo, ainda se espera que os aplicativos funcionem off-line e em um ambiente de conectividade limitada.
Isso se torna ainda mais trabalhoso ao trabalhar com aplicativos que exigem um fluxo constante de dados com o serviço para funcionar de forma eficaz, como aplicativos de colaboração.
Armazenar dados em cache provenientes de um serviço é difícil, mas o Realm pode facilitar o trabalho fornecendo uma maneira muito natural de armazenar e acessar dados. Isso, por sua vez, tornará o aplicativo mais responsivo e permitirá que o usuário final trabalhe sem problemas, independentemente do status da conexão.
O objetivo deste artigo é mostrar como usar o Realm de forma eficaz, especialmente em um aplicativo Xamarin.Forms. Vamos dar uma olhada no SharedGroceries, um aplicativo para compartilhar listas de compras com amigos e familiares, apoiado por uma REST API. Com esse aplicativo, queríamos fornecer um exemplo que fosse simples, mas também completo, para cobrir diferentes casos de uso comuns. O código do aplicativo pode ser encontrado no repositório aqui.
Antes de prosseguir, observe que este não é um artigo introdutório ao Realm ou Xamarin.Forms, portanto, espera-se que você tenha alguma familiaridade com ambos. Se você deseja obter uma introdução ao Realm, você pode dar uma olhada na documentação do Realm .NET SDK. A documentação oficial do Xamarin.Forms e doMVVM são recursos valiosos para aprender sobre esses tópicos.
Crie melhores aplicativos móveis com o Atlas Device Sync: o Atlas Device Sync é um backend como serviço móvel totalmente gerenciado. Aproveite a infraestrutura pronta para uso, os recursos de sincronização de dados, o manuseio de rede integrado e muito mais para lançar rapidamente aplicativos móveis de nível empresarial. Comece agora mesmo a construir: Implemente amostras grátis!
Nesta seção, vamos discutir a diferença entre a arquitetura de um aplicativo apoiado por um banco de dados SQL clássico e a arquitetura de um aplicativo que usa o Realm.
Em um aplicativo apoiado por um banco de dados SQL clássico, a estrutura do aplicativo será semelhante à mostrada no diagrama, em que as setas representam a dependência entre os diferentes componentes do aplicativo. O modelo de visualização solicita dados de um repositório que pode recuperá-los de uma fonte de dados remota (como um serviço web) quando estiver online e de um banco de dados local, dependendo da situação. O repositório também se encarrega de manter o banco de dados local atualizado com todos os dados recuperados do serviço da Web. Esta abordagem apresenta alguns problemas:
- Combinar dados provenientes da fonte de dados remota e da local é difícil. Por exemplo, ao abrir uma exibição em um aplicativo pela primeira vez, é bastante comum mostrar dados armazenados em cache localmente enquanto os dados provenientes de um serviço da Web estão sendo obtidos. Nesse caso, não é fácil sincronizar a recuperação, bem como mesclar os dados provenientes de ambas as fontes para apresentá-los na visualização.
- Os dados provenientes da fonte local são estáticos. Os objetos recuperados do banco de dados geralmente são POCOs (objeto de classe antigo simples) e, como tal, não refletem o estado atual dos dados presentes no cache. Por exemplo, para manter os dados mostrados ao usuário o mais atualizados possível, pode haver um processo de sincronização em segundo plano que está continuamente recuperando dados do serviço Web e inserindo-os no banco de dados. No entanto, é bastante complexo disponibilizar esses dados para o usuário final do aplicativo, pois com um banco de dados SQL clássico podemos obter dados atualizados apenas com uma nova consulta, e isso precisa ser feito manualmente, aumentando ainda mais a necessidade de coordenar diferentes componentes do aplicativo.
- A paginação é difícil. Os objetos são totalmente carregados do banco de dados após a recuperação, e isso pode causar problemas de desempenho ao trabalhar com grandes conjuntos de dados. Nesse caso, a paginação pode ser necessária para manter o desempenho do aplicativo, mas isso não é fácilde implementar.
Ao trabalhar com o Realm, em vez disso, a estrutura do aplicativo deve ser semelhante à do diagrama acima.
Nessa abordagem, o realm é acessado diretamente do modelo de visualização, e não fica oculto atrás de um repositório como antes. Quando as informações são recuperadas do serviço da Web, elas são inseridas no banco de dados, e o modelo de visualização pode atualizar a interface do usuário graças às notificações provenientes do realm. Em nossa arquitetura, decidimos chamar de DataService a entidade responsável pelo fluxo de dados no aplicativo.
Existem várias vantagens para essa abordagem:
- Uma única fonte de verdade remove conflitos. Como os dados são provenientes apenas do realm, não há problemas com a mesclagem e sincronização de dados provenientes de várias fontes de dados na interface do usuário. Por exemplo, ao abrir uma visualização em um aplicativo pela primeira vez, os dados provenientes do realm são mostrados imediatamente. Enquanto isso, os dados do serviço da Web são recuperados e inseridos na região. Isso trigger uma notificação no modelo de exibição que atualizará a interface do usuário de acordo.
- Objetos e coleções estão ativos. Isso significa que os dados provenientes do reino são sempre os mais recentes disponíveis localmente. Não é necessário consultar novamente o banco de dados para obter a versão mais recente dos dados, como acontece com um banco de dados SQL.
- Objetos e coleções são carregados preguiçosamente. Isso significa que não há necessidade de se preocupar com a paginação, mesmo ao trabalhar com grandes conjuntos de dados.
- Vinculações. O Realm funciona imediatamente com vinculações de dados no Xamarin.Forms, simplificando muito o uso do padrão MVVM.
Como você pode ver no diagrama, a linha entre o modelo de visualização e o DataService está pontilhada para indicar que é opcional. Como o modelo de visualização mostra apenas dados provenientes do reino, ele não precisa realmente depender do DataService, e a recuperação de dados provenientes do serviço web pode ocorrer de forma independente. Por exemplo, o DataService pode solicitar dados continuamente ao serviço da Web para mantê-los atualizados, independentemente do que esteja sendo mostrado ao usuário em um horário específico. Essa abordagem de solicitação contínua também pode ser usada em uma solução de banco de dados SQL, mas isso exigiria sincronização e consultas adicionais, pois os dados provenientes do banco de dados são estáticos. No entanto, às vezes, os dados precisam ser trocados com o serviço da web em consequência de ações específicas do usuário - por exemplo, com o pull-to-refresh - e, nesse caso, o modelo de visualização precisa depender do DataService.
Nesta seção, vamos apresentar nosso aplicativo de exemplo e como executá-lo.
SharedGroceries é um aplicativo colaborativo simples que permite compartilhar listas de compras com amigos e familiares, apoiado por uma REST API. Decidimos usar o REST, pois é uma escolha bastante comum e nos permitiu criar um serviço facilmente. Não vamos nos concentrar muito no serviço da REST API, pois ele está fora do escopo deste artigo.
Vamos dar uma olhada no aplicativo agora. As capturas de tela aqui foram retiradas apenas da versão do aplicativo para iOS, para simplificar:
- (a) A primeira página do aplicativo é a página de login, na qual o usuário pode inserir seu nome de usuário e senha para fazer o login.
- (b) Após o login, o usuário recebe as listas de compras que está compartilhando no momento. Além disso, o usuário pode adicionar uma nova lista aqui.
- (c) Ao clicar em uma linha, ela vai para a página da lista de compras que mostra o conteúdo dessa lista. A partir daqui, o usuário pode adicionar e remover itens, renomeá-los e marcá-los / desmarcá-los quando forem comprados.
Para executar o aplicativo, primeiro você precisa executar o serviço da web com a REST API. Para fazer isso, abra o projeto
SharedGroceriesWebService
e execute-o. Isso deve iniciar o serviço da web em http://localhost:5000
por padrão. Depois disso, você pode simplesmente executar o projetoSharedGroceries
que contém o código para o aplicativo Xamarin.Forms. A aplicação já está configurada para se conectar ao serviço web no endereço padrão.Para simplificar, não abordamos o caso de usuários registrados, e todos eles já são criados no serviço web. Em particular, há três usuários predefinidos —
alice
, bob
e charlie
, todos com senha definida como 1234
—que podem ser usados para acessar o aplicativo. Algumas listas de compras também já foram criadas no serviço para facilitar o teste do aplicativo.Nesta seção, entraremos em detalhes sobre a estrutura do aplicativo e como usar o Realm de forma eficaz. A estrutura segue a arquitetura descrita na seção de arquitetura.
Se começarmos da parte inferior do esquema de arquitetura, temos o namespace
RestAPI
que contém o código responsável pela comunicação com o serviço web. Em particular, o RestAPIClient
está fazendo solicitações HTTP para o SharedGroceriesWebService
. Os dados são trocados na forma de DTOs (Data Transfer Objects), objetos simples usados para a serialização e desserialização de dados pela rede. Nesse aplicativo simples, poderíamos evitar o uso de DTOs e usar diretamente nossos objetos de modelo de Realm, mas é sempre uma boa ideia usar objetos específicos apenas para a transferência de dados, pois isso nos permite ter dependência entre o modelo de persistência local e o serviço modelo. Com essa separação, não precisamos necessariamente alterar nosso modelo local caso o modelo de serviço mude.Aqui está o exemplo de um dos DTOs no aplicativo:
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
é apenas um container usado para a serialização/deserialização de dados transmitidos nas chamadas de API e contém métodos para converter de e para o modelo local (neste caso, a classeUserInfo
).RealmService
é responsável por fornecer uma referência a um realm:1 public static class RealmService 2 { 3 public static Realm GetRealm() => Realm.GetInstance(); 4 }
A classe é bastante simples no momento, pois estamos usando a configuração padrão para o reino. No entanto, ter uma classe separada se torna mais útil quando temos uma configuração mais complicada para o reino e queremos evitar a duplicação de código.
Observe que o método
GetRealm
está criando uma nova instância de realm quando é chamado. Como as instâncias de realm precisam ser usadas no mesmo thread em que foram criadas, esse método pode ser usado em qualquer lugar do nosso código, sem a necessidade de se preocupar com problemas de threading. Também é importante descartar as instâncias de realm quando elas não forem mais necessárias, especialmente em threads em segundo plano.A classe
DataService
é responsável por gerenciar o fluxo de dados no aplicativo. Quando necessário, a classe solicita dados de RestAPIClient
e, em seguida, os mantém no domínio. Um método típico desta classe seria assim: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 }
O método
RetrieveUsers
primeiro recupera a lista de usuários (na forma de DTOs) da REST API e depois os insere no realm, após uma conversão de DTOs para objetos de modelo. Aqui você pode ver o uso da declaraçãousing
para descartar o realm no final do bloco try.A definição do modelo para o Realm geralmente é direta, pois é possível usar uma classe C# simples como modelo com pouquíssimos modificações. No trecho a seguir, você pode ver as três classes de modelo que estamos usando em 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 }
Os modelos são bem simples e se assemelham estritamente aos objetos DTO recuperados do serviço web. Uma das poucas advertências ao escrever classes de modelo de Realm é lembrar que as collections (listas, conjuntos e dicionários) precisam ser declaradas com uma propriedade somente de obtenção e o tipo de interface correspondente (
IList
, ISet
, IDictionary
) , como está ocorrendo com ShoppingList
.Outra coisa a notar aqui é que
GroceryItem
é definido como EmbeddedObject
, para indicar que ele não pode existir como um objeto de Realm independente (e, portanto, não pode ter um PrimaryKey
) e tem o mesmo ciclo de vida do ShoppingList
que o contém. Isso implica que GroceryItem
s são excluídos quando oShoppingList
pai é excluído.Agora, examinaremos os dois principais modelos de visualização do aplicativo e discutiremos os pontos mais importantes. Vamos pular
LoginViewModel
, pois ele não é particularmente interessante.ShoppingListsCollectionViewModel
é o modelo de visualização de suporte ShoppingListsCollectionPage
, a página principal do aplicativo, que mostra a lista de listas de compras do usuário atual. Vamos dar uma olhada nos principais elementos: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 }
No construtor do modelo de visualização (1), estamos inicializando
realm
e também Lists
. Essa é uma collection consultável de ShoppingList
elementos, representando todas as listas de compras do usuário. Lists
é definido como uma propriedade pública com um getter, e isso permite vinculá-lo à UI, como podemos ver em 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>
O conteúdo da página é um
ListView
cujo ItemsSource
está vinculado a Lists
(A). Isso significa que as linhas do ListView
estão realmente vinculadas aos elementos de Lists
(ou seja, uma coleção de ShoppingList
). Um pouco mais abaixo, podemos ver que cada uma das linhas do ListView
é um TextCell
cujo texto está vinculado à variável Name
de ShoppingList
(B). Juntos, isso significa que esta página mostrará uma linha para cada uma das listas de compras, com o nome da lista na linha.Um aspecto importante a saber é que, por trás das cortinas, as coleções Realm (como
Lists
, neste caso) implementam INotifyCollectionChanged
e que os objetos Realm implementam INotifyPropertyChanged
. Isso significa que a interface do usuário será atualizada automaticamente sempre que houver uma alteração na coleção (por exemplo, adicionando ou removendo elementos), bem como sempre que houver uma alteração em um objeto (se uma propriedade for alterada). Isso simplifica muito o uso do padrão MVVM, pois implementar essas interfaces manualmente é um processo tedioso e sujeito a erros.Retornando a
ShoppingListsCollectionViewModel
, em OnAppearing
, podemos ver como a collection Realm é realmente preenchida. Se a página não tiver sido carregada antes (2), chamamos os métodos DataService.RetrieveUsers
e DataService.RetrieveShoppingLists
, que recuperam a lista de usuários e as listas de compras do serviço e as inserem no Realm. Devido ao fato de as collections do Realm estarem ativas, Lists
notificará a UI de que seu conteúdo foi alterado, e a lista na tela será preenchida automaticamente. Observe que há também alguns elementos mais interessantes aqui relacionados à sincronização de dados locais com o serviço web, mas os discutiremos mais tarde.Por fim, temos os
AddList
OpenList
métodos e3( ) que são invocados, respectivamente, quando o botãoAdicionar é clicado ou quando uma lista é clicada. O OpenList
método apenas passa o clicado list
para o ShoppingListViewModel
, enquanto AddList
primeiro cria uma nova lista vazia, adiciona o usuário atual na lista de proprietários, adiciona-o ao domínio e depois abre a lista .ShoppingListViewModel
é o modelo de visualização de apoio ShoppingListPage
, a página que mostra o conteúdo de uma determinada lista e nos permite modificá-la: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 }
Como verá em um segundo, a página está vinculada a duas collection diferentes,
CheckedItems
e UncheckedItems
, que representam, respectivamente, a lista de itens que foram verificados (comprados) e aqueles que não foram. Para obtê-los, AsRealmQueryable
é chamado em ShoppingList.Items
, para converter IList
em uma query apoiada pelo Realm, que pode ser consultada com LINQ.O código xaml da página pode ser encontrado em
ShoppingListPage.xaml
. Aqui está o conteúdo principal: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>
Esta página é composta por um
StackLayout
externo (A) que contém:- (B) Um
Editor
cujoText
está vinculado aShoppingList.Name
. Isso permite ao usuário ler e, eventualmente, modificar o nome da lista. - (C) Um
StackLayout
vinculável que está vinculado aUncheckedItems
. Essa é a lista de itens que precisam ser comprados. Cada uma das linhas deStackLayout
está vinculada a um elemento deUncheckedItems
e, portanto, a umGroceryItem
. - (D) Um
Button
que nos permite adicionar novos elementos à lista. - (E) Um separador (o
BoxView
) e umLabel
que descrevem quantos elementos da lista foram marcados, graças à vinculação comCheckedItems.Count
. - (F) Um vinculável vinculado
StackLayout
aCheckedItems
. Essa é a lista de itens que já foram comprados. Cada uma das linhas doStackLayout
está vinculada a um elementoCheckedItems
e, portanto, aGroceryItem
.
Se focarmos nossa atenção no
DataTemplate
do primeiro StackLayout
vinculável, podemos ver que cada linha é composta por três elementos:- (H) Um
Checkbox
que está vinculado aPurchased
deGroceryItem
. Isso nos permite marcar e desmarcar itens. - (I) Um
Entry
que está vinculado aName
deGroceryItem
. Isso nos permite alterar o nome dos itens. - (J) Um
Button
que, quando clicado, executou o comandoDeleteItemCommand
no modelo de visualização, comGroceryItem
como argumento. Isso nos permite excluir um item.
Observe que, para simplificar, decidimos usar um
StackLayout
vinculável para exibir os itens da lista de compras. Em um aplicativo de produção, pode ser necessário usar uma visualização que ofereça suporte à virtualização, como ListView
ou CollectionView
, dependendo da quantidade esperada de elementos na collection.Um aspecto interessante a ser observado é que todos os vínculos são, na verdade, bidirecionais, ou seja, vão do modelo de visualização para a página e da página para o modelo de visualização. Isso, por exemplo, permite que o usuário modifique o nome de uma lista de compras, bem como marque e desmarque itens. Os elementos de visualização são vinculados diretamente aos objetos e collection do Realm (
ShoppingList
, UncheckedItems
e CheckedItems
) e, portanto, todas essas alterações são automaticamente mantidas no realm.Para fazer um exemplo mais completo sobre o que está acontecendo, vamos nos concentrar em verificar/desmarcar itens. Quando o usuário verifica um item, a propriedade
Purchased
de um GroceryItem
é definida como true, graças às ligações. Isso significa que esse item não faz mais parte de UncheckedItems
(definido como a coleção de GroceryItem
com Purchased
definido como false na consulta (1)) e, portanto, desaparecerá da lista superior. Agora o item fará parte de CheckedItems
(definido como a coleção de GroceryItem
com Purchased
definido como true na consulta (1)), e como tal aparecerá na lista inferior. Dado que o número de elementos em CheckedItems
foi alterado, o texto em Label
(E) também será atualizado.Voltando ao modelo de exibição, temos os métodos
AddItem
, DeleteItem
e Delete
(2) que são invocados, respectivamente, quando um item é adicionado, quando um item é removido e quando toda a lista precisa ser removida. Os métodos são bastante diretos e, em sua essência, basta executar uma transação de gravação modificando ou excluindo ShoppingList
.Nesta seção, discutiremos como a edição da lista de compras é feita no aplicativo e como sincronizá-la de volta ao serviço.
Em um aplicativo móvel, geralmente há duas maneiras diferentes de abordar a edição:
- Botão Salvar. O usuário modifica o que precisa no aplicativo e, em seguida, pressiona um botão salvar para persistir suas alterações quando estiver satisfeito.
- Salvamento contínuo. As alterações feitas pelo usuário são salvas continuamente pelo aplicativo, portanto, não há necessidade de um botão de salvamento explícito.
Em geral, a segunda opção é mais comum em aplicativos modernos e, por esse motivo, é também a abordagem que decidimos usar em nosso exemplo.
A edição principal em
SharedGroceries
acontece no ShoppingListPage
, onde o usuário pode modificar ou excluir listas de compras. Como discutimos anteriormente, todas as alterações feitas pelo usuário são automaticamente persistidas no domínio graças às ligações bidirecionais e, portanto, a próxima etapa é sincronizar essas alterações de volta ao serviço Web. Mesmo que as alterações sejam salvas à medida que acontecem, decidimos sincronizá-las com o serviço somente depois que o usuário terminar de modificar uma determinada lista e sair do ShoppingListPage
. Isso nos permite enviar toda a lista atualizada para o serviço, em vez de uma série de atualizações individuais. Esta é uma escolha que fizemos para manter o aplicativo simples, mas obviamente, os requisitos podem ser diferentes em outro caso.Para implementar o mecanismo de sincronização que discutimos, precisamos acompanhar qual lista de compras estava sendo editada em um determinado momento e quais listas de compras já foram editadas (e, portanto, podem ser enviadas para o serviço da Web). Isso é implementado nos seguintes métodos da classe
DataService
: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 }
O método
StartEditing
é chamado ao abrir uma lista em ShoppingListsCollectionViewModel
:1 private async Task OpenList(ShoppingList list) 2 { 3 DataService.StartEditing(list.Id); 4 await NavigationService.NavigateTo(new ShoppingListViewModel(list)); 5 }
Esse método persiste no disco do
Id
da lista que está sendo editada no momento.O método
FinishEditing
é chamado em OnAppearing
em 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 }
Esse método é chamado quando
ShoppingListsCollectionPage
aparece na tela e, em seguida, o usuário possivelmente retornou do ShoppingListsPage
após terminar a edição. Esse método remove o identificador da lista de compras que está sendo editada (se existir)(1) e o adiciona à coleção de identificadores para listas que estão prontas para serem sincronizadas (2). Finalmente, ele chama o método TrySync
(3) em outro thread.Finalmente, o método
TrySync
é chamado em DataService.FinishEditing
e em ShoppingListsCollectionViewModel.OnAppearing
, como vimos antes. Este método se encarrega de sincronizar todas as alterações locais de volta ao serviço web:- Primeiro, ele recupera os IDs das listas que estão prontas para serem sincronizadas (4) e, em seguida, o ID da lista (final) que está sendo editada no momento (5).
- Em seguida, para cada um dos identificadores das listas prontas para serem sincronizadas (
readyForSyncListsId
), se a lista estiver sendo editada agora (6), ele simplesmente pulará essa iteração do loop. Caso contrário, ele atualiza a lista de compras no serviço (7). - Por fim, se a atualização tiver sido bem-sucedida, ela removerá o identificador da collection de listas que foram editadas (8).
Este método também é chamado em
OnAppearing
de ShoppingListsCollectionViewModel
se esta for a primeira vez que a página correspondente é carregada. Fazemos isso porque precisamos sincronizar os dados de volta com o serviço quando o aplicativo for iniciado, caso tenha ocorrido problemas de conexão anteriormente.Em geral, essa é provavelmente uma abordagem muito simplificada da sincronização, pois não consideramos vários problemas que precisam ser resolvidos em um aplicativo de produção:
- O que acontece se o serviço não estiver acessível? Qual é a nossa política de novas tentativas?
- Como resolvemos conflitos no serviço quando os dados estão sendo modificados por vários usuários?
- Como respeitamos a consistência dos dados? Como podemos garantir que as alterações provenientes do serviço Web não estejam substituindo as alterações locais?
Esses são apenas parte dos possíveis problemas que podem surgir ao trabalhar com sincronização, especialmente em aplicativos de colaboração como o nosso.
Neste artigo, mostramos como o Realm pode ser usado de forma eficaz em um aplicativo Xamarin.Forms, graças a notificações, vinculações e objetos ativos.
O uso do Realm como fonte da verdade para o aplicativo simplificou muito a arquitetura do SharedGroceries e as vinculações automáticas, juntamente com as notificações, também simplificaram a implementação do padrão MVVM.
No entanto, a sincronização em um aplicativo participativo como o SharedGroceries ainda é difícil. Em nosso exemplo, cobrimos apenas parte dos possíveis problemas de sincronização que podem surgir, mas você já pode ver a quantidade de esforço necessária para garantir que tudo permaneça sincronizado entre o aplicativo móvel e o serviço da Web.
Em um artigo a seguir, vamos ver como podemos usar o Realm Sync para simplificar muito a arquitetura do aplicativo e resolver nossos problemas de sincronização.