Maven Repositoryで公開されたJetpack Composeを触ってみた
Google IO 2019で発表されたJetpack ComposeがMaven Repositoryで公開されたというツイートを見かけたので、ざっくりとコードを書きながらまとめてみた。今までAOSPでソースコードだけが公開されている状態でしたが、Maven Repositoryで公開されたことで気軽に試せるようになりましたね。個人的なメモなので新しい情報はおそらくないと思います。
Jetpack ComposeがGoogle Maven Repositoryにアップされとる
— すたぜろ (@STAR_ZERO) October 11, 2019
関連ページ
検証環境
- 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(レイアウトを扱う場合)
- Compose
Hello Jetpack Compose
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello Jetpack Compose") } } }
テキスト
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 ) ) } } }
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") } }
Row
Rowで囲むことで要素を横に並べることが出来ます。
@Composable private fun textRows() { Row { styledText("Column 1") styledText("Column 2") styledText("Column 3") } }
Padding
@Composable private fun textWithPadding() { Padding(16.dp) { styledText("Hello Jetpack Compose") } }
状態変更
良くある例として、ボタンを押すとインクリメントされていくやつを実装してみます。
@Composable private fun counter() { val count = +state { 0 } MaterialTheme { Column { Button( text = "Increment", onClick = { count.value++ } ) styledText("Count: ${count.value}") } } }
所感
疑問点
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.
状態変更は必ず副作用のない純粋な関数として記述する必要があります。冪等性を担保することでテスタビリティを担保し、状態変更の予測可能性を高めることにも寄与しているでしょう。
登場人物
Reduxではおおまかに上記の登場人物がいます。それぞれを軽く解説していきます。
- State
まさに今回焦点を当てている状態そのものです。アプリに表示されるデータであったり、UIコンポーネントの状態であったりといった具合に様々なものがStateとして表現されます。
- UI
実際にユーザーが目にすることになる画面です。裏側ではStateとして表現されたデータが、リストやボタンといった目に見える形で表現されます。
- Action
状態の変更内容を表現したものであり、Reduxにおける状態変更を行うための唯一の手段です。
- Reducer
状態変更を担当するものであり、1つ前のStateとActionから次のStateを計算する純粋な関数として表現されます。
- Store
Stateを保持する役割を持ち、外部からActionが渡される度にReducerを使って次のStateを計算してStateを変更します。
実装編
ここからはサンプルとしてTODOアプリの実装を見ながら、コードレベルで実装方法を解説していきます。
今回紹介するコードは以下のリポジトリで公開しています。
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でも非同期処理を行うための仕組みが用意されています。ReduxではMiddlewareで非同期処理をハンドリングすることが多く、公式サイトではいくつかのアプローチが紹介されています。
データの正規化
規模が大きくなるにつれてStateが保持するデータ量が増えていくいきますが、データ量が増えてくるとデータの一貫性を担保するのが困難になってきます。例えば、1つのTODOが色々な画面で参照される状況においては、同じオブジェクトが複数存在することになってしまいます。
この問題の解決策として、Stateの中身を正規化することでデータの重複を抑制し、データの一貫性管理を効率的に行えるようにする手法が存在します。この手法については私が以前に書いた記事があるので、もし興味がある方は以下の記事をご覧ください。
エコシステム
Reduxはエコシステムが充実しており、様々な周辺ライブラリ・ツールが存在します。
React
Reduxはあくまで状態管理に特化しており、UIをどのように組むかは利用者に一任されています。一般的にはReactと組み合わせて使われることが多く、Reduxの公式サイトでもReactと連携が紹介されており、ReduxとReactは相性が良いことが伺えます。
AndroidにおけるReactは、Android JetpackのViewModelとDataBindingの組み合わせが考え方として近いのではないでしょうか。また、Reactという統一的なUIライブラリは存在しませんが、RecyclerViewなどのUIコンポーネントはDiffUtilを組み合わせることで差分更新を行うことができるため、Reactと同じ考え方をAndroidにも導入することは可能そうです。
Redux DevTools
Redux DevToolsはChrome/Firefoxの拡張機能やスタンドアロンアプリでReduxのStateツリーの変化をリアルタイムに表示し、直接値を編集したり、履歴から任意の状態に戻ったりといったことが可能になるツールで、開発作業を効率化することが可能になります。私が探した限りは、Androidにおいては類似のツールは存在しませんでしたが、本記事で紹介したReduxKitの一部として同様の機能を開発中です。
おわりに
本記事では、AndroidにおけるReduxという選択肢について解説してきました。
- Android開発において、状態管理は悩ましい問題の1つである
- Web開発で広く利用されているReduxは、状態管理に特化したアプローチの1つである
- 全ての状態を1つのオブジェクトで管理することで統一的な状態管理が可能になる
- 状態を読み取り専用で定義することで意図しない状態変更を防ぐ
- 状態変更を純粋な関数で実装することで状態変更の予測可能性を高める
- AndroidにもReduxの考え方を取り入れることで、状態管理にまつわる問題を解決できるのではないか
また、本記事で紹介した内容についてはDroidKaigi 2019でも発表予定です。 今回は詳細に紹介することができなかった内容も含めて50分枠で詳細に発表予定ですので、興味のある方は是非聞きに来てみてください!
DroidKaigi 2018にて「マルチログインの実装方法」という発表をしてきた
DroidKaigi 2018の2日目の17:40からRoom 2にて、「マルチログインの実装方法」というタイトルで登壇してきました。
スライド
概要
- マルチログインの定義
- 複数アカウントで同時にログインできる機能
- マルチログイン実装でよくある問題とその原因
- マルチログイン実装の設計方針
- データや非同期処理をアカウント単位で管理する(アプリ内にアカウント毎のDockerコンテナを立ち上げるイメージ)
- 異なるライフタイムを持つインスタンス管理はDaggerなどのDIコンテナを使うと楽
- サンプルアプリ
- まとめ
- アカウント単位でデータや非同期処理を管理すると幸せになれるよ!
感想・補足など
- 登壇
- DroidKaigiでの登壇は今年で3度目ですが、今回初めてスピーチ原稿を書いてみた
- スピーチ原稿を書く過程で、話したい内容が改めて整理できるので良い
- 本番もわりとリラックスして話せた気がする
- 逆に棒読みになってないかがちょっと心配
- Pairsはマルチログインできません!というくだりで小笑いが取れたので良かった
- Pairsはマルチアカウントも禁止ですよ!
- DroidKaigiでの登壇は今年で3度目ですが、今回初めてスピーチ原稿を書いてみた
- 企業ブース
最後に
僕はスタッフではなかったので普通に参加していただけですが、毎年最高なカンファレンスを開いていただいているスタッフの方々、本当にありがとうございます。来年も必ず参加します!
詳解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アーキテクチャの全体像は以下のようになっています。
MVIにおける処理の流れは以下のようになっています。
- 処理の起点として、Intentが生成される
- IntentがIntentInterpretorに渡され、Actionに変換される
- ActionがProcessorに渡され、Repositoryを使ってデータアクセスを行う
- Processorがデータアクセスの結果をResultとして返す
- ResultがReducerに渡され、Stateに変換される
- StateがRenderに渡され、Viewに反映される
今回は参考にしているリポジトリでは、簡易的なTODOアプリを実装しており、その中でタスク一覧を表示する部分にフォーカスして処理の流れを追ってみたいと思います。
Intent
Intentは主に画面の変化やユーザーからの入力を表すもので、MVIにおける処理の起点になります。
例えば、今回フォーカスしているタスク一覧の初期化時は以下のIntentが発行されます。
interface TasksIntent extends MviIntent { @AutoValue abstract class InitialIntent implements TasksIntent { public static InitialIntent create() { return new AutoValue_TasksIntent_InitialIntent(); } } // 省略 }
Action
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
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
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
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
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といった性質を付け加えている
- 実装
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との互換性を保つために行っている工夫をいくつか紹介しました。
- Jvmアノテーション
- Platform Type:KotlinからJavaを呼び出した結果として返却される特殊な型
- Mapped Type:Javaの型がKotlinのどの型にマッピングされるかの定義
今回紹介した以外にも公式リファレンスには涙ぐましい?工夫が載っています。興味のある方は以下のページを覗いてみると面白いかもしれません。
それでは、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:再購読する条件を細かく制御する
- Catch(エラー発生時にリカバリーを行う)
- 基礎編
- エラーをトーストで表示する:onError
- 代わりのデータを表示する:onErrorReturn
- エラーの種類で処理を分岐する:onErrorResumeNext
- リトライする:retry
- リトライするかをユーザーに尋ねる:retryWhen
- 応用編
- リトライ&キャッシュ:retry/onErrorReturn
- リトライ&キャッシュ&トースト:retry/onErrorReturn/onError
- 実践編
- Retrofitのエラーハンドリング
- RxJavaCallAdapterの拡張
- Retrofitのエラーハンドリング
振り返り・感想
資料
1日目の終了後に自分の資料を改めて見直して、後から資料単体で見た場合に情報量が少なく分かりにくいのでは、ということに気が付き資料を修正した。修正後に一度だけ通しで練習をしたものの、基本的には情報量を増やす方向で修正を行ったので、当日は少し急ぎで喋った。結果として、中盤を過ぎた辺りで早すぎることに気が付き、後半は少しゆっくりと喋った。直前に資料修正は良くないし、発表練習では話す内容だけでなく、タイムマネジメントも含めて練習すべきという当然の学びを得た。
登壇直前
ありがたいことに自分のセッションは立ち見が出た。
RxJava エラーハンドリングのルーム4、立ち見発生です #DroidKaigi #DroidKaigi2017 #DroidKaigi4
— バトルプログラマー柴田智也🔄神様になった日をみて (@tomoya_shibata) March 10, 2017
RxJavaエラーハンドリング見たかったけど,超満員だったのでビデオまつ… #DroidKaigi
— Kuchinashi@Androidすき (@kuchinashi_r) March 10, 2017
セッション前の静かな会場の雰囲気に耐えられず、登壇者席からiPhoneでパノラマで写真を撮るということをやってみた。
まさかの発表者プライベート端末がiPhone #droidkaigi #droidkaigi4
— G_devi (@G_devi) March 10, 2017
登壇者席からiPhoneで写真撮影したけど生きてる #DroidKaigi
— Yuya Kaido | Head of Android - Eureka (@yuyakaido) March 10, 2017
登壇中
前回のDroidKaigiでも同じような規模の部屋で登壇していたこともあり、今回は過度に緊張せずに喋れたと思う。発表中に聞いている人の表情や頷き具合とかを観察しながら説明の仕方を調整する余裕もあった。具体的には、retryまでは頷きながら聞いてくれる人がかなりいたので練習のときよりも説明を軽めにして、retryWhenに入った辺りから今まで頷いてくれていた人の表情が曇り始めたのでより詳細に説明するようにした。
登壇後
登壇後はオフィスアワーとして質問を受けていましたが、その場で分かりやすかったです!と言っていただけたりTwitterでもいくつか反響をいただけたりして素直に嬉しかった。
資料と説明が丁寧でわかりやすい #DroidKaigi4
— FURUKI Eiji (@fullfool) March 10, 2017
retryWhenは説明不足なところもあった模様。反省。
RxJavaにおけるエラーハンドリングがわかりやすい(ただしretryWhenを除く) #DroidKaigi #DroidKaigi4
— グーゴルプレックス (@pmtud) March 10, 2017
まとめ
プロポーザルを出す前はこんなネタで登壇していいのか悩んだが、結果的には登壇して本当に良かったと思います。同僚のMoyuruAizawaがオープニングトークでも言っていましたが、自分の当たり前は必ず誰かに需要があるので、悩んだらとりあえずプロポーザルを出せばいいんじゃないかなと思います。僕は来年も絶対プロポーザルを出します。
皆様2日間おつかれさまでした!オープニングトークでも申し上げましたが、あなたの当たり前のは誰かに需要があります。来年は運営がパンクするほどのプロポーザルを出しましょう!!!
— Moyuru Aizawa (@MoyuruAizawa) March 10, 2017
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ライフを!