- 投稿日:2020-08-20T22:38:00+09:00
kotlinとjavaで前の画面に戻りたい!
はじめに…
この記事は5個前の記事で書いた、
7日間毎日記事投稿の6日目
になってます
使うコードは下に貼りますが、このアプリの詳しい機能はその5個前の記事をご覧ください!
↓ここからが今回の記事の本題です↓
それぞれの言語で前の画面に戻るには…
※これはツールバーの戻るボタンから一つ前の画面に戻るための方法です
ツールバーの実装方法
- javaの場合
Toolbar 変数名 = findViewById(R.id.ツールバーのid); setSupportActionBar(変数);例)
WhoActiity.javaToolbar whoTb = findViewById(R.id.who_tb); // 30行目 setSupportActionBar(whoTb);
- kotlinの場合
setSupportActionBar(ツールバーのid)例)
WhoActivity.ktsetSupportActionBar(who_tb) // 21行目javaはToolBar型の変数を作る必要があるが、kotlinは作る必要がない
ツールバーに戻るボタンをつけて戻る方法
- javaの場合
(onCreate関数内)
getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);(onCreate関数外)
@Override public boolean onSupportNavigateUp() { finish(); return super.onSupportNavigateUp(); }例)
(onCreate関数内)WhoActivity.javagetSupportActionBar().setDisplayShowHomeEnabled(true); // 32行目 getSupportActionBar().setDisplayHomeAsUpEnabled(true);(onCreate関数外)
WhoActivity.java@Override // 69行目 public boolean onSupportNavigateUp() { finish(); return super.onSupportNavigateUp(); }
- kotlinの場合
supportActionBar?.let { // 22行目 it.setDisplayHomeAsUpEnabled(true) it.setHomeButtonEnabled(true) } ?: IllegalAccessException("Toolbar cannot be null")AndroidManifest.xml<activity android:name=".WhoActivity" android:parentActivityName=".MainActivity" /> // ←ここを追加例)
WhoActivity.ktsupportActionBar?.let { // 22行目 it.setDisplayHomeAsUpEnabled(true) it.setHomeButtonEnabled(true) } ?: IllegalAccessException("Toolbar cannot be null")AndroidManifest.xml<activity // 20行目 android:name=".WhoActivity" android:parentActivityName=".MainActivity" />javaはコードと新しく関数を定義する必要があり、kotlinはコードとAndroidManifest.xmlを編集する必要がある
最後に…
今回はjavaとkotlinでツールバーを使って前の画面に戻りました。
どちらの言語でも画面遷移よりも複雑なコードが必要だと感じました。明日も記事を投稿するので、引き続き温かく見守ってください。
- 投稿日:2020-08-20T19:33:26+09:00
Github Actions と Firebase App Distribution と Slack で apk を自動でテスターに配布
GitHub Actions を使って Android の CI 環境を整えてみようと思います。
今回は PR が master にマージされたら、ビルドして Firebase App Distribution に apk をデプロイ (+ Slack でデプロイ完了を通知) するまでを目指します。Firebase の準備
まずは Firebase のアカウントを準備しましょう。
Firebase のプロジェクトを作成
Firebase にプロジェクトを作成し、Android アプリを追加します。
追加できたら、サイドメニューから 品質 -> App Distibution を選びます。
App Distribution を開始してテスターを追加してください。アプリを配布するだけなら google-services.json は必要ありません。
FCM とか使う場合は組み込んでおきましょう。
ただし、google-services.json をパブリックリポジトリで使う場合は少し工夫がいるので後ほど説明します。これで Firebase App Distribution で apk を配布する準備ができました。
作成したプロジェクトから アプリ ID を取得
GitHub Actions から App Distribution で配布するために、該当のアプリ ID を控えておきます。
Firebase のトークンを取得
以下のリファレンスを参考に CI サーバ用のアクセストークンを取得します。
まず、Firebase CLI をインストールします。
Mac の方は brew でインストールできます。$ brew install firebase-cli次に、ci サーバにログインするコマンドを入力します。
$ firebase login:ciするとブラウザに連携されるので、Firebase Console にログインするアカウントを選択します。
以下の様な表示がされていればログイン成功です。Woohoo!?
コンソールに戻ってアクセストークンを控えておきましょう。
Firebase 側の設定は以上になります。
Actions で自動ビルドと apk の配布
続いて GitHub 上で自動ビルド -> App Distribution に配布する設定をします。
GitHub の環境を設定
Settings -> Secretsから先程控えたアプリ ID とアクセストークンをアカウントに登録します。
後ほど説明しますが、セキュアな情報はここに登録して使います。workflow を追加
最後に Actions の workflow を定義します。
マーケットプレイスに色々 Action がリリースされており、
App Distibution へのデプロイ用 Action があるので今回はそれを使います。ただしサンプルにはグループの指定が書いてあるので設定していない場合は、削除するかコンソールからグループを作成しておきましょう。
Firebase App Distribution · Actions · GitHub Marketplace · GitHub
今回は PR が master にマージされたら apk を配布する設定にしています。
deploy.ymlname: Build & upload to Firebase App Distribution on: pull_request: types: [closed] branches: - master jobs: build: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - uses: actions/checkout@v1 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: build release run: ./gradlew assembleRelease - name: upload artifact to Firebase App Distribution uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{secrets.FIREBASE_APP_ID}} token: ${{secrets.FIREBASE_TOKEN}} file: app/build/outputs/apk/release/app-release-unsigned.apkこれで PR が master にマージされる度に App Distribution に apk がデプロイされます。
Console を確認すると以下の様にリリースされています。
workflow 内の各プロパティ
name
その Actions や各実行される処理の Description です。
on
workflow が実行されるトリガーを指定します。
types や branches で更に条件を絞ることができます。
今回は master に対する PR が close された時にトリガーされる様な指定になっています。jobs
workflow で実行される処理です。
今回は build と言う ID のジョブが登録されています。runs-on
必須パラメータ。ジョブを実行するマシンの種類。
GitHub にホストされているマシンや自分でホストしているマシン等が選べます。if
処理を実行するための条件を指定します。
今回はマージされずにクローズされた PR でもトリガーされて配布されるのを防ぐために、マージされた時だけ処理される様にしています。steps
ジョブが実行する処理の集まりです。
各ステップはプロセスが別です。with
action で定義されているパラメータを指定します。
Firebase-Distribution-Github-Action ではアプリID、アクセストークン、 apk ファイルを指定して実行します。gradle-wrapper.jar が見つからない
ワクワクしながら一度 Actions を走らせてみたらビルドに失敗していました?
失意の中さまよっていると下記の様な記事を見つけました。
gradlewが謎のエラーで動かない件について - Qiita
何も考えずに Gibo で kotlin プロジェクトを gitignore にダンプすると jar ファイルが ignore されます。
それに引っかかって GitHub のリポジトリ上にgradle/wrapper/gradle-wrapper.jarが存在しないせいでビルドができなくなっていました。.gitignore に以下を追加して gradle-wrapper.jar だけ ignore から除外する様にしておかないといけません。
!gradle/wrapper/gradle-wrapper.jar後は gradle-wrapper.jar を作成してコミットしましょう。
jar ファイルの作成は以下のコマンドでできます。$ gradle wrapSecrets でセキュアな情報を管理
既存のプロジェクトに GitHub Actions を導入した時の話です。
SDK の認証情報を外部ファイルに切り出して管理し ignore していたため、
上の workflow のままだとこれまたリポジトリにファイルがないのでビルドに失敗します。以下の様にファイルの内容を Base64 でエンコードして Secrets に保存し、
ビルドのステップの前にデコードして追加してあげるとビルドが通る様になりました。GithubActionsでgitignoreしたgoogle-services.jsonを読み込む - Qiita
追加の仕方が悪いのか平文で Secrets に登録すると改行とかがあると上手くいかなかったりしたので、エンコード -> デコードの手順が必要なのかなと思います。
ワンラインの内容なら特に問題はないかと。google-services.json 等セキュアな情報を抱き込む必要がある場合は Secrests に登録すると安全に CI できます。
ついでにデプロイされたら Slack に通知する様にしてみた
Firebase App Distribution は apk が新しく登録されたらテスターにメールが飛ぶのでそれでいいのかもしれませんが、
できれば Slack で通知されると見落としが減っていいなぁと思ってやってみました。
PR の URL も一緒に流せたらもっと素敵。過去に Slack の Incoming Webhook をセットアップした事があるんですが、もう一度調べてみたところアップデートされており、
ペイロードの attachments はいつ消えてもおかしくないから blocks 使いなよって事らしいです。
ただ、左側のカラー付きサイドバーで引用っぽく表示するやり方 (color の指定) は今のところ attachments にしか無いみたいなのでどちらを使うかはメッセージのレイアウトによりますね。Slack にポストする Actions もマーケットプレイスにたくさんありましたが、今回は
- 新しい Incoming Webhook に対応している
- レイアウトを自由に決めれる様に attachments, blocks 等 Slack のメッセージペイロード に対応している
を元にこちらの Actions を使わせていただきました。
ありがとうございます。Slack Incoming Webhook · Actions · GitHub Marketplace · GitHub
Webhook のセットアップの仕方は今回は割愛します。
Webhook URL を secrets に登録しておきましょう。deploy.ymlname: Build & upload to Firebase App Distribution on: pull_request: types: [closed] branches: - master jobs: build: runs-on: ubuntu-latest if: github.event.pull_request.merged == true steps: - uses: actions/checkout@v1 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: build release run: ./gradlew assembleRelease - name: upload artifact to Firebase App Distribution uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{secrets.FIREBASE_APP_ID}} token: ${{secrets.FIREBASE_TOKEN}} file: app/build/outputs/apk/release/app-release-unsigned.apk - name: post message to Slack if: success() uses: tokorom/action-slack-incoming-webhook@master env: INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} with: text: "New app was deployed!!" blocks: | [ { "type": "section", "text": { "type": "mrkdwn", "text": "New app was deployed!! :clap:" } }, { "type": "divider" }, { "type": "section", "text": { "type": "mrkdwn", "text": "*${{ github.event.pull_request.title }}*\nPushed by ${{ github.event.pull_request.user.login }}\n\n*URL*\n${{ github.event.pull_request.url }}" }, "accessory": { "type": "image", "image_url": "${{ github.event.pull_request.user.avatar_url }}", "alt_text": "User Icon" } }, { "type": "actions", "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "Join Test" }, "url": "${{ secrets.APP_DISTRIBUTION_URL }}" } ] }, { "type": "divider" } ]これで workflow が成功すると Slack にデプロイされた通知が飛んでくる様になります。
色々とパラメータが使えるみたいで好きな様にレイアウトやテキストがカスタマイズできそうでいいですね。
もっと素敵なメッセージにしたかった。デザイン力をください。。。どういったパラメータが使えるかはドキュメントに記載してあります。
GitHub Actions のコンテキストおよび式の構文 - GitHub Docs
他のパラメータも知りたい場合は workflow の途中とかでダンプすると出てきます。
steps: - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT"Conclusion
こういう設定周りのファイルを弄るのは、手間暇かけた分楽できるのがダイレクトに感じられて楽しいです。
触ってみるととても分かりやすく、ドキュメントもしっかりしていました。
Enterprise でリリースされたらぜひプロジェクトで使ってみたいですね?今後は aab にシフトしていくらしいですけど、その時は DeployGate に流したほうがいいんでしょうかね?
参考
GitHub Actionsのワークフロー構文 - GitHub Docs
- 投稿日:2020-08-20T14:00:30+09:00
SQLiteOpenHelperを用いたDBのRoom移行〜NonPrimaryKeyの場合を添えて〜
はじめに
息が長いプロダクトの場合、SQLiteOpenHelperを用いたSQLiteDatabaseがアプリ内にあることがあります。
現在はRoomが主流で、公式ページでも以下のように記載があります。SQLite の代わりに Room を使用することを強くおすすめします。
メンテナンス性、安定性、モチベーションなど、どれを取ってもRoomの方が良いのですが、既存のSQLiteDatabaseをRoomに置き換えたい場合どうすればよいか?ということで試行錯誤したのでメモします。
Roomのバージョンは2.2.5です。
注意
※もしかしたら、特定の条件で移行がうまく行かないこともあるかもしれません。
※DBは様々な状態があり、クラッシュの幅も広いので、試す場合は自己責任でお願いいたします。移行手順
基本的には、同条件のRoom定義を作成すれば、そのままRoomで既存DBを扱うことが可能です。
しかし、既存テーブルにprimary keyがない場合には、マイグレーションが必要になります。同じ条件のRoom定義を作成する
既存SQLiteのcreate table文を見ながら、Roomの基本的な3クラスを作っていきます。
- Entity
- Dao
- Database
移行する既存DBは例として以下のようなものとします。
- DB名は、user.db
- テーブル名は、task_t
細かいところは適当です。
CreateTable文create table task_t ( _id integer primary key autoincrement not null, item_id text, item_description text, is_done integer not null default 0)Entity
Roomでのテーブル。
TaskEntity.kt// テーブル名を合わせる @Entity(tableName = "task_t") data class TaskEntity( @PrimaryKey(autoGenerate = true) // PrimaryKey, autoGenerateを合わせる val _id: Int, // カラム名・nonnullを合わせる val item_id: String?, // カラム名・nullableを合わせる @ColumnInfo(name = "item_description") // カラム名はColumnInfoで合わせて変数名を自由にしてもOK val itemDescription: String?, // nullableを合わせる @ColumnInfo(name = "is_done") val isDone: Boolean // integerをbooleanとして扱っていれば、Boolean型でOK )Dao
Daoは好きに作ってOK
Database
TaskDatabase.kt// カラムの変更が無いならversionは同じでOK @Database(entities = [TaskEntity::class], version = 1, exportSchema = false) abstract class TaskDatabase : RoomDatabase() { companion object { // DB名は同じに。「.db」の有無とかは地味に注意。 private const val DB_NAME = "task.db" fun createInstance(context: Context): TaskDatabase { // カラム変更有りでバージョンが上がるのであればマイグレーションを実装する必要がある。 // バージョンが同じ場合マイグレーションは空のものも必要なさそう(私がやったときは不要でした) return Room.databaseBuilder(context, TaskDatabase::class.java, DB_NAME).build() } } abstract fun taskDao(): TaskDao }あとは実行してトライアンドエラー
ビルドして実行し、読み書きしようとした際にクラッシュした場合は失敗しているのでエラーを確認します。
下記のエラーが出る場合、既存DBとRoom定義が異なっています。java.lang.IllegalStateException: Pre-packaged database has an invalid schema:task_t
Expected:
TableInfo{...}
Found:
TableInfo{...}ExpectedがRoomのEntityに記載したテーブル定義、Foundが既存DBのテーブル定義です。
見比べてあってない部分を直しましょう。
これで、移行完了です。PrimaryKeyがない場合の移行
PrimaryKeyがない場合は、少々厄介です。
Entityに@PrimaryKeyをつけないと、
An entity must have at least 1 field annotated with @PrimaryKey
と言われコンパイルエラーになります。
Roomは仕様上、PrimaryKeyが必ず必要なようです。
ですが、なにか既存カラムに@PrimaryKeyをつけたり、EntityクラスにPrimaryKeyのカラム(フィールド)を増やすと今度は
Pre-packaged database has an invalid schema
実行時に怒られます。(テーブル構成が変わってしまっているので当たり前)こうなっては、マイグレーションを行って、既存テーブルにPrimaryKeyを追加するしかなさそうです。
Roomへの移行の際、既存DBからバージョンを上げることでRoomのmigrationを通すことが出来るので、alter tableなどでPrimaryKeyを追加できれば良さそう。
と思いきや、SQLiteは仕様上、後からPrimaryKeyを変更することが出来ないようでした。Unlike other database systems e.g., MySQL and PostgreSQL, you cannot use the ALTER TABLE statement to add a primary key to an existing table.
https://www.sqlitetutorial.net/sqlite-primary-key/上記サイトで、対処方法も記載されていて、参考になりました。
簡単に言うと、テーブルの作り直しです。
- 既存テーブルをold_tableにrename
- primarykeyを追加したテーブルをcreatetable
- old_tableの値を全てtableに追加
primarykeyの移行手順
例として、先程の例からprimarykeyカラムをなくしたテーブルを想定します。
CreateTable文create table task_t ( item_id text, item_description text, is_done integer not null default 0)作成するRoom関連クラスは基本的にはPrimaryKeyがある場合と同じですが、少し変更します。
Entity
- A:PrimaryKeyのカラムを新たに追加する場合は、Entityクラスに追加(先ほどと同じクラス)
- B:既存カラムにnon-nullカラムがあり、PrimaryKeyをつけるのであれば、そのカラムに
@PrimaryKeyを追加Database
A・Bで既存データを戻す部分が変わります。
TaskDatabase.kt// 既存DBからバージョンを1上げる @Database(entities = [TaskEntity::class], version = 2, exportSchema = false) abstract class TaskDatabase : RoomDatabase() { companion object { private const val DB_NAME = "task.db" fun createInstance(context: Context): TaskDatabase { return Room.databaseBuilder(context, TaskDatabase::class.java, DB_NAME) .addMigrations(object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { // 既存テーブルをrename database.execSQL("alter table task_t rename to old_task_t") // PrimaryKeyを追加した新規テーブル作成 database.execSQL("create table...") // 既存データを戻す // A:新規PrimaryKeyカラムを追加した場合 database.execSQL("insert into task_t select null, * from task_t") // B:既存カラムにPrimaryKeyを追加した場合 database.execSQL("insert into task_t select * from task_t") } }).build() } } }AutoIncrementカラムを追加した場合は、nullを入れることで自動採番してくれます。
null, *となっていますが、create tableのカラム順で最初にAutoIncrementを追加した例なので最初にnullを入れています。これでRoomでの初回アクセス時にマイグレーションが走り、RoomでDBを扱えるようになります。
余談:せっかくcreate tableするなら過去を精算する
create tableし直すなら、Roomの定義はきれいにしてしまってもいいのでは?ということで、その手順を記載します。
想定テーブル(PrimaryKeyのないテーブル)CreateTable文create table task_t ( item_id text, item_description text, is_done integer not null default 0)これをPrimaryKey(_id)を追加して移行すると、下記のようなEntityになります。
TaskEntity.kt@Entity(tableName = "task_t") data class TaskEntity( @PrimaryKey(autoGenerate = true) val _id: Int, @ColumnInfo(name = "item_id") val itemId: String?, @ColumnInfo(name = "item_description") val itemDescription: String?, @ColumnInfo(name = "is_done") val isDone: Boolean )きれいにしたい点として、下記があるとします。
- item_idは実はnonnullがいい
- そしてitem_idをPrimaryKeyにしたい
- item_descriptionは実際使ってないので消したい
PrimaryKeyがない時点で
create tableすることは確定なので、理想のRoomEntityに寄せてみましょう!理想のEntity@Entity(tableName = "task_t") data class TaskEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "item_id") val itemId: String, @ColumnInfo(name = "is_done") val isDone: Boolean )基本的な流れ
Entityは出来たので、マイグレーション処理を考えていきます。
- 既存テーブルをold_tableにrename
- 理想のテーブルをcreate tableする
- old_tableの値を全て必要に応じて入れる
Databaseのmigrateでいくつか処理を行うので、OldDbMigrationクラスを作成しそこで行うことにします。
TaskDatabase.kt@Database(entities = [TaskEntity::class], version = 2, exportSchema = false) abstract class TaskDatabase : RoomDatabase() { companion object { private const val DB_NAME = "task.db" fun createInstance(context: Context): TaskDatabase { return Room.databaseBuilder(context, TaskDatabase::class.java, DB_NAME) .addMigrations(object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { // ここでマイグレーション処理 OldDbMigration.migrate(database) } }).build() } } abstract fun taskDao() : TaskDao }OldDbMigration.ktobject OldDbMigration { private const val TASK_T_CREATE = // TaskDatabase_Implから持ってこられる(後述) "CREATE TABLE IF NOT EXISTS `task_t` (`item_id` TEXT PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL)" // TaskDao_Implから持ってこられる(後述) private const val TASK_T_INSERT = "INSERT OR ABORT INTO `task_t` (`item_id`,`is_done`) VALUES (?,?)" private const val OLD_TASK_T = "old_task_t" fun migrate(database: SupportSQLiteDatabase) { // 既存テーブルをrename database.execSQL("alter table task_t rename to $OLD_TASK_T") // 理想のEntityテーブル作成 database.execSQL(TASK_T_CREATE) // 古いデータを全件取得 val cursor = database.query("select * from $OLD_TASK_T") cursor.use { while (cursor.moveToNext()) { runCatching { val itemId = cursor.getString(0) val isDone = cursor.getInt(2) != 0 // 古いデータを新しいテーブルにinsert database.execSQL( TASK_T_INSERT, arrayOf(itemId, isDone) ) } } } } }理想のEntityの
create tableは、Entity/Databaseを作った状態でビルドすると出来るTaskDatabase_Implから拝借できます。TaskDatabase_Impl.java@Generated("androidx.room.RoomProcessor") @SuppressWarnings({"unchecked", "deprecation"}) public final class TaskDatabase_Impl extends TaskDatabase { private volatile TaskDao _taskDao; @Override protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) { final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(2) { @Override public void createAllTables(SupportSQLiteDatabase _db) { // ↓これが自動生成されたcreate table文 _db.execSQL("CREATE TABLE IF NOT EXISTS `task_t` (`item_id` TEXT PRIMARY KEY AUTOINCREMENT NOT NULL, `is_done` INTEGER NOT NULL)"); ...同じく、insert文に関しても、Daoを作ってDatabaseにメソッドを追加した状態でビルドすると出来る
TaskDao_Implから拝借できます。
Daoに@Insertのメソッドを作り、Databaseにdao取得関数を作る必要はあります。TaskDao.kt@Dao interface TaskDao { @Insert fun insert(entity: TaskEntity) }TaskDao_Impl.java@Generated("androidx.room.RoomProcessor") @SuppressWarnings({"unchecked", "deprecation"}) public final class TaskDao_Impl implements TaskDao { private final RoomDatabase __db; private final EntityInsertionAdapter<TaskEntity> __insertionAdapterOfTaskEntity; public TaskDao_Impl(RoomDatabase __db) { this.__db = __db; this.__insertionAdapterOfTaskEntity = new EntityInsertionAdapter<TaskEntity>(__db) { @Override public String createQuery() { // ↓これが自動生成されたinsert文 return "INSERT OR ABORT INTO `task_t` (`item_id`,`is_done`) VALUES (?,?)"; }今回の例ように2カラム分であれば自力でSQL文を作ってもいいですが、新規インストール者はRoomのこれらの生成文で作られたDBを使うので、合わせておくのが楽で安全かと思います。
PrimaryKeyだけでなくいくつか変更したいことがあるなら、この方法を取らなくても
- SQLiteOpenHelperを使ってRoomに流すマイグレーションを自作
- 既存DB向けのRoomEntityと理想のRoomEntityを作りマイグレーションを自作
などの選択肢はありますが、Roomのmigration内でやるのは難しいので、ここに負債を押し込めたい場合は上記の方法が良さそうです。
さいごに
欲を出して余談までやっていたらRoomの生成コードと戯れることになりました。
ともあれ、既存のSQLiteDatabaseのRoomへの移行は割と簡単に出来ることが分かりました。
誰かのお役に立てれば幸いです。







