ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Go言語でgit checkoutを補助するコマンドを作った

こんにちは、tkyです。

Go勉強してコマンドを作ってみました。

github.com

git checkout をサポートする gith というコマンドです。

f:id:ticktakclock:20201123102923p:plain
デモ1

こんな感じにブランチが選択式で表示されるので選べばそのブランチに移動できるようなものです。

リモートブランチをチェックアウトするときはチェックアウトするブランチ名を決めることができます。

f:id:ticktakclock:20201123103223p:plain
デモ2

※例えばorigin/developブランチからfeature/XXXXのようなことをできるように

主要モジュール

これがすべてです。対話式のコマンドを完成させるのに一番ラクな方法かと思います。

サンプルも結構あって実際に見て使い方の理解も深まりました。

github.com

また今回はコマンドに引数とか細かいことは何もやらないでいたために 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やってました。基本ここで学びます。

tour.golang.org

あとはこちらのスライドで並行で理解していく感じでした。

docs.google.com

ざっくりとこの辺を理解してからコマンド作成をはじめました。

  • 変数の定義方法
  • 関数の定義方法
  • 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になっています。

developブランチにマージされた時にしたい場合 Only build pull requests をOFFにしてfilterでdevelopのみ動作するワークフローを作ればよいのですが

そうするとPR以外のリモートへのコミットでもCIが回るのでそれもなぁ・・・という感じです。

上記をを許容できるならGitHubActions使う必要ないのでこの話は終わりですw

もう少し頑張りたいよね、というところでdevelopブランチにコミットされた時のトリガーだけをGitHubActionで切り出すことにしました。

実際に検討で使った図です

f:id:ticktakclock:20201121141635p:plain:w400
検討図

そんなこんなで以下のように話をまとめて現在のプロジェクトで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の拡張関数のような表現になっていますね。

ここで拡張関数のドキュメントを見に行きます。

kotlinlang.org

以下抜粋

拡張関数を宣言するには レシーバータイプ (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 のドキュメントを見に行ってみましょう。

kotlinlang.org

コンテキストオブジェクトは、レシーバー(this)として使用できます。

ここでもちゃんとレシーバーが出てきました。

新しい単語(コンテキストオブジェクト)も出てきました。関数のスコープ内で使用する特定のオブジェクトのことを指しているのであっていると思いますが、その特定のオブジェクトにthisでアクセスできるということですね。

続いてラムダ式のリファレンスも見に行ってみましょう。きっと何かが書いてありそうな予感。

kotlinlang.org

ありました。関数型はレシーバーを使用することで関数型のインスタンスを呼び出す事ができる、というようなことが書いてあります。

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使ってるじゃん・・・!ということはダメというわけでもないのかと考えます。

developer.android.com

気になったのはこの一文

『このパターンは、データベースに格納されているデータと同期したデータが常に UI に表示されるようにするうえで役立ちます。』

こういったユースケースを実現する場合は許容しても良いのかもしれません。(前提としてRoomもAndroidライブラリだからLiveDataが入っててもおかしくない)

Dev Summit から得た知見

以下の'18セッションで実際にRepositoryにLiveDataを扱うケースが存在しており、許容されていそうということが伺えます。

youtu.be

また同時にアンチパターンについても解説がありました。

  • HTTPリクエストから得た巨大なデータ群にLiveDataを使うとその分がメモリにとどまる
    • APIのRepositoryには使わないほうが良さそう
  • LiveDataインスタンスを複数画面で共有すると意図しないUI更新が起こる可能性がある
  •  データに対してたくさんのオペレータ(filterなど)が必要ならRxを使いましょう
  • ライフサイクルやUIに関係ないところにLiveDataは使わない。そのために設計していない。(もしくはRxを使いましょう)
  • 1回きりのデータ(動画では 1ショットオペレーションと言っている)ならCoroutinesを使いましょう(もしくはRxを使いましょう)

以下の'19セッションでCoroutines FlowでLiveDataの扱い方を解説していました。

www.youtube.com

  • 去年('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なので完全に移行できるかと言われると微妙

何を実現したいのかによってどの技術を使うか正しく選択できるようになるのが今の自分には必要だなと思います。日々勉強ですね。

参考文献

developer.android.com

developer.android.com

ViewModelのテストを書く

こんにちは、tkyです。

テスト、書いてますか? 僕はぼちぼち書いてます。

今日はViewModelのテストを書いてみたいと思います。

サンプリリポジトリです。サンプルではGitHubAPIにアクセスする簡単なサンプルにテストを書いてみた感じです。

github.com

本稿ではこの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も同様に正しく動作しないことがあります。

理由については上記の通りで、図解するとこのようなイメージです。

f:id:ticktakclock:20200818042912p:plain:w500

この図解はAndroid Dev Summit '19にて説明されたもので、詳しくはこちらのアーカイブを見ていただいたらよいかと思います。 www.youtube.com

まとめ

  • livedataとCoroutinesのテストがかけるようになった
  • Coroutinesのテストのテストを見越してDispatcherもDIしておくと後々楽になるかも

JetpackComposeのチュートリアルをやってみた

こんにちは、tkyです。

かねてから触ってみたかったJetpackComposeのチュートリアルをやってみました。

本稿は2020/08/09時点のAndroid Jetpack compose チュートリアルを実施するに当たり ハマらないため+これからやることをイメージできるようにすることを目的とした共有的な位置づけで記載します。

すでにチュートリアルをやったことがあったり、必要な環境を

事前情報

事前、というか最初にまとめ書いときます。

  1. jetpack composeを試すにはAndroidStudio canary版が必要
  2. build gradleバージョンはほぼ最新じゃないとコンパイルエラーになるかも
  3. 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を動かそうとすると画像のような通知が表示されます。

f:id:ticktakclock:20200809135830p:plain
StableのASで動かそうとすると通知

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")
    }
}

一応これでビルドするとこちらのようなレイアウトが完成します。

f:id:ticktakclock:20200809140752p:plain
チュートリアル完成

これだけじゃ面白くないし、そもそもTextViewが左によりすぎてますね。

ご安心ください、チュートリアルでちゃんとレイアウト組みます。

いろいろ端折りますが、こんな感じにColumnというレイアウト用のcomposeUIを使ってLinearLayoutのような事ができます。

        setContent {
            Column(modifier = Modifier.padding(16.dp)) {
                Greeting("Android")
                Greeting("Android")
                Greeting("Android")
            }
        }

f:id:ticktakclock:20200809141141p:plain
レイアウトチュートリアル

ここで私が抱いた感想はこちらです

「Flutterと同じじゃん、てことは横はRowかな」

はい、Rowでした。ちゃんと横方向レイアウトになりました。

f:id:ticktakclock:20200809142041p:plain
Rowの場合のレイアウト

チュートリアルとしてはこの先 ・画像 ・マテリアルデザインの実現(角丸つけたりする)

を行って終了です。

Jetnews という存在

公式のサンプル。チュートリアルの後はこのアプリで実際にどうやってjetpack composeアプリを作るのか学んで行くことになりそうです。

github.com

Canary版で新規プロジェクト

を作ることもできます。

f:id:ticktakclock:20200810132505p:plain
EmptyComposeActivity

ここから作ることで最新のJetPackCompose環境が作れるので自身の環境でビルドエラーになってしまったときなどは このプロジェクトとの差分を見ることでトラブルシューティングできるかもしれません。

これから

jetpack compose  チュートリアルをやってみました。 ReactやFlutterなどの宣言的UIでレイアウト組んだことはあったので自然に馴染むことはできました。

今後は ・どのようにUIを作るか、どの単位でUIを作るか ・命名規則 ・パッケージ構成の考察 ・コピペで使える逆引きCompose ・FlutterとかReactとか宣言型UIと比較してみて

このあたりを書いてみるのも面白いかなと思いました。

Shared Elementで画面遷移にアニメーションを加える

こんにちは、tkyです。

Androidで動きのある画面遷移に挑戦してみようと思います。

画面間遷移にアニメーション

今回は画面から画面の遷移にSharedElement(これって機能名になるんですかね?)を指定して

「遷移元のUIの一部が遷移先のUIの一部になるように見せる遷移」

を実現してみたいと思います。

文面だけではかなり想像がつきにくいのでSharedElementしない画面遷移と対応した完成形イメージです。

対応前 対応後
f:id:ticktakclock:20200628165626g:plain
対応前
f:id:ticktakclock:20200628164953g:plain
完成形

github.com

Activitiy -> Activity のSharedElement

今回やったのはActivitiy -> Activityの画面遷移です。 他にはFragmentがあると思いますが、別の記事にしようと思います。

  • Fragment -> Fragment

僕自身どうやって画面遷移を実現してるのがあまり理解していなかったのでざっくりメモ含めて解説していきます。

順序としてはこんな感じのことをやればSharedElement実現できます。

この3つをこのコミットで対応していますので、差分だけ見るとより理解しやすいかもしれません。

github.com

  • startActivity時に遷移元のViewを指定
  • 遷移先のViewを指定
  • 遷移中のTransitionを指定

https://developer.android.com/training/material/images/SceneTransition.png

引用元:https://developer.android.com/training/transitions/start-activity

上記引用元はAndroidXで書かれていないので、参考にしつつAndroidX対応で書いていきます。

遷移元のViewを指定

アニメーションさせたいViewを指定します。第3引数のStringは後に出てくる遷移先のViewで指定するStringと同じものにする必要があります。

遷移元でやることはこれだけです。

val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity, view, "photo")
ActivityCompat.startActivity(
    this,
    intent,
    options.toBundle()
)

遷移先のViewを指定

最終的に到達したい場所のViewを指定します。この場合ImageView->ImageViewの遷移です。 onCreate() 等でレイアウトをInflateしたあとに遷移させたいViewを指定します。この時setTransitionNameの第2引数は遷移元のViewで指定したStringと同じものにします。

ViewCompat.setTransitionName(binding.imageView, "photo")

ハマったところ

ここで1つハマったところがあって画像は基本的にURLがあってロードしてあとから表示の流れになると思います。

こうなるとImageViewの高さがあとから決まるのでwrap_contentとかにしていると高さ0のImageViewとかになってしまいます。

その高さ0のViewめがけてアニメーションしてしまうのでちょっと変な動きになります。下の動画で「ペスカトーレ」「ジャンバラヤ」をタップした時にViewが上に吸い込まれるような挙動になってしまったりします。

f:id:ticktakclock:20200628182530g:plain
微妙にうまく動いていない

遷移先のViewは高さを指定するなどしてレイアウト時点でどこに配置されるべきかちゃんと定義する、を意識するのが良いのかもしれません。

遷移中のTransitionを指定

res/transition/change_image_transform.xml (名前は何でも良いです)に遷移中どのようにViewを移動させるかを定義していきます。

それぞれのChangeBounds、ChangeImageTransformについてはあまり調べていません。

(transformで位置(x, y)を変更、boundsで大きさ(w, h)変更ですかね?この2つを組み合わせてアニメーションします)

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds />
    <changeImageTransform />
    <targets>
        <target android:excludeId="@android:id/statusBarBackground" />
        <target android:excludeId="@android:id/navigationBarBackground" />
    </targets>
</transitionSet>

まとめ

  • Activity -> Activity の画面遷移でSharedElementを使ってみた
  • 直線的なレイアウトは結構簡単にできた
  • Fragmentの画面遷移もトライしてみたい

参考にしたURL

Start an activity using an animation  |  Android Developers

Shared Element Activity Transition | CodePath Android Cliffnotes