Realm を使ったシンプルなサンプルアプリを作ってみました。
最初にダミーデータを作成しRecyclerViewを使ってリスト表示します。
今回はセルがタップされたときにデータを削除し、メッセージをToastで表示するようにしました。
全てのソースコードはこちらにあります。(GitHub)
環境
- Kotlin 1.3.50
- Realm 5.15.2
- realm-android-adapter 3.1.0
- recyclerview-v7:29.1.0
- cardview-v7:29.1.0
- targetSDK: 29
手順
- Realmの初期設定
- CustomAdapterの作成
- データ作成と一覧表示
- Itemをクリックしたときの処理を追加する
1. Realm の初期設定
Realm 公式ドキュメントに乗っ取りながらやると良いと思います。
https://realm.io/docs/java/5.15.2/
build.gradle の dependencies に以下を追加
classpath "io.realm:realm-gradle-plugin:5.15.2"
app/build.gradle の上部に以下を追加
apply plugin: 'kotlin-kapt'
apply plugin: "realm-android"
app/build.gradleのdependenciesに以下を追加
implementation 'com.android.support:recyclerview-v7:29.1.0'
implementation 'com.android.support:cardview-v7:29.1.0'
implementation 'io.realm:android-adapters:3.1.0'
[補足] realm-android-adapters について
Realm公式が出しているAdapterのライブラリです。表示しているデータの中身が変わったときにViewを更新するためnotifyDataSetChanged()
等を呼ぶことが多いですが、Realmを使う場合DBの内容が書き変わったことを受け取りView を更新しなければならず、処理が複雑になることが考えられます。
このライブラリを使うと、RealmのDB内が書き換わったときに自動でViewを更新してくれて便利なので、今回はこれを使用します。
(ちなみに、ListViewとRecyclerViewの2つに対応しています)
参考: realm/realm-android-adapter
Applicationクラスの作成
Applicationクラスを作成します。アプリを起動したときに実行するようにし、Realmのinitializeの処理を行います。
今回はアプリ名がRealmTodoなのでRealmTodoApplication.kt
という名前で作成しました。
class RealmTodoApplication : Application() {
override fun onCreate() {
super.onCreate()
Realm.init(this)
val realmConfig = RealmConfiguration.Builder()
.deleteRealmIfMigrationNeeded()
.build()
Realm.setDefaultConfiguration(realmConfig)
}
}
※ すでにDBにデータがある状態でモデルとなるクラスを変更(例えばプロパティを追加など)した場合、アプリを削除し再インストールする必要が出てきて正直手間になります。そこで、RealmConfigurationにて.deleteRealmIfMigrationNeeded()
を設定すると、もともとあったデータを全て削除してくれるようになりこの手間を省くことができるようになります。
参考: Migrations | Realm Java 5.15.2
また、AndroidManifest.xml
のタグにname属性を追加し、先ほど作成したApplicationクラスを指定します。
<application
android:name=".RealmTodoApplication"
/* 省略 */
></application>
</manifest>
※ここでApplicationクラスを確認するとクラス名が灰色になっており参照できていないように見えますが、実際はちゃんと参照できているのでこのままでOKです。
これでRealm関連の初期設定は完了です。
2. CustomAdapterの作成
リストのItemとなるレイアウトの作成
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardElevation="2dp">
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="8dp"
card_view:srcCompat="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/contentTextView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:gravity="center_vertical"
android:text="やること"
android:textSize="18sp" />
<TextView
android:id="@+id/dateTextView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_vertical|end"
android:text="2019/11/20 12:00:00" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
モデルの作成
RealmObject
を継承したモデルを作成します。@PrimaryKey
というアノテーションをつけると一意なプロパティとして定義することができ、その値をUUID.randomUUID().toString()
とすると一意なIDを生成してくれます。(他にもAutoIncrement
する方法もあります。)
参考: Realmで一意なPrimaryKeyを設定する方法二つ
open class Task(
@PrimaryKey open var id: String = UUID.randomUUID().toString(),
open var imageId: Int = 0,
open var content: String = "",
open var createdAt: Date = Date(System.currentTimeMillis())
) : RealmObject()
CustomAdapterの作成
ここでRealmRecyclerViewAdapterを継承したCustomAdapterを作成します。
タスクのリストを表示するのでTaskAdapter.kt
としました。
class TaskAdapter(
private val context: Context,
private var taskList: OrderedRealmCollection<Task>?,
private val autoUpdate: Boolean
) :
RealmRecyclerViewAdapter<Task, TaskAdapter.TaskViewHolder>(taskList, autoUpdate) {
override fun getItemCount(): Int = taskList?.size ?: 0
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
val task: Task = taskList?.get(position) ?: return
holder.imageView.setImageResource(task.imageId)
holder.contentTextView.text = task.content
holder.dateTextView.text =
SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPANESE).format(task.createdAt)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TaskViewHolder {
val v = LayoutInflater.from(context).inflate(R.layout.list_item, viewGroup, false)
return TaskViewHolder(v)
}
class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView = view.imageView
val contentTextView: TextView = view.contentTextView
val dateTextView: TextView = view.dateTextView
}
}
autoUpdate
というパラメータをtrue
にして渡すと、DBを更新した際にRecyclerViewを自動で更新してくれるようになります。
またSimpleDateFormat
を使うと、Date型を指定フォーマットでStringに変換できます。
参考: SimpleDateFormat | Android Developers
これでCustomAdapter周りも完了です。
3. データ作成と一覧表示
次に、Realmを用いたDBへの書き込みと読み込み、リストへの一覧表示を実装します。今回は一旦ダミーデータを10件作成し、そのリストを表示するような処理にしました。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private val realm: Realm by lazy {
Realm.getDefaultInstance()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val taskList = readAll()
// タスクリストが空だったときにダミーデータを生成する
if (taskList.isEmpty()) {
createDummyData()
}
val adapter = TaskAdapter(this, taskList, true)
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter
}
override fun onDestroy() {
super.onDestroy()
realm.close()
}
fun createDummyData() {
for (i in 0..10) {
create(R.drawable.ic_launcher_background, "やること $i")
}
}
fun create(imageId: Int, content: String) {
realm.executeTransaction {
val task = it.createObject(Task::class.java, UUID.randomUUID().toString())
task.imageId = imageId
task.content = content
}
}
fun readAll(): RealmResults<Task> {
return realm.where(Task::class.java).findAll().sort("createdAt", Sort.ASCENDING)
}
}
realm
の変数ですが不変なオブジェクトとして扱いたかった(valで定義したかった)ので、lazy
という委譲プロパティを使って遅延初期化をしています。これはこの変数が最初に参照されたときに{}
内の値をsetして初期化します(2回目以降の参照では最初にsetした値を参照します)。
参考: 委譲プロパティ - Kotlin Programming Language
readAll()
の中で.sort("createdAt", Sort.ASCENDING)
をして作成したときの時刻を基準に昇順で取得しています。これは要素の削除をしたときに、リストの順番がバラバラになってしまう仕様を回避するためにこの実装にしました。
参考: Realm: Order of records was changed - Stack Overflow
また、create()
内のcreateObject()
ですが、1つ目の引数にモデル、2つ目の引数に@PrimaryKey
で設定したプロパティの値を代入して生成します。(これがないと落ちます)
createObject()
以外にもcopyToRealm()
やinsert()
などがあります。
参考: Creating objects | Realm Java 5.15.2
実行すると以下のようになります。(DB内に保存しているので、アプリを終了し再起動しても見えるようになっていると思います。)
4. Itemをクリックしたときの処理を追加する
セルをタップしたときの処理を書いていきます。簡単な処理であればCustomAdapter側に直接記述すれば良いと思いますが、今回はRealmの処理がActivityに依存しているため、CustomAdapterからActivity内の処理(編集、削除など)を呼び出したいです。
そこで、今回はClickListenerとなるinterfaceを定義し、そのインスタンスをActivityからAdapterに渡すことで実現します。
class TaskAdapter(
private val context: Context,
private var taskList: OrderedRealmCollection<Task>?,
private var listener: OnItemClickListener, // ---------追加----------
private val autoUpdate: Boolean
) :
RealmRecyclerViewAdapter<Task, TaskAdapter.TaskViewHolder>(taskList, autoUpdate) {
override fun getItemCount(): Int = taskList?.size ?: 0
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
val task: Task = taskList?.get(position) ?: return
// ----------------追加---------------
holder.container.setOnClickListener{
listener.onItemClick(task)
}
// -----------------------------------
holder.imageView.setImageResource(task.imageId)
holder.contentTextView.text = task.content
holder.dateTextView.text =
SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.JAPANESE).format(task.createdAt)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TaskViewHolder {
val v = LayoutInflater.from(context).inflate(R.layout.list_item, viewGroup, false)
return TaskViewHolder(v)
}
class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val container : LinearLayout = view.container // ---------追加----------
val imageView: ImageView = view.imageView
val contentTextView: TextView = view.contentTextView
val dateTextView: TextView = view.dateTextView
}
// ----------------追加---------------
interface OnItemClickListener {
fun onItemClick(item: Task)
}
// -----------------------------------
}
class MainActivity : AppCompatActivity() {
/* 省略 */
override fun onCreate(savedInstanceState: Bundle?) {
/* 省略 */
val adapter =
TaskAdapter(this, taskList, object : TaskAdapter.OnItemClickListener {
override fun onItemClick(item: Task) {
// クリック時の処理
Toast.makeText(applicationContext, item.content + "を削除しました", Toast.LENGTH_SHORT).show()
delete(item.id)
}
}, true)
/* 省略 */
}
/* 省略 */
fun update(id: String, content: String) {
realm.executeTransaction {
val task = realm.where(Task::class.java).equalTo("id", id).findFirst()
?: return@executeTransaction
task.content = content
}
}
fun update(task: Task, content: String) {
realm.executeTransaction {
task.content = content
}
}
fun delete(id: String) {
realm.executeTransaction {
val task = realm.where(Task::class.java).equalTo("id", id).findFirst()
?: return@executeTransaction
task.deleteFromRealm()
}
}
fun delete(task: Task) {
realm.executeTransaction {
task.deleteFromRealm()
}
}
fun deleteAll() {
realm.executeTransaction {
realm.deleteAll()
}
}
}
今回はセルがタップされたときにそれを削除し、それをToastで表示するようにしました。
これで一旦完成です。
終わりに
Realm周り結構ハマりどころがあるので、適宜調べながらやっていくと良いと思います。
また、SQLに慣れているのであればRoomを使うのもありなので、Realmが大変であれば別のものを採用するのもありだと思います。