へいへいブログ

殴り書き、メモ書きレベルで雑に📝📝📝📝📝📝

"XXX is a boxed field but needs to be un-boxed to execute YYY. This may cause NPE so Data Binding will safely unbox it. You can change the expression and explicitly wrap XXX with safeUnbox() to prevent the warning."の警告を見て、Data Binding周りを調べたことについて

TL;DR

  • Data Bindingライブラリで<data>タグと<variable>タグを利用してXML上に変数定義をする際には、プリミティブ型で定義できる際には積極的にプリミティブ型を使っていこう
  • 参照型の変数を定義した際には、Data Bindingライブラリが内部でunboxingする
  • safeUnbox()を利用することもできる

詳細

本記事執筆時点のライブラリバージョンは下記です:

  • com.android.support:appcompat-v7: 27.0.1
  • com.android.databinding:compiler: 3.0.0

Data Bindingライブラリを使ってXML上に変数を定義してViewの見た目を変えるとします。下記は、isLoadingであればProgressBarを表示、そうでなければProgressBarを非表示(View.GONE指定)にする簡単なXMLの例です:

<data>
  <import type="android.view.View"/>
  <variable
    name="isLoading"
    type="Boolean"
  />
</data>
...
  <ProgressBar
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      android:visibility="@{isLoading ? View.VISIBLE : View.GONE}"
      />
...

一見なにも問題ないように見えますが、この状態でプロジェクトをビルドすると、タイトルのような警告が表示されます。

safeUnbox()

Booleanは参照型なので、値にnullが代入される可能性があります。nullが代入される可能性があるということは、NullPointerExceptionが発生する可能性が生まれるということです。

今回のコードを改善しようとするなら、2つ改善策がありそうです。

1つ目は、変数定義の際のtypeに、unboxする必要のない型を指定することです。ここではBoolean型(=参照型)の代わりにboolean型(=プリミティブ型)を指定すれば良さそうです。

<data>
  <import 
    type="android.view.View" />
  <variable
    name="isLoading"
    type="boolean"
  />
</data>
...

二つ目は、これはタイトルの警告文言にも記載されているのですが、

w: 警告: XXX is a boxed field but needs to be un-boxed to execute YYY. This may cause NPE so Data Binding will safely unbox it. You can change the expression and explicitly wrap XXX with safeUnbox() to prevent the warning.

safeUnbox()というData Bindingライブラリが用意してくれているstaticメソッドを利用することもできるようです。

<ProgressBar
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:visibility="@{safeUnbox(isLoading) ? View.VISIBLE : View.GONE}"
      app:layout_constraintTop_toTopOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      />

このsafeUnbox()というメソッドはandroid.databindingパッケージに内包されているDynamicUtilクラスのstaticメソッドでした。

package android.databinding;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.databinding.BindingConversion;
@javax.annotation.Generated("Android Data Binding")
public class DynamicUtil {
    public static int safeUnbox(java.lang.Integer boxed) {
        return boxed == null ? 0 : (int)boxed;
    }
    public static long safeUnbox(java.lang.Long boxed) {
        return boxed == null ? 0L : (long)boxed;
    }
    public static short safeUnbox(java.lang.Short boxed) {
        return boxed == null ? 0 : (short)boxed;
    }
    public static byte safeUnbox(java.lang.Byte boxed) {
        return boxed == null ? 0 : (byte)boxed;
    }
    public static char safeUnbox(java.lang.Character boxed) {
        return boxed == null ? '\u0000' : (char)boxed;
    }
    public static double safeUnbox(java.lang.Double boxed) {
        return boxed == null ? 0.0 : (double)boxed;
    }
    public static float safeUnbox(java.lang.Float boxed) {
        return boxed == null ? 0f : (float)boxed;
    }
    public static boolean safeUnbox(java.lang.Boolean boxed) {
        return boxed == null ? false : (boolean)boxed;
    }
}

ただ、実際にはこのメソッド呼び出しをXML上で明示的に行わなくても、Data Bindingライブラリが生成するJavaソースコード(下記)内でこのメソッドを呼び出して安全にunboxしてくれているようなので、これに関しては書いても書かなくてもどちらでも良さそうです。(詳しいことはしっかりとは調べれていませんので、言い切る自信はないですが)

// read android.databinding.DynamicUtil.safeUnbox(isLoading)
androidDatabindingDynamicUtilSafeUnboxIsLoading = android.databinding.DynamicUtil.safeUnbox(isLoading);

DynamicUtilsafeUnbox()について今回初めて知ったのでメモとして記載した次第です!以上です!

参考リンク

https://stackoverflow.com/questions/42872201/data-binding-safeunbox-warning

Androidの(海外開催含む)カンファレンス情報について

Androidアプリケーション開発者に関わらず、国内、国外の大きなカンファレンスが行われる際、どのようなカンファレンスなのか(どこでいつ頃あるのか、登壇者は誰なのか、等)気になることがあります。Androidアプリケーション開発に関するカンファレンスに関しては、今後どのようなイベントがあるのかをひと目で確認できるサイト・Githubレポジトリが存在します。知っているよ!という方もいるかと思いますが本記事ではそれをさらっと紹介します。

サイトはこちら:

androidstudygroup.github.io

GitHubレポジトリはこちら:

github.com

カンファレンス毎の下記情報が閲覧できます:

  • カンファレンス開催日
  • カンファレンス名
  • カンファレンス開催地
  • カンファレンス
  • 発表申込を受付中かどうか

上記レポジトリは世界中のAndroidアプリケーション開発者やカンファレンス主催者のPull Requestによって更新されています。 また、Call For Papers期間中(発表申込を受付中)の場合にはそれもひと目でわかるように緑のラベルのようなものがつけられていて親切です。

以上です。

これからJavaで書かれたAndroidアプリケーションのソースコードをKotlinに書き換える際に気をつける、やるべきこと2点

Kotlin歴2ヶ月程度で勉強中なのですが、現在プロジェクトのソースコードを少しずつですがKotlinに置き換えていて、本日11/7時点でKotlin率は40%弱です。

この記事の内容は、そんな自分がKotlin置き換え始めた頃の失敗、問題点を雑に振り返り、

JavaソースコードをKotlinに書き換える際には下記2つを行なうのが良いのではないかという提案です:

  1. 自動変換コードは常に疑う
  2. デコンパイルされたバイトコードのレビューをする

尚、内容はYahoo JAPAN!様で開催されたBonfire#2にて発表したものから抜粋しています。(発表資料はこの記事の下部にあります)

Kotlin置き換え始めた頃の自分の問題点

1. 自動変換を信用しすぎていたこと

Android Studioを用いたAndroidアプリケーション開発において、JavaソースコードをKotlinに置き換える際には、自動変換のツールが用意されています。

Macの場合は、

⌘ Option Shift K

のショートカットを利用することで、IDE側で.javaファイルを.ktファイルに置き換えてくれます。素晴らしい!

KotlinはNull安全な言語です。Kotlin言語を使った開発では、値がnullになりえるのか、そうでないのかを明示的に記述する必要があります。

もちろん上記の自動変換によってある程度動くコードには変換されるのですが、手直しは必ず必要です。自動変換されたコードが問題なく動作するコードであっても、そのコードがKotlinらしい文法のコード、パフォーマンス性の高いコードであるという保証はありません。「自動変換のコードは絶対に使わず自分の手できれいなコードを書け!」というわけではなくて、「そんなの当たり前じゃん」と言われればそれまでですが、自動変換されて動くからと言って信用はせずどのようなコードに変換されているのか、もしKotlinを始めた方であれば必ず見て、もっと良い書き方はないかと模索するのが良いかもということが伝えたい事です。

ということで、自動変換は常に疑いましょう。手直しは必ず必要です。また、一件問題なさそうなコードであっても入念にチェックしておくに越したことはありません。

たとえばonActivityResultoverrideしているActivityクラスをJavaからKotlinに置き換えるとしましょう。

変換後のonActivityResultのKotlinのソースコードがこのような形になったとしましょう。一件問題ないようにみえますが、どうでしょうか?

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
  super.onActivityResult(requestCode, resultCode, data)
  ...
}

onActivityResultの第三引数のdataの値はnullに成り得ます。この場合、KotlinではNull許容であるということを明示的に示す?マークを付与する必要があります。?マークがついていない変数等にnullが動的に代入された場合NullPointerExceptionが発生しアプリが終了してしまいます。

ということでこのように記述する必要があります:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)
  ...
}

自動変換が便利で、ある程度信用しきっていた部分があったため、自分の場合この問題に出くわして( ゚д゚)ハッ!としました。何度も書きますが、自動変換は常に疑いましょう。手直しは必ず必要です。

2. (デコンパイルされたバイト)コードレビューをしていなかったこと

Javaにおけるstaticな定数をKotlinで定義したい場合にはcompanion objectを利用できます。

companion object {
  val SOME_VALUE = 1
}
  
println(“$SOME_VALUE")

このようなコードは一件問題ないように見えるのですが、このKotlinのソースコードが生成するバイトコードデコンパイルしてみると、

public static final int SOME_VALUE = 1;
public static final class Companion {
  public final int getSOME_VALUE() {
    return MainActivity.SOME_VALUE;
  }
}
…

String var2 = "" + Companion.getSOME_VALUE();
System.out.println(var2);

このようになっています。定数に対するgetterが定義されているしメソッド名もヮ(゚д゚)ォ!な感じになっていて、Javaで書く際には決して書かないようなコードが生成されています。

Kotlinでは、プロパティに対してデフォルトでsetter / getterが定義可能であれば定義される仕組みとなっています。

たとえばあるクラス内にvar someValue = 1というプロパティを定義した場合、KotlinではsetSomeValue()getSomeValue()メソッドが新たに生成されます。

ここでは詳しくは記載しませんが、varは可変なプロパティを宣言する際に利用します。なのでsetterが定義されます。対してvalは不変なプロパティ宣言の際に利用され、valによって定義されたプロパティにはデフォルトでgetterが生成されます。

先程のコードを改めて確認すると、今回の場合は定数定義を行いたいだけなので、Kotlinには新たにgetterの生成は行ってほしくありません、そのような場合には、

  1. String型もしくはプリミティブ型のプロパティ: const val定義
  2. 1.以外のプロパティ: @JvmFieldアノテーション付与

を行なうことで解決できます。

  1. constキーワードは、object内のトップレベルもしくはメンバーとして定義されているString型、プリミティブ型の変数に対してのみ付与できます。const定義されたプロパティはコンパイル時定数(=Compile-Time-Constants)として定義され、getter生成が省略されます。
companion object {
  const val SOME_VALUE = 1
}
  
println(“$SOME_VALUE")

このようなコードを記述して生成されたバイトコードデコンパイルすると、

public static final int SOME_VALUE = 1;
public static final class Companion {
  …
}

String var2 = "1";
System.out.println(var2);

先程のバイトコードとは違い、getterがなくなっていることがわかります。(getterがなくなったことで、コードが幾分か効率化されたこともわかります)

  1. @JvmFieldアノテーションconstが利用できないプロパティに対して付与でき、Kotlinに対してgetterを生成せず、Javaにおけるフィールドとして公開することを伝えるためのものです。

Kotlinのバイトコードデコンパイルすることで、自分のKotlinのソースコードをもっと効率化できないか、無駄なメソッドが生成されていないか等を確認することができます。

まとめ

世の中では「Kotlinかわいい😍」というツイートをよく見かけますが、Null許容を表す一文字の?をつけるのか、そうでないのかによって大きな違いが生まれるのがKotlinです。怖いでしょ!?Javaでよくない!?とKotlinを否定しているのではありません。僕も思います!Kotlinかわいいです😍 !

ただ、JavaソースコードをKotlinに置き換える際には特に、変換後のKotlinのコードに不備や問題はないか、チェックすることが必要なのかもと思います。(自分の場合は、デコンパイルされたバイトコードを意識して確認するようにしています。)

Kotlinの良さに気づいた時、「Kotlinかわいい😍 」と目が♡になってしまい盲目になりKotlinに噛まれることのないよう、特にこれからJavaソースコードをKotlinに置き換えていく方に向けて、下記の二点をオススメします:

  1. 自動変換を行う際には常に変換後のコードをチェック
  2. 可能であればバイトコードデコンパイルしてレビュー

最後に、下記は、Yahoo! JAPAN Bonfire#2での登壇資料です。上記内容以外に自分がハマったこと等についていくつか紹介しています:

おまけ: 来年2月に開催されるDroidKaigiにて、fluxについて発表します。初の30分という長い時間と英語のプレゼンなので緊張しますが、選ばれたからにはしっかりやろうと思います!

以上です!

AS3.0-stable + RobolectricによるUnitテストでResourceNotFoundExceptionが出る際の対策

バージョンは下記:

現象はタイトルの通りで、上記バージョンのAndroidとRobolectricを使ってUnitテストを実行すると、AndroidのResourcesにアクセスする際にResourceNotFoundExceptionが発生しテストが失敗するというものです。(ちなみに自分の場合は、ローカルでの実行は正常に動きCI(Bitrise)上での実行時のみ失敗する、というものでした。)

あくまで自分のケースのみ有効かもしれませんが、下記に解決策を紹介します:

ResourceNotFoundExceptionに対する解決策

app/build.gradleandroid {}内にtestOptionsを追加し、Unitテスト実行時の設定を記述します。

testOptions {
  unitTests {
    includeAndroidResources = true
  }
}

includeAndroidResourcesに関するドキュメントは下記です: UnitTestOptions - Android Plugin 3.0.0-dev DSL Reference

このoptionをtrueにすることで、Unitテストの実行前にアセット、リソースやマニフェストのマージをプラグインが行ってくれるそうです。

これを指定することでResourceNotFoundExceptionを防ぎテストを正常に実行できるようになりました。

(テスト実行時にthreetenbpに依存している場合の)ZoneRulesExceptionに対する解決策

includeAndroidResourcesオプションを設定することで大半のテストは正常に実行されるようになったのですが、今度は下記のようなエラーログが出てテストが終了するという現象がありました。

    org.threeten.bp.zone.ZoneRulesException
    at android.net.LocalSocketImpl.bind(LocalSocketImpl.java:303)
    at android.net.LocalServerSocket.__constructor__(LocalServerSocket.java:48)
    at android.net.LocalServerSocket.<init>(LocalServerSocket.java)
    at com.facebook.stetho.server.LocalSocketServer.bindToSocket(LocalSocketServer.java:142)
    at com.facebook.stetho.server.LocalSocketServer.listenOnAddress(LocalSocketServer.java:78)
    at com.facebook.stetho.server.LocalSocketServer.run(LocalSocketServer.java:74)
    at com.facebook.stetho.server.ServerManager$1.run(ServerManager.java:40)

アプリではThreeTenABPを利用しているのですが、

IllegalStateException: TZDB.dat missing from assets

というエラーログと共にテストが実行できないことに過去に悩まされた結果、

参考: IllegalStateException: TZDB.dat missing from assets · Issue #24 · JakeWharton/ThreeTenABP · GitHub

上記issueに記載されている解決策を元にテスト実行時にはThreeTenBPに依存するようにしていました。今回ZoneRulesExceptionが出たタイミングでこの依存を試しに解消してみたところテストが正常に通ることがわかりました。(oh..)

予想としてはincludeAndroidResourcesが影響しているものと思っているのですが、具体的なところまでは調査できていません。

下記に今回調査する際にお世話になったサイト一覧を記載しておきます。雑ですが、以上です!

AndroidにおけるColorUtilsを用いた色の配合

Androidsupport.v4.graphicsパッケージにはColorUtilsというユーティリティクラスが存在します。

ColorUtils | Android Developers

Androidアプリケーション開発では、このユーティリティクラスを用いることで簡単に2つの色をコード上から動的に配合することができます。

blendARGB関数には2つの色を表すInt型と、0~1のratio=割合を表すfloat型のオブジェクトを引数として渡すことができます。 0に近ければ第一引数に渡した色が濃く、1に近づくごとに第二引数に渡した色が濃くなります。

このratio値をViewの動きに合わせて変化させることで、ユーザーの動きに合わせて色を動的に配合することができます。たとえば、ViewPagerのonPageScrolledコールバックの値を利用することで、ページ間移動の動きに合わせて色を変化できます。

具体的な例やサンプルgif等については下記サンプルレポジトリを御覧ください。

github.com

また、ColorUtilsについては先日のpotatotipsでも発表しました。

下記はそちらの発表資料です。合わせてご覧ください。

雑ですが、以上です!

Androidアプリ開発を行なう上でよくお世話になる英語のウェブサイト、Podcast一覧

Androidアプリ開発に限らずだと思いますが、)開発に関する最新情報は英語でまず第一情報として公開され、その後に日本語訳された公式サイトやブログ記事等が公開されることがほとんどだと思います。

この記事ではAndroidアプリ開発を行なう上で自分がよくお世話になる英語のウェブサイト(キュレーションサイトが主です)やPodcastをいくつか雑に紹介します。

また、新しく見つけたサイト等あれば追記していこうと考えているので、他にもこれもあるよ!というものやオススメのサイト等ありましたら教えていただけると幸いです。

ウェブサイト

Androidアプリ開発に関する英語の人気記事等の紹介サイトです。毎週更新されます。最近だとKotlinの記事が多かったりする印象です。 個人のブログ記事や公式サイト等、幅広く紹介している印象です。また、チュートリアル、最新OSSの紹介、求人情報等も掲載されています。

Android Weeklyとほとんど似たようなサイトで、毎週更新されるサイトです。 ただ紹介されている記事の内容等がAndroid Weeklyでは紹介されていない記事等が多く紹介されている印象です。(重ならないように注意しているのかな?) こちらもOSSの紹介等もあります。

Androidに特化したウェブサイトではないですが、カンファレンス(Droidconなど)のイベントの録画動画が配信されることがあり、時折お世話になります。

ポッドキャスト

Donn FelkerさんとKaushik Gopalさんのポッドキャストです。 ゲストを呼んだり、少し前からEffective Javaの内容をかいつまんで紹介したりしています。

Chiu-Ki ChanさんとHuyen Tueさんがメインで行っているポッドキャストです。基本的にはゲストを一人から数人呼んでAndroidに関する話をしています。通常のPodcastとの違いに、動画による撮影があり、基本的にはYouTubeにアップロードされていることが挙げられます。

ポッドキャストだと他にAndroid Developers Backstageというものが有名と伺っているのですがまだ聴けていません・・!

まとめ

暇あればどんどん追記していきます。 (雑すぎる)

10/31(火)追記

  • ウェブサイトにSkillMasterを追記しました。
  • ポッドキャストにAndroidDialogを追記しました。

Room Persistence Libraryを訳してみた(※2017年5月末時点)

f:id:shaunkawano:20170530122939p:plain

自分へのメモ程度にRoom Persistence Library | Android Developersを日本語訳しましたので、この記事ではその内容を記載いたします。(※注意: この翻訳記事は2017年5月末時点での上記の公式ドキュメントの日本語訳です。)

雑に訳しています。タイポ、細かい表現の差異、その他誤りなどありましたら(あくまでも個人用メモとして殴り書きしておりますので、ご了承下さい。)、ご連絡頂けますと幸いです。なるべく早く修正致します。m(__)m

※2017年5月30日修正: 細かいタイポや文言の修正と、本記事の最下にCA.apk #3 - Google I/O 2017 報告会(2017/05/29開催)でのRoomに関する発表スライドを埋め込みました。

Room Persistence Library

コアフレームワークSQLコンテンツをサポートしている。API自体はパワフルだが、低レイヤーなものであるため使いこなすまでになかなかの時間と努力を必要とする。さらに、

  1. 生のSQLクエリにはコンパイルタイム検証・解析が存在しない。データグラフの変更に伴い、変更による影響を受けたSQLクエリを手動で更新する必要がある。時間がかかると同時に、バグの原因となりやすい。
  2. SQLクエリをJavaのデータオブジェクトに変換するために、たくさんの決まり文句のコードを利用する必要がある

RoomはSQLiteの抽象的レイヤーの機能を提供しつつ、上記1、2の問題を解決するためのライブラリ。

Roomには3つの主要なコンポーネントがある

1. Database

Database Holderを生成するために利用。@Databaseアノテーションを利用してEntityの一覧を定義し、Databaseクラスの内容にはDAOの一覧を記載。同様に、Databaseオブジェクトは根本的な接続のための中枢アクセスポイントである。

2. Entity

データベースの列を持つクラスを表す。それぞれのEntityに対してデータベーステーブルが生成され要素を保存する。Databaseクラスのentities配列でEntityを参照する必要がある。@Ignoreをフィールドに付与しない限り、Entityクラス内のすべてのフィールドはデータベース内に永続化される。

Note: 
Entityクラスには、(DAOクラスがフィールドに直接アクセスすることができる場合のみ)
空のコンストラクタ、もしくは最低1つのフィールドパラメーターを受け取るコンストラクタを定義する必要がある。

3. DAO

DAO(Data Access Object)のクラスもしくはインタフェースを表す。DAOとはRoomにおける主要のコンポーネントであり、データベースにアクセスするためのメソッドを定義する責務を持つ。@Databaseアノテーションが付与されているクラスは@Daoアノテーションが付与されているクラスを返す引数が存在しないabstractメソッドを持つ必要がある。コード生成を行うコンパイル時にRoomはこのクラスの実装コードを生成する。

Important:
クエリービルダーや直接クエリーを文字列として記載するのではなくDAOクラスからデータベースにアクセスをすることで、
データベース構成を別の要素に切り分けることができる。
さらにいえば、テストを行う際にはDAOによってデータベースアクセス処理を簡単にモック化することができる。
Note: 
Databseクラスの生成にはコストがかかり、
また複数のインスタンスにアクセスする必要があるケースはほとんどないため、
データベースオブジェクトの生成時にはSingletonデザインパターンに準拠するべきである。

Entities

クラスに@Entityアノテーションが付与されていて、かつそのクラスが@Databaseアノテーションが付与されているクラスのentitiesプロパティ内で参照されている場合、RoomはこのEntityクラスのデータベーステーブルを生成する。

デフォルトでは、RoomはEntityクラス内に定義されているフィールドすべてに対応するカラムを1つ1つ生成する。 もしEntityクラス内に永続化したくないフィールドが存在すれば、@Ignoreアノテーションを付与することで永続化を回避することができる。フィールドを永続化するために、Roomはフィールドにアクセスできる必要がある。フィールド自体にpublic修飾子を付与するか、セッターとゲッターを提供することができる。セッターとゲッターを利用する際には、RoomではJava Beans機構にそっている必要があることを忘れてはいけない。

Primary Key

それぞれのEntityは最低1つのフィールドをPrimary keyとして定義する必要がある。フィールドが1つしか定義されていない場合であっても、@PrimaryKeyアノテーションを付与する必要がある。同様に、RoomにEntityに対して自動ID付与をしてほしい場合、@PrimaryKeyautoGenerateプロパティをセットすることができる。もしEntityが複数のPrimaryKeyを持っている場合には@Entity(primaryKeys = {"", ""}を利用することができる。 デフォルトでは、Roomはクラス名をデータベースのテーブル名として利用する。もし別名をテーブル名として利用したい場合には、@Entity(tableName="")プロパティを利用できる。

注意:
SQLiteにおけるテーブル名はケースセンシティブである。

tableNameプロパティ同様に、デフォルトではRoomはフィールド名をデータベース内のカラム名として利用する。 もし別名をカラム名として利用したい場合には@ColumnInfo(name = "")を利用できる。

Indexとユニーク性

データアクセスの方法によっては、特定のフィールドをインデックス化してクエリを高速化したい場合がある。 Entityに対してインデックスを追加するためには

@Entity(indices = {@Index("name"), @Index("last_name", "address")})

を利用できる。

ときにはデータベース内の特定のフィールド、またはフィールドの集合がユニークである必要がある。 これらの独自性を遵守するには

@Entity(indices= { @Index(value = {""}, unique = true }

を利用できる。

リレーショナルについて

SQLiteはリレーショナルデータベースであるため、オブジェクト間の関係性を細かく指定することができる。 ほとんどのORMライブラリはEntityオブジェクト同士の参照をサポートしているが、Roomは明示的にこれを禁止している。

補足

Entity間のオブジェクト参照の禁止

データベースから対応するオブジェクトモデルへの関係性のマッピングはよく知られている実践方法であり、特に遅延読み込みされるフィールドアクセスに対するパフォーマンスが良いサーバーサイドにおいてはよく機能する。 しかし、クライアントサイドにおいては遅延読み込みはうまくいかない。なぜなら読み込み処理がUIスレッドで発生しやすく、ディスク上の情報のクエリをUIスレッドで行うことは重大なパフォーマンス問題を引き起こす可能性があるため。UIスレッドにはアクティビティのレイアウト更新のための計算と描画におおよそ16msの時間しか用意されていないため、たとえクエリにかかる時間がたったの5msのみであったとしても、アプリケーションがフレームを描画するための時間は足りなくなってしまい、ユーザーが認識できるジャンクを発生させてしまう。さらにもっと悪い場合、複数処理が同時並行で走っている際や端末自体がディスクに負荷のかかる処理を行っているとクエリにかかる時間が単純に16msを超えてしまうかもしれない。しかし遅延読み込み処理を行わない場合だと、アプリケーションは必要以上のデータを読み込み、メモリー消費問題を起こしてしまう。

ORMライブラリはこれらの議論を開発者に託しているため、開発者は自分たちが最善だと思う方法でORMをアプリ開発に利用している。不運なことに、一般的には開発者は遅延読み込みするモデルをアプリケーションとUI間の両方で利用してしまうケースに陥ってしまう。

例えば、Authorオブジェクトを持つBookオブジェクトを複数読み込むようなUIを想定する。初期段階では遅延読み込みを利用したクエリ設計によってBookオブジェクト内にgetAuthor()メソッドを用意し、Authorオブジェクトを返却するようにしたとする。この設計により初回のgetAuthor()メソッド実行時にはデータベースへのクエリ処理が走る実装となる。月日が経ち、仕様変更などによりUIに今までのものに加えて筆者の名前を表示する必要が出てきた。getAuthor().getName()メソッドを追加することでとても容易にUI実装をすることができた:

authorNameTextView.setText(user.getAuthor().getName());

このような何事もないようなシンプルな変更によって、気づかぬうちにAuthorテーブルへのクエリ処理がメインスレッドで行われるようになってしまう。

これらの理由から、RoomはEntity間のオブジェクト参照を禁止し、代わりに開発者にアプリが本当に必要なデータのみを明示的にリクエストしなければいけないような設計になっている。

直接的なリレーションは行えないが、RoomはEntity間におけるForeignKey制約の定義を許容している。

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))

ForeignKeyはとても強力で、参照されているEntityが更新されてたタイミングで何が起こるかを指定することができる。たとえば、UserオブジェクトがDBから削除されたら、SQLiteにユーザーのBook情報すべてを削除するように指定することができる。

詳細 https://sqlite.org/lang_conflict.html

オブジェクトのネスト

時々、オブジェクトが複数のフィールドを保持している場合であっても、そのEntityやPOJOクラスのオブジェクトをひとまとまりのデータベースロジックとして表現したい場合がある。それらの場合には、@Embeddedアノテーションを利用することで、特定のEntityクラス内に別の@Entityクラスを埋め込むことができる。

たとえば、UserクラスがAdressクラスタイプのフィールド(Addressクラスはstreet, city, state, post Codeのような複数のフィールドを保持したクラス)を内包することができる。

class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code")
    public int postCode;
}

@Entity
class User {
    @PrimaryKey
    public int id;

    public String firstName;

    @Embedded
    public Address address;
}

データベーステーブルはUserというテーブル名で、カラムはid, firstName, street, state, city, post_codeとなる

Note: Embeddedフィールドは同様に他のEmbeddedフィールドを内包することができる。

もしEntityが複数の同じタイプのEmbeddedフィールドを保持していた場合は、それぞれのカラムの別名性を保つためにprefixを付与することができる。

DAO(Data Access Objects)

Roomの主要なコンポーネントはDaoクラス。DAOはデータベースアクセス処理を簡潔に抽象化する。

Insert

@Insert public void insertBothUsers(User user1, User user2);
@Insert public void insertUsersAndFriends(User user, List<Friend> friends);

もし@Insertメソッドの受け取る引数の数が1つだった場合、メソッドは新しいrowIdを表すlong値を戻り値として返すことができる。もし引数が配列もしくはコレクション型であれば、戻り値にはlong[]またはListを指定できる。

https://www.sqlite.org/rowidtable.html

Update

@Update public void updateUsers(User… Users);

ほとんどの場合必要ないかもしれないが、更新がかけられた列の数を表すint型を戻り値として返すことができる。

Delete

@Delete public void deleteUsers(User… Users);

ほとんどの場合必要ないかもしれないが、削除された列の数を表すint型を戻り値として返すことができる。

@Queryアノテーションを使ったメソッド

データベース上の読み込み・書き込み処理を行うためのアノテーション。それぞれの@Queryメソッドはコンパイル時に検証され、問題があればコンパイルエラーが発生し通知が行われる。同様に、Roomはクエリメソッドの戻り値も検証し、もし戻り値のオブジェクト内のフィールド名が対応するクエリ結果のカラム名とマッチしない場合は、以下のいずれかの方法でアラートする。

  • いくつかのフィールド名だけマッチしていない場合は警告
  • 全てのフィールド名がマッチしていない場合はエラー

もしクエリがシンタックスエラーを含んでいる場合やテーブルがデータベース内に定義されていない場合も同様にコンパイル時にエラーとして通知する。

Queryにパラメーターを渡す

データベースアクセスをする際、大抵の場合はパラメータをクエリに渡してフィルター処理を行う。 このような場合にはRoomアノテーション内でメソッドパラメータを利用する。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

クエリがコンパイル時に処理される際、Roomは:minAgeバインド引数名がメソッド引数名と対応しているかを確認する。パラメータ名を利用した整合性チェックをRoomは行う。もしミスマッチが存在する場合にはエラーが発生する。

同様に複数の引数を渡したり1つの引数を複数回参照することができる。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search "
           + "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}

カラムの一部分のみを返す

大抵の場合、Entity内のいくつかのフィールドのみが必要な場合がある。たとえばUIに表示するのはユーザーのファーストネームとラストネームのみで、ユーザーに関する細かい情報すべてを読み取る必要がない場合など。そのような場合には必要なカラムからのみ情報を読み込むことで、変数へのリソース割当を少なくし、より早くデータベースクエリーを完了することにもつながる。

Roomではクエリで選択されるカラムの一覧が戻り値のオブジェクトとマッピングすることができれば、どんなJavaオブジェクトでも戻り値として指定することができる。

public class NameTuple {
    @ColumnInfo(name="first_name")
    public String firstName;

    @ColumnInfo(name="last_name")
    public String lastName;
}
@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

もしカラム数が多すぎる場合や指定されたカラムが戻り値に指定されたクラスと対応できない、存在しない場合には警告が表示される。

NOTE: そのような場合には@Embeddedアノテーションを利用できる。

引数としてCollectionを利用する

時にはランタイム時にしか正確な引数の数がわからないような場合がある。たとえばいくつかの地区に関する情報を取得したい場合、などだ。Roomは引数がCollectionであるかどうかを理解できるため、Collectionの場合には自動的にクエリが拡張されランタイム時に正確な数の引数がメソッドに与えられる。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

Observable Queries

データが更新されたタイミングでアプリケーションのUIも自動で更新したい場合がある。そのためにはRoomのDaoのメソッドの戻り値をLiveData型にする。Roomはデータベース更新と同時にLiveDataの更新に必要なコードすべてを生成する。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}
(!!ここはどう訳すのか、まだ理解が曖昧です。。!!)
バージョン1.0では、Roomはクエリでアクセスされたテーブルの一覧を利用してLiveDataオブジェクトの更新が必要かどうかを判断する?

RxJava

同様に、RoomではRxJava2のPublisherとFlowableオブジェクトをクエリの戻り値として指定できる。

android.arch.persistence.room:rxjava2

Dependencyを追加することで戻り値にPublisherもしくはFlowableを指定できる。

Cursorへの直接アクセス

もしアプリケーションのロジック上、帰ってきたデータベースのRowに直接アクセスする必要がある場合には、Cursorオブジェクトを戻り値として指定することができる。

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}
注意:
Rowが実際に存在するかどうか、またはどのような値がRowに存在するのかの保証がないため、Cursor API利用は推奨されていない。このような実装がすでにアプリ内に存在し、リファクタリングが容易にいかない場合にのみ利用するべきである。

複数テーブルクエリ

クエリには、結果を計算するために複数テーブルにアクセスする必要があるものがある。Roomはどのようなクエリ文であっても記述することを許容しているため、join文も記載することができる。RxJava2のFlowable, PublisherやLiveDataを戻り値として指定している場合には、クエリで参照されているすべてのテーブルをエラー検知のために監視する。

TypeConverter

(筆者メモ: MoshiのTypeAdapterのような機能。) @TyperConverterアノテーションをメソッドに付与し、@TypeConvertersアノテーションをDBクラス、Daoのメソッド、Entityクラス、など様々な場所に付与することで利用できる。

@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

データベースマイグレーション

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
                + "`name` TEXT, PRIMARY KEY(`id`))");
    }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Book "
                + " ADD COLUMN pub_year INTEGER");
    }
};

Migrationクラスを作成する。ランタイム時にRoomはMigrationクラスのmigrate()メソッドを順番に実行する。 もし必要なマイグレーション処理がない場合にはRoomはDBを再構築するので、データは消える。

Migration完了後にRoomはスキーマを確認しMigrationが正しく行われたかどうかを確認する。もし失敗した場合にはExceptionが発生する。

Testing migrations

データベーススキーマをエクスポートすることでマイグレーション処理のテストを事前に行うことができる。

Exporting Schemas

Roomはコンパイル時にスキーマ情報をJSONファイルに吐き出すことができる。スキーマをエクスポートするには build.gradleに以下を追加する:

android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation":
                             "$projectDir/schemas".toString()]
            }
        }
    }
}

エクスポートされたJSONファイルはバージョン管理しておくことで今後マイグレーションテストの際、Roomから古いスキーマのデータベース生成を行うことができる。

android.arch.persistence.room:testing 

dependencyを追加する。 スキーマ管理を行っているパスをasset folderとして指定する。

android {
  … 
  sourceSets {
    androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
  }
}

testingパッケージにはMigrationTestHelperクラスが存在し、このrunMigrationsAndValidate()メソッドを実行することでhelperクラスが自動でスキーマ変更を検証する。データの変更に関しては自分で検証する必要がある。


最後に

上記が自分が行ったRoomの公式ドキュメントの和訳の全文となります。メモ程度に書いたものですので、あくまで参考程度に気になった箇所をつまむ程度に読んでいただけると幸いです。

また、2017年5/29日にCA.apkにてRoomに関する発表を行いましたので、Roomに関する情報をお求めの方はこちらも合わせて御覧ください。