モバイル環境でのデータベースといったらSQLiteがデファクト!ということでXamarin.Forms + Prism.Formsの環境で試してみましょう。
NuGetパッケージの導入
使用するパッケージはSQLite-net-pcl
です。(似た名前のが多いので注意)
プラットフォーム固有処理を作成
残念なことにPCLに閉じて完結という感じではなさそうです。
コネクションの作成時にパスを渡すのですが、このパスがプラットフォーム固有文字列になるので以下のようにSQLiteConnection
を返すインターフェースを定義していい感じにやる必要があります。
using SQLite; namespace PrismUnityApp17.Services { publicinterface ISQLiteConnectionProvider { SQLiteConnection GetConnection(); } }
Androidの実装
Personalフォルダをとってきて、そこにファイルを作る感じにします。
using PrismUnityApp17.Services; using SQLite; using System.IO; namespace PrismUnityApp17.Droid.Services { publicclass SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteConnection Connection { get; set; } public SQLiteConnection GetConnection() { if (this.Connection != null) { returnthis.Connection; } var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); path = Path.Combine(path, "database.db3"); returnthis.Connection = new SQLiteConnection(path); } } }
iOSの実装
iOSはLibraryフォルダに作る感じです。(ちょっとAndroidに比べてめんどい)
using PrismUnityApp17.Services; using SQLite; using System; using System.IO; namespace PrismUnityApp17.iOS.Services { publicclass SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteConnection Connection { get; set; } public SQLiteConnection GetConnection() { if (this.Connection != null) { returnthis.Connection; } var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal); path = Path.Combine(path, "..", "Library", "database.db3"); returnthis.Connection = new SQLiteConnection(path); } } }
悩み
SQLiteConnection
はIDispoasble
なのでDispose
しないとなのですが、毎回やるのとstaticに持ってて、使いまわすのどっちが正解なのか悩んでます…。とりあえず今回の例ではアプリ内で1つのコネクションにしてます。
PlatformInitializerへの登録
上記で作成したクラスをIPlatformInitializer
で登録します。
Android
MainActivity
の下に定義されてるので以下のように追加します。
publicclass AndroidInitializer : IPlatformInitializer { publicvoid RegisterTypes(IUnityContainer container) { container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager()); } }
iOS
iOSはAppDelegate
の下に定義されているので、そこにも追加します。
publicclass iOSInitializer : IPlatformInitializer { publicvoid RegisterTypes(IUnityContainer container) { container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager()); } }
テーブルの定義
こんな感じでテーブルを表すクラスを定義します。
using SQLite; namespace PrismUnityApp17.Businesses { publicclass TodoItem { [PrimaryKey] [AutoIncrement] publicint Id { get; set; } [NotNull] publicstring Title { get; set; } } }
そして、テーブルにアクセスするためのクラスを作ります。
using PrismUnityApp17.Businesses; using SQLite; using System.Collections.Generic; using System.Linq; namespace PrismUnityApp17.Services { publicinterface ITodoItemService { IEnumerable<TodoItem> GetAll(); TodoItem GetById(int id); void Update(TodoItem todoItem); void Insert(TodoItem todoItem); void Delete(int id); } publicclass TodoItemService : ITodoItemService { private ISQLiteConnectionProvider ConnectionProvider { get; } private SQLiteConnection Connection { get; } public TodoItemService(ISQLiteConnectionProvider connectionProvider) { this.ConnectionProvider = connectionProvider; this.Connection = this.ConnectionProvider.GetConnection(); this.Connection.CreateTable<TodoItem>(); } publicvoid Delete(int id) { this.Connection.Delete<TodoItem>(id); } public IEnumerable<TodoItem> GetAll() { returnthis.Connection.Table<TodoItem>().ToList(); } public TodoItem GetById(int id) { returnthis.Connection.Table<TodoItem>().FirstOrDefault(x => x.Id == id); } publicvoid Insert(TodoItem todoItem) { this.Connection.Insert(todoItem); } publicvoid Update(TodoItem todoItem) { this.Connection.Update(todoItem); } } }
画面とVMを作ろう
あとは、これを使う画面を作るだけです。とりあえず追加と削除を。
using Prism.Commands; using Prism.Mvvm; using Prism.Navigation; using PrismUnityApp17.Businesses; using PrismUnityApp17.Services; using System.Collections.Generic; namespace PrismUnityApp17.ViewModels { publicclass MainPageViewModel : BindableBase, INavigationAware { private ITodoItemService TodoItemService { get; } private IEnumerable<TodoItem> todoItems; public IEnumerable<TodoItem> TodoItems { get { returnthis.todoItems; } set { this.SetProperty(refthis.todoItems, value); } } privatestring inputText; publicstring InputText { get { returnthis.inputText; } set { this.SetProperty(refthis.inputText, value); } } public DelegateCommand AddCommand { get; } public DelegateCommand<TodoItem> DeleteCommand { get; } public MainPageViewModel(ITodoItemService todoItemService) { this.TodoItemService = todoItemService; this.AddCommand = new DelegateCommand(this.AddTodoItem, () => !string.IsNullOrEmpty(this.InputText)) .ObservesProperty(() => this.InputText); this.DeleteCommand = new DelegateCommand<TodoItem>(this.DeleteTodoItem); } privatevoid DeleteTodoItem(TodoItem todoItem) { this.TodoItemService.Delete(todoItem.Id); this.TodoItems = this.TodoItemService.GetAll(); } privatevoid AddTodoItem() { this.TodoItemService.Insert(new TodoItem { Title = this.InputText }); this.InputText = ""; this.TodoItems = this.TodoItemService.GetAll(); } publicvoid OnNavigatedFrom(NavigationParameters parameters) { } publicvoid OnNavigatedTo(NavigationParameters parameters) { this.TodoItems = this.TodoItemService.GetAll(); } } }
XAMLはこんな感じで。
<?xml version="1.0" encoding="utf-8"?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"prism:ViewModelLocator.AutowireViewModel="True"x:Class="PrismUnityApp17.Views.MainPage"Title="MainPage"x:Name="Page"><ContentPage.Padding><OnPlatform x:TypeArguments="Thickness"iOS="0,20,0,0" /></ContentPage.Padding><ContentPage.ToolbarItems><ToolbarItem Text="Add"Command="{Binding AddCommand}" /></ContentPage.ToolbarItems><StackLayout><Entry Text="{Binding InputText, Mode=TwoWay}" /><ListView ItemsSource="{Binding TodoItems}"VerticalOptions="FillAndExpand"><ListView.ItemTemplate><DataTemplate><ViewCell><ViewCell.ContextActions><MenuItem Text="Delete"Command="{Binding BindingContext.DeleteCommand, Source={x:Reference Page}}"CommandParameter="{Binding}" /></ViewCell.ContextActions><Label Text="{Binding Title}" /></ViewCell></DataTemplate></ListView.ItemTemplate></ListView></StackLayout></ContentPage>
仕上げにApp.xaml
ITodoItemService
の登録や、ページの登録(NavigationPage
の登録など)をやります。
using Microsoft.Practices.Unity; using Prism.Unity; using PrismUnityApp17.Services; using PrismUnityApp17.Views; using Xamarin.Forms; namespace PrismUnityApp17 { publicpartialclass App : PrismApplication { public App(IPlatformInitializer initializer = null) : base(initializer) { } protectedoverride async void OnInitialized() { InitializeComponent(); await this.NavigationService.NavigateAsync("NavigationPage/MainPage"); } protectedoverridevoid RegisterTypes() { this.Container.RegisterTypeForNavigation<MainPage>(); this.Container.RegisterTypeForNavigation<NavigationPage>(); this.Container.RegisterType<ITodoItemService, TodoItemService>(new ContainerControlledLifetimeManager()); } } }
これで、追加と削除ができるアプリが出来上がり。意外とお手軽ですね。
Async対応
Async対応版に改造してみます。
ISQLiteConnectionProviderを改造
SQLiteAsyncConnection
を返すようにします。
using SQLite; namespace PrismUnityApp17.Services { publicinterface ISQLiteConnectionProvider { SQLiteAsyncConnection GetConnection(); } }
プラットフォーム固有実装でもSQLiteAsyncConnection
を返すようにします。
Android
using PrismUnityApp17.Services; using SQLite; using System.IO; namespace PrismUnityApp17.Droid.Services { publicclass SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteAsyncConnection Connection { get; set; } public SQLiteAsyncConnection GetConnection() { if (this.Connection != null) { returnthis.Connection; } var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); path = Path.Combine(path, "database.db3"); returnthis.Connection = new SQLiteAsyncConnection(path); } } }
iOS
using PrismUnityApp17.Services; using SQLite; using System; using System.IO; namespace PrismUnityApp17.iOS.Services { publicclass SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteAsyncConnection Connection { get; set; } public SQLiteAsyncConnection GetConnection() { if (this.Connection != null) { returnthis.Connection; } var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal); path = Path.Combine(path, "..", "Library", "database.db3"); returnthis.Connection = new SQLiteAsyncConnection(path); } } }
TodoItemServiceの非同期化
TodoItemService
を非同期に書き換えます。
using PrismUnityApp17.Businesses; using SQLite; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace PrismUnityApp17.Services { publicinterface ITodoItemService { Task<IEnumerable<TodoItem>> GetAllAsync(); Task<TodoItem> GetByIdAsync(int id); Task UpdateAsync(TodoItem todoItem); Task InsertAsync(TodoItem todoItem); Task DeleteAsync(TodoItem todoItem); } publicclass TodoItemService : ITodoItemService { private ISQLiteConnectionProvider ConnectionProvider { get; } private SQLiteAsyncConnection Connection { get; } public TodoItemService(ISQLiteConnectionProvider connectionProvider) { this.ConnectionProvider = connectionProvider; this.Connection = this.ConnectionProvider.GetConnection(); } public async Task DeleteAsync(TodoItem todoItem) { await this.Connection.CreateTableAsync<TodoItem>(); await this.Connection.DeleteAsync(todoItem); } public async Task<IEnumerable<TodoItem>> GetAllAsync() { await this.Connection.CreateTableAsync<TodoItem>(); return await this.Connection.Table<TodoItem>().ToListAsync(); } public async Task<TodoItem> GetByIdAsync(int id) { await this.Connection.CreateTableAsync<TodoItem>(); return await this.Connection.Table<TodoItem>().Where(x => x.Id == id).FirstOrDefaultAsync(); } public async Task InsertAsync(TodoItem todoItem) { await this.Connection.CreateTableAsync<TodoItem>(); await this.Connection.InsertAsync(todoItem); } public async Task UpdateAsync(TodoItem todoItem) { await this.Connection.CreateTableAsync<TodoItem>(); await this.Connection.UpdateAsync(todoItem); } } }
ViewModelの非同期対応
最後にViewModelを非同期対応にします。
using Prism.Commands; using Prism.Mvvm; using Prism.Navigation; using PrismUnityApp17.Businesses; using PrismUnityApp17.Services; using System.Collections.Generic; namespace PrismUnityApp17.ViewModels { publicclass MainPageViewModel : BindableBase, INavigationAware { private ITodoItemService TodoItemService { get; } private IEnumerable<TodoItem> todoItems; public IEnumerable<TodoItem> TodoItems { get { returnthis.todoItems; } set { this.SetProperty(refthis.todoItems, value); } } privatestring inputText; publicstring InputText { get { returnthis.inputText; } set { this.SetProperty(refthis.inputText, value); } } public DelegateCommand AddCommand { get; } public DelegateCommand<TodoItem> DeleteCommand { get; } public MainPageViewModel(ITodoItemService todoItemService) { this.TodoItemService = todoItemService; this.AddCommand = new DelegateCommand(this.AddTodoItem, () => !string.IsNullOrEmpty(this.InputText)) .ObservesProperty(() => this.InputText); this.DeleteCommand = new DelegateCommand<TodoItem>(this.DeleteTodoItem); } private async void DeleteTodoItem(TodoItem todoItem) { await this.TodoItemService.DeleteAsync(todoItem); this.TodoItems = await this.TodoItemService.GetAllAsync(); } private async void AddTodoItem() { await this.TodoItemService.InsertAsync(new TodoItem { Title = this.InputText }); this.InputText = ""; this.TodoItems = await this.TodoItemService.GetAllAsync(); } publicvoid OnNavigatedFrom(NavigationParameters parameters) { } public async void OnNavigatedTo(NavigationParameters parameters) { this.TodoItems = await this.TodoItemService.GetAllAsync(); } } }
これでばっちり!非同期でも動きますね。