2020年12月25日

Androidで単語帳を作ろう(3)- SQLiteデータベースの使い方

6. SQLiteデータベースの使い方



公式の開発者向けサイトでは、Room というライブラリを使ってデータベースアクセスを行うことが推奨されています。




もちろんこれにしたがって Room を使っても良いのですが、今回はまずはシンプルに自分でSQLクエリーを発行してデータベース処理を実装していくことにしました。



SQLiteOpenHelperを使う


SQLiteデータベースの初期化と接続の管理を行うには、SQLiteOpenHelperを継承したクラスを作成するのが便利です。


--- DatabaseHelper.java
---


テーブル名、カラム名などの固定文字列は、後述するWordsRepositoryクラス内に定数として定義してあるので、二重で定義しないようにそちらを参照しています。


今後データベースにテーブルを追加したりカラムを追加したりする場合は、データベースのバージョン番号を上げて、onUpgrade() メソッド内で必要なSQLを発行する処理を実行します。今は最初のバージョンなのでまだ何も実装していません。



アプリケーション全体を通してデータベース接続を保持する


では、上で作成したDatabaseHelperクラスをどのように使うかを見てみましょう。

    DatabaseHelper dbHelper = new DatabaseHelper(this);
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    try(final Cursor cursor = db.rawQuery("SELECT * FROM words", null)){
        while (cursor.moveToNext()){
            (...)
        }
    }

tryのカッコ内で Cursor をオープンすると、tryを抜けたときに自動でクローズしてくれるので便利ですね。


このようにデータベースアクセスが必要になる度に毎回 DatabaseHelperのインスタンスを生成してから getWritableDatabase()メソッドを呼んでも構わないのですが、この方法だとデータベースのオープン/クローズ処理が毎回行われることになります。


今回のサンプルアプリケーションでは毎回データベース接続をオープン/クローズするのではなく、「カスタムアプリケーションクラス」を使ってアプリケーション全体を通してデータベース接続を保持する方法を使うことにします。


    🎬[Android]Applicationクラスとは



--- MyApplication.java
---


Applicationクラスのインスタンスはアプリケーションのプロセスが動いているかぎり破棄されることはないので、このクラス内のインスタンス変数として変数を宣言しておけばアプリケーションのどこからでも共通に使うことができます。


今回はこれを利用してDatabaseHelperのインスタンスをApplicationクラスで保持することにしました。getDb()というメソッドが初めて呼ばれたときにデータベースをオープンします。

また、アプリケーションが終了するときに onTerminate() が呼ばれるので、このタイミングでデータベース接続をクローズしています。



カスタムアプリケーションクラスが起動時に正しく呼ばれるようにするには、マニフェストファイルでクラス名を指定しておく必要があります。







リポジトリクラスを使ってデータベース処理を一箇所にまとめる


次に、実際にデータベースに対してSQLクエリーを発行してデータを取得したり更新したりする処理を実装します。これらの処理はテーブル単位で実行されることが多いので、その単位で「リポジトリクラス」を作ってまとめるのが良いでしょう。


今回のサンプルアプリケーションでは、WordsRepository というクラスを作って単語データに関するデータベース処理を記述しました。


全件を取得する


    public List<Word> getList() {
        ArrayList<Word> list = new ArrayList<>();

        // tryの括弧内でCursorを生成することで自動的にcloseされる。
        try(final Cursor cursor = mDb.rawQuery("SELECT * FROM " + TABLE_NAME + " ORDER BY " + COL_ID, null)){
            while (cursor.moveToNext()){
                final Word word = buildWordFromCursor(cursor);
                list.add(word);
            }
        }

        return list;
    }


1件だけを取得する


    // idで指定された単語を返す。idが0の場合は新規インスタンスを生成して返す。idが見つからない場合はnullを返す。
    public Word getById(int id) {
        if (id == 0){
            return new Word(0, "", "");
        }

        Word word = null;
        String[] args = { Integer.toString(id) };

        // tryの括弧内でCursorを生成することで自動的にcloseされる。
        try (final Cursor cursor = mDb.rawQuery("SELECT * FROM " + TABLE_NAME + " WHERE " + COL_ID + " = ?", args)) {
            // 主キーで絞っているため結果は1行か0行かのどちらかなのでwhileでループする必要はない。
            if (cursor.moveToFirst()){
                word = buildWordFromCursor(cursor);
            }
        }

        return word;
    }


単語を追加または更新する


    // idが0の場合は新規追加、0以外の場合は更新処理を行う。
    public void save(Word word) throws InvalidKeyException, SQLException {
        if (word._id == 0) {
            String sql = "INSERT INTO " + TABLE_NAME + " ("
                    + COL_ENGLISH + ", "
                    + COL_JAPANESE + ", "
                    + COL_DONE
                    + ") VALUES (?, ?, ?) ";
            String[] args = { word.english, word.japanese, boolToString(word.done) };
            mDb.execSQL(sql, args);
            return;
        }

        Word existing = getById(word._id);
        if (existing == null) {
            throw new InvalidKeyException("");
        }

        String sql = "UPDATE " + TABLE_NAME + " SET "
                + COL_ENGLISH + " = ?, "
                + COL_JAPANESE + " = ?, "
                + COL_DONE + " =? "
                + " WHERE (" + COL_ID + " = ?) ";
        String[] args = { word.english, word.japanese, boolToString(word.done), Integer.toString(word._id) };
        mDb.execSQL(sql, args);
    }



単語を削除する


    // idで指定された単語を削除する。単語が見つからない場合は何もしない。
    public void delete(int id) throws SQLException {
        Word existing = getById(id);
        if (existing == null) return;

        String sql = "DELETE FROM " + TABLE_NAME + " WHERE (" + COL_ID + " = ?) ";
        String [] args = { Integer.toString(id)};
        mDb.execSQL(sql, args);
    }




今回はSQLiteデータベースの使い方について見てみました。
ひとまずこれで単語帳アプリのVersion 0.1が動くようになりました。


ここまでのソースコードは下のURLで公開していますので良ければプロジェクト全体をクローンして動かしてみてください。




Androidで単語帳を作ろう - 目次
Androidで単語帳を作ろう(3)- SQLiteデータベースの使い方






🍻

2020年12月17日

Androidで単語帳を作ろう(2)- ListViewの使い方

今回は単語一覧画面の実装方法を詳しく見てみましょう。


5. ListViewの使い方





このリストでは、1行に「英語」と「覚えたフラグ(チェックマーク)」の2つの項目を表示したいので、カスタムアダプタを実装しました。


    Androidでリストビュー(ListView)をカスタムして表示する - Qiita 



--- WordListActivity.java



--- WordListViewAdapter.java
---





ListViewとカスタムアダプターの実装方法はほぼ決まりきったイディオムのようなものなので、ひとまずこれはこういうものとして覚えておけば良いのではないかと思います。




ちなみに最近のAndroidではListViewよりもRecyclerViewを使う方が推奨されているようです。ListViewと実装方法においてそれほど大きな違いはなさそうですが、こちらも一応確認しておきたいところです。

    🎬[Android]RecyclerViewの仕組み




次回は、
6. SQLiteデータベースの使い方

についてまとめたいと思います。


2020年12月15日

Androidで単語帳を作ろう(1) - 画面遷移、アクティビティのライフサイクルなど

今回から3回に分けてAndroid用の単語帳アプリを作って行こうと思います。

ひとまず Version 0.1として最低限の機能を実装してみます。

Version 0.1の機能

  • 単語一覧(英語のみ表示。覚えたものにはチェックマークを表示。)

  • 単語確認(英語・日本語を表示。「覚えた」「忘れた」ボタンでフラグ更新)

  • 単語新規登録

  • 単語編集・削除




作成する画面は以下の4画面ですが、新規登録と編集・削除は同じアクティビティなので実質的には3画面になります。









ソースコード





実装する上でポイントになる点としては、大まかに分けて以下の6つがあります。

1. ConstraintLayoutによるレイアウト
2. ActionBar(Toolbar)の使い方
3. 画面遷移の実装方法
4. Activityのライフサイクル
5. ListViewの使い方
6. SQLiteデータベースの使い方


以下にそれぞれのポイントについて簡単にメモして、参考になりそうなリンクなどを紹介していきます。


*なお、今回の説明のために参考になる資料を探していたところ、みんなのプログラミング by Telulu LLC 様の説明が大変分かりやすかったので、以下の説明で特にたくさんリンクさせていただきました。(ありがとうございます!)


1. ConstraintLayoutによるレイアウト


従来通りのRelativeLayoutやLinerLayoutを入れ子にして画面を作成する方法でも良かったのですが、どうせなら新しいバージョンで推奨されているやり方を使おうと言うことで、ConstraintLayoutを使って画面レイアウトを作成しました。

この方法だとほとんど入れ子構造を使わずにフラットな構成でウィジェット同士の相対的な位置関係を定義することでレイアウトが出来るので、慣れれば確かにこちらの方が効率が良いかも知れません。特にAndroid Studioのレイアウトエディタを使いこなせるようになればドラッグ&ドロップで画面をデザイン出来るので、かなり強力ですね。


下の動画を観ればおおよその使い方は分かると思います。

    🎬[Android]Constraint Layoutはこれだけ知っておこう① 

    🎬[Android]Constraint Layoutはこれだけ知っておこう② 

    🎬[Android]Constraint Layoutはこれだけ知っておこう③

    🎬[Android]ConstraintLayoutでレイアウトを作成する場合の注意点


公式のガイドも必読ですね。

    Layout Editor を使用して UI を作成する  |  Android デベロッパー





2. ActionBar(Toolbar)の使い方


「戻る」「編集」「削除」などのボタンを画面上部のActionBar内に表示する方法です。


最近(と言ってもAndroid 5以降らしいですが)のAndroidではActionBarではなく「Toolbar」を使うほうが何かとメリットが大きいようです。

    AndroidのToolBar(新しいActionBar)メモ - Qiita 


ただ今回のサンプルでは特に不都合は無かったのでActionBarを使って実装しました。

    アプリバーの設定  |  Android デベロッパー 


また単語の新規登録と編集で同じアクティビティを使っていますが、新規登録の場合はActionBar内の「削除」メニューを非表示にする制御を行っています。

    OptionMenuの内容を動的に変更する - Qiita 

    実行時におけるメニュー項目の変更  |  Android デベロッパー 




3. 画面遷移の実装方法


Androidでの画面遷移を上手く制御するためには、アクティビティの「スタック」の概念をしっかりと理解しておく必要があります。






たとえば、新しい画面に遷移する場合は startActivity() を使いますが、前の画面に戻るときは finish() で現在のアクティビティを「終了」する必要があります。

終了せずにさらに startActivity() で元の画面に遷移してしまうと、スタックの上にさらに新しいアクティビティのインスタンスが積まれることになります。


    🎬[Android]今の画面を閉じて前の画面に戻る方法


また、複数の画面(A → B → C)を開いたあとで、(Bを飛ばして)一気に最初の画面(A)に戻りたいという場合には、ちょっと工夫が必要になります。


今回のサンプルでは、単語一覧(A) → 単語確認(B) → 単語編集(C)と遷移した状態で、単語が削除されたらBではなくAに戻すためにこの方法を使いました。


この場合は finish() で戻るのではなく startActivity() を使いますが、そのさいに特別なフラグをパラメータに追加することで  C → A  という画面遷移が可能になります。

    private void backToList(){
        Intent intent = new Intent(getApplicationContext(), WordListActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        startActivity(intent);
    }


詳しくは下のサイトを参照してください。

最初のActivityに戻る - Qiita 



次の画面にデータを渡す


単語一覧画面から単語確認画面に遷移するケースでは、画面間で「どの単語がタップされたのか」という情報を受け渡す必要が出てきます。これは、インテントの putExtra() メソッドを使って渡したいデータをインテント内に詰め込むことで実現出来ます。

            Intent intent = new Intent(getApplicationContext(), WordViewActivity.class);
            intent.putExtra("_id", word._id);
            startActivity(intent);


渡されたデータを取得する


遷移先の画面では、onCreate または onResume でインテントの getIntExtra(), getStringExtra() などのメソッドを使って詰め込まれたデータを取り出します。


    protected void onResume() {
        super.onResume();

        // 渡されたインテントから単語IDを得る。
        Intent intent = getIntent();
        mWordId = intent.getIntExtra("_id", 0);

        // 指定された単語のデータをデータベースから取得して表示する。
        loadWord(mWordId);
    }




4. Activityのライフサイクル


サンプルの WordViewActivity では、上に書いた「元の画面から渡されたデータを取り出す」という処理を onResume でおこなっていますが、これは単語一覧画面から開かれたときだけでなく単語編集画面から戻ってきた場合にも単語の表示を更新したい(=編集後の最新の値を反映したい)ためです。


onCreate はアクティビティが生成されたタイミングでしか呼ばれませんが、onResume はアクティビティがスタックの先頭に来て画面に表示される度に毎回呼ばれます。


Android用のアプリケーションを開発する上では、このようにアクティビティのライフサイクルを理解することが非常に重要になります。


これについては下の動画が大変分かりやすいのでオススメです。

    🎬[Android]画面(Activity)のライフサイクルとは




今回は、
1. ConstraintLayoutによるレイアウト
2. ActionBar(Toolbar)の使い方
3. 画面遷移の実装方法
4. Activityのライフサイクル

について書きました。

次回は、

についてまとめたいと思います。




Androidで単語帳を作ろう - 目次
Androidで単語帳を作ろう(1)- 画面遷移、アクティビティのライフサイクルなど 







🍻