Xamarin逆引きTips
Xamarin.Formsでローカルデータベースを使用するには?
アプリを終了して再起動したときに、ユーザーデータを復活させたい場合、ローカルやクラウドにデータを保存することになる。その一つの方法として、SQLite.Netを使ってローカルDBに保存する方法を説明する。
1. ローカルデータベース
ユーザーが扱っているデータを、アプリを終了しても次回起動時に、再度扱いたい場合がある。このような場合、ローカルのファイルシステムやクラウド上にデータを保存する仕組みが必要になる。
今回は、そのための手法の一つとして、簡単にローカルにデータベースを構築できるSQLite.Netの使用方法を紹介する*1。
- *1 なお本Tipsは、Windows上でVisual Studio 2013を使用してXamarin.Forms開発をすることを前提としている(※編集部注: Mac上のXamarin Studioでも同様の手順で、本稿の内容が実現できることは確認している)。使用しているXamarin.Formsのバージョンは、プロジェクト作成時に利用されている「1.3.1.6296」である。
2. Xamarin.Formsプロジェクトを作成する
メニューバーの[ファイル]-[新規作成]-[プロジェクト]から表示したダイアログで、[テンプレート]-[Visual C#]-[Mobile Apps]-[Blank App (Xamarin.Forms Portable)]を選択し、名前を「SQLiteSample」として[OK]ボタンを押す。
3. SQLite.Netパッケージの追加
SQLite.NetはNuGetパッケージで簡単にインストールできる。
Visual StudioでのNuGetパッケージのインストールは、メニューバーの[ツール]-[NuGet パッケージマネージャー]-[ソリューションの NuGet パッケージの管理]を選択して表示された[NuGet パッケージの管理]ダイアログから行う。検索欄に「SQLite.Net.PCL」と入力すると、目的のパッケージを簡単に見つけることができる。
図2の[インストール]ボタンを押した後、続いて表示される[プロジェクトの選択]ダイアログでは、全てのプロジェクトにチェックを入れて[OK]ボタンを押す(図3)*2。
- *2 WindowsPhoneの場合は、依存関係からSQLite.Net-PCLの他にsqlite-net-wp8 3.8.5もインストールされる。
4. インターフェースの定義
SQLite.Netで使用されるデータベースの実体は、ローカルに保存されるファイルである。ローカルのファイル操作やパス指定については、プラットフォーム固有の処理となるため、SQLite.Netでも、その初期化処理で、プラットフォームごとに用意されたメソッドを使用する。
今回は、このSQLite.Netの初期化にあたる、SQLiteConnection
インスタンスを取得するまでの処理を、DependencyService*3を使用して、PCL(Portable Class Library)から共通的に使用できるようにした。
- *3 DependencyServiceについての詳しい解説は、「Tips:Xamarin.Formsからプラットフォーム固有の機能を利用するには?(DependencyService利用)」を参照してほしい。
DependencyServiceで使用するインターフェースの定義のために、PCLプロジェクトであるSQLiteSampleプロジェクトにISQLite.cs
ファイルを追加して、以下のように修正した。
using SQLite.Net;
namespace SQLiteSample {
public interface ISQLite {
SQLiteConnection GetConnection(); // <-1
}
}
|
GetConnection
メソッドを実行すると、SQLiteConnection
のインスタンスが取得できる(1)。
5. 各プラットフォームの実装
続いて、プラットフォームごとの処理を実装していく。
(1)iOS
iOS用のプロジェクトであるSQLiteSample.iOSプロジェクトにSQLite_iOS.cs
ファイルを追加し、以下のように修正する。
using System;
using System.IO;
using SQLite.Net;
using SQLite.Net.Platform.XamarinIOS;
using SQLiteSample.iOS;
using Xamarin.Forms;
[assembly: Dependency(typeof(SQLite_iOS))] // <-1
namespace SQLiteSample.iOS {
public class SQLite_iOS : ISQLite {
public SQLiteConnection GetConnection() {
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // <-2
var libraryPath = Path.Combine(documentsPath, "..", "Library"); // <-3
var path = Path.Combine(libraryPath, "TodoSQLite.db3"); // <-4
return new SQLiteConnection(new SQLitePlatformIOS(), path); // <-5
}
}
}
|
1は、DependencyServiceでプラットフォーム側の固有の処理を記述する場合の定型句である。
Environment.GetFolderPath
メソッド*4を使うと、特定のフォルダーのパスを取得できる。ここでは、パラメーターにEnvironment.SpecialFolder
列挙体のPersonal
を指定しているので、ユーザーディレクトリのDocumentsフォルダーが取得されている(2)。
続いて、このDocumentsフォルダーを基準として、その親フォルダーに位置するLibraryというフォルダーを取得している(3)。ここで使用されているPath
クラスの静的メソッドであるCombine
は、2つの文字列要素を結合して、パスを作成するためのものである。
最後にここまでで取得したフォルダーと、データベース名(ここではTodoSQLite.db3とした)を組み合わせて、最終的なデータベースファイル名としている(4)。
SQLite.Netでは、SQLiteConnection
オブジェクトのインスタンスを使用して、各種の操作を行うことになるが、このインスタンスの生成には、データベースのファイル名の他に、プラットフォーム固有のオブジェクトが必要になる。そして、SQLitePlatformIOS
オブジェクトが、iOSでのそれになる(5)。
- *4 System.Environment.GetFolderPathについての詳しい解説は、「Tips:Xamarin.iOS/Androidでアプリのデータディレクトリを取得するには?」を参照してほしい。
(2)Android
Android用のプロジェクトであるSQLiteSample.Droidプロジェクトには、SQLite_Droid.cs
ファイルを追加し、以下のように修正する。
using System;
using System.IO;
using SQLite.Net;
using SQLite.Net.Platform.XamarinAndroid;
using SQLiteSample.Droid;
using Xamarin.Forms;
[assembly: Dependency(typeof(SQLite_Android))]
namespace SQLiteSample.Droid {
public class SQLite_Android : ISQLite {
public SQLiteConnection GetConnection() {
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // <-1
var path = Path.Combine(documentsPath, "TodoSQLite.db3"); // <-2
return new SQLiteConnection(new SQLitePlatformAndroid(), path); // <-3
}
}
}
|
Androidでは、Environment.SpecialFolder.Personal
を指定してEnvironment.GetFolderPath
メソッドを呼び出すと、アプリケーションごとのデータフォルダーのパスが取得できる(1)。
ここでは、このデータフォルダーの中にデータベースファイルを置くことにした(2)。
SQLiteConnection
クラスのインスタンス生成で必要になる、プラットフォーム固有のオブジェクトは、AndroidではSQLitePlatformAndroid
オブジェクトである(3)。
6. データの定義
サンプル用のデータとして、PCLプロジェクトであるSQLiteSampleプロジェクトにTodoItem.cs
ファイルを追加して、以下のように修正する。
using System;
using SQLite.Net.Attributes;
namespace SQLiteSample {
public class TodoItem {
[PrimaryKey, AutoIncrement]
public int Id { get; set; } // <-1
public string Text { get; set; } // <-2
public DateTime CreatedAt { get; set; } // <-3
public bool Delete { get; set; } // <-4
}
}
|
キーとなるId
(1)の他に、文字列を保存するText
(2)、作成日時のCreatedAt
(4)、そして、削除状態を表現するDelete
(5)がある。
サンプルアプリでは、Delete
がtrueのとき、削除されたレコードということで、表示の対象外としている(そのコードは後述する)。
7. データベースへのアクセスクラス
データベースへアクセスするクラスとして、PCLプロジェクトであるSQLiteSampleプロジェクトにTodoRepository.cs
を追加して、以下のように修正した。
using System.Collections.Generic;
using SQLite.Net;
using Xamarin.Forms;
namespace SQLiteSample {
class TodoRepository {
static readonly object Locker = new object(); // <-1
readonly SQLiteConnection _db; // <-2
public TodoRepository() {
_db = DependencyService.Get<ISQLite>().GetConnection(); // <-3
_db.CreateTable<TodoItem>(); // <-4
}
public IEnumerable<TodoItem> GetItems() { // <-5
lock (Locker) { // <-6
// Delete==falseの一覧を取得する(削除フラグが立っているものは対象外)
return _db.Table<TodoItem>().Where(m => m.Delete == false); // <-7
}
}
public int SaveItem(TodoItem item) { // <-8
lock (Locker) { // <-9
if (item.Id != 0) { // Idが0でない場合は、更新
_db.Update(item); // <-10
return item.Id;
}
return _db.Insert(item); // <-11
}
}
}
}
|
2は、SQLite.Netでデータを操作する際に使用する、SQLiteConnection
クラスのインスタンスを保持する変数である。
この変数は、コンストラクターで初期化されているが、ここでは、DependencyServiceで定義した、GetConnection
メソッドを使用している(3)。
そして4は、テーブルを初期化する処理であり、指定したデータ型(ここではTodoItem)とデータベース上のテーブルが一致していない場合は、この時点で生成される。
TodoRepository
クラスでは、データベースを操作する2つのメソッドを定義した。
その1つ目は、一覧のためのGetItems
メソッドである(5)。Delete
プロパティがfalseのデータだけが表示対象であるため、それをLINQのWhere
メソッドで抽出している(7)。
2つ目は、追加および更新のSaveItem
メソッドである(8)。パラメーターで受け取ったTodoItem
オブジェクトのId
プロパティが、0でない場合は、更新とし(10)、0の場合は、新規の追加となっている(11)。
最後にロックの機能であるが、このデータベース・アクセス・クラスをスレッドセーフとするために、データベースにアクセスする前後でロック処理をしている(69)。そして、(1)は、そのロックオブジェクトである。
8. データベースの操作
ここまでの作業で、SQLite.Netを使用するための準備は整った。最後に、画面を作成して、データベースを操作するためのコードとして、SQLiteSampleプロジェクトのApp.cs
ファイルを以下のように修正する。
using System;
using Xamarin.Forms;
namespace SQLiteSample {
public class App : Application {
public App() {
MainPage = new MyPage();
}
}
class MyPage : ContentPage {
readonly TodoRepository _db = new TodoRepository(); // <-1
public MyPage() {
var listView = new ListView { // <-2
ItemsSource = _db.GetItems(), // <-3
ItemTemplate = new DataTemplate(typeof(TextCell)) // <-4
};
listView.ItemTemplate.SetBinding(TextCell.TextProperty, "Text");
listView.ItemTemplate.SetBinding(TextCell.DetailProperty, new Binding("CreatedAt", stringFormat: "{0:yyy/MM/dd hh:mm}"));
listView.ItemTapped += async (s, a) => { // <-5
var item = (TodoItem)a.Item;
if (await DisplayAlert("削除してい宜しいですか", item.Text, "OK", "キャンセル")) {
item.Delete = true; // 削除フラグを有効にして
_db.SaveItem(item); // データベースの更新
listView.ItemsSource = _db.GetItems(); // リスト更新
}
};
var entry = new Entry { // <-6
HorizontalOptions = LayoutOptions.FillAndExpand
};
var buttonAdd = new Button { // <-7
WidthRequest = 60,
TextColor = Color.White,
Text = "Add"
};
buttonAdd.Clicked += (s, a) => { // <-8
if (!String.IsNullOrEmpty(entry.Text)) { // Entryに文字列が入力されている場合に処理する
var item = new TodoItem { Text = entry.Text, CreatedAt = DateTime.Now, Delete = false };
_db.SaveItem(item);
listView.ItemsSource = _db.GetItems(); //リスト更新
entry.Text = ""; // 入力コントロールをクリアする
}
};
Content = new StackLayout { // <-9
Padding = new Thickness(0, Device.OnPlatform(20, 0, 0), 0, 0),
Children = {
new StackLayout {
BackgroundColor = Color.Navy, // 入力部の背景色はネイビー
Padding = 5,
Orientation = StackOrientation.Horizontal,
Children = {entry, buttonAdd} // Entryコントロールとボタンコントロールを横に並べる
},
listView // その下にリストボックスを置く
}
};
}
}
}
|
1は、先に定義したデータベースへのアクセス・クラスのTodoRepositoryである。以降、このオブジェクトを使用して、データの追加や更新を行うことになる。
画面に配置しているコントロールは、Entryコントロール(6)、Buttonコントロール(7)、およびListViewコントロール(2)の3つであり、StackLayoutを組み合わせて配置している(9)。
ListViewコントロールでは、そのデータソースをTodoRepositoryの一覧メソッドであるGetItems
メソッドで初期化している(3)。また、データを表示するためのデータテンプレートも指定している(4)。
Buttonコントロールをクリックしたときのイベントハンドラー内で、Entryコントロールのテキストを使って新規にTodoItem
クラスのオブジェクトを生成し、TodoRepositoryのSaveItem
メソッドで追加している(8)。Id
プロパティが0であるため、追加となるのである。
ListViewコントロールのタップイベントでは、選択された行のDelete
プロパティをTrueに変更して、TodoRepositoryのSaveItem
メソッドで更新している(5)。今度は、Id
プロパティが0でないため、更新処理となっている。また、更新後にデータソースをGetItems
メソッドで最新の状態にしている。
このコードを実行すると次のような画面になる。
9. まとめ
今回紹介したように、SQLite.Netを使用すると、簡単にローカルにデータベースを構築できる。
なお、クラウド上のデータベースでデータを保持する場合も、このようなローカルのデータベースと組み合わせることで、オフラインのときでも継続して作業ができるアプリが構築可能である。
※以下では、本稿の前後を合わせて5回分(第48回~第52回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
48. Xamarin.Formsでプラットフォームごとの微調整を行うには?
カスタムレンダラーやDependencyServiceの仕組みを使わず、Deviceクラスを利用してプラットフォーム間で異なる部分を微調整する方法を説明する。
49. MvvmCrossでAndroidの画面の再生成に対応するには?
Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。
50. 【現在、表示中】≫ Xamarin.Formsでローカルデータベースを使用するには?
アプリを終了して再起動したときに、ユーザーデータを復活させたい場合、ローカルやクラウドにデータを保存することになる。その一つの方法として、SQLite.Netを使ってローカルDBに保存する方法を説明する。
51. MvvmCrossでカスタムコンバーターを作成するには?
MvvmCrossでのiOS/Androidアプリ開発において、バインディングする値を変換できるカスタムコンバーターの使い方を説明する。
52. Xamarin.FormsでTwitterクライアントを作成するには?
TwitterのAPIを扱えるライブラリであるCoreTweetを使用して、Twitterデータを検索するアプリを作成。CoreTweetの導入と、検索したテキストの表示までを紹介する。