かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Xamarin.Forms + PrismでSQLiteを使ってみよう

モバイル環境でのデータベースといったらSQLiteがデファクト!ということでXamarin.Forms + Prism.Formsの環境で試してみましょう。

NuGetパッケージの導入

使用するパッケージはSQLite-net-pclです。(似た名前のが多いので注意)

www.nuget.org

プラットフォーム固有処理を作成

残念なことに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);
        }
    }
}

悩み

SQLiteConnectionIDispoasbleなので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();
        }
    }
}

これでばっちり!非同期でも動きますね。