Maven Repositoryで公開されたJetpack Composeを触ってみた

Google IO 2019で発表されたJetpack ComposeがMaven Repositoryで公開されたというツイートを見かけたので、ざっくりとコードを書きながらまとめてみた。今までAOSPでソースコードだけが公開されている状態でしたが、Maven Repositoryで公開されたことで気軽に試せるようになりましたね。個人的なメモなので新しい情報はおそらくないと思います。

関連ページ

検証環境

  • Android Studio 3.5.1
  • Android Gradle Plugin 3.5.1
  • Kotlin 1.3.5
  • minSdkVersion 21
  • Android Jetpack
    • Compose
      • androidx.compose:compose-runtime:0.1.0-dev01
      • androidx.compose:compose-compiler:0.1.0-dev01
    • UI
      • androidx.ui:ui-core:0.1.0-dev01:
      • androidx.ui:ui-framework:0.1.0-dev01
      • androidx.ui:ui-text:0.1.0-dev01(テキストを扱う場合)
      • androidx.ui:ui-layout:0.1.0-dev01(レイアウトを扱う場合)

Hello Jetpack Compose

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent { Text("Hello Jetpack Compose") }
  }
}

f:id:yuyakaido:20191014192723p:plain

テキスト

Text

色々とカスタマイズが可能そうです。

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier.None,
    style: TextStyle? = null,
    paragraphStyle: ParagraphStyle? = null,
    softWrap: Boolean = DefaultSoftWrap,
    overflow: TextOverflow = DefaultOverflow,
    // TODO(siyamed): remove suppress
    @SuppressLint("AutoBoxing")
    maxLines: Int? = DefaultMaxLines,
    selectionColor: Color = DefaultSelectionColor
) {
    Text(
        text = AnnotatedString(text),
        modifier = modifier,
        style = style,
        paragraphStyle = paragraphStyle,
        softWrap = softWrap,
        overflow = overflow,
        maxLines = maxLines,
        selectionColor = selectionColor
    )
}

TextStyle

TextStyleを利用するとTextViewに生えているようなスタイルの設定が可能そうです。

data class TextStyle(
    val color: Color? = null,
    val fontSize: Sp? = null,
    val fontSizeScale: Float? = null,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontSynthesis: FontSynthesis? = null,
    var fontFamily: FontFamily? = null,
    val fontFeatureSettings: String? = null,
    val letterSpacing: Float? = null,
    val baselineShift: BaselineShift? = null,
    val textGeometricTransform: TextGeometricTransform? = null,
    val localeList: LocaleList? = null,
    val background: Color? = null,
    val decoration: TextDecoration? = null,
    val shadow: Shadow? = null
)

主要なパラメータを設定してみます。

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Text(
        text = "Hello Jetpack Compose",
        style = TextStyle(
          color = Color.DarkGray,
          fontSize = 28.sp,
          fontWeight = FontWeight.bold,
          background = Color.LightGray,
          decoration = TextDecoration.Underline
        )
      )
    }
  }
}

f:id:yuyakaido:20191014195138p:plain

Composable

Composableを利用することで独自コンポーネントを定義することが出来ます。

  @Composable
  private fun styledText(text: String) {
    return Text(
      text = text,
      style = TextStyle(
        color = Color.DarkGray,
        fontSize = 20.sp,
        fontWeight = FontWeight.bold
      )
    )
  }

レイアウト

Column

Columnで囲むことで要素を縦に並べることが出来ます。

  @Composable
  private fun textColumns() {
    Column {
      styledText("Column 1")
      styledText("Column 2")
      styledText("Column 3")
    }
  }

f:id:yuyakaido:20191014202214p:plain

Row

Rowで囲むことで要素を横に並べることが出来ます。

  @Composable
  private fun textRows() {
    Row {
      styledText("Column 1")
      styledText("Column 2")
      styledText("Column 3")
    }
  }

f:id:yuyakaido:20191014202229p:plain

Padding

  @Composable
  private fun textWithPadding() {
    Padding(16.dp) {
      styledText("Hello Jetpack Compose")
    }
  }

f:id:yuyakaido:20191014203732p:plain

状態変更

良くある例として、ボタンを押すとインクリメントされていくやつを実装してみます。

  @Composable
  private fun counter() {
    val count = +state { 0 }
    MaterialTheme {
      Column {
        Button(
          text = "Increment",
          onClick = {
            count.value++
          }
        )
        styledText("Count: ${count.value}")
      }
    }
  }

f:id:yuyakaido:20191014211926p:plain

所感

  • 関連情報が少ないのでライブラリ自体のコードを読みながら実装を進める必要がある
  • Androidアプリ開発の経験があれば、大体当たりをつけてコードを書いていけると思う

疑問点

  • リストやグリッドの実装方法
    • コードを眺めてみると、StackやTableといったレイアウトが用意されており、これらを活用すると実装できそうな予感
    • StackがLinearLayoutManagerのような挙動で、TableがGridLayoutManagerのような挙動かな?
  • 内部実装
    • 内部のコードを軽く読んでみたが、どのようにUIが描画されているのかまでは追いきれなかった
    • 既存のWidgetとの互換性はあるのだろうか?そもそもパラダイムが違うのでブリッジは無理?

AndroidにおけるReduxという選択肢

これはAndroid Advent Calendar 2018の22日目の記事です。

はじめに

突然ですが、Androidアプリ開発で悩ましい問題といえば何でしょうか?

ライフサイクル、端末やOS依存のバグ、状態管理などなど色々とありますが、今回は状態管理に焦点を当ててみたいと思います。本記事における状態管理とは、複数の画面に同一のデータが表示され、それらの一貫性を担保しつつ、リアルタイムに反映する必要があるものと定義します。この状態管理はいいねボタン問題と呼ばれることもあり、多くのAndroid開発者を悩ませてきた問題の1つです。

さて、Androidにおいていいねボタン問題を解決するためにはどのようなアプローチがあるでしょうか?

  • ActivityやFragmentのonActivityResultメソッドを使って画面を更新する
  • EventBusなどを使って状態変更を通知する
  • ReduxのStoreを各画面で共有する

大まかには3つのアプローチがありますが、今回はタイトルにもあるように3つ目のReduxというアプローチを紹介したいと思います。

基礎編

背景

そもそもReduxはどういった背景で登場したのでしょうか。

これは公式サイトのMotivationに明確に記載されています。

要点だけ抜き出すと、

  • 複数のデータソースを扱ったり、様々なUIコンポーネントの状態を制御したり、といった具合に近年のアプリは複雑化してきている
  • 複雑化したアプリはメンテンナンスが困難になり、新機能の追加が難しくなったり、再現性のないバグに苦しめられたりする
  • 複雑化の正体は非同期性と状態変更を混同していることにあり、Reduxは状態変更の方法に制限をかけることで複雑性に対抗しようとしている

一言でまとめると、Reduxは状態管理の複雑性を解消するためのアプローチであると言えます。

概要

Reduxは状態管理をスマートに行うためのアプローチであり、状態管理に以下3つの制限をかけています。

  • Single source of truth

The state of your whole application is stored in an object tree within a single store.

アプリが管理する状態を1つのオブジェクトとして表現し、このオブジェクトからアプリ内のUIを構築することで統一的な状態管理を実現します。全てを1つのオブジェクトとして表現することで、データの流れが1方向に制限されて処理が非常に追いやすくなります。

  • State is read-only

The only way to change the state is to emit an action, an object describing what happened.

状態の変更内容を表現したアクションを投げることでのみ状態を変更することが可能です。Reduxのない世界では誰でも状態変更を行うことが可能で、規模が大きくなるにつれて制御が難しくなりますが、Reduxは状態変更のフローを厳密に制限することで状態変更がどこで行われているのかが追いやすくなります。また、状態は読み取り専用として定義することで意図しない状態変更を防いでいます。

  • Changes are made with pure functions

To specify how the state tree is transformed by actions, you write pure reducers.

状態変更は必ず副作用のない純粋な関数として記述する必要があります。冪等性を担保することでテスタビリティを担保し、状態変更の予測可能性を高めることにも寄与しているでしょう。

登場人物

f:id:yuyakaido:20181222172752p:plain
Lessons Learned Implementing Redux on Android

Reduxではおおまかに上記の登場人物がいます。それぞれを軽く解説していきます。

  • State

まさに今回焦点を当てている状態そのものです。アプリに表示されるデータであったり、UIコンポーネントの状態であったりといった具合に様々なものがStateとして表現されます。

  • UI

実際にユーザーが目にすることになる画面です。裏側ではStateとして表現されたデータが、リストやボタンといった目に見える形で表現されます。

  • Action

状態の変更内容を表現したものであり、Reduxにおける状態変更を行うための唯一の手段です。

  • Reducer

状態変更を担当するものであり、1つ前のStateとActionから次のStateを計算する純粋な関数として表現されます。

  • Store

Stateを保持する役割を持ち、外部からActionが渡される度にReducerを使って次のStateを計算してStateを変更します。

実装編

ここからはサンプルとしてTODOアプリの実装を見ながら、コードレベルで実装方法を解説していきます。

f:id:yuyakaido:20181222214309p:plain

今回紹介するコードは以下のリポジトリで公開しています。

github.com

Interface

統一的な記述を行うために、各種インターフェースを定義します。

interface StateType

interface ActionType

interface ReducerType<STATE : StateType, ACTION : ActionType> {
    fun reduce(state: STATE, action: ACTION): STATE
}

Model

TODOを表現するモデルとしてTodoを定義します。

data class Todo(
    val title: String,
    val date: Date,
    val isCompleted: Boolean
)

State

アプリ全体の状態を管理するクラスとしてAppStateを定義します。Stateは読み取り専用として定義することを強く推奨します。

data class AppState(
    val todos: List<Todo> = emptyList()
) : StateType

Action

TODOアプリを実現する上で必要になる状態変更をAppActionとして定義します。今回はTODOの追加・削除・完了をActionとして定義します。

細かなテクニックとして、AppActionをSealed Classとして定義することで、後述のReducerの実装をシンプルにすることができます。

sealed class AppAction : ActionType {
    data class AddTodo(val todo: Todo) : AppAction()
    data class CompleteTodo(val todo: Todo) : AppAction()
}

Reducer

先に紹介したActionに対応する状態変更をAppReducerとして定義します。AppReducerはAppStateとAppActionを引数に取るメソッドを持ち、このメソッドで次のAppStateを計算して返却します。ReducerはReduxの3原則にある通り、副作用のない純粋な関数として定義します。これによってメソッドの冪等性が担保され、テスタビリティが高まります。

class AppReducer : ReducerType<AppState, AppAction> {

    override fun reduce(state: AppState, action: AppAction): AppState {
        return when (action) {
            is AppAction.AddTodo -> {
                state.copy(todos = state.todos.plus(action.todo))
            }
            is AppAction.CompleteTodo -> {
                state.copy(todos = state.todos.map { if (it == action.todo) { action.todo } else { it } })
            }
        }
    }

}

Store

AppStateを保持するためのクラスとしてAppStoreを定義します。AppStoreは初期状態とReducerを引数に取り、内部では状態の保持とReducerを使った状態変更を行います。

今回はRxJavaを使って実装していますが、RxJavaは必ずしも必須ではなく、Observerパターンを自作する形でももちろん大丈夫です。

class AppStore(
    private val initial: AppState = AppState(),
    private val reducer: AppReducer = AppReducer()
) : StoreType<AppState, AppAction> {

    private val state = BehaviorRelay.createDefault(initial)

    override fun dispatch(action: AppAction) {
        state.value?.let { current ->
            state.accept(reducer.reduce(current, action))
        }
    }

    override fun observable(): Observable<AppState> {
        return state
    }

}

UI

val adapter = TodoAdapter()
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = adapter

store.observable()
    .map { state -> state.todos }
    .map { todos -> todos.filterNot { todo -> todo.isCompleted } }
    .subscribeBy { todos ->
        adapter.setTodos(todos)
        adapter.notifyDataSetChanged()
    }
    .addTo(disposables)

TODOの更新

TODOを追加する場合はAppAction.AddTodoをStoreに渡します。

store.dispatch(AppAction.AddTodo(Todo.new()))

TODOを完了する場合はAppAction.CompleteTodoをStoreに渡します。

store.dispatch(AppAction.CompleteTodo(todo.copy(isCompleted = true)))

Reduxでは1つのStoreをアプリ全体で共有するため、どこかの画面でTODOが変更されると、それらが即座に全ての画面に伝わることになります。つまり、いいねボタン問題が解決されますね!

応用編

Middleware

Reduxでは状態変更の過程に任意の処理を挟むことができるようになっています。この仕組みはMiddlewareと呼ばれており、本家のReduxでは非常に多くのMiddlewareが開発されています。良くある例としては、Storeに渡されたActionや状態変更の過程をログ出力したり、StoreにActionを渡す直前で非同期処理を実行したり、といったものがあります。

redux.js.org

非同期処理

今回のサンプルアプリでは非同期処理は登場しませんでしたが、現代のアプリでは非同期処理が必要になる状況が多く、Reduxでも非同期処理を行うための仕組みが用意されています。ReduxではMiddlewareで非同期処理をハンドリングすることが多く、公式サイトではいくつかのアプローチが紹介されています。

redux.js.org

データの正規化

規模が大きくなるにつれてStateが保持するデータ量が増えていくいきますが、データ量が増えてくるとデータの一貫性を担保するのが困難になってきます。例えば、1つのTODOが色々な画面で参照される状況においては、同じオブジェクトが複数存在することになってしまいます。

この問題の解決策として、Stateの中身を正規化することでデータの重複を抑制し、データの一貫性管理を効率的に行えるようにする手法が存在します。この手法については私が以前に書いた記事があるので、もし興味がある方は以下の記事をご覧ください。

medium.com

エコシステム

Reduxはエコシステムが充実しており、様々な周辺ライブラリ・ツールが存在します。

React

Reduxはあくまで状態管理に特化しており、UIをどのように組むかは利用者に一任されています。一般的にはReactと組み合わせて使われることが多く、Reduxの公式サイトでもReactと連携が紹介されており、ReduxとReactは相性が良いことが伺えます。

AndroidにおけるReactは、Android JetpackのViewModelとDataBindingの組み合わせが考え方として近いのではないでしょうか。また、Reactという統一的なUIライブラリは存在しませんが、RecyclerViewなどのUIコンポーネントはDiffUtilを組み合わせることで差分更新を行うことができるため、Reactと同じ考え方をAndroidにも導入することは可能そうです。

reactjs.org

Redux DevTools

f:id:yuyakaido:20181222194241p:plain
reduxjs / redux-devtools

Redux DevToolsはChrome/Firefox拡張機能スタンドアロンアプリでReduxのStateツリーの変化をリアルタイムに表示し、直接値を編集したり、履歴から任意の状態に戻ったりといったことが可能になるツールで、開発作業を効率化することが可能になります。私が探した限りは、Androidにおいては類似のツールは存在しませんでしたが、本記事で紹介したReduxKitの一部として同様の機能を開発中です。

github.com

おわりに

本記事では、AndroidにおけるReduxという選択肢について解説してきました。

  • Android開発において、状態管理は悩ましい問題の1つである
  • Web開発で広く利用されているReduxは、状態管理に特化したアプローチの1つである
    • 全ての状態を1つのオブジェクトで管理することで統一的な状態管理が可能になる
    • 状態を読み取り専用で定義することで意図しない状態変更を防ぐ
    • 状態変更を純粋な関数で実装することで状態変更の予測可能性を高める
  • AndroidにもReduxの考え方を取り入れることで、状態管理にまつわる問題を解決できるのではないか

また、本記事で紹介した内容についてはDroidKaigi 2019でも発表予定です。 今回は詳細に紹介することができなかった内容も含めて50分枠で詳細に発表予定ですので、興味のある方は是非聞きに来てみてください!

f:id:yuyakaido:20181222222549p:plain

DroidKaigi 2018にて「マルチログインの実装方法」という発表をしてきた

DroidKaigi 2018の2日目の17:40からRoom 2にて、「マルチログインの実装方法」というタイトルで登壇してきました。

スライド

概要

  • マルチログインの定義
  • マルチログイン実装でよくある問題とその原因
    • 別アカウントのデータが表示されてしまう
    • 別アカウントのデータを上書きしてしまう
      • データや非同期処理がアカウント単位で管理されていないこと
    • コードが複雑化してしまう
  • マルチログイン実装の設計方針
    • データや非同期処理をアカウント単位で管理する(アプリ内にアカウント毎のDockerコンテナを立ち上げるイメージ)
    • 異なるライフタイムを持つインスタンス管理はDaggerなどのDIコンテナを使うと楽
  • サンプルアプリ
  • まとめ
    • アカウント単位でデータや非同期処理を管理すると幸せになれるよ!

感想・補足など

  • 登壇
    • DroidKaigiでの登壇は今年で3度目ですが、今回初めてスピーチ原稿を書いてみた
      • スピーチ原稿を書く過程で、話したい内容が改めて整理できるので良い
      • 本番もわりとリラックスして話せた気がする
      • 逆に棒読みになってないかがちょっと心配
    • Pairsはマルチログインできません!というくだりで小笑いが取れたので良かった
    • Pairsはマルチアカウントも禁止ですよ!
  • 企業ブース
    • Twitterアイコンしか認知していかなった人とたくさん話せて楽しかった
    • Pairsのソースコードを公開する企画をやり、結構な人が興味を持ってくれた
    • アーキテクチャ警察やDagger警察な方もきてくれて、色々とフィードバックを貰えたのでさらに改善していきたいお気持ち
    • DroidKaigi期間中はブースに付きっきりだったので一切セッションを聞けなくて残念
    • ブースで喋りすぎて登壇本番でちょっと声がかすれていた気がする

最後に

僕はスタッフではなかったので普通に参加していただけですが、毎年最高なカンファレンスを開いていただいているスタッフの方々、本当にありがとうございます。来年も必ず参加します!

詳解MVIアーキテクチャ

これはAndroid Advent Calendar 2017の12日目の記事です。

この記事はMVIアーキテクチャの概要とその実装を紹介するものです。概要については、MODEL-VIEW-INTENT ON ANDROIDからの抜粋で、実装についてはTODO-MVI-RxJavaをもとにしています。

概要

MVI is inspired by some other js frameworks like redux and react, but the key principle comes from Model-View-Controller (MVC)

MVIはMVCのコンセプトがもとになっていて、そこにReduxやReactで採用されている考え方を取り入れています。

MVCのコンセプトとは、

A Model that is observed by the View and a Controller that manipulates the Model.

のことを指しており、それに加えて、

  • 単方向データフロー
  • StateをImmutableとして定義

というReduxやReactにあるような考え方を加えています。

MVIの特徴を以下にまとめます。

  • The State Problem
    • StateをImmutableとして定義し、単方向データフローで実装することで状態管理にまつわる問題を解決できる
  • Screen orientation changes
    • 画面回転ではViewが再生成されることになるが、再生成後にModelの状態をViewに反映することで画面状態を復元できる
  • Navigation on the back stack
    • 画面回転時と同様に、Modeの状態をViewに反映することで画面を復元できる
  • Process death
    • 画面回転と同様の方法で画面を復元できる
  • Immutability and unidirectional data flow
    • StateをImmutableとして定義し、単方向データフローで実装することでシンプルに実装できる
  • Debuggable and reproducible states
    • Stateを逐一ログ出力したり、クラッシュした際にCrashlyticsにStateを送信しておくことで、アプリの状態を容易に再現可能になりデバッグが簡単になる
  • Testability
    • StateはImmutableとして定義されており、単方向データフローであることからテストを容易に実装できる

実装

MVIアーキテクチャの全体像は以下のようになっています。

f:id:yuyakaido:20171212234204p:plain

MVIにおける処理の流れは以下のようになっています。

  1. 処理の起点として、Intentが生成される
  2. IntentがIntentInterpretorに渡され、Actionに変換される
  3. ActionがProcessorに渡され、Repositoryを使ってデータアクセスを行う
  4. Processorがデータアクセスの結果をResultとして返す
  5. ResultがReducerに渡され、Stateに変換される
  6. StateがRenderに渡され、Viewに反映される

今回は参考にしているリポジトリでは、簡易的なTODOアプリを実装しており、その中でタスク一覧を表示する部分にフォーカスして処理の流れを追ってみたいと思います。

Intent

f:id:yuyakaido:20171212234222p:plain

Intentは主に画面の変化やユーザーからの入力を表すもので、MVIにおける処理の起点になります。

例えば、今回フォーカスしているタスク一覧の初期化時は以下のIntentが発行されます。

interface TasksIntent extends MviIntent {
    @AutoValue
    abstract class InitialIntent implements TasksIntent {
        public static InitialIntent create() {
            return new AutoValue_TasksIntent_InitialIntent();
        }
    }
    // 省略
}

Action

f:id:yuyakaido:20171212234242p:plain

MVIはUIとデータフローを明確に区別しており、IntentをActionに変換します。これにより、異なるIntentで同じActionを再利用することを可能にしています。

例えば、「タスク一覧の初期化」は「データを読み込む」といった具合に変換されています。

    private TasksAction actionFromIntent(MviIntent intent) {
        if (intent instanceof TasksIntent.InitialIntent) {
            return TasksAction.LoadTasks.loadAndFilter(true, TasksFilterType.ALL_TASKS);
        }
        // 省略
    }

Processor

f:id:yuyakaido:20171212234303p:plain

ProcessorはActionを受け取り、必要に応じてRepositoryを介したデータアクセスを行います。

まず、Actionから該当するProcessorを判定します。

    ObservableTransformer<TasksAction, TasksResult> actionProcessor =
            actions -> actions.publish(shared -> Observable.merge(
                    // Match LoadTasks to loadTasksProcessor
                    shared.ofType(TasksAction.LoadTasks.class).compose(loadTasksProcessor),
                    // Match ActivateTaskAction to populateTaskProcessor
                    shared.ofType(TasksAction.ActivateTaskAction.class).compose(activateTaskProcessor),
                    // Match CompleteTaskAction to completeTaskProcessor
                    shared.ofType(TasksAction.CompleteTaskAction.class).compose(completeTaskProcessor),
                    // Match ClearCompletedTasksAction to clearCompletedTasksProcessor
                    shared.ofType(TasksAction.ClearCompletedTasksAction.class).compose(clearCompletedTasksProcessor))
                    .mergeWith(
                            // Error for not implemented actions
                            shared.filter(v -> !(v instanceof TasksAction.LoadTasks)
                                    && !(v instanceof TasksAction.ActivateTaskAction)
                                    && !(v instanceof TasksAction.CompleteTaskAction)
                                    && !(v instanceof TasksAction.ClearCompletedTasksAction))
                                    .flatMap(w -> Observable.error(
                                            new IllegalArgumentException("Unknown Action type: " + w)))));

今回はタスク一覧を読み込むためのProcessorが選択されます。

    private ObservableTransformer<TasksAction.LoadTasks, TasksResult.LoadTasks> loadTasksProcessor =
            actions -> actions.flatMap(action -> mTasksRepository.getTasks(action.forceUpdate())
                    // Transform the Single to an Observable to allow emission of multiple
                    // events down the stream (e.g. the InFlight event)
                    .toObservable()
                    // Wrap returned data into an immutable object
                    .map(tasks -> TasksResult.LoadTasks.success(tasks, action.filterType()))
                    // Wrap any error into an immutable object and pass it down the stream
                    // without crashing.
                    // Because errors are data and hence, should just be part of the stream.
                    .onErrorReturn(TasksResult.LoadTasks::failure)
                    .subscribeOn(mSchedulerProvider.io())
                    .observeOn(mSchedulerProvider.ui())
                    // Emit an InFlight event to notify the subscribers (e.g. the UI) we are
                    // doing work and waiting on a response.
                    // We emit it after observing on the UI thread to allow the event to be emitted
                    // on the current frame and avoid jank.
                    .startWith(TasksResult.LoadTasks.inFlight()));

タスク一覧を読み込むためにTaskRepositoryのgetTasksメソッドを呼び出し、タスクの取得結果をTasksResultに変換しています。

Result

f:id:yuyakaido:20171212234323p:plain

ResultはProcessorがActionを実行した結果として吐き出すもので、タスク一覧の表示では以下のようなResultが定義されています。

interface TasksResult extends MviResult {
    @AutoValue
    abstract class LoadTasks implements TasksResult {
        @NonNull
        abstract LceStatus status();

        @Nullable
        abstract List<Task> tasks();

        @Nullable
        abstract TasksFilterType filterType();

        @Nullable
        abstract Throwable error();

        @NonNull
        static LoadTasks success(@NonNull List<Task> tasks, @Nullable TasksFilterType filterType) {
            return new AutoValue_TasksResult_LoadTasks(SUCCESS, tasks, filterType, null);
        }

        @NonNull
        static LoadTasks failure(Throwable error) {
            return new AutoValue_TasksResult_LoadTasks(FAILURE, null, null, error);
        }

        @NonNull
        static LoadTasks inFlight() {
            return new AutoValue_TasksResult_LoadTasks(IN_FLIGHT, null, null, null);
        }
    }
}

タスク一覧の取得に成功した場合はTasksResult.LoadTasks.successとして表現され、失敗した場合はTasksResult.LoadTasks.failureとして表現されます。また、読み込み状態はTasksResult.LoadTasks.isFlightとして表現されるようです。

Reducer

f:id:yuyakaido:20171212234339p:plain

ReducerはResultを受け取り、Viewの状態を定義したViewStateを吐き出します。

    private static BiFunction<TasksViewState, TasksResult, TasksViewState> reducer =
            (previousState, result) -> {
                TasksViewState.Builder stateBuilder = previousState.buildWith();
                if (result instanceof TasksResult.LoadTasks) {
                    TasksResult.LoadTasks loadResult = (TasksResult.LoadTasks) result;
                    switch (loadResult.status()) {
                        case SUCCESS:
                            TasksFilterType filterType = loadResult.filterType();
                            if (filterType == null) {
                                filterType = previousState.tasksFilterType();
                            }
                            List<Task> tasks = filteredTasks(checkNotNull(loadResult.tasks()), filterType);
                            return stateBuilder.isLoading(false).tasks(tasks).tasksFilterType(filterType).build();
                        case FAILURE:
                            return stateBuilder.isLoading(false).error(loadResult.error()).build();
                        case IN_FLIGHT:
                            return stateBuilder.isLoading(true).build();
                    }
                }
            };

Reducerは1つ前のViewStateと、新しく生成されたResultを引数に取り、その2つを使って新しいViewStateを組み上げます。 今回取り上げているタスク一覧画面のViewStateはTasksViewStateとして定義されており、TasksViewState.Builderを使ってインスタンスを組み立てています。

Render

f:id:yuyakaido:20171212234358p:plain

RenderはReducerが吐き出したViewStateを受け取り、Viewに反映する作業を担当します。

    @Override
    public void render(TasksViewState state) {
        if (state.tasks().isEmpty()) {
            // 省略
        } else {
            mListAdapter.replaceData(state.tasks());

            mTasksView.setVisibility(View.VISIBLE);
            mNoTasksView.setVisibility(View.GONE);
        }
    }

まとめ

今回はMVIアーキテクチャの概要と実装方法を紹介しました。

  • 概要
    • MVIはMVCにおけるModelのコンセプトを引き継いでいる
    • それに加えて、ReduxやReactに見られるImmutabilityやUnidirectional Data Flowといった性質を付け加えている
  • 実装
    • Intent:画面の変化やユーザーの入力を表すもので、MVIにおける処理の起点になる
    • Action:Intentをデータフローに変換したもの
    • Processor:Actionを受け取り、データアクセスなどを行うレイヤー
    • Result:Processorが処理の結果として吐き出すもの
    • Reducer:1つ前のViewStateとResultから新しいViewStateを吐き出すレイヤー
    • ViewState:Viewの状態を定義したもの
    • Render:ViewStateを受け取り、Viewに反映させるためのレイヤー

MVIのコードは初見では取っ付きにくいという印象を持つ方が多いと思いますが、そのコンセプトから紐解いていくとネイティブアプリにおける状態管理をうまくハンドリングしており、個人的にはかなり勉強になりました。この記事がMVIに興味を持っている人の参考になれば嬉しいです!

KotlinにおけるJavaとの相互運用性を高めるための工夫

これはKotlin Advent Calendar 2017の6日目の記事です。

はじめに

KotlinはJavaとの相互運用性を重視していますが、JavaとKotlinは言語仕様的に異なる部分があり、Kotlinはその違いを吸収するために様々な工夫をしています。

Jvmアノテーション

JvmStaticアノテーション

KotlinのCompanion Objectで定義したコードをJavaから呼び出すと、Companionを経由することになります。Companion経由であっても動作上の問題はありませんが、出来れば普通にJavaで定義した場合と同様な呼び出し方をしたいですよね?そういった場合はJvmStaticアノテーションを付けることで、Staticメソッドとして定義されているかのように呼び出すことができます。

// Kotlin
class JvmStaticSampleForCompanionObject {
    companion object {
        @JvmStatic
        fun doSomething() { /* 省略 */ }
    }
}
// Java
JvmStaticSampleForCompanionObject.Companion.doSomething(); // JvmStaticアノテーションなし
JvmStaticSampleForCompanionObject.doSomething(); // JvmStaticアノテーションあり

また、Objectで定義したクラスのメソッドにJvmStaticアノテーションをつけた場合も同様の挙動になります。

// Kotlin
object JvmStaticSampleForObject {
    @JvmStatic
    fun doSomething() { /* 省略 */ }
}
// Java
JvmStaticSampleForObject.INSTANCE.doSomething(); // JvmStaticアノテーションなし
JvmStaticSampleForObject.doSomething(); // JvmStaticアノテーションあり

単純に呼び出し方の問題ならCompanion経由でも動作上はまったく問題ありません。しかし、Javaライブラリの中にはStaticメソッドとして定義されていることを期待して実装されているものもあり、そういったライブラリを使う場合はJvmStaticアノテーションが有効です。

例えば、AndroidにおけるData Bindingでカスタムバインディングを定義する場合、Staticメソッドとして定義されていることを期待しているため、Kotlinでカスタムバインディングを実装する場合はJvmStaticアノテーションが必要になります。具体例に関しては、Android Data Binding Tipsをご覧ください。

JvmFieldアノテーション

Kotlinでプロパティとして定義したものをJavaから呼び出す場合は自動生成されたGetter/Setterを経由してアクセスすることになりますが、このプロパティにJvmFieldアノテーションを付与することでJavaのフィールドとして定義されているかのように呼び出すことができます。

// Kotlin
class JvmFieldSample {
    @JvmField
    val foo = 0
}
// Java
JvmFieldSample jvmFieldSample = new JvmFieldSample();
jvmFieldSample.getFoo(); // JvmFieldアノテーションなし
jvmFieldSample.foo; // JvmFieldアノテーションあり

JvmOverloadsアノテーション

Kotlinはデフォルト引数をサポートしており、メソッドのオーバーロードを簡潔に記述することができますが、そのままではJavaから呼び出した場合にオーバーロードされていることになりません。このような場合にはJvmOverloadsアノテーションをつけることでオーバーロードされているかのように呼び出すことができます。

// Kotlin
class JvmOverloadsSample @JvmOverloads constructor(val foo: Int, val bar: Int = 0)
// Java
new JvmOverloadsSample(0);
new JvmOverloadsSample(0, 0);

その他

Jvmアノテーションは今回紹介した以外にもいくつかあります。

これらに関してはこの辺に解説があるので、興味がある方は覗いてみてください。

Platform Type

KotlinはJavaとの相互運用性を重視しており、Kotlin/Javaが混在するプロジェクトではJavaからKotlinのコードを呼び出すこともあると思います。ここで疑問なのが、Kotlinは言語仕様としてNonNullとNullableを明確に区別している点です。Javaから呼び出す以上はこれらを区別することは不可能ですが、JavaからKotlinのコードを呼び出すこと自体は問題なくできます。このような型の扱い方の違いを吸収するためにKotlinはPlatform Typeという特殊な型を持っています。

具体例を交えつつ説明すると、以下のようなJavaクラスを定義し、それをKotlinから呼び出す場合を考えてみます。

// Java
public class PlatformTypeSample {
    public static Integer getInteger() {
        return 0;
    }
}
// Kotlin
val int = PlatformTypeSample.getInteger()

さて、上記のintはどんな型として推論されるでしょうか? Intでしょうか、Int?でしょうか。

答えはそのどちらでもなく、Int!になります。

この最後に!の付いた型がPlatform Typeで、Intかもしれないし、Int?かもしれない型になります。つまり、実行時にnullが渡された場合は実行時エラーが発生することになります。これではせっかくKotlinで書いているメリットが半減してしまいますが、NonNullやNullableを使うことでPlatform Typeとして推論されることを防ぐことができます。

// Java
public class PlatformTypeSample {
    @NonNull or @Nullable
    public static Integer getInteger() {
        return 0;
    }
}
// Kotlin
val nonNullInt: Int = PlatformTypeSample.getInteger() // NonNullアノテーション
val nullableInt: Int? = PlatformTypeSample.getInteger() // Nullableアノテーション

アノテーションをうまく活用することでJavaと共存しつつも型安全なコードを書くことができます。

Mapped Type

JavaとKotlinは言語仕様が異なるため、以下のようなマッピングが定義されており、相互運用する場合はこれに沿ってマッピングされることになります。

Primitive Type

Java Kotlin
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Non-Primitive Built-in Class

Java Kotlin
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.Annotation kotlin.Annotation!
java.lang.Deprecated kotlin.Deprecated!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Javaの参照型は基本的にPlatform Typeにマッピングされるようなので、積極的にNonNullアノテーションをつけておきたいところです。

Boxed Primitive Type

Java Kotlin
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Character kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

まとめ

この記事ではKotlinがJavaとの互換性を保つために行っている工夫をいくつか紹介しました。

今回紹介した以外にも公式リファレンスには涙ぐましい?工夫が載っています。興味のある方は以下のページを覗いてみると面白いかもしれません。

それでは、Have a nice Kotlin!

DroidKaigi 2017で登壇してきた

2日目の16:00から「Error Handling in RxJava」というタイトルで、RxJavaにおけるエラーハンドリングについて発表してきました。

スライド

概要

  • RxJavaのおさらい
    • Reactive Extensions for Java
    • マーブルダイアグラム
  • RxJavaにおけるエラーハンドリング
    • Catch(エラー発生時にリカバリーを行う)
      • onError:エラーをキャッチする
      • onErrorReturn:エラーをキャッチして代わりのデータを返す
      • onErrorResumeNext:エラーをキャッチして代わりのObservableを返す
    • Retry(エラー発生時に再購読を行う)
      • retry:エラー発生時に再購読する
      • retryWhen:再購読する条件を細かく制御する
  • 基礎編
    • エラーをトーストで表示する:onError
    • 代わりのデータを表示する:onErrorReturn
    • エラーの種類で処理を分岐する:onErrorResumeNext
    • リトライする:retry
    • リトライするかをユーザーに尋ねる:retryWhen
  • 応用編
    • リトライ&キャッシュ:retry/onErrorReturn
    • リトライ&キャッシュ&トースト:retry/onErrorReturn/onError
  • 実践編
    • Retrofitのエラーハンドリング
      • RxJavaCallAdapterの拡張

振り返り・感想

資料

1日目の終了後に自分の資料を改めて見直して、後から資料単体で見た場合に情報量が少なく分かりにくいのでは、ということに気が付き資料を修正した。修正後に一度だけ通しで練習をしたものの、基本的には情報量を増やす方向で修正を行ったので、当日は少し急ぎで喋った。結果として、中盤を過ぎた辺りで早すぎることに気が付き、後半は少しゆっくりと喋った。直前に資料修正は良くないし、発表練習では話す内容だけでなく、タイムマネジメントも含めて練習すべきという当然の学びを得た。

登壇直前

ありがたいことに自分のセッションは立ち見が出た。

セッション前の静かな会場の雰囲気に耐えられず、登壇者席からiPhoneでパノラマで写真を撮るということをやってみた。

登壇中

前回のDroidKaigiでも同じような規模の部屋で登壇していたこともあり、今回は過度に緊張せずに喋れたと思う。発表中に聞いている人の表情や頷き具合とかを観察しながら説明の仕方を調整する余裕もあった。具体的には、retryまでは頷きながら聞いてくれる人がかなりいたので練習のときよりも説明を軽めにして、retryWhenに入った辺りから今まで頷いてくれていた人の表情が曇り始めたのでより詳細に説明するようにした。

登壇後

登壇後はオフィスアワーとして質問を受けていましたが、その場で分かりやすかったです!と言っていただけたりTwitterでもいくつか反響をいただけたりして素直に嬉しかった。

retryWhenは説明不足なところもあった模様。反省。

まとめ

プロポーザルを出す前はこんなネタで登壇していいのか悩んだが、結果的には登壇して本当に良かったと思います。同僚のMoyuruAizawaがオープニングトークでも言っていましたが、自分の当たり前は必ず誰かに需要があるので、悩んだらとりあえずプロポーザルを出せばいいんじゃないかなと思います。僕は来年も絶対プロポーザルを出します。

RxJavaを使ったAndroidにおけるエラーハンドリング

これはRxJava Advent Calendar 2016の17日目の記事です。

はじめに

みなさん、RxJava使ってますか?私は個人でも仕事でもガッツリ使っています。

この記事では、RxJavaを使ったAndroidのエラーハンドリングに関して書きたいと思います。ちなみに私は「Error Handing in RxJava」というタイトルで DroidKaigi 2017に登壇予定で、この記事は発表予定の内容をざっくりとまとめたものになります。

エラーハンドリングでよく使うOperatorの紹介

RxJavaにはエラーハンドリング用に以下のようなOperatorが定義されています。

  • onError
  • onErrorReturn
  • onErrorResumeNext
  • retry
  • retryWhen

onError

これはストリーム内で投げられたExceptioinを受け取るためのOperatorです。

        Observable observable = Observable.just(0);
        observable
                .map(new Func1() {
                    @Override
                    public Object call(Object o) {
                        throw new RuntimeException();
                    }
                })
                .subscribe(new Subscriber() {
                    @Override
                    public void onCompleted() {}

                    @Override
                    public void onError(Throwable e) {}

                    @Override
                    public void onNext(Object o) {}
                });

mapでthrowしたExceptionがonErrorに渡ってきます。

onErrorReturn

これはExceptionが投げられた場合に代わりのデータを流すためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .onErrorReturn(new Func1<Throwable, Integer>() {
                    @Override
                    public Integer call(Throwable throwable) {
                        return 10;
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、onErrorReturnが発火し、代わりのデータとして「10」が返ります。

onErrorResumeNext

これはExceptionが投げられた場合に代わりのストリームを流すためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends Integer>>() {
                    @Override
                    public Observable<? extends Integer> call(Throwable throwable) {
                        return Observable.just(0);
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、onErrorResumeNextが発火し、代わりのストリームとして「Observable.just(0)」が返ります。

retry

これはExceptionが投げられた場合にリトライするためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retry()
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、通常はストリームが終了しますが、retryを書くことで再度subscribeされます。上記のコードは無限にリトライされるますが、リトライ回数を設定することも可能で、通常はリトライ上限を設定した方がいいでしょう。

retryWhen

retryWhenは少し分かりにくいですが、リトライするトリガーとなるストリームを返すためのOperatorです。retryの場合はExceptionが投げられた瞬間にリトライが実行されますが、retryWhenを使うことで10秒待ってからリトライするというといった具合にリトライのタイミングを細かく制御することができます。

10秒待ってからリトライするコードは以下の通りです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                    @Override
                    public Observable<?> call(Observable<? extends Throwable> observable) {
                        return observable.flatMap(new Func1<Throwable, Observable<?>>() {
                            @Override
                            public Observable<?> call(Throwable throwable) {
                                return Observable.timer(10, TimeUnit.SECONDS);
                            }
                        });
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

ユースケース

Androidアプリでは以下のようなエラーハンドリングがよく用いられます。これをRxJavaを使って実装してみたいと思います。

  • Toastを表示する
  • リトライするかをユーザーに尋ねる

Toastを表示する

これは簡単ですね。onErrorでToastを表示するだけです。

        Observable observable = Observable.just(0);
        observable
                .map(new Func1() {
                    @Override
                    public Object call(Object o) {
                        throw new RuntimeException();
                    }
                })
                .subscribe(new Subscriber() {
                    @Override
                    public void onCompleted() {}

                    @Override
                    public void onError(Throwable e) {
                        Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onNext(Object o) {}
                });

リトライするかをユーザーに尋ねる

これは先程紹介したretryWhenを使います。retryWhen内でSnackbarを用いてユーザーにリトライするかどうかを尋ねます。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                    @Override
                    public Observable<?> call(Observable<? extends Throwable> observable) {
                        return observable.flatMap(new Func1<Throwable, Observable<?>>() {
                            @Override
                            public Observable<?> call(Throwable throwable) {
                                return Observable.create(new Observable.OnSubscribe<Void>() {
                                    @Override
                                    public void call(final Subscriber<? super Void> subscriber) {
                                        Snackbar snackbar = Snackbar.make(view, "Retry?", Snackbar.LENGTH_INDEFINITE);
                                        snackbar.setAction("Yes", new View.OnClickListener() {
                                            @Override
                                            public void onClick(View view) {
                                                subscriber.onNext(null);
                                            }
                                        });
                                        snackbar.show();
                                    }
                                });
                            }
                        });
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

まとめ

今回は以下のようなエラーハンドリング用のOperatorを紹介しました。

  • onError
    • Exceptionを受け取ることができる
  • onErrorReturn
    • Exceptioinが投げられた場合に代わりのデータを流すことができる
  • onErrorResumeNext
    • Exceptionが投げられた場合に代わりのストリームを流すことができる
  • retry
    • Exceptionが投げられた場合にリトライすることができる
  • retryWhen
    • リトライのタイミングを細かく制御することができる

上記のOperatorを踏まえて、Androidでよくあるエラーハンドリングの実装例を紹介しました。

  • Toastを表示する
    • onErrorでToastを表示する
  • リトライするかどうかをユーザーに尋ねる
    • retryWhenとSnackbarを組み合わせる

この他にもコメントなどいただければ実装例を追記していきたいと思います。

それでは、良いRxJavaライフを!