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)- 画面遷移、アクティビティのライフサイクルなど 







🍻

2020年11月27日

Mac上のChromeで自己署名のサーバー証明書を使ったサイトが開けないとき

ある開発中のWebサイトがあるのですが、最近開発マシンを変えたところ、今までGoogle Chromeでアクセス出来ていたのに急に出来なくなりました。

表示されるエラーメッセージはこんな感じです。

今までは、このメッセージが出ても下の方に「危険を承知で開く」という意味のリンクがあってそこをクリックすれば続行出来ました。


試しに古い方の開発マシンからやってみると、確かにそのようになっていてそちらでは開けます。


マシンが変わっただけで開けなくなったということは、何か新しいマシン上で設定が足りていないのかな、と思って調べてみたところ、下の方法で解決したのでメモしておきます。


Chromeの「Settings」メニューから、「Privacy and Security」→ 「Security」を開きます。



その中の「Manage certificates」メニューをクリックすると、Appleの「Keychain Access」アプリが開きます。(直接このアプリを開いた方が早かった。。。)


Keychain AccessアプリのCertificatesの中に該当のサイトの証明書があればそれをダブルクリックします。



証明書の詳細が表示されるので、「Always Trust」にしてこの画面を閉じればOKです。


この後、ブラウザで該当のURLを開くと、いつも通りにセキュリティの警告が出ますが、一番下の「危険を承知で開く」という意味のリンクをクリックすればサイトが開きます。








🍻 

2020年10月31日

Windows, Linux, Macで英語キーボードの左右ALTキーで日本語入力ON/OFFを切り替える

一つのキーでトグルするのではなく別々のキーを使うことのメリット


下で紹介している alt-ime-ahk の作者さんのブログに書かれているとおりです。

”これの何が便利かというと、現在の IME の状態が何であろうと、日本語を入力したいときは「かな」英語を入力したいときは「英数」を押せばよい所です。いちいち IME の状態を気にしなくてもよいので、とても楽なのです。”


左でOFF、右でONこれに慣れるともうトグル方式には戻れません。


ちなみに、「英語配列」のキーボードでの話です。



Windowsでの設定 


alt-ime-ahk」を使います。

Win+Rキーから「shell:startup」と入力して開くフォルダにExeファイルへのショートカットを作成しておけば、ログイン時に自動的に起動してくれます。

この設定をしてから、英語キーボードで毎回「ALT+`」で切り替えていたのが「どれだけ時間の無駄だったのか」を痛感しました。



Linuxでの設定


Mozcでは上手く行かないので、「Fcitx」で設定します。詳細はこちら



Macでの設定


Karabiner-Elements」を使います。

普通に設定すると左右のCommandキーで切り替え可能になります。

私はさらにCommandキーに加えて左右のOptionキーでも切り替わるように設定を追加して使っています。 ました。

こうするとWindowsやLinuxと共通になるので便利ですでした。

Karabiner-Elementsの設定ファイルを開いて、left_command, right_commandをleft_option, right_optionに変更すればOptionキーで切り替え可能に。


その後、 MacでOptionキーのアサインを変えてしまうと「Optionを押しながらメニューをクリック」した時の挙動が変わってしまうことに気付いたので、やっぱりMacではこの設定はやめて左右Commandキーで切り替えることにしました。Windos, LinuxではALTキー、MacではCommandキーという点が異なりますが、「左でOFF、右でON」というのは共通しているので、ひとまずこれで良しとしています。







🍺

2020年10月30日

データベースとTypeScriptの型定義を同期したいときに便利そうなライブラリ

下の動画を見て、やはりデータベース ⇔ バックエンド ⇔ フロントエンドの間でスキーマ定義が半自動的に同期される仕組みがあるというのは便利だな〜と思ったので、そのために使えそうなライブラリを調べました。


===
 
===


目標は、データベースを変更 → TypeScriptの型定義が半自動で更新される → バックエンドとフロントエンドの両方のプロジェクトで型定義が共有される、という状態にすることです。

バックエンドのDBアクセス、REST APIから、フロントエンドのReactコンポーネントのPropsまで、アプリケーション全体で共通の型定義を使いたい






動画で使われているのは、schemats というライブラリでした。これは、データベースからテーブルのスキーマを読み取って、TypeScriptの型定義を生成してくれるというもののようです。

サンプルコードやこちらのブログ記事を見ても分かるとおり、このライブラリは特にORMのようなものを介さずに自分で直接SQLクエリーを書いてデータベースにアクセスしたいという場合に向いているようです。



2. Knex.js / sql-ts 

Knex.js という軽量なDBアクセスライブラリを使う場合は、sql-ts もしくは上に挙げた schemats を利用して生成した型定義を使うことができそうです。




専用のスキーマ定義ファイルからTypeScriptの型定義を生成します。

このPrismaスキーマをデータベースから自動生成するワークフローと、まず自分でPrismaスキーマを書いて、それをマイグレーションでデータベースに反映するワークフローの2種類をサポートしているそうです。

DBからPrismaスキーマを生成

 
PrismaスキーマからDBに反映


ただ、こちらのNext.jsでのサンプルを見た感じでは、データベース⇔バックエンドの間では型定義の同期が取れていますがフロントエンド側まではそれが共有されていません。例えば個々のReactコンポーネントのPropsについてはそれぞれに型定義をおこなっているようです。フロントエンド側にまで型定義を共有出来る方法があるのかどうかについてはよく分かりませんでした。




TypeScriptを使う場合のORMライブラリとしてかなり人気があると思われるTypeORMですが、別途 typeorm-model-generator というCLIツールを組み合わせるとデータベースからTypeORM用のモデルクラスを生成することが出来るようです。




Node.js用のORMとしておそらく最も使われているSequelizeですが、これ向けにも sequelize-typescript-generator というCLIツールが見つかりました。Sequelize を採用することが決まっている場合は、こちらの組み合わせも良いかも知れません。 ただ、Sequlize向けに生成されたモデルクラスをそのままフロントエンド側で参照してReactコンポーネントなどで使えるかというと、それは厳しいのではないかと思います。



考察


「DBから生成した型定義をバックエンドからフロントエンドまで共通で使いたい」と書きましたが、結局よく考えてみると、

①バックエンド側でDBアクセスをするための型(テーブル単位)
②APIのインターフェースとしての型(ユースケース単位)
③フロントエンドのコンポーネント間でやり取りするための型(UI部品単位)

はそれぞれに用途が異なるので完全に同じものを使い回すというようなことは現実的ではなさそうです。

ただ、①②③の中でもカラム単位で見れば共通の定義を抽出して参照することは可能ではないかと思います。

例えば冒頭の動画で紹介されているように「通知メールの許可設定」を表すカラムについて、当初 Boolean だったものが後から Number に変更されたというようなケースでは、カラム単位で共通の型定義を参照していれば、一箇所が(コマンド一つで半自動的に)変更されれば(バックエンド、フロントエンド含めて)それを参照している全ての箇所がコンパイル時にエラーとなって容易に修正出来るようになるというイメージです。

このような用途で考えると、今回調べたライブラリの中では schemats は一度試してみる価値があるかも知れないと思いました。








🍻

2020年10月28日

MacのKarabiner-Elementsで「PC−Style Home/End」を有効にしたときにChrome Remote Desktopを除外する方法

数年前からRealforceのキーボードをMacBook Proにつないで使っています。とても打ちやすくて快適なのですが、長い間解決できずに困っている問題がありました。


それは、HomeキーとEndキーで行頭・行末にカーソルを移動するために Karabiner-Elements の「PC-Sylte Home/End」という設定を有効にすると、Google Chrome Remote Desktop を使ってWindowsマシンを操作しているときに、Homeが「Windows+左矢印キー」、Endが「Windows+右矢印キー」として伝わってしまい、アプリケーションのウィンドウがいきなり画面の左半分にリサイズされたり右半分にリサイズされたりしてしまう、というものです。

Karabiner-Elementsの設定で「PC-Style Home/End」を追加




Microsoft Remote Desktop などではこのような問題は起こりません。「Chrome Remote Desktop」 を使ったときにだけ起きる問題です。

これは Karabiner-Elements の「PC-Sylte Home/End」設定の「除外対象(frontmost_application_unless)」に、Chrome Remote Desktop が含められていないのが原因のようです。


どうやって Chrome Remote Desktop を除外対象に含めれば良いのか分からなかったのですが、今日は長い間避けて通ってきたこの問題にようやく時間をとって真剣に向き合うことにしました。


調査 & 試行錯誤すること1時間、ついに解決策を発見したのでメモしておきます。




設定のJSONファイルをエディタで開く


Chrome Remote Desktopを除外する記述を追加


"type": "frontmost_application_unless",  
の下の
 "bundle_identifiers": [ ... ]  
の配列に
    "^com\\.google\\.Chrome\\.app\\.*",
の行を追加すればOKです! 

(HomeキーとEndキーでそれぞれに設定があるので2箇所とも追加します。)


検索して見つけた一部のサイトでは、

  "^com\\.google\\.Chrome\\.app",

と書かれていたのですが、これだと上手く行かず、末尾に「\\.*」を追加することで正しく除外されるようになりました。


ちなみに末尾が「\\.*」だとChrome Remote Desktop だけではなく「全てのChromeアプリ」が除外対象になると思います。


もし不都合がある場合はそれぞれのChromeアプリ固有のIDを調べて個々に指定する必要があります。



アプリ固有のBundle Identifierの値は、Karabiner-Elementsに付属の「EventViewer」を起動すれば簡単に調べることが出来ます。


EventViewer で Bundle Identifierを調べる



最近は Microsoft のRemote Desktop よりも Chrome Remote Desktop を使う機会の方が多くなってきているので、これでさらに快適にリモート接続出来そうです!





🍺 

2020年10月1日

ReactによるWebアプリ開発を体系的に学べるコース「Epic React」を購入!

購入したコース




内容
  • React Fundamentals
  • React Hooks
  • Advanced React Hooks
  • Advanced React Patterns
  • React Performance
  • Testing React Apps
  • React Suspense
  • Build an Epic React App
  • Epic React Expert Interviews



購入の動機


以前からEgghead.ioの動画ブログでKent C. DoddsさんのReactやJavascriptに関する解説を読んだり観たりしていて、とても参考になっていたため。

今回のコースはそのKentさんが何年にもわたって数々の有料セミナーなどで教えてきた経験を元に作成された、渾身の作ということなので、まず期待はずれになることはないだろうと確信が持てました。


インストラクター



元PayPalのフロントエンド開発者で、Reactのテスト用ライブラリとして人気の「React Testing Library」の作者でもあります。

ブログを読むと、初めてソフトウェア開発者として職を得たきっかけ、Facebookからのオファーを断ってPayPalに入った経緯、PayPalでの仕事内容、AngularからReactに軸足を移した経緯や、人に教えることへの情熱などがよくわかります。



Egghead.ioでおすすめのコース


Kentさんのコースに興味を持たれた方は、まずはEgghead.ioで下の動画レッスンなどを視聴してみてはいかがでしょうか。英語のリスニング練習にも最適です! 😀

React Tutorial for Beginners

Collection - Testing JavaScript with Jest





 

2020年7月30日

Parcelでasync/awaitを使うと「regeneratorRuntime is not defined」エラーが出る場合の対処法

Parcel を使ってJavaScriptアプリケーションを書いているときに、async/awaitを使おうとすると下のエラーが出ました。

Uncaught ReferenceError: regeneratorRuntime is not defined

調べたところ、次のサイトに解決策が載っていました。





対処法1


ブラウザを比較的新しいものだけに限定して構わないのであれば、次の方法が簡単です。

package.jsonに以下を追記します。

  "browserslist": [
    "since 2017-06"
  ]
これでasync/awaitを使ってもエラーが出なくなります。



対処法2


もう一つの対処法としては、「regenerator-runtime」パッケージをインストールするというものがあります。

> npm install regenerator-runtime

でインストールしておき、index.jsなどから

import 'regenerator-runtime/runtime';

として読み込みます。

バンドルサイズが25KBほど増えてしまうようですが、こちらの方法でもエラーが出なくなります。古いブラウザもサポートする必要がある場合はこちらを使うことになると思います。






 .


2020年6月16日

データベース不要! ReactとGoogle Drive APIで任意のデータを保存する

前回はReactアプリにGoogle認証を組み込む方法を試しました。

今回はそのコードを使いながら、さらにGoogle Drive APIを使って任意のデータをログインしているユーザーのGoogle Driveに保存することに挑戦してみました。


完全にブラウザのみで動作するReactアプリケーションなので、バックエンドサーバは不要です。Netlifyなど静的サイトのホスティングサービスを使ってデプロイすることが可能です。


Screen shot on a PC

Screen shot on a phone


  • Google Drive内の所定のフォルダからファイル一覧を表示。
  • ファイル名がクリックされるとその内容(テキストデータ)を表示。
  • テキストを入力・編集して保存ボタンを押し、ファイル名を入力するとGoogle Driveにテキストデータを保存。

ということを行っています。



2つのContextプロバイダー


認証状態とアプリケーション固有の状態を分けて2つのContextプロバイダーで別々に共有するようにします。

import React from 'react';
import { FileList } from './FileList';
import { FileContent } from './FileContent';
import { AuthProvider } from './auth-state';
import { AppStateProvider } from './app-state';
import { Header } from './Header';
import './App.css';

function App() {
return (
<AuthProvider>
<AppStateProvider>
<div className="App">
<Header />
<FileList />
<FileContent />
</div>
</AppStateProvider>
</AuthProvider>
);
}
export default App;



Google Drive APIの有効化とAPI Keyの作成


下のドキュメントにしたがってGoogle Drive APIを有効化し、API Keyを作成しておいてください。

https://developers.google.com/drive/api/v3/quickstart/js



Google DriveへのCRUD処理


次にGoogle Driveにアクセスする処理を「google-api.js」に記述します。

google-api.js
---
---

前回作成した認証関係の関数に加えて、

 getFiles ファイル一覧取得  
 getFileContent     ファイル内容取得
 uploadFile ファイル保存
 deleteFile ファイル削除

などの関数をエクスポートしています。



アップロード処理がなかなか動かず苦労しましたが、最終的には下のサイトの情報にしたがってMultipart形式でPOST(更新のときはPATCH)リクエストを送ることで解決しました。


またファイルの内容を取得する方法も最初は分からず試行錯誤しました。こちらは下のURLにGETリクエストを送ることでダウンロード出来ました。

https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&source=downloadUrl

もちろんリクエストヘッダーにはGoogle認証で得られたアクセストークンを付加する必要があります。


ファイルの保存先フォルダは現状では APP_FOLDER という定数で定義していますが、これはユーザーが任意のフォルダ名を指定出来るようにした方が便利ですね。


以上、今回のサンプルを作成してみて、バックエンドサーバ無しでもブラウザ上のJavaScriptだけでかなり柔軟にGoogle Driveへのデータ保存が出来るということが分かりました。


単純なテキストではなく例えばJSONを保存すれば、ある意味簡易的なデータベースとしても使えるかも知れませんね。何よりアプリの開発者側で保存先のデータベースやストレージを用意する必要が無いのが魅力的です。



今回作成したアプリケーションのソースコードはこちらにあります。






 

2020年6月14日

ReactのSPAアプリケーションでContextとHooksを使ってGoogle認証を実装する

ReactのSPAアプリケーションにおいて、ContextとHooksを使うことでGoogle認証の処理をラップして利用するというサンプルを作成しました。


Google Cloud Consoleでアプリケーションを登録する




OAuth consent screenを設定する  







OAuth client IDを作成する








Create React AppでReactアプリケーションを作成する


npx create-react-app project-name
cd project-name


public/index.html を編集する


WebアプリケーションでGoogle認証を利用するための最もシンプルなサンプルコードがこちらにあります。

Google Sign-In for Websites  |  Google Developers 

<html lang="en">
  <head>
    <meta name="google-signin-scope" content="profile email">
    <meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">
    <script src="https://apis.google.com/js/platform.js" async defer></script>
  </head>
  <body>
    <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div>
    <script>
      function onSignIn(googleUser) {
        // Useful data for your client-side scripts:
        var profile = googleUser.getBasicProfile();
        console.log("ID: " + profile.getId()); // Don't send this directly to your server!
        console.log('Full Name: ' + profile.getName());
        console.log('Given Name: ' + profile.getGivenName());
        console.log('Family Name: ' + profile.getFamilyName());
        console.log("Image URL: " + profile.getImageUrl());
        console.log("Email: " + profile.getEmail());
        // The ID token you need to pass to your backend:
        var id_token = googleUser.getAuthResponse().id_token;
        console.log("ID Token: " + id_token);
      }
    </script>
  </body>
</html>

これを参考に public/index.htmlを編集します。

上のサンプルコードの中でReactアプリで必要になるのはGoogleのスクリプトを読み込んでいる行だけなので、その部分をコピーしてheadタグ内に挿入します。

    <script src="https://apis.google.com/js/platform.js"></script>

Googleのサンプルコードでは「async defer」という属性が付いていますが、Create React Appの場合これがあるとスクリプトが実行されるタイミングの関係で上手く動かないので削除しています。



.env.localファイルを作成する


次にプロジェクトのルートフォルダに「.env.local」 ファイルを作成して値を設定しておきましょう。

REACT_APP_CLIENTID=41702000-xxxxxxxxxxxxx.apps.googleusercontent.com

この環境変数の名前は、Create React Appの規約上「REACT_APP_」から始まる必要があるので注意してください。

ClientIdを環境変数に持たせておくことによって、本番用、ステージング用など、ビルド環境に応じて柔軟に変更することが出来るようになります。


src/google-auth.jsファイルを作成する


Google APIを実際に呼び出す際に使う関数をこのファイルにまとめておきます。

init(), signIn(), signOut()の3つの関数をエクスポートしています。
------


src/auth-state.jsファイルを作成する


上のgoogle-auth.jsに定義した関数をReactアプリ内で使うためのContextプロバイダーとフックを提供するファイルになります。

内部的にはuseReducerを使って認証状態を管理していますが、それはファイル内に隠蔽して外部へはAuthProviderコンポーネントとuseAuthStateフックをエクスポートしています。

useAuthStateフックからの戻り値は、認証状態を表す state オブジェクトとサインイン、サインアウト処理を呼び出すための関数を提供する actions オブジェクトの2要素配列となっています。

---
---


ここまで準備が出来れば、あとは

1. AuthProviderコンポーネントでアプリをラップする。
2. 任意のコンポーネント内でuseAuthStateフックを使って認証機能にアクセスする。

という形になります。


App.js
function App() {
  return (
    <AuthProvider>
      <div className="App">
        <header className="App-header">
          <h1>Google Auth React Examle</h1>
          <SignInOutButton />
        </header>
        <div className="App-main">
          <UserInfo />
        </div>
      </div>
    </AuthProvider>
  );
}


SignInOutButton.js
 
import React from 'react'; import { useAuthState } from './auth-state'; 
export function SignInOutButton() {
  const [state, actions] = useAuthState();
  const { isSignedIn } = state;
  const { signIn, signOut } = actions;
  if (isSignedIn === undefined) {
    return null;
  }
  return (
    <div>
      {isSignedIn ? (
        <button onClick={signOut}>Sign Out</button>
      ) : (
        <button onClick={signIn}>Sign In</button>
      )}
    </div>
  );
}




今回作成したアプリケーションのソースコードはこちらにあります。





以上、ReactのSPAアプリケーションにおいて、ContextとHooksを使ってGoogle認証の処理をラップするサンプルでした。



次回は、認証した後さらにGoogle Drive APIを使ってアプリケーションのデータを(バックエンドサーバー無しで)ブラウザから直接Google Driveに保存するということに挑戦したいと思います。





 

2020年5月23日

Macの音声読み上げ機能を言語別にキー一発で呼び出す方法

読み上げ機能は目に優しい


最近、ブラウザでニュースを読む時にMacの音声読み上げ機能を使うと、「目を閉じていてもニュースが頭に入ってくる」ことに気付きました。


これは慣れるとかなり便利。なんと言っても目の疲れが大幅に軽減できることが最大の利点です。







もちろんMacの標準状態でも、ブラウザで文章を選択して「Edit」メニュー → 「Start Speaking」を選べば読み上げ機能を使うことは可能です。

アクセシビリティの設定で任意のショートカットキーを設定することも出来ます。



もし一つの言語しか読み上げさせる必要がないのであれば、これで大丈夫です。


ただ、複数の言語を使い分けたい場合は、これだと読み上げに使用する言語(音声)をその都度選ぶことが出来ません。

日本語の音声を設定している状態で英語の文章を読み上げさせても、カナカナ読みの変な発音でしか読まれません。逆に英語の音声が選択されている状態で日本語を読み上げさせようとしても、上手く発音してくれません。

つまり、選択されている文章の言語によって読み上げに使う音声を切り替える必要があるわけです。


これが出来るように今回設定した方法を、以下にメモしておきます。


1. 言語別にAutomatorでサービスを作成


Automatorを起動して、新しい Quick Action を作成します。




Actionsから System → Speak Text を選んで、ドラッグアンドドロップで追加します。




読み上げに使う言語に対応した音声を選んで、後から分かる名前を付けて保存します。


これを、使いたい言語の数だけ繰り返します。



2. キーボードショートカットを設定


システム設定から「Keyboard」を選ぶと、「Services」のカテゴリ内に先ほどAutomatorで作ったQuick Actionが表示されます。




それぞれの言語に対応したQuick ActionのチェックをONにして、好きなショートカットキーを設定すれば完了です。

私の場合は、

Ctrl + Cmd + w  → 日本語読み上げ
Ctrl + Cmd + e  → 英語読み上げ

としています。

なるべく片手ですぐ押さえられる組み合わせが良いと思います。


ブラウザを開いて何らかの文章を選択状態にすると、Servicesメニューに上で作成したQuick Actionが追加されているのが分かります。






この設定で、Google Chrome、Safari, Firefox のいずれのブラウザでも文章を選択してキー一発で言語別に読み上げ機能を使うことが出来るようになりました!







 

2020年5月7日

Firebase Hostingにデプロイした静的サイトにFirebase Analyticsを追加する(Next.js)

まずは、Firebaseの管理画面から、該当のプロジェクトでAnalyticsを有効化します。


Google アナリティクスを使ってみる






ここではNext.jsからエクスポートした静的サイトを例にします。

pages/index.js など、全てのページで共通に含まれる部分で、下のようにHeadコンポーネントを使ってscriptタグを埋め込みます。

要は下の赤字部分の3行のscriptタグがHTMLのhead部分に含まれるようにすれば良いということですね。

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
        <script src="/__/firebase/7.14.2/firebase-app.js"></script>
        <script src="/__/firebase/7.14.2/firebase-analytics.js"></script>
        <script src="/__/firebase/init.js"></script>
      </Head>
      <section className={utilStyles.headingMd}>
        <p>[Your Self Introduction]</p>
      </section>
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>Blog</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              <Link href="/posts/[id]" as={`/posts/${id}`}>
                <a>{title}</a>
              </Link>
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}


次に、下のドキュメントの通りにイベントを送信する必要があります。

イベントをロギングする

このために、「send-ga-event.js」 と 「use-send-screenview.js」という2つのファイルを作成しました。


send-ga-event.js

export function sendGAEvent(eventName, params) {
  const firebase = window && window.firebase;
  if (!firebase) return;
  try {
    firebase.analytics().logEvent(eventName, params);
  } catch (error) {
    console.error(error);
  }
}


use-send-screenview.js

import { useEffect } from 'react';
import { sendGAEvent } from './send-ga-event';
export function useSendScreenView(screenName) {
  return useEffect(() => {
    const params = { screen_name: screenName };
    sendGAEvent('screen_view', params);
  }, []);
}

上で定義したuseSendScreenView()という名前のフックを pages/index.jsから呼び出すことでAnalyticsにイベントを送信します。


pages/index.js

import { useSendScreenView } from '../utils/use-send-screenview';
export default function Home({ allPostsData }) {
  useSendScreenView('home');
  return (
    <Layout home>
      <Head>
        <title>{siteTitle}</title>
        {process.env.NODE_ENV === 'production' && (
          <>
            <script src="/__/firebase/7.14.2/firebase-app.js"></script>
            <script src="/__/firebase/7.14.2/firebase-analytics.js"></script>
            <script src="/__/firebase/init.js"></script>
          </>
        )}
      </Head>
      <section className={utilStyles.headingMd}>
        <p>[Your Self Introduction]</p>

(以下省略...)


これらの変更を行ったあと、

npm run build && npm run export
firebase deploy 

でデプロイしてサイトを更新します。

更新後のページを開くと、数秒でAnalyticsの画面にイベントが表示されるのが確認出来ます。








  

Next.jsで静的サイトを出力してFirebase Hostingでホスティングする

まずは公式のドキュメントにしたがってNext.jsのアプリケーションを作成しましょう。

Create a Next.js App | Learn Next.js 


package.jsonファイルのscriptsセクションに下記を追加しておきます。

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "export": "next export"
  },


npm run build && npm run export を実行して outフォルダに結果が出力されるのを確認します。



次にFirebase CLIを初期化します。


firebase login

firebase init



カーソルキーで「Hosting」を選択してスペースキーを押し、Enterで確定です。

Firebaseの管理画面でまだ「プロジェクト」を作成していない場合は、プロジェクトIDと名称を入力して新規作成します。既に使われているIDを入れてしまうとエラーになるので、ユニークなIDを入力しましょう。

アップロード対象のフォルダを聞かれるので、デフォルトの「public」ではなく「out」を指定します。


firebase deploy を実行して、「Deploy complete!」と表示されれば完了です!


慣れれば3分もかからずに出来てしまいますね。


バックエンド処理にCloud Functionsを使ったりするような場合も、下のドキュメントの通り設定すれば決して難しくはありません。

本当に便利な世の中になったものです。^^)


あ、特にFirebase Hostingでなくても良いのであれば、もちろんNext.jsの開発元であるVercel(旧Zeit)社のサービスを使うのが一番簡単でオススメです。



こちらであれば、静的サイトでなくSSRありのサイトであっても、またバックエンドのAPIが必要な場合でも、何も考えずにデプロイしてしまえばあとはサービス側で上手くやってくれるみたいなので、こちらも素晴らしいと思います。







 

2020年5月4日

ReactでSVGファイルをコンポーネントとしてレンダリングする

egghead.ioのレッスンを観ていて良さそうなものがあったので、メモしておきます。


Add SVGs as React Components with Create React App 2.0
https://egghead.io/lessons/react-add-svgs-as-react-components-with-create-react-app-2-0

Create React App のドキュメントにも記述があります。
https://create-react-app.dev/docs/adding-images-fonts-and-files


Create React Appで作ったReactアプリケーションでは画像ファイルを下のようにインポートしてコンポーネント内で参照することが出来ます。

import logo from './logo.png';
function Header() {
  return <img alt="Logo" src="{logo}" />;
}

もちろん画像の形式がSVGであっても同様です。

import logo from './logo.svg';
function Header() {
  return <img alt="Logo" src="{logo}" />;
}

ただ、SVG形式の場合はインポートの仕方を下のように変えるとReactコンポーネントとしても使えるようになるそうです。

import { ReactComponent as Logo } from './logo.svg';
function App() {
  return (
    <div className="App">
<header className="App-header">
<Logo className="App-logo" />
</header>
</div>);
}


このようにすると、HTMLとしてレンダリングされる際のタグが imgタグではなく、svgタグになります。

svgタグになってうれしい点は、SVG内の個々の要素(図形)をCSSでコントロール出来るようになることです。


例えば、線の色や太さを変えたり、アニメーションを付け加えたりすることが可能になります。


下の例では、Reactロゴのpathエレメントの部分のみ点線にして、さらに点線のオフセットをアニメーションで動かしています。

.App-logo path {
  stroke: palegoldenrod;
  stroke-width: 10px;
  fill: none;
  stroke-dasharray: 35px 15px;
  animation: orbit 1s infinite linear;
} @keyframes orbit {
  to {
    stroke-dashoffset: 50px;
  }
}

実行するとこんな感じになります。









2020年4月20日

AWS AmplifyとDynamoDBでサーバーレスなREST APIを構築する

前回はユーザー認証機能を付けましたが、まだデータの永続化が出来ていないのでTodoアプリとしては未完成です。



  1) Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

  2) AWS AmplifyでReactアプリにユーザー認証機能を追加する

  3) AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する




今回はサーバーレスなREST APIを追加してデータをデータベースに保存出来るようにします。

amplify status で現状を確認すると、下のようになっています。



下のコマンドでAPIカテゴリを追加します。

amplify add api 

まず GraphQL か REST かを選ぶように言われます。

Web上で見つかる情報にはGraphQLを使ったものが圧倒的に多いように思いますが、ここではあえてRESTを選びます。



DynamoDB上に作成するテーブル名まで入力すると、次に作成するカラム情報の入力になります。

ドキュメントをざっと読んだところ、DynamoDBでは基本的に1アプリケーションで使うテーブルは出来るだけ少ない方が望ましいらしく、「1テーブルで済ませられればそれが最も良い」との事です。



長年リレーショナルデータベースに馴染んだ身としてはなかなか目から鱗な考え方です。

どうやらDynamoDBでのスキーマ設計の勘所は、「パーティションキー」と「ソートキー」および「グローバル・セカンダリインデックス」「ローカル・セカンダリインデックス」を上手く使うことにあるようです。

DynamoDBのテーブル設計をするとき、自分に問いかけていること – 或る阿呆の記 ( https://hack-le.com/dynamodb-query/ )

この辺り、非常に奥が深そうで面白いのですがとりあえず今はシンプルなTodoリストアプリを作りたいだけなので、極力簡単な方法で行きたいと思います。



パーティションキーを「pk」、ソートキーを「sk」とし、3つ目のカラムはmap型で「data」としておきました。map型にはJSONを格納できるので、こうしておけば後からの仕様変更にもある程度柔軟に対応することが可能になるかと思います。

パーティションキーとソートキーを何にするかというのは、アプリケーションの要件によって大きく変わる部分で、開発効率にも大きく関わってきます。


今回は、pkカラムには認証されたユーザーのidに "user:" というプリフィックスを付加して格納し、skカラムにはTodoアイテムのid(ランダムに生成されたもの)の先頭に "todo:"というプリフィックスを付加して格納することにしました。

こうすることでユーザーのidが分かればそのユーザーに属するタスクの一覧を簡単に取得することが出来るようになります。

実際のクエリーとしては、
- pkが "user:" + ユーザーid に一致する
- skが "todo:" で始まる
という条件で検索することになります。

DynamoDBではパーティションキーは完全一致でしか検索出来ませんが、ソートキーは部分一致や範囲検索が可能なので、このようなクエリーが可能になります。

また、ユーザーに属するデータでTodo項目以外のデータを保存したい場合には、skに付けるプリフィックスを "todo:" 以外のものにすれば対応出来ます。

例えば、ユーザーごとのアプリ設定を保存したい場合は、pkは同じでskを "pref:" として保存すれば、簡単にそのレコードを一意に指定して読み出す事が出来ます。

1アプリケーションで一つのテーブルしか使わない、というのはこのように2つのキー(およびセカンダリインデックス)を上手く使って複数の種類のデータを保存する、ということになるのかなと思います。


カラム設定の入力が終わると、テーブルへのアクセス権の設定をするか聞かれるので、認証していないユーザーはAPIへのアクセスが出来ない様に設定しておきます。




次に amplify push を実行して、追加した設定をAWS側に反映します。


完了するとAWS側では下記のリソースが作成されています。

- API Gateway
- Lambda
- DynamoDB


これらのうち、Lambda関数は自動生成されたコードだと上手く動かず、何をしているのかを理解した上でアプリケーションの要件に合わせてそこそこ手を加える必要がありました。


/amplify/backend/function/(Function名)/src/app.js (一部のみ抜粋)
-----

-----


アプリケーション側で扱うModelとしては

    {      
        id: 'todo:12312-312132',      
        text: 'ミルクを買う',      
        done: false 
    }

のような形になっているのですが、DynamoDBに保存されるのは、

    {
        pk: 'user:aaaaaaaaaaa',
        sk: 'todo:12312-312132',
        data: {
            text: 'ミルクを買う',
            done: false
        }
    }

という形になっています。

このため app.js の内部でAPI Gatewayから受け取ったオブジェクトをDynamoDBのスキーマに合った形式に適宜変換してから保存しています。



さて、次はReactアプリの方を変更して行きます。


この辺りのドキュメントを見ながらAPIへのアクセス処理を実装します。

Amplify Docs –– Fetching Data 



基本的には、

import { API } from 'aws-amplify';

でAPIクラスをインポートした後、

await API.post(apiName, path, options);  // CREATE
await API.get(apiName, path, options);  // READ
await API.put(apiName, path, options);  // UPDATE
await API.del(apiName, path, options);  // DELETE

でLamdaで作成してあるAPIにアクセスすることが出来るので、各アクションからこれらを適宜呼び出しています。


React側の実装としては、Hooks(useEffect, useReducer)を使って作っています。


/services/todo-service.js
-----

-----



ここまででなんとかシンプルなTodoリストアプリが完成しました。



  • Amplify Consoleによるスタティックなサイトのホスティングは簡単・超便利!

  • Amplify CLIでのユーザー認証機能の追加はパラメータの設定が最初ちょっと大変だけど、何回か試行錯誤して分かってしまえばこちらも簡単で素晴らしい!

  • Amplify CLIでのREST APIの作成は、DynamoDBの特性を理解して設定する事が超重要!(*



* もちろんDynamoDB以外の任意のデータベースを使うことも可能。


現在のソースコードは下記から確認可能です!

https://github.com/mikehibm/amplify-test01




  1) Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

  2) AWS AmplifyでReactアプリにユーザー認証機能を追加する

  3) AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する