2021年7月20日

TailwindCSS + Alpine.js でモーダルダイアログを作ろう

最近知ったのですが、 TailwindCSSAlpine.js の組み合わせがなかなか使いやすかったので紹介します。



作った手順は次の通りです。


1. TailwindCSS をCDNから読み込む。


headタグ内に次のlinkタグを追加します。

<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />





2. Alpine.js をCDNから読み込む。


headタグ内に次のstyleタグを追加します。

<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>



3. ページのレイアウトを作る。


bodyタグ直下のdivタグの中に、header, main, footer の3つの要素を作成します。

親のdivのスタイルで display: grid とし、それぞれのエリアの高さが header 6rem, main 1fr, footer 4rem  になるように指定します。また w-screen, h-screen で親のdivが画面一杯に広がるようにしています。

<div
class="m-0 p-0 w-screen h-screen grid"
style="grid-template-rows: 6rem 1fr 4rem;"
>


詳細はソースコードを参照してください。


下の画像のような表示になればOKです。



mainに overflow-y-auto クラスを指定しているので、コンテンツが画面に収まらない場合はフッターやヘッダーを固定にしたままmain部分だけを縦スクロールさせることが出来ます。

<main class="p-6 overflow-y-auto">




画面の中央にある「開く」ボタンのクラス指定は次のようになっています。(一部省略)

<button
class="py-2 px-4 max-h-12 bg-blue-600
hover:bg-blue-400 text-white border rounded-md shadow"
>


TailwindCSS のおかげでそれなりに見栄えの良いボタンになります。





4. モーダルダイアログのHTMLを追加。


親divの一番最後に下のHTMLを追加します。

<!-- モーダルダイアログのラッパー -->
<div>
<!-- 背景を暗くするための半透明部分 -->
<div></div>

<!-- 実際のモーダルダイアログ部分 -->
<div>
<header>
<h1>確認してください</h1>
</header>

<main>
<p>本当にこの操作を実行しますか?</p>
</main>

<footer>
<button>キャンセル</button>
<button>実行!</button>
</footer>
</div>
</div>


5. モーダルダイアログのスタイルを設定。


ラッパー用divに次のクラスを指定して、画面一杯をカバーするようにします。

<div class="absolute top-0 left-0 w-screen h-screen">


次に背景を暗くするためのdivのスタイルを設定します。

<div class="absolute w-full h-full bg-black opacity-80"></div>


ダイアログ本体部分のスタイル指定は下のようになります。

<div
class="relative w-5/6 max-w-xl h-1/2 m-auto grid bg-gray-300 border rounded-md shadow"
style="top: 20vh; grid-template-rows: 4rem 1fr 6rem;"
>


TailwindCSSで定義されていない値についてはstyle属性で独自に指定しています。

最終的にはこのようなダイアログになります。






6.  初期状態でモーダルダイアログを非表示にする。


ここから Alpine.js が活躍します。

まず、ページの一番親(body直下)のdivに x-data 属性を追加します。

<div
x-data="{ open : false }"
class="m-0 p-0 w-screen h-screen grid"
style="grid-template-rows: 6rem 1fr 4rem;"
>

次にモーダルダイアログのラッパーのdivに x-show 属性を追加します。

<!-- モーダルダイアログ -->
<div x-show="open" class="absolute top-0 left-0 w-screen h-screen">


これでモーダルダイアログが初期状態では表示されなくなります。




7. 「開く」ボタンでモーダルダイアログを表示する。


ページ中央の「開く」ボタンに @click="open = true" を追加します。

<button
@click="open = true"
class="py-2 px-4 max-h-12 bg-blue-600 ...
>開く</button>


これで「開く」ボタンをクリックするとモーダルダイアログが表示されるようになりました!


8. 「キャンセル」ボタンでモーダルダイアログを閉じる。


モーダルダイアログ内の「キャンセル」ボタンでは「開く」ボタンと反対に "open = false" とすることでダイアログを非表示にします。

<button
@click="open = false"
class="py-2 px-4 text-white bg-gray-600 ..."
>キャンセル</button>


Alpine.js シンプルで良いですね!


ちなみにx-showでの表示・非表示切替時にトランジションアニメーションを付けるには、x-transition 属性を付加するだけで大丈夫です。


x-transition ではデフォルトで250msの scale + opacityのアニメーションが実行されます。これをopacityのみに変更したい場合は、x-transitionx-transition.opacity とすればOKです。もちろん実行時間の指定も可能です。





9.  外側をクリックされたらモーダルダイアログを閉じる。


@click.outside という表記を使えば、その要素の外側がクリックされた時の処理を書くことが出来ます。

<!-- モーダルダイアログ -->
<div x-show="open" class="absolute top-0 left-0 ...">
<div class="absolute w-full h-full bg-black ..."></div>
<div
@click.outside="open = false"
class="relative w-5/6 max-w-xl h-1/2 m-auto ..."
style="top: 20vh; grid-template-rows: 4rem 1fr 6rem;"
>



10.  「実行!」ボタンが押されたらモーダルダイアログを閉じてカスタムイベントを発行する。


「実行!」ボタンの処理もキャンセルと同じように @click="..."の中に全て書いてしまっても良いのですが、ここでは Alpine.js のカスタムイベント発行の機能を使ってみます。 

<button
@click="open = false; $dispatch('dialog-ok')"
class="py-2 px-4 text-white bg-red-600 ..."
>実行!</button>





11.  カスタムイベントを受け取ったら何らかの処理を実行する。


$dispatch() で発行したカスタムイベントはDOMツリーの上位にある全ての要素で受け取ることが可能です。

ここでは body直下のdivにイベント処理を追加しました。

<div
x-data="{ open : false }"
@dialog-ok="setTimeout(() => alert('Hi'), 100)"
class="m-0 p-0 w-screen h-screen grid"
>


setTimeout()を使っているのは、これが無いとモーダルダイアログが閉じる前にアラートが表示されてしまっていたためです。



まとめ


TailwindCSSもAlpine.jsも、覚えないといけないことが少なくてすんなりと習得出来そうです。

TailwindCSSに慣れるとスタイルの設定にかかる時間が格段に短縮されます。Alpine.jsも必要最小限の使い方さえ覚えれば簡単な動作であればさくさくと実装出来て、とても便利です。


興味を持った方はぜひ試してみてください!








 🍻

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データベースの使い方

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