詳解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 Annotation

@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でカスタムバインディングを定義する場合、BindingAdapterアノテーションを付与したメソッドはstaticメソッドとして定義されていることを期待しているため、Kotlinでカスタムバインディングを実装する場合はJvmStaticアノテーションが必要になります。具体例に関しては、Android Data Binding Tipsをご覧ください。

@JvmField

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

// 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 Annotationは今回紹介した以外にもいくつかあります。

  • @JvmName
  • @JvmMultifileClass
  • @JvmWildcard
  • @JvmSuppressWildcards

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

Platform Type

KotlinはJavaとの相互運用性を重視しており、Kotlin/Javaが混在するプロジェクトではJavaからKotlinのコードを呼び出すこともあると思います。ここで疑問なのが、Kotlinは言語仕様としてNullableとNonNullを明確に区別している点です。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で書いているメリットが半減してしまいますが、Nullableアノテーション、NonNullアノテーションを使うことで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 Kotin!

DroidKaigi 2017で登壇してきた

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

スライド

概要

  • RxJavaのおさらい

    • Reactive Extensions for Java
    • マーブルダイアグラム
  • RxJavaにおけるエラーハンドリング

    • Catch(エラー発生時にリカバリーを行う)
      • onError: エラーをキャッチする
      • onErrorReturn: エラーをキャッチして代わりのデータを返す
      • onErrorResumeNext: エラーをキャッチして代わりのObservableを返す
    • Retry(エラー発生時にresubscibeを行う)
      • retry: エラーをキャッシュしてresubscribeする
      • retryWhen: resubscribeする条件を細かく制御する
  • 基礎編

    • エラーをトーストで表示する:onError
    • 代わりのデータを表示する:onErrorReturn
    • エラーの種類で処理を分岐する:onErrorResumeNext
    • リトライする:retry
    • リトライするかをユーザーに尋ねる:retryWhen
  • 応用編

    • リトライ&キャッシュ:retry, onErrorReturn
    • リトライ&キャッシュ&トースト:retry, onErrorReturn, onError
  • 実践編

    • Retrofitのエラーハンドリング
      • RxJavaCallAdapterの拡張

振り返り・感想

資料

1日目の終了後に自分の資料を改めて見直して、後から資料単体で見た場合に情報量が少なく分かりにくいのでは、ということに気が付き資料を修正した。修正後に一度だけ通しで練習をしたものの、基本的には情報量を増やす方向で修正を行ったので、当日は少し急ぎで喋った。結果として、中盤を過ぎた辺りで早すぎることに気が付き、後半は少しゆっくりと喋った。

直前に資料修正は良くないし、発表練習では話す内容だけでなく、タイムマネジメントも含めて練習すべきという当然の学びを得た。

登壇直前

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

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

登壇中

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

登壇後

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

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

まとめ

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

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ライフを!

Android Data Binding Tips

はじめに

Android Advent Calendar 2016の10日目の記事です。

Data Binding使ってますか?便利ですよね。私も個人でも仕事でも使っています。 今回の記事ではData BindingのTipsを紹介したいと思います。

カスタムバインディングを定義する

Data Bindingはデータオブジェクトの値をViewに反映するためのものですが、標準で用意されているバインディングだけではちょっと物足りません。例えば、Glide/Picassoを使ってImageViewに画像を表示するようなユースケースでは、カスタムバインディングを定義する必要があります。

Data Bindingでは以下のようにBindingAdapterアノテーションを使ってカスタムバインディングを定義します。

public class CustomBinder {
    @BindingAdapter("app:imageUrl")
    public static void imageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

上記のようなカスタムバインディングを定義した上で、xmlで以下のようなバインディングを行います。 (app:imageUrlに渡す値は適宜書き換えてください。)

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{article.thumbnail()}"/>

今回はImageViewに対して画像URLをバインドする方法を紹介しましたが、これを応用すれば基本的には何でもバインドすることが出来ます。が、あまりにアクロバティックなバインディングを行うと後からプロジェクトにジョインした人が困るので、用法用量を守って使いましょう。

ちなみにカスタムバインディングの悪用事例は以下の記事で紹介されています。

include先にデータオブジェクトを渡す

レイアウトの再利用を目的として別のレイアウトファイルをincludeすることがあると思います。Data Bindingではinclude先にデータオブジェクトを渡すことが出来ます。

まずは、includeされる側で以下のようにデータオブジェクトの定義を書きます。

<layout>
    <data>
        <variable
            name="article"
            type="com.yuyakaido.android.flow.domain.entity.Article"/>
    </data>

    (省略)
</layout>

次に、includeする側でデータオブジェクトを渡す処理を書きます。

<include
    layout="@layout/include_item_article"
    app:article="@{article}"/>

以上でinclude先にデータオブジェクトを受け渡すことが出来ます。

おわりに

今回は以下の2つのTipsを紹介しました。

どちらもある程度な規模のアプリを開発する上で必要になってくるかなと思います。

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

Kotlin×Data Bindingの問題点と解決法

はじめに

Kotlin Advent Calendar 2016の3日目の記事です。

KotlinとData Binding、両方とも便利ですよね。この記事ではこれらを組み合わせて使った場合に発生するいくつかの問題点と解決法を紹介したいと思います。ググるといくつか情報が出てきますが、断片的であったり、新しい解決法が登場していたりするため、情報を集約して解決法を列挙する形で書いています。

この記事では、

  1. Kotlinから自動生成されたクラスが参照できない
  2. BindingAdapterを使ったカスタムバインディングが使えない

という2つの問題を扱います。

1. Kotlinから自動生成されたクラスが参照できない

これはData Bindingに限った話ではなく、KotlinとAnnotation Processingを組み合わせた場合に発生します。

Data Bindingライブラリを使う場合、xmlで記述したレイアウトをlayoutタグで囲むことで、FooBindingのようなクラスが生成されます。しかし、このFooBindingをKotlinから参照すると正常にビルドが出来ず、以下のようなビルドエラーが表示されると思います。

Error:(7, 35) Unresolved reference: databinding
Error:(19, 27) Unresolved reference: ItemArticleBinding
Error:(40, 35) Unresolved reference: ItemArticleBinding

この記事で詳しくは説明しませんが、KotlinとJavaコンパイル順序が関係しています。以下の記事で詳しい解説があるので、興味のある方はご覧ください。

この問題を解決する方法として、2016年12月現在では2つの方法があります。

generateStubsを有効にする

build.gradleに以下の記述を追加することでKotlinとJavaコンパイル順序に関連した問題が解消されて、KotlinからFooBindingのような自動生成されたクラスが参照できるようになると思います。

kapt {
    generateStubs = true
}

kotlin-kaptを使う

もう1つの解決方法として、同じくbuild.gradleに以下の記述を追加することも解決できます。

apply plugin: 'kotlin-kapt'

しかし、公式ブログには以下のような記述があり、kotlin-kaptはまだ実験段階のようです。この記事を書くにあたって簡単なサンプルを実装した限りは正常に動作しましたが、まだ問題が残っている可能性もありそうです。

The new annotation processing still has known issues and may not be compatible with all annotation processors. You should enable it only if you’ve run into problems with the default kapt annotation processing implementation.

2. BindingAdapterを使ったカスタムバインディングが使えない

Data BindingではTextViewにStringをバインドするといった標準で用意されているバインディングに加えて、カスタムバインディングを実装することができます。よくある例としては、ImageViewにGlideやPicassoなどを使ってURLをバインディングするものだと思います。これをJavaで実装すると以下のようになります。

public class CustomBinder {
    @BindingAdapter("app:imageUrl")
    public static void imageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url).into(imageView);
    }
}

JavaではBindingAdapterアノテーションを付与したstaticなメソッドとしてカスタムバインディングを実装することができます。これをKotlinで実装すると以下のようになります。

object CustomBinder {
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでいけるかと思いきや、実行すると以下のようなエラーが発生します。

java.lang.IllegalStateException: Required DataBindingComponent is null in class ItemArticleBinding. A BindingAdapter in com.yuyakaido.android.flow.presentation.binder.CustomBinder.Companion is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

一言で要約すると、BindingAdapterはstaticメソッドとして実装してね、という感じです。

この問題を解決する方法はいくつも考えられますが、大きくは2つあります。

JvmStaticアノテーションを付与する

Kotlinにはstaticというキーワードはありませんが、以下のようにJvmStaticアノテーションを付与すると、Javaから見たときにstaticとして見えるようになります。

object CustomBinder {
    @JvmStatic
    @BindingAdapter("app:imageUrl")
    fun imageUrl(imageView: ImageView, url: String?) {
        Glide.with(imageView.context).load(url).into(imageView)
    }
}

これでめでたくカスタムバインディングが動作するはずです。

拡張関数としてカスタムバインディングを実装する

もっとKotlinらしく書くとしたら、拡張関数としてカスタムバインディングを実装してもいいかもしれません。

@BindingAdapter("android:imageUrl")
fun ImageView.imageUrl(url: String?) {
    Glide.with(context).load(url).into(this)
}

上記のようにImageViewクラスにimageUrlという拡張関数を実装すると、以下のようにカスタムバインディングであることを意識せずにバインドできるようになります。

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:imageUrl="@{article.thumbnail()}"/>

さいごに

この記事ではKotlinとData Bindingを組み合わせて使う場合の問題点と解決策を紹介しました。

また、この記事で紹介したコードは以下のリポジトリで公開しています。興味がある方は覗いてみてください。

それでは、良いKotlin×Data Bindingライフを!

繰り返しのある予定表示に対応したカレンダーを作った

はじめに

Android Advent Calendar 2015の10日目の記事です。

2014年のAndroid Advent Calendarは25日目を担当したので、今年は無難な日付に収まることにしました。今回は繰り返しのある予定表示に対応したカレンダーライブラリを作ったので、その紹介をさせていただきます。

リポジトリ

繰り返しのある予定について

予定の繰り返しをどのように表現するかはRFC2445にて定義されており、一般にRRuleと呼ばれています。RRuleは予定の繰り返し周期や繰り返し曜日などを表現することができ、本ライブラリはそのRRuleを用いた予定の繰り返しに対応しています。例えば、毎週月曜日という繰り返し条件は以下のRRuleで表現されます。

FREQ=WEEKLY;BYDAY=MO

もう少し具体的な例を考えてみます。 予定の開始日時が2015-09-05T10:00:00.000Zで、RRuleがFREQ=WEEKLYとし、予定の展開範囲を2015-09-01T00:00:00.000Zから2015-09-30T23:59:59.999Zとします。この場合は、以下の4つの予定に展開されます。

  • 2015-09-05T10:00:00.000Z
  • 2015-09-12T10:00:00.000Z
  • 2015-09-19T10:00:00.000Z
  • 2015-09-26T10:00:00.000Z

機能紹介

土日の色付け

以下のように土曜日は青、日曜日は赤で表示されます。

f:id:yuyakaido:20171210092914p:plain

予定の表示

予定がある日には以下のようにドットが表示されます。最大2つまで表示され、3つ以上ある場合には右下にプラスマークが付きます。

f:id:yuyakaido:20171210092938p:plain

予定の色分け

予定には任意の色を設定することが出来ます。デフォルトでは赤、オレンジ、黄、緑、ティファニーブルー、ライトブルー、青、紫、ピンク、藍のテーマカラーを用意してあります。詳しくはThemeクラスをご参照ください。

f:id:yuyakaido:20171210092949p:plain

テーマカラー

本ライブラリでは日付セルをタップするとその日付がテーマカラーにもとづいてハイライトされます。ライブラリの初期化時にテーマカラーを渡すことで切り替えが可能です。

f:id:yuyakaido:20171210093001p:plain

繰り返しのある予定の表示

本ライブラリではRRuleをもとに自動的に予定が展開されて表示されます。繰り返しの周期は毎日(FREQ=DAILY)毎週(FREQ=WEEKLY)毎月(FREQ=MONTHLY)毎年(FREQ=YEARLY)に対応しています。例えば、開始時間が2015-08-01T00:00:00.000Zで毎週繰り返し(FREQ=WEEKLY)というRRuleを持った予定と開始時間が2015-08-02T00:00:00.000Zで隔週繰り返し(FREQ=WEEKLY;INTERVAL=2)という2つの予定を設定すると以下のように表示されます。

f:id:yuyakaido:20171210093013p:plain

連日予定の帯表示

複数日にわたる予定の場合には、以下のように帯表示となります。予定が被っている場合には重なって表示されます。

f:id:yuyakaido:20171210093028p:plain

使い方紹介

カレンダーの表示

カレンダー本体はCouplesCalendarFragmentで実装されています。カレンダーを表示するだけであれば以下のようにFragmentTransactionCouplesCalendarFragmentを表示するだけでOKです。

CouplesCalendarFragment fragment = CouplesCalendarFragment.newInstance();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.main_activity_fragment_container, fragment);
transaction.commit();

予定の表示

以下のように任意のオブジェクトにCouplesCalendarEventインターフェースを実装することでライブラリで表示可能な予定オブジェクトとなります。CouplesCalendarFragmentsetEvents(List<CouplesCalendarEvent> events)にこの予定オブジェクトのリストを渡すことで予定が表示されます。

public class SampleEvent implements CouplesCalendarEvent {

    private Date mStartAt;
    private Date mEndAt;
    private String mRRule;
    private int mEventColor;

    public void setStartAt(Date startAt) {
        mStartAt = startAt;
    }

    @Override
    public Date getStartAt() {
        return mStartAt;
    }

    public void setEndAt(Date endAt) {
        mEndAt = endAt;
    }

    @Override
    public Date getEndAt() {
        return mEndAt;
    }

    public void setRecurrenceRule(String recurrenceRule) {
        mRRule = recurrenceRule;
    }

    @Override
    public String getRecurrenceRule() {
        return mRRule;
    }

    public void setEventColor(int eventColor) {
        mEventColor = eventColor;
    }

    @Override
    public int getEventColor() {
        return mEventColor;
    }

}

CouplesCalendarEventインターフェースには以下の4つの情報を取得するためにメソッドが定義されています。

  • 予定の開始時間
  • 予定の終了時間
  • 予定の繰り返し条件
  • 予定の色

このうちでカレンダーに予定を表示するためには最低限以下の情報が必要になります。

  • 予定の開始時間
  • 予定の終了時間

予定の色を設定しなかった場合はデフォルトカラーが設定され、繰り返し条件(RRule)に何も設定しなかった場合は繰り返しのない予定として扱われます。