Continuity is The Father of Success

Androidアプリとかゲームとか。毎日続けてるものについて。

Github Actionsの悩ましいところ

昨日Githubがリリース出してGithub Actionsがprivateでfreeプランでも使えるようになりました。

github.blog

そこでGithub Actionsでキャッシュを設定するにあたり調べたことをまとめておきます。

help.github.com

github.com

CircleCIとかBitriseとかのサービスから移行したとき、混乱するのがキャッシュ周りだと思います。 実行環境のスペックを見てみると分かる通り、環境自体にそこまでの差は感じません。

Github Actionsのキャッシュが独特な点は、Github Repositoryのブランチが影響する点です。 キャッシュの key はCircleCIのキャッシュと同じような key の一致判定と、一致しなかった場合に検索される restore-keys になります。 (Bitriseのキャッシュはあれはあれで特殊なので、比較は控えておきます。。。)

- uses: actions/cache@v1
  with:
    path: ~/.gradle/caches
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

https://github.com/actions/cache/blob/master/examples.md#java---gradle

保存されるキャッシュは「1つのRepositoryあたり5GBまで」です。 CircleCIやBitriseでは明示的に制限されていない箇所になるため、大量のPRが同時に存在したりするケースでは期待するキャッシュの動作を異なることがあり得ます。

https://help.github.com/ja/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy

扱いが難しいのは、キャッシュのアクセス制限です。 キャッシュへのアクセスについての制限から引用をします。

pull_requestのclosedイベントの場合を除く、push及びpull_requestイベントで起動されたワークフロー内のキャッシュにのみアクセスできます。 詳しい情報については、「ワークフローをトリガーするイベント」を参照してください。

ワークフローは、現在のブランチ、baseブランチ(フォークされたリポジトリのbaseブランチを含む)、デフォルトブランチ(通常はmaster)で作成されたキャッシュにアクセスし、リストアできます。 たとえばデフォルトブランチのmasterで作成されたキャッシュは、どのプルリクエストからもアクセスできます。 また、feature-bブランチがfeature-aをbaseブランチとして持つなら、feature-bで起動されたワークフローはデフォルトブランチ(master)、feature-a、feature-bで作成されたキャッシュにアクセスできます。

アクセス制限は、異なるワークフローとブランチ間の論理的な境界を作成することによって、キャッシュの分離とセキュリティを提供します。 たとえばfeature-aというブランチ(baseブランチはmaster)用に作成されたキャッシュは、feature-bというブランチ(baseブランチはmaster)へのプルリクエストからはアクセスできません。

なお"baseブランチ"はGitの概念にない(調べた限り……)ため、GithubのPRで指定するマージ先のブランチを指していると思われます。

help.github.com

CircleCIのように key が一致したかどうかだけで考える必要がないため、誤ったビルドキャッシュを利用することによるビルドクラッシュが防がれます。一方で、先述の5GB制限もあるため想定外のタイミングでキャッシュがヒットしないことも発生します。

つまり、ステップの記述と動作までは問題なく済んだ場合でも、キャッシュが想定通りに扱われているかは注意深く調整する必要があります。 一度作ったキャッシュを使い回すことでビルド時間を大幅に短縮していた場合には、想定外のビルド時間延長が発生することがあり得るため、ご注意ください。

ここまでが少し"ネガティブ"な話をしてしまったので、最後に"ポジティブ"な話を一つ。

Github Actionsのcacheステップは、呼び出した順に"restore"を実行し、その逆順に"store"を実行します。実例は下記のステップとその実行結果を見てみてください。

Studyplus-Android-SDK/unit_test.yml at 2.6.2 · studyplus/Studyplus-Android-SDK · GitHub

Update README · studyplus/Studyplus-Android-SDK@78bb889 · GitHub

このためキャッシュ→タスクの実行という処理を書けば、キャッシュのリストア→タスクの実行→キャッシュのストアがタスクが実行した時のみ行われるようになります。 結果として、ビルドキャッシュの保存が非常に簡単に行えるようになりました。

キャッシュへのアクセス制限にある通り"baseブランチ"で作成されたキャッシュを利用することができるため、適切に扱えば機能開発時のDangerチェックを簡単に差分ビルドで行うことができるようになっています。

自分もまだまだGithub Actionsに入門したばかりで、できることはわかっていても、なかなか最適なステップを組むことができていません。 是非、便利な使い方やより正しい理解などを指摘いただければと思っています。

Pagingライブラリ3.0で入りそうな変更の話

ここのところ諸事情があってPagingライブラリの開発中コードを見に行っていたのですが、面白そうな変更が入っていたので技術メモ。 まだalphaにもなっていないのでまだまだ変更はあるだろうし、リリースされないかもしれません。

PagedListがPagedDataへ

PagedList *1PagingData *2 に置き換えになります。 大抵の場合 PagingListPagedData も直接いじることはありません。 つまりRepository層でPagingライブラリを利用して作成したらRecyclerViewのAdapterにそのまま渡すだけ、と考えられるため実装上では影響はほぼありません。

@Deprecated("PagedList is deprecated and has been replaced by PagingData")
abstract class PagedList<T : Any> internal constructor(
    /**
     * The [PagingSource] that provides data to this [PagedList].
     *
     * @suppress
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    open val pagingSource: PagingSource<*, T>,
    internal val storage: PagedStorage<T>,
    /**
     * Return the Config used to construct this PagedList.
     *
     * @return the Config of this PagedList
     */
    val config: Config
) : AbstractList<T>() {
class PagingData<T : Any> internal constructor(
    internal val flow: Flow<PageEvent<T>>,
    internal val receiver: UiReceiver
) {

すんごい余談になりますが、今年のDroidKaigiへ出したネタの中で一番自信があったのが「Pagingライブラリと組み合わせるならPagedListAdapterとGroupieとEpoxyのどれがいいの?」という話でした。 RecyclerView.Adapter を独自に拡張してPagingライブラリへの対応を行っていたりする場合、ライブラリ側で対応するためにはKotlin Coroutinesをライブラリの依存関係に追加することが必要になりそうです。 今回の更新により、結論は PagingDataAdapter を使う話になりそうですね。なんてこったい。

PagedListAdapterがPagingDataAdapterへ

PagedListAdapter *3PagingDataAdapter *4 に変わっています。

@Deprecated(
    message = "PagedListAdapter is deprecated and has been replaced by PagingDataAdapter",
    replaceWith = ReplaceWith(
        "PagingDataAdapter<T, VH>",
        "androidx.paging.PagingDataAdapter"
    )
)
abstract class PagedListAdapter<T : Any, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH> {

    /**
     * Creates a [PagedListAdapter] with default threading and
     * [androidx.recyclerview.widget.ListUpdateCallback].
     *
     * Convenience for [PagedListAdapter], which uses default threading behavior.
     *
     * @param diffCallback The [DiffUtil.ItemCallback] instance to
     * compare items in the list.
     */
    protected constructor(diffCallback: DiffUtil.ItemCallback<T>) {
        @Suppress("DEPRECATION")
        differ = AsyncPagedListDiffer(this, diffCallback)
        differ.addPagedListListener(listener)
    }
    protected constructor(config: AsyncDifferConfig<T>) {
        @Suppress("DEPRECATION")
        differ = AsyncPagedListDiffer(AdapterListUpdateCallback(this), config)
        differ.addPagedListListener(listener)
    }
abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder>(
    diffCallback: DiffUtil.ItemCallback<T>,
    mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
    workerDispatcher: CoroutineDispatcher = Dispatchers.Default
) : RecyclerView.Adapter<VH>() {

AsyncDifferConfig をセットしていたケースでは修正が必要になるっぽいのですが、大抵の場合 DiffUtil.ItemCallback を使うので置き換えは簡単そうな見込みです。 一方で当然のように Dispatchers.Main がdefault valueとして指定されているなど、構造がマルっと変わっています。 Kotlin CoroutinesがPagingライブラリに導入されていく話はKotlin愛好会の技術同人誌*5に寄稿しているので、興味があればお手に取ってみてください。(露骨な宣伝)

これまた余談なのですが MergeAdapter が入っていたりと、いくつか実装の参考にできそうなところもあります。

    /**
     * Create a [MergeAdapter] with the provided [LoadStateAdapter]s displaying the
     * [LoadType.START] and [LoadType.END] [LoadState]s as list items at the start and end
     * respectively.
     *
     * @see LoadStateAdapter
     * @see withLoadStateHeader
     * @see withLoadStateFooter
     */
    fun withLoadStateHeaderAndFooter(
        header: LoadStateAdapter<*>,
        footer: LoadStateAdapter<*>
    ): MergeAdapter {
        addLoadStateListener { loadType, loadState ->
            if (loadType == LoadType.START) {
                header.loadState = loadState
            } else if (loadType == LoadType.END) {
                footer.loadState = loadState
            }
        }
        return MergeAdapter(header, this, footer)
    }

カスタムのAdapterを作るときに、参考にしていけそうです。たのしみ。

PageKeyed / ItemKeyed / PositionalDataSourceがPagingSourceへ

DataSource の実装である PageKeyedDataSource *6 , ItemKeyedDataSource *7PositionalDataSource *8PagingSource *9 に置き換えられます。

この影響は loadInitial loadBefore loadAfter の各メソッドで書き分けられていた処理が load メソッドにまとめられ、 LoadType *10 で処理を分ける形に変わります。

enum class LoadType {
    /**
     * [PagingData] content being refreshed, which can be a result of [PagingSource]
     * invalidation, refresh that may contain content updates, or the initial load.
     */
    REFRESH,
    /**
     * Load at the start of a [PagingData].
     */
    START,
    /**
     * Load at the end of a [PagingData].
     */
    END
}

小さな変更ですが、Pagingライブラリの更新で書き換えが発生する個所となります。 loadsuspend になることと合わせて、簡単な更新の準備をしておいた方が良さそうです。

おわりに

正式リリースが楽しみです。 首を長くして待ちたいなと思います。

*1:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/PagedList.kt

*2:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/PagingData.kt

*3:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/runtime/src/main/java/androidx/paging/PagedListAdapter.kt

*4:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/runtime/src/main/java/androidx/paging/PagingDataAdapter.kt

*5:Love Swift & Kotlin #2

*6:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/PageKeyedDataSource.kt

*7:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/ItemKeyedDataSource.kt

*8:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/PositionalDataSource.kt

*9:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/PagingSource.kt

*10:https://android.googlesource.com/platform/frameworks/support/+/2a9a199e0b4b2967f3e7446c29bd97ec2d7510f9/paging/common/src/main/kotlin/androidx/paging/LoadType.kt

Androidアプリ開発を始める 2020年春版 ライブラリ編

全体の概要からの続きみたいなもの。 前回がアプリ開発を始める時にざーっと考える(ベースであればとりあえず開発が開始できる)話だとすると、今回はアプリを作る時に何を組み合わせるかと言う話にしたいなと思っています。

blog.dr1009.com

去年のエントリがこちら。

blog.dr1009.com

雑感

「今から新規でアプリを作ります」という時に選択するライブラリについて、技術として2019年から2020年にかけて大きく変わったと言う印象はありません。 このため、ライブラリの選定が変わったもの以上にライブラリの枠組みが変わった点を2点あげ、ライブラリの読み方を書いておきたいと思います。

ktx

ktx は様々なライブラリのKotlin用拡張関数ライブラリです。 リリース時の資料なんかはこちら。

jakewharton.com

Android Jetpackでも提供されている core-ktx や、fragment-ktx などは知らない間に使っているかもしれません。

developer.android.com

一応コードを引用して紹介しておくと、ktxを使わないケースと使うケースが次のようになります。

// 使わない
sharedPreferences
            .edit()  // create an Editor
            .putBoolean("key", value)
            .apply()

// 使う
sharedPreferences.edit { putBoolean("key", value) }

// 使わない
sharedPreferences
            .edit()  // create an Editor
            .putBoolean("key", value)
            .commit()

// 使う
sharedPreferences.edit(commit = true) { putBoolean("key", value) }

この時、ktxで提供されているのは次のような拡張関数です。

inline fun SharedPreferences.edit(
         commit: Boolean = false,
         action: SharedPreferences.Editor.() -> Unit)

ktxは「Kotlinでライブラリを利用する時に、利用を簡単に/便利にする」ような位置付けであって、Kotlinのプロジェクトでも別に必須のものではありません。 2020年に大きく変わったのは、様々なライブラリがktxの提供を始めたことです。

例えば、Firebaseにktxが追加されるようになりました。 個人の経験になりますが、下記のktxは実際に利用して(それなりに)メリットを感じています。

また、3rd partyのライブラリでもktxが提供される流れもあります。 ライブラリの選定時にktxが提供されているかを調べてみるのも良さそうです。 (後述のCoilのように、そもそもKotlinで作りJavaでも利用できるようにしているライブラリもあるので、数年後にはktxは消えているかもしれませんが)

github.com

※注意、PermissionsDispatcherのktxは2020年4月5日時点でalphaリリースです

BOM

docs.gradle.org

Android向けのライブラリがBOM(bill of materials)に対応するようになりました。 利用シーンが多いのはFirebaseやOkHttpではないかと思います。

BOMGradle 5.0より導入された機能になります。 この機能を用いると、BOMで管理されるライブラリのバージョンをそれぞれ指定する必要がなくなります。

firebase.google.com

SpringのBOMなどは割と利用されている印象があるので、サーバーサイドの方からすると「ようやくか」というところかもしれません。

mvnrepository.com

FirebaseやOkHttpのような一つのライブラリの中でも複数のモジュールが存在するもの、またライブラリの中でも依存してる可能性が高いものについて。 BOMの活用によりより快適なビルド環境が構築されそうな予感がしています。

ライブラリ

Android Jetpack

developer.android.com

TargetSDKの関係でAndroid Jetpackの利用をする年となりました。

developer.android.com

旧来のsupport libは28の時期までしか更新されていないため、29以降が必須となる今年はAndroid Jetpackに移行する(AndroidXライブラリに移行する)必要があります。 とはいえ少し古い資料を参考にしつつAndroidXへのマイグレーション処理を行えば、旧来のバージョンからの変更を行うのは難しくありません。

大変なのは、AndroidXになり新たに提案されつつあるAPI更新に対応することです。 特に変更が大きいクラスはFragmentでしょう。

developer.android.com

Fragmentを利用してアプリを作成する場合には、1.0.0から最新までの変更点を全て確認することを強くお勧めします。

Network

square.github.io

square.github.io

github.com

github.com

REST APIであればOkHttp + Retrofit2 + Moshi-Codegen、GraphQLであればOkHttp + Apollo-Androidになるかと思います。 gRPCの場合は……リファレンスを参考に実装することになるかと。

OkHttpとRetrofit、MoshiがKotlinでリライトされました。 一時期移行に伴う不具合が散見されましたが、2020年4月5日時点では収束したと言えます。

昨年からの大きな変更としては、Gsonが下火になったことではないでしょうか。 下記の問題はGsonの利用を続けることを非常に難しくしました。

r8.googlesource.com

確認したところ開発が続いているようですが、KotlinとGsonの仕組みの相性がよくない(ハズ)のため、新規に開発を行うのであればMoshi-Codegenをお勧めします。

github.com

DI

Android Developerにページができました。

developer.android.com

Daggerの使い方も記載されるようになり、今後より書きやすくする対応を始めているところのようなので、Daggerを選んでおけば良いと思います。

developer.android.com

dagger.dev

medium.com

Image

developer.android.com

現時点では下記4つの選択肢があります。 このうち、Picassoは(開発が動いている様子はあるのですが)リリースが止まっているためお勧めできません。 Frescoは ImageView の代わりに com.facebook.drawee.view.SimpleDraweeView を利用する点などがあるため、ライブラリへの依存が(他のライブラリに比べると)強くなってしまう印象があります。このため、自分はあまり選択しようと言う気持ちになりません。

GlideとCoilを比べると、Glideの方が(歴史が長いため)APIの変更などの開発が少なく、使用例も豊富な印象です。 一方でCoilは0.9.5になりstableリリース間近であることや、Glideに比べてメソッド数が少ないことなどから、利用しても良いかなと感じるようになっています。

ゼロからKotlinでアプリを作るのであれば、Coilで開発を始めてみても良いのではないでしょうか。

Other

ViewBinding

ライブラリに含めるか迷ったのですが、一旦ここで。

developer.android.com

medium.com

リンクの内容そのままなのですが findViewById を使わなくてよくなるライブラリです。 DataBindingじゃないかと言う気もすると思うのですが、データのバインドをしないケースならViewBindingを利用できるようになりました。

これにより、ButterKnifeがDeprecatedになりました。 古いプロジェクトであれば、これの影響が一番大きいのではないかと思います。

github.com

PermissionsDispatcher

github.com

RxJavaではなくKotlin Coroutinesを使うのであればRxPermissionsを使う必要性も低いと思うので、Androidパーミッション周りで困ったら使いましょう。 先述の通りktxによる利用も、kaptを利用する場合もincremental buildへの対応もされているので、それなりのPCを利用していればビルドで気になることはないと思います。

ThreeTenABP

github.com

LocalDateOffsetDateTime などのJava8で提供されるTime & Date APIAndroidで利用するためのライブラリです。 新規アプリであればAndroid O以降を対象にして開発が開始されるであろうことや(しますよね?)、Android Gradle Pluging 4系で入るJava8 APIのR8によるDesugarにより、来年には不要になっていそうです。

jakewharton.com

一部のライブラリが依存していることもあるので、ゆっくりとライブラリ側の対応を待ちつつ(もしくはPRを作りつつ)、移行していく1年になりそうです。 本当にお世話になりました。

終わりに

ざっくり書くつもりが去年よりも長いエントリになってしまいました。 若干誰得な話になっている気がするのですが、本エントリが開発の助けになれば幸いです。