はじめに
Androidの開発をしている人ならば一度はぶつかるであろう「データベース」という存在。
すでにSQLを触ったことがある人ならばSQLiteなんてお手の物なのだろうが、私はそうではない。
そこで猫の手を借りようと思い、色々なプラグインを調べてみたところ"Realm"に出会った。
すでに色々なアプリにも使われており、iOS界隈では一定の評価があるとのことだったので弄ってみることにした。
開発環境
- MacOS Sierra (10.12.6)
- Android Studio (2.3.3)
さっそく導入
Project直下のgradleに以下のコードを追加します。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'io.realm:realm-gradle-plugin:3.5.0' // 追加
}
}
次にRealmを使ってデータベースを構築したいアプリのgradleに以下のコードを追加します。
apply plugin: 'realm-android' // 追加
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:26.0.0-alpha1'
compile 'com.android.support:recyclerview-v7:26.0.0-alpha1'
compile 'com.android.support:support-v4:26.0.0-alpha1'
compile 'io.realm:android-adapters:2.1.0' // 追加
}
(各Support Libraryはそれぞれの環境に合わせてバージョン等調整してください。)
実装してみる
-
Model作成
データベースの構築でもっとも大切なテーブル設計。Realmでは非常に簡単に設計することができます。以下にサンプルコードを記しておきます。Friend.javapublic class Friend extends RealmObject { @PrimaryKey private String address; // addressカラム private String name; // nameカラム // getter setter... public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
Friendというオブジェクト(レコード)に、String型の"address"パラメータ(カラム)とString型の"name"パラメータ(カラム)を定義しました。
その下には各カラムに値を読み取ったり書き込むためのGetter,Setterを定義します。
今回はaddressカラムをプライマリキーに設定したかったので、@PrimaryKey
アノテーションをつけています。
その他アノテーションや詳しい説明は公式ドキュメントにあります。
RealmにおいてModel作成は比較的自由度が高いので、アノテーションや独自のMethodをどんどん記述していけば大抵のことはできます。
また独自のMethodを記述することで、よく行う操作などをController側に記述することなく可読性を高めることもできます。
-
View作成
先ほど作ったModel(Friendテーブル)にデータを追加する画面と一覧表示する画面をサンプルで作っていきます。-
データ追加(友達登録)
fragment_addfriend.xml<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/White" tools:context=".AddFriendFragment"> <EditText android:id="@+id/address_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:ems="12" android:hint="アドレス" android:inputType="text" android:maxLines="1"/> <EditText android:id="@+id/name_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/address_text" android:layout_centerHorizontal="true" android:layout_marginTop="10dp" android:ems="12" android:hint="名前" android:inputType="text" android:maxLines="1"/> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/name_text" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:text="登録"/> </RelativeLayout>
アドレス、名前の入力を受け付けるEditTextと登録確認ダイアログのButtonを追加します。
-
データ一覧表示(友達一覧)
fragment_friendlist.xml<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/White" tools:context=".FriendListFragment"> <android.support.v7.widget.RecyclerView android:id="@+id/friend_list" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/button"/> </FrameLayout>
一覧表示をするためにRecyclerViewを追加します。
row_friend.xml<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <TextView android:id="@+id/label1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="名前 :"/> <TextView android:id="@+id/name_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/label1" android:layout_toEndOf="@+id/label1"/> <TextView android:id="@+id/label2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@+id/label1" android:layout_below="@+id/label1" android:layout_marginTop="10dp" android:text="アドレス :"/> <TextView android:id="@+id/address_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/label2" android:layout_toEndOf="@+id/label2"/> </RelativeLayout>
名前,アドレスをそれぞれ表示するTextViewを追加します。
-
-
Controller作成
アプリ起動時の画面は今回省略していますので、Rootアクティビティにボタンを二つ追加するなどして各フラグメントを表示するよう設定してください。-
友達追加画面
AddFriendFragment.javapublic class AddFriendFragment extends Fragment { private EditText addressText; private EditText nameText; private Button mButton; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_forth, container, false); addressText = view.findViewById(R.id.address_text); nameText = view.findViewById(R.id.name_text); mButton = view.findViewById(R.id.button); return view; } @Override public void onStart() { super.onStart(); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { DialogFragment friendRegisterDialog = new FriendRegisterDialog(); Bundle args = new Bundle(); args.putString("name", nameText.getText().toString()); args.putString("address", addressText.getText().toString()); friendRegisterDialog.setArguments(args); friendRegisterDialog.show(getActivity().getSupportFragmentManager(), "friendRegister"); } }); } public static class FriendRegisterDialog extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Bundle args = getArguments(); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()) builder.setTitle("登録確認") .setMessage("友達登録しますか?") .setPositiveButton("はい", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { final Realm realm = Realm.getDefaultInstance(); // Realmをインスタンス化 final Friend friend = new Friend(); // Friendテーブルをインスタンス化 friend.setName(args.getString("name")); friend.setAddress(args.getString("address")); realm.executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm bgRealm) { bgRealm.copyToRealmOrUpdate(friend); // PrimaryKeyに設定した値が既に存在していれば更新し、無ければ新規登録 } }, new Realm.Transaction.OnSuccess() { @Override public void onSuccess() { realm.close(); // データベース操作が終了する時には必ずclose()させる事! Toast.makeText(getContext(), args.getString("name") + "さんを登録しました", Toast.LENGTH_SHORT).show(); getActivity().getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); // Fragmentを閉じる } }, new Realm.Transaction.OnError() { @Override public void onError(Throwable error) { realm.close(); // データベース操作が終了する時には必ずclose()させる事! Toast.makeText(getContext(), "登録に失敗しました", Toast.LENGTH_SHORT).show(); } }); } }) .setNegativeButton("いいえ", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dismiss(); } }); return builder.create(); } } }
アドレス,名前入力をEditTextで受け取り、その値をAlertDialogにBundle経由で渡しています。そしてAlertDialogのPositiveButtonが押された時に、入力されたアドレスの値がすでにテーブル上に存在していれば名前を更新(Update)し、存在していなければ新規登録(Insert)をするフローにしました。
これらはRealmのメソッドcopyToRealmOrUpdate()
たった一行で実現しているのだから驚きです。 -
友達一覧画面
FriendListFragment.javapublic class FriendListFragment extends Fragment { private RecyclerView mRecyclerView; private FriendAdapter adapter; private Realm realm; @Override public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_fifth, container, false); realm = Realm.getDefaultInstance(); // Realmをインスタンス化 final RealmResults<Friend> result = realm.where(Friend.class).findAll(); // Friendテーブルから全レコード取得 adapter = new FriendAdapter(result); // その結果をアダプターに渡す mRecyclerView = view.findViewById(R.id.friend_list); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); mRecyclerView.setAdapter(adapter); mRecyclerView.setHasFixedSize(true); mRecyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL)); // RecyclerViewの特徴であるスワイプ操作を実装 ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; } @Override public void onSwiped(final RecyclerView.ViewHolder viewHolder, int direction) { // アイテム毎のプロパティを取得 TextView nameText = viewHolder.itemView.findViewById(R.id.name_text); TextView address_text = viewHolder.itemView.findViewById(R.id.address_text); final String oldName = nameText.getText().toString(); final String address = address_text.getText().toString(); // スワイプの向きによって処理分け if (direction == ItemTouchHelper.LEFT) { // アイテムを削除 realm.executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm bgRealm) { Friend obj = bgRealm.where(Friend.class).equalTo("address", address).findFirst(); obj.deleteFromRealm(); } }, new Realm.Transaction.OnSuccess() { @Override public void onSuccess() { adapter.notifyDataSetChanged(); // アダプター再描画 Toast.makeText(getContext(), oldName + "さんを削除しました", Toast.LENGTH_SHORT).show(); } }, new Realm.Transaction.OnError() { @Override public void onError(Throwable error) { adapter.notifyDataSetChanged(); // アダプター再描画 Toast.makeText(getContext(), "削除に失敗しました", Toast.LENGTH_SHORT).show(); } }); } else { // アイテムのプロパティを変更 final EditText input = new EditText(getContext()); input.setInputType(TYPE_CLASS_TEXT); input.setMaxLines(1); input.setText(oldName); AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setTitle("名前変更") .setMessage("変更後の名前を入力してください") .setView(input) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { final String newName = input.getText().toString(); final Friend friend = new Friend(); friend.setName(newName); friend.setAddress(address); realm.executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm bgRealm) { bgRealm.copyToRealmOrUpdate(friend); } }, new Realm.Transaction.OnSuccess() { @Override public void onSuccess() { adapter.notifyDataSetChanged(); // アダプター再描画 Toast.makeText(getContext(), oldName + "さんを" + newName + "さんに名前変更しました", Toast.LENGTH_SHORT).show(); } }, new Realm.Transaction.OnError() { @Override public void onError(Throwable error) { adapter.notifyDataSetChanged(); // アダプター再描画 Toast.makeText(getContext(), "名前変更に失敗しました", Toast.LENGTH_SHORT).show(); } }); } }) .setNegativeButton("キャンセル", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { } }) .create() .show(); } } // スワイプ操作時表示されるメニュー @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { Bitmap icon; Paint p = new Paint(); View itemView = viewHolder.itemView; float height = (float) itemView.getBottom() - (float) itemView.getTop(); float width = height / 3; if (dX > 0) { p.setColor(Color.parseColor("#388E3C")); RectF background = new RectF((float) itemView.getLeft(), (float) itemView.getTop(), dX, (float) itemView.getBottom()); c.drawRect(background, p); icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); RectF icon_dest = new RectF((float) itemView.getLeft() + width, (float) itemView.getTop() + width, (float) itemView.getLeft() + 2 * width, (float) itemView.getBottom() - width); if (dX > itemView.getLeft() + icon.getWidth()) { c.drawBitmap(icon, null, icon_dest, p); } } else if (dX < 0) { p.setColor(Color.parseColor("#D32F2F")); RectF background = new RectF((float) itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom()); c.drawRect(background, p); icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); RectF icon_dest = new RectF((float) itemView.getRight() - 2 * width, (float) itemView.getTop() + width, (float) itemView.getRight() - width, (float) itemView.getBottom() - width); if (dX < -(itemView.getLeft() + icon.getWidth())) { c.drawBitmap(icon, null, icon_dest, p); } } } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } }); touchHelper.attachToRecyclerView(mRecyclerView); return view; } @Override public void onDestroy() { super.onDestroy(); mRecyclerView.setAdapter(null); realm.close(); // データベース操作が終了する時には必ずclose()させる事! } }
RecyclerViewにFriendテーブルの全レコードを一覧表示しています。
ここでお気づきになった方もいるかもしれませんが、Realmではテーブルから取得したレコードが更新されたり削除された場合に、再度データを取得し直す必要がありません。今回のコードでは「result」という変数に取得したレコードが格納されていますが、その中のデータに変更があれば、逐次データが同期されるのです。素晴らしいですね。 -
カスタムアダプター
FriendAdapter.javaclass FriendAdapter extends RealmRecyclerViewAdapter<Friend, FriendAdapter.FriendViewHolder> { private OrderedRealmCollection<Friend> objects; FriendAdapter(OrderedRealmCollection<Friend> friends) { super(friends, true); this.objects = friends; setHasStableIds(true); } @Override public FriendViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_friend, parent, false); return new FriendViewHolder(view); } @Override public void onBindViewHolder(FriendViewHolder holder, int position) { final Friend obj = getItem(position); //noinspection ConstantConditions holder.name.setText(obj.getName()); holder.address.setText(obj.getAddress()); } @Override public int getItemCount() { return objects.size(); } static class FriendViewHolder extends RecyclerView.ViewHolder { TextView name; TextView address; FriendViewHolder(View view) { super(view); name = view.findViewById(R.id.name_text); address = view.findViewById(R.id.address_text); } } }
ViewHolderを使い多少高速化(軽量化)しています。
RealmRecyclerViewAdapter<テーブル名, アダプター名.ViewHolder名>を継承したあなた独自のカスタムアダプターを作りましょう。
-
Tips
今回MainActivityのコードは載せていないのですが、アプリ起動時のActivityでRealmを初期化する必要があります。
以下のコードを追加していただければ問題ないかと思います。
Realm.init(this);
RealmConfiguration config = new RealmConfiguration.Builder().build();
Realm.setDefaultConfiguration(config);
configに関しても様々なパラメータがあるようなので、公式ドキュメントで...
またアプリ配布後に「新しくカラムを追加したい!」や「カラムの属性を変更したい!」,「そもそもテーブルを追加したい」といった要望が出ると思いますが、
Realmではマイグレーション機能で対応することができます。しかしながら開発途中でまだ配布前であれば、いちいちマイグレーションを記述するのも面倒です。
そこで、以下のコードを先ほどの初期化時に追加すれば、データベースを一度綺麗に削除してくれるのでマイグレーション不要です。
Realm.deleteRealm(config);
開発初期段階でまだデータベース仕様書が固まっていない時は便利ですね!
終わりに
Realmを実際に使ってみると、その使いやすさに感激してしまいました。
関連する他のプラグインと比較して圧倒的にレスポンスが速い事も素晴らしいです。
SQLを触ったことがない,あまり経験がない方にはうってつけだと思います。
Androi開発のデータベースでお困りの方は気軽に試してみる価値ありです!
まだまだ紹介仕切れていないメソッド、機能ありますので、公式ドキュメントで...
Realm Java 3.5.0