minSdkVersionを19->23にする時に考慮したこと
こんにちは、tkyです。
私が業務で担当しているAndroidのアプリサポートバージョンをAPI19->23に引き上げました。
すでにリリースから数ヶ月経過しましたが、その時にやったことなどをまとめたいと思います。
OSの分布を知る
- 世界的なOS分布 developer.android.com
プロダクトのOS分布
アナリティクスツールなどで確認
切りたいバージョンのユーザーが何%行っていたら切るか、というのは社内でもそこまで定まっておらず、他の会社で採用している5%や3%などといった数値を参考に踏み切りました。
ちなみに5系以下は全体の6%でした。そこそこいました。
社内に持ちかけ
POと話す機会は何回かあり、以前より
- 古いOSVerでWebViewの表示不正やクラッシュがある
- 特定の動画フォーマットが再生できない
などの問題が起こっていたことと、たまたまOSサポートバージョンの話になった時に「やりましょう」という流れになりました。
開発しやすくなるなら、という目線で快く進める方向になったのは一番大きいです。
(正直意思決定権を持つ人が納得してくれるかがサポートバージョンを上げる最難関ポイントだと思っています)
クリティカルな不具合は直しておく
目に見えてわかっている不具合は予め対応しておきましょう。後に修正できても古いOSを持つユーザーにはそれは届きません。
minsdkversionを23に引き上げる
単純にapp.gradleの記述を返るだけです。
- minSdkVersion 19 + minSdkVersion 23
MultiDexサポートライブリは不要に
minSdkVersion が 21未満の場合、64k制限のためにMultidexオプションを付けることがありますが、
minSdkVersion が 21 以上の場合は multidex がデフォルトで有効のため、multidex サポートライブラリは必要ありません。
ApplicationクラスやGradleにmultidex関連の処理を書いている場合削除しておきましょう。
VERSION分岐コードの精査
例えば以下のようなコードがプロジェクト内に存在するとします。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // lollipop以上でしか使えないAPIを使用するコード doSomething() } else { // lollipop未満の場合はこちらを使う doSomethingLegacy() }
minSdkVersionが23になる場合、lollipop未満の条件に当てはまることが無いので
doSomething()
だけで問題ないことになります。条件が if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
であっても同様ですね。
このように VERSION_CODES
でGrepをかけて削除できそうな箇所を探してリファクタリングしていきました。
Marshmallowでランタイムパーミッションなどが入っているでこういった条件分岐を書いたりしている箇所があるかもしれません。自身のプロジェクト内を探してみてください。
動作確認で何をしたか
特に何もしていませんが、検証端末に5系端末を使っていたので念の為インストール出来ないよね、という確認はしました。
次回からはこの端末もお役御免という形になりました。ありがとう。
ユーザーに事前に通知すること
運用面で考慮したことですが、事前にOSのサポートが終了となる旨をユーザーにお伝えしなければびっくりしてしまいますよね。
そのため直前のバージョンを以てサポートを終了とする旨をお知らせページなどで通知するようにしました。
ユーザーはアプデできないということを意識する
当たり前ですが、5系を切ると5系を使っているユーザーは最新Verにアプデができなくなります。
サーバーレベルでサポートを切るかどうかは別の話で、今回はアプリをアプデできないもののサービスは引き続き利用できる状態としました。
アプリ起動時「新しいバージョンのアプリがあります。アップデートしてください」というようなアプデを促すダイアログを出しているアプリは多いでしょう。
ただし5系ユーザーにこのダイアログを出すとPlay Storeに行っても勿論最新アプリはないので混乱を招いてしまいます。
実際にアプデが入ったときのユーザーの行動を事前に予測しておくことが大事だと思いました。
リリース後に起こった問題について
- 間違ったAPIを消していた
WebViewのとあるメソッドで、TARGETがLOLLIPOPのメソッドがあったのでそもそも使われることが無いだろうというので消したコードが実はMASHMALLOW以下のメソッドだった。。
WebViewに手を入れるときは全OSバージョンチェックしたほうが良いな・・・と改めて反省・・・
まとめ
- OSサポートバージョンを23に引き上げた
- これでWebView関連の不具合に当たることも少なくなるといいな
- 他にもこんなことを考慮した、みたいなのがあればぜひ教えていただきたい
Go言語でgit checkoutを補助するコマンドを作った
こんにちは、tkyです。
Go勉強してコマンドを作ってみました。
git checkout
をサポートする gith
というコマンドです。
こんな感じにブランチが選択式で表示されるので選べばそのブランチに移動できるようなものです。
リモートブランチをチェックアウトするときはチェックアウトするブランチ名を決めることができます。
※例えばorigin/developブランチからfeature/XXXXのようなことをできるように
主要モジュール
これがすべてです。対話式のコマンドを完成させるのに一番ラクな方法かと思います。
サンプルも結構あって実際に見て使い方の理解も深まりました。
また今回はコマンドに引数とか細かいことは何もやらないでいたために urfave/cli
使用はしませんでした。
作った経緯など
僕はSourceTreeやGitKrakenなどのGUIツールは使っておらず、CUIでブランチの移動とかcommit, pushなどをしています。
gitでブランチを切り替えながら作業している時に
「あれ〜?あのブランチ名なんだったけ〜??」
となることがよくあります。
例えば「ホーム画面のレイアウトを修正」で
- feature/home_layout
- feature/home_screen_layout
- feature/update_home_layout
なんだったかな〜と・・・ブランチ名をノリで付けている弊害であとで困る典型的なパターンに陥っています。
一応運用回避としてGitHubのIssueで管理している場合、
feature/#{IssueMumber}_your_task_name
という感じでIssue番号をプレフィックスにつけておくと判別できますね。
とはいえやっぱりブランチ名調べて git checkout
する必要があるのでもう少し楽にしたいな〜というところからコマンド作成に至りました。
Goを選んだ理由は「Goだとコマンドが作りやすい」「Goやったことないからやってみたい」です。
Goの勉強方法
Tour of Goやってました。基本ここで学びます。
あとはこちらのスライドで並行で理解していく感じでした。
ざっくりとこの辺を理解してからコマンド作成をはじめました。
- 変数の定義方法
- 関数の定義方法
- for文, if文の定義方法
- ポインタ
- 配列、スライス
- 構造体
※ポインタについてはもともとC言語やってたので何も困ることはありませんでした
おまけ
.bash_profile
とかにこんな感じのことを書いておくと検索文字列に引っかかった一番最初のブランチをチェックアウトする事ができます。
コマンドを作る方法は色々ありますね!
fun gitissue() { git branch | grep $1 -m1 | xargs git checkout }
$gitissue #123
みなさんも自作コマンドで開発を楽にしていきましょう!
GitHubActionsとCircleCIを使い分ける
こんにちは、tkyです。
今私が携わっているAndroidプロジェクトのCIは基本的にCircleCIを採用しています。
そんな中、同時にGitHubActionsも採用した経緯とどんなことをしているのか、ちょっとハマったことを書き留めておこうと思います。
また、本記事は「こうしたら良い」というわけではなく、「とある課題から解決策を模索した」結果なだけなので
皆さんのプロジェクトに合わせて最適な形を模索するための材料の1つとなれば幸いです。
今までのCIの運用
コードはGitHubで管理しており、PullRequestをトリガーにCIが回るようになってます。
※コミット単位にするとCIの作動回数が多くなるので節約の目的があります。
- UnitTest
- Lint
- DangerでTestとLint結果を報告
- DeoloyGateのdebug apkアップロード
[課題]developブランチとapkが一致しない
pull requestごとにdebug apkが作られるため最新Developブランチとapkが一致せず、debug用のapkはPRの動作確認用になっていました。
しかも実際はブランチをチェックアウトして自分でビルドして動作確認しており現状のapkは事実上形骸化していました。
チームメンバーと会話して「develop最新と紐付いているapkを常に確認できる状況作っておきたいよね」ということで
DeoloyGateのdebug apkアップロード
のワークフローのトリガーを
developブランチにマージされた時(developにコミットされた時)に変更します。
Circle CIの設定見直し
現在CircleCIの管理画面ではこの2がONになっています。
- Only build pull requests (PRコミットのみCIが回る)
- Auto-cancel redundant builds (そのブランチの最新のコミットでCIを回す。古いものは自動キャンセル)
developブランチにマージされた時にしたい場合 Only build pull requests
をOFFにしてfilterでdevelopのみ動作するワークフローを作ればよいのですが
そうするとPR以外のリモートへのコミットでもCIが回るのでそれもなぁ・・・という感じです。
上記をを許容できるならGitHubActions使う必要ないのでこの話は終わりですw
もう少し頑張りたいよね、というところでdevelopブランチにコミットされた時のトリガーだけをGitHubActionで切り出すことにしました。
実際に検討で使った図です
そんなこんなで以下のように話をまとめて現在のプロジェクトで2つのCIツールを運用している、という話でした。
- CircleCI: PRした時に処理したい場合
- GitHubActions: (特定ブランチに)コミットされた時に処理したい場合
なぜGitHubActionsにすべて移動しないのか?
GitHubの各トリガーに柔軟に対応できるGitHubActionsを使えばやりたいことは全部できるでしょう。
しかしながら後述するskip-buildの機能やSSHデバッグなどCircleCIがデフォでできる機能は各々でアクションを定義する必要があります。
ローカルCLIもないため実際にPR上げてtry&errorで動作確認になるのでコストも高いです。(一応CLI実行できるnodeプロジェクトがあったような気がする)
仮に私がaction全部作れたとして、ほかメンバーがそれを管理運用できるか、という問題も浮上してきます。
そういう部分もあり、いきなり全てをGitHubActionsにして逆にコストがかかることを考慮して一部だけ切り出しただけなので、今後の状況によってはありえるかなと思っています。(当分先になりそうですが・・・)
GitHubActions導入してみた
こんな感じのymlを作成します。これがDeploygateにアップロードする(gradleタスクを実行する)ワークフローとなります。
色んな人がAndroidのビルドしていたりするのでコピペで色々貼り付けてます。
.github/workflows/upload_apk.yml
name: upload_apk # CIツールの使い分け # ・CircleCI: PRした時に処理したい場合 # ・GitHubActions: (特定ブランチに)コミットされた時に処理したい場合 on: push: branches: - develop jobs: build_canceller: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: stop old workflow uses: yellowmegaman/gh-build-canceller@master with: github_token: ${{ secrets.GITHUB_TOKEN }} workflows_filter: "upload_apk" deploygate-develop: name: Upload apk to deploygate for developDebug runs-on: ubuntu-latest needs: build_canceller steps: - name: Check out uses: actions/checkout@v2 - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: NDK cache id: cache-primes uses: actions/cache@v2 with: path: ${ANDROID_HOME} # GithubActionsコンテナに入っているNDKバージョンとプロジェクトの必要NDKバージョンが異なるため # コンテナに必要NDKバージョンをインストールする # ビルドエラーが出た場合cacheステップとInstallステップに記述のバージョンを更新してください key: ${ANDROID_HOME}/ndk/21.0.6113669 restore-keys: | ${{ runner.os }}-ndk- - name: Install NDK run: echo "y" | sudo $ANDROID_HOME/tools/bin/sdkmanager --install "ndk;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT} - name: Gradle cache uses: actions/cache@v2 with: path: ~/.gradle key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: | ${{ runner.os }}-gradle- - name: Upload DeployGateDevelopDebug run: | ./gradlew uploadDeployGateDevelopDebug
[ハマったこと]古いビルドはキャンセルされない
CircleCIのskip-buildと同じような機能はGitHubActionsにはないので自力でなんとかする必要があります。
そういうActionがあるのでありがたく使わせていただくことにします。
GitHubActionsを無料稼働枠内で使うための節約テクニックみたいな感じです・・・
※2020/11/21現在最新は
build_canceller: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: stop old workflow uses: yellowmegaman/gh-build-canceller@master with: github_token: ${{ secrets.GITHUB_TOKEN }} workflows_filter: "upload_apk"
以上です。
Kotlin applyから理解するレシーバー付きラムダ
こんにちは、tkyです。
『レシーバー付きラムダ』という用語をご存知でしょうか。Kotlinインアクションとかで使われている表現です。
英語では Function literals with receiver
と言われています。
今回は apply{}
がどのように動作するのか確認しながら『レシーバー付きラムダ』何なのかというものを理解して行きたいと思います。
ラムダ式とは、スコープ関数、拡張関数といった用語の説明はしません。
apply
まず apply
をおさらいします。こんな感じでインスタンスを生成したあとにプロパティに値を設定したりとかそんな使い方をするのが一般的ですね。
class User(val name: String) { var age: Int = 0 var email: String = "" } val user = User("ticktakclock").apply { age = 10 email = "example@example.com" }
apply
の実装を見てみるとこの様になっています。
public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }
注目していただきたいのは関数の引数である block: T.() -> Unit
です。
引数にラムダを渡しているわけなのですが、 T.()
というように Tの拡張関数のような表現になっていますね。
ここで拡張関数のドキュメントを見に行きます。
以下抜粋
拡張関数を宣言するには レシーバータイプ (receiver type) を関数名の前に付ける必要があります。 次の例では、 swap 関数を MutableList<Int> に追加しています: fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' がリストに対応する this[index1] = this[index2] this[index2] = tmp } 拡張関数内での this キーワードは、レシーバオブジェクト(ドットの前に渡されたもの)に対応しています。これで、この関数を任意の MutableList<Int> からでも呼べるようになりました:
はいでました!レシーバー!
この関数を実行(受ける)ときのthisの型またはオブジェクトを レシーバー
というのですね。
swap関数をレシーバーはMutableList
block: T.() -> Unit
は拡張関数で定義しようとすると次のようになりますが、このときのthisはTですね。別の視点からみてもこの理解でおおよそあってそうです。
fun T.block() { // thisはT }
ラムダ式におけるthisはあくまでそのラムダ式を実行するオブジェクトとなりますが、 レシーバータイプを指定するとことでthisの型を指定することができるということです。
いったん apply
のドキュメントを見に行ってみましょう。
コンテキストオブジェクトは、レシーバー(this)として使用できます。
ここでもちゃんとレシーバーが出てきました。
新しい単語(コンテキストオブジェクト
)も出てきました。関数のスコープ内で使用する特定のオブジェクトのことを指しているのであっていると思いますが、その特定のオブジェクトにthisでアクセスできるということですね。
続いてラムダ式のリファレンスも見に行ってみましょう。きっと何かが書いてありそうな予感。
ありました。関数型はレシーバーを使用することで関数型のインスタンスを呼び出す事ができる、というようなことが書いてあります。
applyに戻ります。
class User(val name: String) { var age: Int = 0 var email: String = "" } val user = User("ticktakclock").apply { // applyを呼んだUserインスタンスをレシーバーとしてラムダ式を実行する age = 10 // thisはUserインスタンスなのでthis.age =10 と同じ email = "example@example.com" }
おお〜理解できるかも〜!
まとめ
- レシーバー付きラムダというのは拡張関数をラムダ式で記述したもののこと
- レシーバーというのはこの関数を実行(受ける)ときのthisの型またはオブジェクトのこと
- ドキュメントって何でも書いてあるんだなぁ
理解の仕方や調べ方の助けになったら幸いです。
LiveDataをどのレイヤー(Repository層)まで許容するか
こんにちは、tkyです。
先日、とあるcodelabをやっていて『Repository層でLiveDataって使っていいの?』と感じたことがあり。
この疑問からLiveDataを使用するレイヤーとそこから生じるメリット・デメリットなどを考察した内容をまとめておきます。
こういうことを考えながらプログラミングしてるんだなと感じていただけたらと思います。
結論から申し上げると『用法用量を守ればRepository層にLiveDataを使っても良い』になりました。
codelabs.developers.google.com
上記Codelabではkotlin coroutine Flowの使い方を学習するためにあえてそういった書き方をしているとも思いますが、後のYoutubeリンクのDevsummitでも同様の書き方が見られました。
LiveDataとは、といった事などに関することは触れません。
何を疑問視しているのか
私のLiveDataの普段の使い方はこんな感じです。特に言うことはないです。
- ViewModel <-> Repository でデータの取得
- 取得したデータをLiveDataにpostValue()する
- FragmentまたはDataBindingでLiveDataをobserveしてViewに反映する
interface BookRepository { fun fetchBooks(): List<Books> } class BookViewModel(private val bookRepository: BookRepository): ViewModel() { private val _books = MutableLiveData<List<Book>> val books: LiveData<Book> = _books init { runCatching { val books = bookRepository.fetchBooks() _books.postValue(books) } } } class BookFragment: Fragment() { fun onViewCreated() { bookViewModel.books.observe(Observer{ books -> // リスト更新 }) } }
このときのRepositoryでLiveDataを直接返すようにするのってありなの?という疑問です。
interface BookRepository { fun fetchBooks(): LiveData<List<Books>> } class BookViewModel(private val bookRepository: BookRepository) { val books: LiveData<Book> = bookRepository.fetchBooks() }
LiveDataはViewModelまでかなと思っている派
疑問を持ったということは想定と実態が異なっていたためですが、以下の理由でRepository層にLiveDataを持ってくることにためらいを持っていたために生まれたものでした。
- RepositoryのテストにAndroidライブラリの依存が入る
- LiveDataはViewにリアルタイムに通知するものだからViewに近い層で利用するほうが管理しやすそう、という先入観
逆にこの2つくらいしかなくて、普通ViewModelまでしか使われていないから程度のことしか出てこないですし、大きい否定材料にもなりません。
Repository層までLiveDataを持ってきた場合何が良さそう?
- ViewModelでデータを一次受けしないでよくなる
- _books.postValue(books) のような記述をしなくて良いということ(特にメリットとも言えない・・・)
- LiveDataが最新の値を保持するためキャッシュっぽい動きしてくれそう
逆にデメリットも考えてみました
- LiveDataは複数でObserveするのには向いてなさそう
- 2箇所でObserveしても1個しか処理しないということ
- 特定の場合のみpostValueするみたいな仕様が入ってしまう場合Repositoryにロジックが入る。設計が乱れる。
- RxならViewModel側でFilterすればRepositoryにロジックが入ることなく実装できそう
- RepositoryのテストにAndroidライブラリの依存が入るためテストしづらい
- 実際Contextは使われていないのでUnitテストで処理できますが例えばKotlin MultiPlatformProjectなどではLiveData使えない
RoomはLiveDataを返すクエリをサポートしている
冒頭にも書いたとおりFlowを導入するために敢えてRepository層までLiveDataを持ってきて書き換えるという手順をやっているのかなと思いましたが、
RoomでRepository層とDaoまでLiveData使ってるじゃん・・・!ということはダメというわけでもないのかと考えます。
気になったのはこの一文
『このパターンは、データベースに格納されているデータと同期したデータが常に UI に表示されるようにするうえで役立ちます。』
こういったユースケースを実現する場合は許容しても良いのかもしれません。(前提としてRoomもAndroidライブラリだからLiveDataが入っててもおかしくない)
Dev Summit から得た知見
以下の'18セッションで実際にRepositoryにLiveDataを扱うケースが存在しており、許容されていそうということが伺えます。
また同時にアンチパターンについても解説がありました。
- HTTPリクエストから得た巨大なデータ群にLiveDataを使うとその分がメモリにとどまる
- APIのRepositoryには使わないほうが良さそう
- LiveDataインスタンスを複数画面で共有すると意図しないUI更新が起こる可能性がある
- 都度LiveDataインスタンスを生成すれば解決しそう
- データに対してたくさんのオペレータ(filterなど)が必要ならRxを使いましょう
- ライフサイクルやUIに関係ないところにLiveDataは使わない。そのために設計していない。(もしくはRxを使いましょう)
- 1回きりのデータ(動画では 1ショットオペレーションと言っている)ならCoroutinesを使いましょう(もしくはRxを使いましょう)
以下の'19セッションでCoroutines FlowでLiveDataの扱い方を解説していました。
- 去年('18)話したRepository層でLiveDataを使えることは話したが、LiveDataはRxのようにリアクティブストリームビルダーとして設計されていない。
- LiveDataのかわりにCoroutines Flowでできるようになる
確かにFlowの場合Androidライブラリ関係ないのでMultiPlatformProject考慮しても特に問題なさそうだなと思いました。
まとめ
- LiveDataはUIに紐づくことは忘れずに
- データが1回きりの表示(1 shot operation)ならCoroutinesで良い、都度変わる(observable)ならLiveDataでも良い。
- 永続化したデータの内容とUIの表示内容を常に同期したいときはRepositoryでLiveDataを返すのはありかも
- 巨大なデータクラスをLiveDataで扱うとメモリ圧迫につながるので注意
- 用法用量を守ればRepository層にLiveDataを使っても良いけどCoroutines Flowで同じことができる
- ただしFlowの細かい部分はまだExperimentalなので完全に移行できるかと言われると微妙
何を実現したいのかによってどの技術を使うか正しく選択できるようになるのが今の自分には必要だなと思います。日々勉強ですね。
参考文献
ViewModelのテストを書く
こんにちは、tkyです。
テスト、書いてますか? 僕はぼちぼち書いてます。
今日はViewModelのテストを書いてみたいと思います。
サンプリリポジトリです。サンプルではGitHubのAPIにアクセスする簡単なサンプルにテストを書いてみた感じです。
本稿ではこの2つを扱いたいと思います。
- LiveDataのアサート
- Coroutinesを含むテスト
モックライブラリにはMockKを使っています。mockkの使い方については触れません。
LiveDataのアサート
例えば『View(ActivityやFragment)にデータを渡せているか』をテストしたいときに用います。
val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val observer = mockk<Observer<List<Project>>>(relaxed = true) val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.projects.observeForever(observer) viewModel.onResume() verify { observer.onChanged(emptyList()) }
ポイントはここ。mockkで作成したObserverをLiveDataに設定します。
val observer = mockk<Observer<List<Project>>>(relaxed = true) viewModel.projects.observeForever(observer)
LiveDataに値が設定されたときにobserver.onChange()が呼ばれることを利用してこのonChange()をアサートします。
verify { observer.onChanged(emptyList()) }
Coroutinesを含むテスト
ほぼCoroutinesを使うケースになるかと思いますが、
- CoroutinesTestRuleを作成
- TestCoroutineScopeでテストを実行
という流れでやっていきます。
@ExperimentalCoroutinesApi class CoroutinesTestRule( val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
// このPrivate functionを作っておくと `coroutinesTestRule.testDispatcher.runBlockingTest` を毎回書かなくて良くて便利です private fun test(block: suspend TestCoroutineScope.() -> Unit) = coroutinesTestRule.testDispatcher.runBlockingTest { block() } @Test fun onResume_repositoryのAPIが叩ける() = test { val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.onResume() coVerify(exactly = 1) { repository.getProjects(any()) } }
MockkでCoroutines用のモック(coEvery)とアサーション(coVerify)を使うだけです。 上記の場合『ライフサイクルのonResume()が呼ばれたとき、repository.getProjects()が呼ばれることをアサート』することになります。
ポイントはViewModelのコンストラクタにCoroutinesのDispatcherをDIしていることです。
ViewModelではこの様になっています。
fun onResume() { viewModelScope.launch(dispatcherProvider.io()) { // 普通はviewModelScope.launch(Dispatchers.IO) で良いのだが・・・ try { val response = repository.getProjects("ticktakclock") _projects.value = response } catch (e: Exception) { e.printStackTrace() _projects.value = emptyList() } } }
なぜこのようなことをしているのかというと、 テスト用のコルーチンスコープで動くIOスレッドとViewModelで動くDispatchers.IOのスレッドが異なるため、 テストのcoVerify{}が正しく動作しないことがあるためです。LiveDataも同様に正しく動作しないことがあります。
理由については上記の通りで、図解するとこのようなイメージです。
この図解はAndroid Dev Summit '19にて説明されたもので、詳しくはこちらのアーカイブを見ていただいたらよいかと思います。 www.youtube.com
まとめ
- livedataとCoroutinesのテストがかけるようになった
- Coroutinesのテストのテストを見越してDispatcherもDIしておくと後々楽になるかも
JetpackComposeのチュートリアルをやってみた
こんにちは、tkyです。
かねてから触ってみたかったJetpackComposeのチュートリアルをやってみました。
本稿は2020/08/09時点のAndroid Jetpack compose チュートリアルを実施するに当たり ハマらないため+これからやることをイメージできるようにすることを目的とした共有的な位置づけで記載します。
すでにチュートリアルをやったことがあったり、必要な環境を
事前情報
事前、というか最初にまとめ書いときます。
- jetpack composeを試すにはAndroidStudio canary版が必要
- build gradleバージョンはほぼ最新じゃないとコンパイルエラーになるかも
- jetpack composeバージョンを1つ上げるとコンパイルエラーになることはよくある
Android Studio Canary版をインストールする
ここからCanary版をインストールします。筆者環境はMacなので既存のASと競合することはなかったですが、Win環境の人はインストール先を工夫したりすると良いかもですね。
https://developer.android.com/studio/preview
DL当時は AS 4.2 CANARY7でした。
ちなみにStableのASでjetpack composeを動かそうとすると画像のような通知が表示されます。
app.gradleに必要なもの
Jetpack Composeに必要なものは以下の様です、現時点ではDev13まで進んでいる模様
android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } composeOptions { kotlinCompilerExtensionVersion "0.1.0-dev13" } buildFeatures { // Enables Jetpack Compose for this module compose true } } dependencies { implementation 'androidx.ui:ui-core:0.1.0-dev13' implementation 'androidx.ui:ui-tooling:0.1.0-dev13' implementation 'androidx.ui:ui-layout:0.1.0-dev13' implementation 'androidx.ui:ui-material:0.1.0-dev13' }
build.gradleの方も更新が必要です。 最新じゃないと「java.lang.AbstractMethodError」というメッセージのコンパイルエラーが発生してちょっとだけハマります。
classpath "com.android.tools.build:gradle:4.2.0-alpha07"
チュートリアル自体は簡単
こういう関数を作成して、
@Composable fun Greeting(name: String) { Text(text = "Hello $name") }
ActivityのonCreateでsetContentする。 layoutの場合はsetContentView()だったのでまぁまぁ覚えやすいのではないでしょうか。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Greeting("Android") } }
一応これでビルドするとこちらのようなレイアウトが完成します。
これだけじゃ面白くないし、そもそもTextViewが左によりすぎてますね。
ご安心ください、チュートリアルでちゃんとレイアウト組みます。
いろいろ端折りますが、こんな感じにColumnというレイアウト用のcomposeUIを使ってLinearLayoutのような事ができます。
setContent { Column(modifier = Modifier.padding(16.dp)) { Greeting("Android") Greeting("Android") Greeting("Android") } }
ここで私が抱いた感想はこちらです
「Flutterと同じじゃん、てことは横はRowかな」
はい、Rowでした。ちゃんと横方向レイアウトになりました。
チュートリアルとしてはこの先 ・画像 ・マテリアルデザインの実現(角丸つけたりする)
を行って終了です。
Jetnews という存在
公式のサンプル。チュートリアルの後はこのアプリで実際にどうやってjetpack composeアプリを作るのか学んで行くことになりそうです。
Canary版で新規プロジェクト
を作ることもできます。
ここから作ることで最新のJetPackCompose環境が作れるので自身の環境でビルドエラーになってしまったときなどは このプロジェクトとの差分を見ることでトラブルシューティングできるかもしれません。
これから
jetpack compose チュートリアルをやってみました。 ReactやFlutterなどの宣言的UIでレイアウト組んだことはあったので自然に馴染むことはできました。
今後は ・どのようにUIを作るか、どの単位でUIを作るか ・命名規則 ・パッケージ構成の考察 ・コピペで使える逆引きCompose ・FlutterとかReactとか宣言型UIと比較してみて
このあたりを書いてみるのも面白いかなと思いました。