モバイル環境でのデータベースといったらSQLiteがデファクト!ということでXamarin.Forms + Prism.Formsの環境で試してみましょう。
NuGetパッケージの導入
使用するパッケージはSQLite-net-pcl
です。(似た名前のが多いので注意)
プラットフォーム固有処理を作成
残念なことにPCLに閉じて完結という感じではなさそうです。
コネクションの作成時にパスを渡すのですが、このパスがプラットフォーム固有文字列になるので以下のようにSQLiteConnection
を返すインターフェースを定義していい感じにやる必要があります。
using SQLite; namespace PrismUnityApp17.Services { public interface ISQLiteConnectionProvider { SQLiteConnection GetConnection(); } }
Androidの実装
Personalフォルダをとってきて、そこにファイルを作る感じにします。
using PrismUnityApp17.Services; using SQLite; using System.IO; namespace PrismUnityApp17.Droid.Services { public class SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteConnection Connection { get; set; } public SQLiteConnection GetConnection() { if (this.Connection != null) { return this.Connection; } var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); path = Path.Combine(path, "database.db3"); return this.Connection = new SQLiteConnection(path); } } }
iOSの実装
iOSはLibraryフォルダに作る感じです。(ちょっとAndroidに比べてめんどい)
using PrismUnityApp17.Services; using SQLite; using System; using System.IO; namespace PrismUnityApp17.iOS.Services { public class SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteConnection Connection { get; set; } public SQLiteConnection GetConnection() { if (this.Connection != null) { return this.Connection; } var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal); path = Path.Combine(path, "..", "Library", "database.db3"); return this.Connection = new SQLiteConnection(path); } } }
悩み
SQLiteConnection
はIDispoasble
なのでDispose
しないとなのですが、毎回やるのとstaticに持ってて、使いまわすのどっちが正解なのか悩んでます…。とりあえず今回の例ではアプリ内で1つのコネクションにしてます。
PlatformInitializerへの登録
上記で作成したクラスをIPlatformInitializer
で登録します。
Android
MainActivity
の下に定義されてるので以下のように追加します。
public class AndroidInitializer : IPlatformInitializer { public void RegisterTypes(IUnityContainer container) { container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager()); } }
iOS
iOSはAppDelegate
の下に定義されているので、そこにも追加します。
public class iOSInitializer : IPlatformInitializer { public void RegisterTypes(IUnityContainer container) { container.RegisterType<ISQLiteConnectionProvider, SQLiteConnectionProvider>(new ContainerControlledLifetimeManager()); } }
テーブルの定義
こんな感じでテーブルを表すクラスを定義します。
using SQLite; namespace PrismUnityApp17.Businesses { public class TodoItem { [PrimaryKey] [AutoIncrement] public int Id { get; set; } [NotNull] public string Title { get; set; } } }
そして、テーブルにアクセスするためのクラスを作ります。
using PrismUnityApp17.Businesses; using SQLite; using System.Collections.Generic; using System.Linq; namespace PrismUnityApp17.Services { public interface ITodoItemService { IEnumerable<TodoItem> GetAll(); TodoItem GetById(int id); void Update(TodoItem todoItem); void Insert(TodoItem todoItem); void Delete(int id); } public class 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>(); } public void Delete(int id) { this.Connection.Delete<TodoItem>(id); } public IEnumerable<TodoItem> GetAll() { return this.Connection.Table<TodoItem>().ToList(); } public TodoItem GetById(int id) { return this.Connection.Table<TodoItem>().FirstOrDefault(x => x.Id == id); } public void Insert(TodoItem todoItem) { this.Connection.Insert(todoItem); } public void 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 { public class MainPageViewModel : BindableBase, INavigationAware { private ITodoItemService TodoItemService { get; } private IEnumerable<TodoItem> todoItems; public IEnumerable<TodoItem> TodoItems { get { return this.todoItems; } set { this.SetProperty(ref this.todoItems, value); } } private string inputText; public string InputText { get { return this.inputText; } set { this.SetProperty(ref this.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 void DeleteTodoItem(TodoItem todoItem) { this.TodoItemService.Delete(todoItem.Id); this.TodoItems = this.TodoItemService.GetAll(); } private void AddTodoItem() { this.TodoItemService.Insert(new TodoItem { Title = this.InputText }); this.InputText = ""; this.TodoItems = this.TodoItemService.GetAll(); } public void OnNavigatedFrom(NavigationParameters parameters) { } public void 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 { public partial class App : PrismApplication { public App(IPlatformInitializer initializer = null) : base(initializer) { } protected override async void OnInitialized() { InitializeComponent(); await this.NavigationService.NavigateAsync("NavigationPage/MainPage"); } protected override void 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 { public interface ISQLiteConnectionProvider { SQLiteAsyncConnection GetConnection(); } }
プラットフォーム固有実装でもSQLiteAsyncConnection
を返すようにします。
Android
using PrismUnityApp17.Services; using SQLite; using System.IO; namespace PrismUnityApp17.Droid.Services { public class SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteAsyncConnection Connection { get; set; } public SQLiteAsyncConnection GetConnection() { if (this.Connection != null) { return this.Connection; } var path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); path = Path.Combine(path, "database.db3"); return this.Connection = new SQLiteAsyncConnection(path); } } }
iOS
using PrismUnityApp17.Services; using SQLite; using System; using System.IO; namespace PrismUnityApp17.iOS.Services { public class SQLiteConnectionProvider : ISQLiteConnectionProvider { private SQLiteAsyncConnection Connection { get; set; } public SQLiteAsyncConnection GetConnection() { if (this.Connection != null) { return this.Connection; } var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal); path = Path.Combine(path, "..", "Library", "database.db3"); return this.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 { public interface ITodoItemService { Task<IEnumerable<TodoItem>> GetAllAsync(); Task<TodoItem> GetByIdAsync(int id); Task UpdateAsync(TodoItem todoItem); Task InsertAsync(TodoItem todoItem); Task DeleteAsync(TodoItem todoItem); } public class 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 { public class MainPageViewModel : BindableBase, INavigationAware { private ITodoItemService TodoItemService { get; } private IEnumerable<TodoItem> todoItems; public IEnumerable<TodoItem> TodoItems { get { return this.todoItems; } set { this.SetProperty(ref this.todoItems, value); } } private string inputText; public string InputText { get { return this.inputText; } set { this.SetProperty(ref this.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(); } public void OnNavigatedFrom(NavigationParameters parameters) { } public async void OnNavigatedTo(NavigationParameters parameters) { this.TodoItems = await this.TodoItemService.GetAllAsync(); } } }
これでばっちり!非同期でも動きますね。