【Androidアプリチュートリアル】 メモアプリを作ろう

メモアプリの作成

Android(Kotlin)でメモアプリを作成していくチュートリアルを解説していきます。

特徴としては、Realmというデータベースを利用することです。

Realmはアプリ開発でよく使われるので、ここで使い方を学習していきましょう。

完成形

goal_image

テキストのメモを入力ができ、追加ボタンを押すと一覧に表示されていくアプリです。

この記事で学習できること
  • Realmの使い方
  • ローカルDBの理解
  • ローカルDBを使ったアプリの作成方法

バージョン

  • Android Studio Chipmunk | 2021.2.1 Patch 1
  • Kotlin 1.6.21

プロジェクトの作成

まずは、プロジェクトを作成していきましょう。

詳細な説明はこちらにありますので、必要があれば参考にしてください。

Choose your project は Empty Activity を選択してください。

アプリ名はなんでもOKですが、ここではMemoとしました。

ここでビルドをした時に Android Gradle plugin requires Java 11 to run のエラーが出た時にはこの記事を参考にしてください。

Realm導入

早速Realm の導入をしていきましょう。

root配下にある build.gradle を開いてください。

一番上に以下の buildscript を追加してあげます。

中にはrealmのpluginが書かれています。

// Top-level build file where you can add configuration options common to all sub-projects/modules.
// ↓これを追加する
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:10.11.0"
    }
}
...

次に app 配下にある build.gradle を開いてください。

一番上に plugins でまとめられた箇所があります。

そこに2行ほど追加しましょう。

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    // この行を追加
    id 'org.jetbrains.kotlin.kapt'
}
// この行を追加
apply plugin: "realm-android"
...

最後に上部に Sync Now というボタンが表示されていると思います。

こちらを押してください。

しばらくしてこのような表示が出ればOKです。

Realm初期化

次にRealmを使うためにRealmの初期化を行なっていきます。

Applicationクラス追加

まずは Application クラスを追加しましょう。

MainActivity と同じ階層に MemoApplication というクラスを追加します。

中身は以下のように記述してください。

アプリ起動時に呼ばれる onCreate() の中で Realm を初期化しています。

import android.app.Application
import io.realm.Realm

class MemoApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        Realm.init(this)
    }
}

Manifestに記述

先ほど追加した MemoApplication を AndroidManifest.xml に追加しましょう。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hiring.memo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:name=".MemoApplication"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        ...
    </application>
</manifest>

application タグに android:name=".MemoApplication" というのを追加しましょう。
これでこのアプリの Application クラスが MemoApplication になります。

これでRealmの初期化は完了です。

レイアウト

次はレイアウトを作成していきましょう。

今回はレイアウトにこだわらないので、簡単に実装していきます。

RecyclerViewの追加

今回はメモリストを用意するのでリスト表示のできる RecyclerView を使っていきましょう。

まずはライブラリの追加です。先ほども app 配下にある build.gradle を開きましょう。

dependencies 内に下記のように1行追加して下さい。

dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.2'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    // この行を追加する
    implementation 'androidx.recyclerview:recyclerview:1.2.1'
    ...
}

また先ほどと同様に Sync Now ボタンが表示されるはずなので押しましょう。

これで RecyclerView の準備は完了です。

画面レイアウト

次に画面のレイアウトを実装しましょう。

activity_main を開いて以下のように実装して下さい。

<?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">

    <EditText
        android:id="@+id/memo_edit_text"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:hint="メモを入力"
        app:layout_constraintEnd_toStartOf="@id/add_button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/add_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="追加"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/memo_list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/memo_edit_text" />

</androidx.constraintlayout.widget.ConstraintLayout>

Design タブを開いてこのようになっていればOKです。

次はリストのアイテムです。

item_memo.xml というファイルを作成して下さい。
中身の実装は以下のような TextView だけで十分です。

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/memo_text_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:textSize="20sp" />

これでレイアウトの実装は完了しました。

RecyclerViewAdapter実装

次はAdapterを実装していきます。

MemoListAdapter というクラスを作成して下さい。

中身は以下のように実装して下さい。 onBindViewHolder() の中で MemoViewHolder の bind() を呼んでいます。これで TextView に文字列を入れています。

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class MemoListAdapter: RecyclerView.Adapter<MemoListAdapter.MemoViewHolder>() {
    private val memoList = mutableListOf<String>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MemoViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_memo, parent, false)
        return MemoViewHolder(view)
    }

    override fun onBindViewHolder(holder: MemoViewHolder, position: Int) {
        holder.bind(memoList[position])
    }

    override fun getItemCount(): Int = memoList.size

    fun updateMemoList(memoList: List<String>) {
        // 一度クリアしてから新しいメモに入れ替える
        this.memoList.clear()
        this.memoList.addAll(memoList)
        // データに変更があったことをadapterに通知
        notifyDataSetChanged()
    }

    class MemoViewHolder(view: View): RecyclerView.ViewHolder(view) {
        fun bind(memo: String) {
            val textView = itemView.findViewById<TextView>(R.id.memo_text_view)
            textView.text = memo
        }
    }
}

Adapterの作成が完了したら RecyclerView にセットしていきます。

MainActivity を開いて以下のように実装して下さい。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: MemoListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        adapter = MemoListAdapter()
        val recyclerView = findViewById<RecyclerView>(R.id.memo_list)
        recyclerView.adapter = adapter
    }
}

ここでは MemoListAdapter の adapter を定義しておき、 onCreate() で代入しています。

さらに findViewById() メソッドで activity_main の memo_list を取得し、先ほど作成した adapter を代入しています。

build

ここで一度ビルドしてみましょう。このようになればOKです!

データベースとの連携

それでは次にデータベースと連携していきましょう。

まずは保存するデータ形式を定義していきます。

Memo というクラスを作成してください。

中身は以下のように実装してください。

import io.realm.RealmObject

open class Memo(
    open var name: String = ""
) : RealmObject()

このMemoというクラスがDBへ書き込むための形式となります。

今回は入力されるメモだけを保存すれば良いのでnameだけになっています。

気をつけることが2点あり1つ目は RealmObject を継承していることです。Realmライブラリの中で処理をするために必要となります。

2つ目はopen修飾子を付けることです。これもRealmライブラリの中でこのMemoクラスを継承するために必要となります。

理由までを理解することは難しいと思うので必要な操作として押さえておくだけでいいと思います。

次は EditText に文字列が入力されている状態で 追加 ボタンを押すと一覧に追加されていくように実装していきます。

それでは MainActivity を開き、以下のように実装してみてください。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView
import io.realm.Realm

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: MemoListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val recyclerView = findViewById<RecyclerView>(R.id.memo_list)
        adapter = MemoListAdapter()
        recyclerView.adapter = adapter

        val editText = findViewById<EditText>(R.id.memo_edit_text)
        val addButton = findViewById<Button>(R.id.add_button)

        val realm = Realm.getDefaultInstance()

        addButton.setOnClickListener {
            val text = editText.text.toString()
            if (text.isEmpty()) {
                // テキストが空の場合には無視
                return@setOnClickListener
            }
            // Realmのトランザクション
            realm.executeTransactionAsync {
                // Memoのオブジェクトを作成
                val memo = it.createObject(Memo::class.java)
                // nameに入力してあったtextを代入
                memo.name = text
                // 上書きする
                it.copyFromRealm(memo)
            }
            // テキストを空にする
            editText.text.clear()
        }
        // DBに変更があった時に通知がくる
        realm.addChangeListener {
            // 変更があった時にリストをアップデートする
            val memoList = it.where(Memo::class.java).findAll().map { it.name }
            // UIスレッドで更新する
            recyclerView.post {
                adapter.updateMemoList(memoList)
            }
        }
        // 初回表示時にリストを表示
        realm.executeTransactionAsync {
            val memoList = it.where(Memo::class.java).findAll().map { it.name }
            // UIスレッドで更新する
            recyclerView.post {
                adapter.updateMemoList(memoList)
            }
        }
    }
}

それでは1つ1つ説明してきます。

val editText = findViewById<EditText>(R.id.memo_edit_text)
val addButton = findViewById<Button>(R.id.add_button)

まずは View の取得です。 findViewById() メソッドによって EditText と Button を取得しています。

val realm = Realm.getDefaultInstance()

次は Realm のインスタンス取得です。これはRealmに対して操作を行う場合には必ず行う操作です。

取得できた Realm インスタンスに対して処理をしていきます。

addButton.setOnClickListener {
    val text = editText.text.toString()
    if (text.isEmpty()) {
        // テキストが空の場合には無視
        return@setOnClickListener
    }
    // Realmのトランザクション
    realm.executeTransactionAsync {
        // Memoのオブジェクトを作成
        val memo = it.createObject(Memo::class.java)
        // nameに入力してあったtextを代入
        memo.name = text
        // 上書きする
        it.copyFromRealm(memo)
    }
    // テキストを空にする
    editText.text.clear()
}

次に addButton を押した時の処理です。

addButton を押した時は editText に入力されている文字列を Realm に保存させます。

まず最初に行なっているのは入力された文字列が空かどうかをチェックしています。空の時には Realm に保存させたくないので無視しているわけですね。

次に realm.executeTransactionAsync() を呼んでいる箇所です。 executeTransactionAsync() の引数はリスナーですが、Kotlinだと {} で書けるようになっています。
ここの中での it は Realm ですので、この it に対して処理を書いていけばOKです。

処理の内容を以下に抜き出すと、

// Memoのオブジェクトを作成
val memo = it.createObject(Memo::class.java)
// nameに入力してあったtextを代入
memo.name = text
// 上書きする
it.copyFromRealm(memo)

となっています。

createObject() で Memo のオブジェクトを作成しています。

それと同時に Realm のDBにも登録されます。

その下の行で memo の name に入力された文字列を代入させています。

memo 自体はすでにDBに登録されているので最後の copyFromRealm() メソッドで上書きしています。

最後の editText.text.clear() は editText の中身を空にさせておき、次のメモの入力をしやすくさせるためにしています。

// MemoListAdapter
private fun updateList(memoList: List<String>) {
    // 一度クリアしてから新しいメモに入れ替える
    adapter.memoList.clear()
    adapter.memoList.addAll(memoList)
    // データに変更があったことをadapterに通知
    adapter.notifyDataSetChanged()
}

次は場所が変わりますが MemoListAdapter の updateList() です。

これは引数の memoList を元に RecyclerView の中身を更新しています。

1度 adapter の memoList を空にしてから新しいリストを追加しています。

最後の行では更新したことを自身の Adapter に伝えるために notifyDataSetChanged() メソッドを呼んでいます。

// DBに変更があった時に通知がくる
realm.addChangeListener {
    // 変更があった時にリストをアップデートする
    val memoList = it.where(Memo::class.java).findAll().map { it.name }
    // UIスレッドで更新する
    recyclerView.post {
        adapter.updateMemoList(memoList)
    }
}
// 初回表示時にリストを表示
realm.executeTransactionAsync {
    val memoList = it.where(Memo::class.java).findAll().map { it.name }
    // UIスレッドで更新する
    recyclerView.post {
        adapter.updateMemoList(memoList)
    }
}

最後に updateList() を呼んでいる箇所です。2つありますが、中でやっていることは同じことです。

まず1つ目の addChangeListener() は名前の通り、変更があった時にだけ呼ばれます。今回の場合だと文字が入力された状態で addButton を押した時に呼ばれます。

しかしこれだけだと Memo を追加した時にしか表示されません。つまりアプリを起動した直後には何も表示されないことになります。これでは困るので2つ目としては executeTransactionAsync() を使って表示されるようにしています。

それでは中身の説明をすると where() メソッドで、どのclassを対象にするかを設定します。今回は Memo クラスなので Memo::class.java としました。

次に findAll() メソッドを呼んでおりDBに登録されている全ての Memo を取得してきています。あとはそれを引数にして updateList() を呼ぶだけですね。

1つ目はDBに変更があった時で2つ目はアプリ起動時のためです。

goal

それではここでビルドしてみましょう。このような動きになれば完成です!

まとめ

お疲れ様でした!

メモアプリを作成していきました。データベースはほとんどのアプリで利用されています。

アプリを作りながら使い方を覚えていきましょう!