[Android] SQLiteをやめてRealmを使ってみた

  • 6
    Like
  • 0
    Comment

はじめに

Androidの開発をしている人ならば一度はぶつかるであろう「データベース」という存在。
すでにSQLを触ったことがある人ならばSQLiteなんてお手の物なのだろうが、私はそうではない。
そこで猫の手を借りようと思い、色々なプラグインを調べてみたところ"Realm"に出会った。
すでに色々なアプリにも使われており、iOS界隈では一定の評価があるとのことだったので弄ってみることにした。

realm.gif

開発環境

  • MacOS Sierra (10.12.6)
  • Android Studio (2.3.3)

さっそく導入

Project直下のgradleに以下のコードを追加します。

build.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に以下のコードを追加します。

build.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はそれぞれの環境に合わせてバージョン等調整してください。)

実装してみる

  1. Model作成
    データベースの構築でもっとも大切なテーブル設計。Realmでは非常に簡単に設計することができます。以下にサンプルコードを記しておきます。

    Friend.java
    public 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側に記述することなく可読性を高めることもできます。

  2. 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を追加します。

  3. Controller作成
    アプリ起動時の画面は今回省略していますので、Rootアクティビティにボタンを二つ追加するなどして各フラグメントを表示するよう設定してください。

    • 友達追加画面

      AddFriendFragment.java
      public 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.java
      public 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.java
      class 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