Android Roomの自動マイグレーション

AndroidにRoomというSQLiteのライブラリがありますが、マイグレーションに関してはサポートが薄く単純な変更ですらコストがかかっていると思います。

という状況だったのですが、 2.4.0-alpha01 にて一部の変更の自動マイグレーションがサポートされたようです 🎉🎉

medium.com

全自動でマイグレーションされる場合

明文化はされていないようですが、以下の場合は全自動でマイグレーションされるようです。

  • テーブルの追加
  • カラムの追加

スキーマをエクスポートするように設定した後、以下のように Database アノテーションに変更バージョン情報を持った AutoMigration を指定してあげるだけです。

@Database(
        entities = [Todo::class],
        version = 2,
        autoMigrations = [
            AutoMigration(from = 1, to = 2)
        ]
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDao
}

サポートが必要な場合

Roomのコードを少し読んでみると、SchemaDiffer クラススキーマのdiffを取り、AutoMigrationProcessor クラス ではその追加されたテーブル/カラム情報は利用していますが削除された情報は利用していません。
これはテーブル/カラムが削除されていた場合、それが単なる削除なのかリネームなのかが判定できないからです。

ということで、以下の変更は AutoMigrationSpec を実装してどう変わったかをRoomに教えてあげる必要があります。

  • テーブル名の変更 (@RenameTable(fromName, toName) )
  • テーブルの削除 ( @DeleteTable(name) )
  • カラム名の変更 ( @RenameColumn(tableName, fromColName, toColName) )
  • カラムの削除 ( @DeleteColumn(tableName, colName) )

gist.github.com

上記変更が必要な場合は、生SQLを書いてねというわけではなくしっかりサポートがあるあたり優しいですね。

また、試してみたところspecの指定が不足していた場合コンパイル時に何が必要か優しく教えてくれます。

error: 
public abstract class AppDatabase extends androidx.room.RoomDatabase {
                ^
              AutoMigration Failure: Please declare an interface extending 'AutoMigrationSpec',
              and annotate with the @RenameColumn or @RemoveColumn annotation to specify the
              change to be performed:
              1) RENAME:
                  @RenameColumn(
                          tableName = "Todo",
                          originalColumnName = "isDone",
                          newColumnName = <NEW_COLUMN_NAME>
                  )
              2) DELETE:
                  @DeleteColumn=(
                          tableName = "Todo",
                          deletedColumnName = "isDone"
                  )

複数変更がある場合は以下のように指定してあげれば良いです。

@DeleteColumn(
        tableName = "Todo",
        columnName = "isDone"
)
@RenameColumn(
        tableName = "Todo",
        fromColumnName = "text",
        toColumnName = "content"
)
class V3AutoMigrationSpec : AutoMigrationSpec

SQLが必要な場合

それ以外の複雑なテーブルの変更は生SQLマイグレーションを書いてあげる必要があります。 例えば以下のような場合です。

  • テーブルを分割/統合する
  • テーブルの正規化/非正規化
  • Viewに対する変更

Viewは試してみた限り、今回のサポートには入っていないようです。(多分)

SQLが必要な場合でも、AutoMigration機能と併用することができます。

まとめ

まだ現時点ではalpha版なのでそこは注意ですが、かなりマイグレーションの負担が減るのではないでしょうか。
個人的にはかなり嬉しいアップデートでした。

Fiddler Everywhereを使ってMac上でAndroidアプリのHTTP(S)通信をキャプチャする

導入

Androidアプリを開発していると、アプリ上でどういう通信をしているか覗きたいときありますよね。

そんなときに、よく使われるのがCharlesですが、

www.charlesproxy.com

みんな大好きFiddlerMacLinuxでも使えるFiddler Everywhereとして今年の春頃にリリースされていたのでした。

forest.watch.impress.co.jp

あまりFiddler Everywhereについての日本語記事がなかったので、
今回はMac上でFiddler Everywhereを使ってAndroidアプリのHTTP(S)通信をキャプチャするやり方について、簡単にお話します。

Fiddler Everywhereのセットアップ

まず、Fiddler Everywhereを公式サイトから適当にダウンロードしてインストールしてください。

www.telerik.com

起動しましたら、この時点でMac上のHTTP通信が丸裸にされていると思います。
f:id:Yuiki0627:20200813162406p:plain

右上の設定ボタンを押して、
HTTPS の項目で Trust root certificate します。
さらに Cpature HTTPS traffic がオンになっているか確認してください。
これでMac上にルート証明書がインストールされて、Mac上でのHTTPS通信が一度Fiddlerで赤裸々にされるようになります。
f:id:Yuiki0627:20200813161941p:plain

次に、 Connections の項目で、ポート番号を確認し、 Allow remote computers to connect にチェックを入れます。
これで他の端末がFiddlerのプロキシを利用できるようになります。
f:id:Yuiki0627:20200813161956p:plain

右下のSaveボタンを押して、Fiddler Everywhereの設定は完了です。
一回、Fiddler Everywhereを再起動しておきましょう。

Android端末のセットアップ

次に、Android端末にプロキシを設定します。
まず、Fiddler Everywhereが動いているMacIPアドレスを確認しておきます。
一番簡単なのは、System Preferences.appのNetworkで確認する方法でしょうか。

次に、HTTP(S)通信をキャプチャしたいAndroid端末をMacと同じLANに接続しておきます。
そして接続しているWi-Fiの設定を開き、プロキシを手動に切り替え、先ほど確認したIPとポート番号を入力して保存します。
f:id:Yuiki0627:20200813162949p:plain

これで当該Android端末で通信される通信がFiddler Everywhereを仲介するようになったので、
ブラウザで適当なHTTP通信を行うとFiddler Everywhereで確認できるようになったと思います。

しかし、ルート証明書をインストールしていないので、
http://ipv4.fiddler:{先ほど確認したポート番号} にアクセスし、下部リンクからルート証明書をダウンロードして、 適当な名前を付け、端末にインストールさせてください。

これでAndroid上のブラウザで適当なHTTPS通信をすると、Fiddler EverywhereでHTTP(S)通信を閲覧できるようになったと思います。

Androidアプリの設定

とはいえ、プロキシ設定のダイアログに書いてあった通り、ブラウザ以外のアプリではプロキシが利用できないので、
開発しているアプリ上の設定を弄ります。

まず、以下の内容のファイル res/xml/network_security_config.xml を作成してください。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

次に AndroidManifest.xmlapplication に以下のプロパティを追加します。

<application
    ...
    android:networkSecurityConfig="@xml/network_security_config">
   ...

これで当該アプリをデバッグ実行すると、Fiddler EverywhereでHTTPS通信が見れるようになったはずです。

Firebase Hosting x ReactでOGP対応する

概要

Firebase HostingにデプロイされたReactアプリにて、
Twitterでシェアされた時にページ毎に違うOGPを設定する方法について、備忘録がてら述べます。

経緯

React Helmetを使って動的にOGPを設定しても、TwitterのクローラがJSを実行しないようなので、
ReactプロジェクトでOGPをTwitterで表示する方法として
主に、

  1. ページ毎にHTMLを事前に生成しておく
  2. アクセス時に動的にHTMLを生成する

という方法があります。

今回のプロジェクトの都合上、2の方法を選択したのでそのやり方を説明します。

もちろん、お金を使える場合はNetlifyのPrerendering機能を利用したり、
GatsbyJSなどを利用したりしている場合は、1の方法が採用できますね。
Prerendering | Netlify Docs

動的にHTMLを変更する

アクセス時に動的にHTMLを生成する方法としても、
例えばRailsがHTMLを返すシステムの場合、Rails側でリクエストに応じてHTMLを返却すれば問題なさそうです。

ここではFirebase Hostingを使っているケースで、OGPを設定する方法について述べます。

firebase.json の設定

まず、プロジェクトのfirebase.jsonで以下のように設定します。
以下の場合/posts/**にアクセスすると、Firebase FunctionsにデプロイされたaddOgTagsInPost関数に処理が移譲されます。
その他のページにアクセスすると/index.htmlが表示される設定になっています。
ここでsourcefunctionはよしなに名前を設定してください。

{
  "hosting": {
    ...
    "rewrites": [
      {
        "source": "/posts/**",
        "function": "addOgTagsInPost"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  ...
}

関数の実装

上記の例では/posts/**にアクセスされると、以下の関数が呼ばれるので、
ここでいい感じにHTMLを返してやればいいわけです。

クローラ以外の場合、正規ページにリダイレクトする方法もあるのですが、
OGPが表示されるページと、されないページが存在するのを避けたかったので、
今回は、Firebase Functions側にもHTMLファイルをデプロイしておく方法を採用しました。

以下は例なので、よしなに修正してください。
基本的にはHTMLファイルを読み込んで、</head>タグの直前にmetaタグを挿入しています。
また、ページに応じて画像のURLを変更しています。

import * as functions from "firebase-functions"
import * as fs from "fs"
import * as path from "path"

export const addOgTagsInPost = functions.https.onRequest((req, res) => {
  const uid = req.path.split("/")[2]
  const ogTags =
    '<meta name="twitter:card" content="summary_large_image"></meta>' +
    '<meta property="og:title" content="{title}" />' +
    '<meta property="og:description" content="{description}" />' +
    `<meta property="og:image" content="https://www.example.com/user_icons/${uid}.png" />`
  const htmlFile = fs.readFileSync(path.join(__dirname, "./index.html"))
  const html = htmlFile.toString().replace(`</head>`, `${ogTags}</head>`)
  res.send(html)
})

HTMLファイルのデプロイ

デプロイ前に、デプロイするjsファイルと同じディレクトリ直下に、
フロントエンドで用いているHTMLファイルを設定してください。

package.jsonscriptsに処理を、設定するといいと思います。

問題点

上記のやり方には、以下のような問題点があるので注意してください。

  • req.path.split("/")[2] のように /posts/****の部分の取り方が適当
  • HTMLが変更された場合に、当該関数の再デプロイが必要
    • 忘れがち
    • フロントエンドのコードと同じプロジェクトではない場合、どうする?
  • Firebase Functionsを挟むので、ロード時間が比較的長くなる
    • React-Routerで遷移したなどは、もちろん問題ない
  • すでにHTMLにOGPが設定されていた場合、重複するという問題が発生する
  • </body> を置き換えるというナイーブな実装

まとめ

以下のようにTwitter上で設定したOGPが表示されました。

f:id:Yuiki0627:20200730113058p:plain

※ちなみにこの曲一覧はテスト用なので、本当に僕がよく聴いている曲ではないです。

画像生成については、様々な方法があるので、よしなにやってみてください。

今回は、enPiTという大学の授業でチームで実装したアプリに、OGPを設定しようと思っていろいろやったので、せっかくなので記事化しました。
作成したアプリ↓
https://musicle-app.web.app/

DroidKaigi 2019で登壇しました

DroidKaigi 2019で「いかにしてビットコインを扱うか」というセッションで登壇しました。

droidkaigi.jp

題名に関してはポリア著の『いかにして問題をとくか』に影響を受けています。

登壇資料はこちらです。

speakerdeck.com

:thinking_face: が文字化けしてるんですけど、補完してください。

久しぶりの登壇で緊張度がMAXだったんですが、良かったとか面白かったとか言ってくださった人がいてスピーカーとして冥利に尽きますね。

結構尖った内容だったのでCfP出す時は正直どうかなと思ってましたが、結果このテーマで出してよかったなと思っています。

登壇の場数を増やしてもう少し場に慣れたいと切実に感じたので頑張ります。

30分のセッションだと話せなかったことも多かったので、何か質問等ありましたら気軽に@yuikijpまでメンションもしくはDMしてください!

運営、参加者、スポンサーの皆さん、本当にありがとうございました!

Kotlinでファミコンのエミュレータを書いた

f:id:Yuiki0627:20181018001606p:plain

ファミコン(NES)のエミュレータを書いてみたいとずっと思っていたので、書いてみることにしました。
そう思っている人は少なからず居るようで、先日もPHPで書いたというエントリが出たようです。

とりあえず先人たちがやっているように僕もSuper Mario Brothers(SMB.)を動かすのを目標にしました。

やったことがあるファミコンのゲームはSMB.しかないのでぴったりそうです。
(僕は1998年生まれの20歳で、実はファミコンの実機を触ったことが無い)

SMB.を動かすまで

SMB.を動かすまでの過程を解説していきます。
基本的に既存の文献やコードを読みつつ、コードに落としていきました。

1. Hello, World! を動かす

f:id:Yuiki0627:20181017232148j:plain

Hello, World!を表示するだけのROMを動かしましょう。
ROMは以下のページから手に入ります。
NES研究室 - サンプル

Hello, World!を動かすところまでは、以下の記事がとても詳しいので参照してみるのが良いでしょう。
ファミコンエミュレータの創り方 - Hello, World!編 -

Hello, World!が表示されたときはドーパミンがドバドバ出た感じがして、この時点でエミュレータを書いた意味がありました。最高。

おまけ

上手く動かなかったHello, World!の図

f:id:Yuiki0627:20181017232540p:plain

なんやこれ

2. nestest.nesの全テストに通す

CPUのテストをするROMがあるのでそれを動かして全テストに通しましょう。
ROMは以下のページから手に入ります。
Emulator tests - Nesdev wiki

既存の他のエミュレータで吐き出したログと作っているエミュレータのログを突き合わせてみるというのは良い手法です。
nes-test-roms/nestest.log at master · christopherpow/nes-test-roms · GitHub

ちなみにここで非公式オペコードをひたすら実装していく必要があります。
非公式と言いつつめっちゃ多いのでここは気合で💪

それと、コントローラも実装しておくと良いでしょう。

f:id:Yuiki0627:20181017233556p:plain

3. gikoシリーズを動かす

ギコ猫でもわかるファミコンプログラミング さんではいくつかの簡単な、しかしファミコンの機能の要点が抑えられているROMがいくつか公開されているので、それらを動作させていきましょう。

f:id:Yuiki0627:20181017234336p:plainf:id:Yuiki0627:20181017234349p:plain

だんだん動くものができてきて面白くなってきます。

アセンブリも公開されているので、それを元にプログラムの流れを掴んでいくのは効果的です。

それにしてもラスタースクロールってすごいですね...。

4. SMB.に挑戦する

ここまできたらSMB.に挑戦できるくらいエミュレータが育ってきているはずです。

何も出なくてもくじけてはいけません。

f:id:Yuiki0627:20181017235251p:plain

変なのが出てもくじけてはいけません。(自戒)

f:id:Yuiki0627:20181017235253p:plain

nestest.nes を通している時点でCPUはほとんど問題がないはずなので、大抵の問題はPPU(画像処理)にあるはずです。

頑張ってbug fixするとなんとか動きます!

f:id:Yuiki0627:20181017235747g:plain

ROMの入手について

僕はAmazonで吸出し機を、メルカリでカセットを購入しました。

f:id:Yuiki0627:20181017234959j:plain

まとめ

まだ音の実装ができてなかったり描画に問題があったりと難点は多いのですが、60fps近く出ていて、とりあえず遊べる程度にはSMB.が動いているので一旦良しとします。

エミュレータ開発、結構面白かったです。

コードはMITライセンスで公開しています!
github.com

参考文献