GitHubで人気なFlux(とMVI)のAndroidアプリのコードを読んでみるメモ

MVPやClean Architectureはなんとなくは雰囲気をつかめてきましたが、全くFluxやちょっと話題のMVIがわからないので調べてみました。


[lgvalle/android-flux-todo-app](http://github.com/lgvalle/android-flux-todo-app) ★1,170
---
flux androidでGitHubで検索すると一番スターが多いリポジトリです。
しかし、2015年に作られたということでRxJavaなどが利用されていないなど、多少古い気がしています。

![](https://raw.githubusercontent.com/lgvalle/lgvalle.github.io/master/public/images/flux-graph-simple.png)
https://github.com/lgvalle/android-flux-todo-app より

**起動してADDボタンを押したときの動き**を追ってみます。

![screen.png](https://qiita-image-store.s3.amazonaws.com/0/27388/712ccb9e-7fec-e253-0bfe-12c872d462db.png)

### View -> Action


![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/7865e285-0eb4-8b7a-1846-f964cd38fdbf.png)



まず普通にActivityがあり、そこでイベントリスナのセットなどを行っています。
`resetMainInput()`メソッドはただEditTextの中身をsetText("")して空にするものでした。

```java:TodoActivity.java
        Button mainAdd = (Button) findViewById(R.id.main_add);
        mainAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                addTodo();
                resetMainInput();
            }
        });
```

addTodoではバリデーションして、`actionsCreator.create(String)`を呼び出します。(・・・ここでバリデーションでいいの、、？)

```java:TodoActivity.java
    private void addTodo() {
        if (validateInput()) {
            actionsCreator.create(getInputText());
        }
    }

    private boolean validateInput() {
        return !TextUtils.isEmpty(getInputText());
    }
```

ActionsCreator.create()メソッドの中を読んでいきます。
以下のようにdispatcherのdispatchメソッドを呼び出します。
TodoActions.TODO_CREATEもTodoActions.KEY_TEXTもただのキー("todo-create"など)が入った文字列のようです。
Actionのインスタンスはまだ作っていないけど、DispatcherにActionに必要なデータを渡しています。

```java:ActionsCreator.java
    public void create(String text) {
        dispatcher.dispatch(
                TodoActions.TODO_CREATE,
                TodoActions.KEY_TEXT, text
        );

    }
```



### Action -> Dispatcher

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/89b37047-15f5-6a08-cea0-74fdb6b73a65.png)


Dispatcher#dispatchメソッドでは固定の処理が書かれていました。
多分ここは固定で変えないみたい。
一言で言うとActionクラスのインスタンスを作って、それをEventBus(Otto)でイベントを飛ばす感じ。


```java:
    public void dispatch(String type, Object... data) {
        if (isEmpty(type)) {
            throw new IllegalArgumentException("Type must not be empty");
        }

        if (data.length % 2 != 0) {
            throw new IllegalArgumentException("Data must be a valid list of key,value pairs");
        }

        Action.Builder actionBuilder = Action.type(type);
        int i = 0;
        while (i < data.length) {
            String key = (String) data[i++];
            Object value = data[i++];
            actionBuilder.bundle(key, value);
        }
        post(actionBuilder.build());
    }

    private boolean isEmpty(String type) {
        return type == null || type.isEmpty();
    }

    private void post(final Object event) {
        bus.post(event);
    }
```



ちなみにActionクラスは以下のような形。

```java:Action.java
public class Action {
    private final String type;
    private final HashMap<String, Object> data;
...
```

### Dispatcher -> Store


![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/1b5dc6a3-99ff-7189-7b09-db82f98278d4.png)

TodoStoreクラスはシングルトンのようですね。
todoのリストを持っています。

EventBusからonAction()メソッドが呼び出されます。
自身が持っているタスクのリストに追加して、EventBusにTodoStoreChangeEventを飛ばします。


```java:TodoStore.java
public class TodoStore extends Store {

    private static TodoStore instance;
    private final List<Todo> todos;
    private Todo lastDeleted;
...
    @Override
    @Subscribe
    @SuppressWarnings("unchecked")
    public void onAction(Action action) {
        long id;
        switch (action.getType()) {
            case TodoActions.TODO_CREATE:
                String text = ((String) action.getData().get(TodoActions.KEY_TEXT));
                create(text);
                emitStoreChange();
                break;
....
        }
    }
    private void create(String text) {
        long id = System.currentTimeMillis();
        Todo todo = new Todo(id, text);
        addElement(todo);
        Collections.sort(todos);
    }

    private void addElement(Todo clone) {
        todos.add(clone);
        Collections.sort(todos);
    }
```

```java:Store.java
    void emitStoreChange() {
        dispatcher.emitChange(changeEvent());
    }
```

```java:TodoStore.java
    @Override
    StoreChangeEvent changeEvent() {
        return new TodoStoreChangeEvent();
    }
```


### Store -> View
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/ea72416a-1059-113c-03e4-cd4c858d4fe8.png)

EventBusでイベントを受け取って、Storeからデータを取り出して使う感じみたいです。

```java:TodoActivity.java
    @Subscribe
    public void onTodoStoreChange(TodoStore.TodoStoreChangeEvent event) {
        updateUI();
    }

    private void updateUI() {
        listAdapter.setItems(todoStore.getTodos());

        if (todoStore.canUndo()) {
            Snackbar snackbar = Snackbar.make(mainLayout, "Element deleted", Snackbar.LENGTH_LONG);
            snackbar.setAction("Undo", new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    actionsCreator.undoDestroy();
                }
            });
            snackbar.show();
        }
    }
```


[satorufujiwara/kotlin-android-flux](http://github.com/satorufujiwara/kotlin-android-flux) ★202
---
ふじわらさんのリポジトリです。
TODOではなく、GitHubからリポジトリを取得する内容になっています。
KotlinでRxJavaなどなどモダンなものをたくさん使っています。

いくつか発表されていたりして、勉強になります。
**リストの内容が表示されるまで**を追ってみます。

説明としては以下の方が良いので、みてみてください。

[RxJava + Flux (+ Kotlin)によるAndroidアプリ設計](http://qiita.com/satorufujiwara/items/cbf304891daec87ba5b7)

![screen.png](https://qiita-image-store.s3.amazonaws.com/0/27388/6d1f3262-e6e4-c1c0-dd71-5a1d5cc51a11.png)


### View -> Action


![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/7865e285-0eb4-8b7a-1846-f964cd38fdbf.png)


MainFragmentが表示され、Storeの監視とリフレッシュアクションの処理を行います。
RxJavaのObservableを返すメソッドがMainStoreに生えている感じみたいです。
そしてmainAction.refreshList(screenId)を呼び出しています。
ちなみにscreenIdはhashCode()を呼び出した結果が入っています。
おそらくmainAction.refreshList()を呼ぶとAPI通信を行って、変更が通知されてくると思われるので、そこから見ていきましょう。



```kotlin:MainFragment.kt
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ...
        mainStore.repos(screenId)
                .observeOn(AndroidSchedulers.mainThread())
                .bindToLifecycle(this)
                .subscribe {
                    adapter.replaceAll(MainSection.CONTENTS, it.map { MainRepoBinder(activity, it) })
                    adapter.notifyDataSetChanged()
                }
        savedInstanceState ?: mainAction.refreshList(screenId)
    }
```


### Action -> Dispatcher

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/89b37047-15f5-6a08-cea0-74fdb6b73a65.png)

`repository.getRepos()`を呼び出します。repositoryはただRetrofitからAPI通信して取得してくるのみでした。(おそらくRepositoryでキャッシュの実装とかすると思われます。)


```kotlin:MainAction.kt
    fun refreshList(screenId: String) = repository.getRepos("satorufujiwara")
            .subscribeOn(Schedulers.io())
            .subscribe({
                dispatcher.dispatch(screenId, it)
            }, {
                dispatcher.dispatchError("Couldn't get items... ")
            })
```

### Dispatcher -> Store


![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/1b5dc6a3-99ff-7189-7b09-db82f98278d4.png)

DispatcherでDBの更新を行っています。
screenIdはハッシュコードで、まずDBから同じidであれば削除します。
そして、DBにinsertしていきます。
`markSuccessful()`はsqlbriteのメソッドで特に通知のためのメソッドを呼び出していません。
ではどうやってStoreからViewに通知しているかというと、ここにsqlbriteを使っているようです。sqlbriteを使うとDBの変更をRxJavaのイベントとして流すことができます。

```kotlin:MainDispatcher.kt
    fun dispatch(screenId: String, list: List<Repo>) = db.newTransaction().run {
        var rows = 0
        try {
            db.delete(TABLE_NAME, "$SCREEN_ID=?", screenId)
            list.filterNotNull().map { repo ->
                ContentValues().apply {
                    put(SCREEN_ID, screenId)
                    put(REPO_ID, repo.id)
                    put(REPO_NAME, repo.name)
                }
            }.forEach {
                db.insert(TABLE_NAME, it)
                rows++
            }
            markSuccessful()
        } finally {
            end()
        }
        if (list.size == 0 && rows == 0) emptyObservable.onNext(screenId)
        rows
    }
```


### Store -> View

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/ea72416a-1059-113c-03e4-cd4c858d4fe8.png)

MainFragmentでsubscribeしていたのを思い出してください。
dispacherでsqlbriteによる監視が働いて、DBに変更があれば、RxJavaによりViewまで通知されてきます。

```kotlin:MainFragment.kt
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ...
        mainStore.repos(screenId)
                .observeOn(AndroidSchedulers.mainThread())
                .bindToLifecycle(this)
                .subscribe {
                    adapter.replaceAll(MainSection.CONTENTS, it.map { MainRepoBinder(activity, it) })
                    adapter.notifyDataSetChanged()
                }
```

```kotlin:MainStore.kt
class MainStore(val dispatcher: MainDispatcher) {

    fun errorEvents() = dispatcher.errorEventObservable

    fun repos(screenId: String) = dispatcher.repos(screenId)
}
```

DBの変更の監視を行っている。

```kotlin:MainDispatcher.kt
    fun repos(screenId: String) =
            db.createQuery(TABLE_NAME, "SELECT * FROM $TABLE_NAME WHERE $SCREEN_ID=?", screenId)
                    .mapToList { Repo(it.getStringByName(REPO_ID), it.getStringByName(REPO_NAME)) }
                    .mergeWith(emptyObservable.filter { it == screenId }.map { emptyList<Repo>() })
```

[oldergod/android-architecture](http://github.com/oldergod/android-architecture) ★22
---
[Android Architecture Blueprints](https://github.com/googlesamples/android-architecture)のMVI(Model-View-Intent)版を開発しているという話を本人から聞いて、ちょっと読んでみたいと思っていました。(IntentはAndroidのIntentじゃないですよ！)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/19e92c6a-cdfe-4c00-8154-a2b3b84de091.png)

今回は、**フィルターを変更したときの動き**を見ていきます(タスク追加は別画面に遷移してしまうので、、)
![screen.png](https://qiita-image-store.s3.amazonaws.com/0/27388/e619a57f-c846-2a5e-dd95-7e19e74adde7.png)

### USER -> EVENT -> INTENT

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/6ca2bb18-b85b-8ca0-affa-ca3eb0ce5d5b.png)

まず、ClearCompletedTasksIntentを作成し、それをRxJavaのPublisherのmClearCompletedTaskIntentPublisherに流します。

```java:TasksFragment.java
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.menu_clear:
                mClearCompletedTaskIntentPublisher.onNext(TasksIntent.ClearCompletedTasksIntent.create());
                break;
...
```

AutoValueによってequalsメソッドなどを自動的に作ったクラスのインスタンスを作っているようです。

```java:TasksIntent.java
...
    @AutoValue
    abstract class ClearCompletedTasksIntent implements TasksIntent {
        public static ClearCompletedTasksIntent create() {
            return new AutoValue_TasksIntent_ClearCompletedTasksIntent();
        }
    }
...
```

mClearCompletedTaskIntentPublisherに流したClearCompletedTasksIntentはどこに行くのかというと、まず、イベントは他のIntentを流すObservableとマージされます。(changeFilterIntent()でmClearCompletedTaskIntentPublisherを返す。)

```java:TasksFragment.java
    @Override
    public Observable<TasksIntent> intents() {
        return Observable.merge(initialIntent(), refreshIntent(), adapterIntents(),
                clearCompletedTaskIntent()).mergeWith(changeFilterIntent());
    }
```

で、実際Intentはどこでどう処理されていくのかというと、まずこのintents()で返ってくるものをmViewModelに渡します。

```java:TasksFragment.java
mViewModel.processIntents(intents());
```

### Intent -> Action -> Result -> State

![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/ec323a94-41b6-a3a8-2ea5-8bcba111b1e5.png)


図にVIEW MODELとあるように、ViewModelのクラスの中で、上のすべてを行います。
まずmIntentsSubjectでsubscribeします。

```java:TasksViewModel.java
    @Override
    public void processIntents(Observable<TasksIntent> intents) {
        intents.subscribe(mIntentsSubject);
    }
```

その**mIntentsSubjectから以下のようにStateのObservableまで一気に変換**しています。

```java:TasksViewModel.java
    private Observable<TasksViewState> compose() {
        return mIntentsSubject.doOnNext(MviViewModel::logIntent)
                .scan(initialIntentFilter)
                .map(this::actionFromIntent)
                .doOnNext(MviViewModel::logAction)
                .compose(mActionProcessorHolder.actionProcessor)
                .doOnNext(MviViewModel::logResult)
                .scan(TasksViewState.idle(), reducer)
                .doOnNext(MviViewModel::logState);
    }
```

それぞれactionFromIntent()でIntent -> Action、 
mActionProcessorHolder.actionProcessorで Processorで処理してResultを取得、
scan()でreducerを使ってStateを取得
を行っています。

それぞれみていきましょう

### Intent -> Action
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/5e0e5f74-0a65-7aec-a564-7d1b6b4b544e.png)

actionFromIntent()は以下のようになっており、intentのインスタンスを判定して、Actionに変換しています。今回はFilterしたいので、以下の処理を出しています。

```java:TasksViewModel.java
private TasksAction actionFromIntent(MviIntent intent) {
...
        if (intent instanceof TasksIntent.ChangeFilterIntent) {
            return TasksAction.LoadTasks.loadAndFilter(false,
                    ((TasksIntent.ChangeFilterIntent) intent).filterType());
        }
...
        throw new IllegalArgumentException("do not know how to treat this intent " + intent);
    }
```

((TasksIntent.ChangeFilterIntent) intent).filterType())ではTasksAction.LoadTasksのインスタンスを作って返します。このクラスはfilterTypeを保持しています。(AutoValueにより自動生成したクラスを利用します)

```java:TasksAction.java
    @AutoValue
    abstract class LoadTasks implements TasksAction {
        public abstract boolean forceUpdate();

        @Nullable
        public abstract TasksFilterType filterType();

        public static LoadTasks loadAndFilter(boolean forceUpdate, TasksFilterType filterType) {
            return new AutoValue_TasksAction_LoadTasks(forceUpdate, filterType);
        }

        public static LoadTasks load(boolean forceUpdate) {
            return new AutoValue_TasksAction_LoadTasks(forceUpdate, null);
        }
    }
```


### Action -> Result
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/04fce779-b798-dd4f-2f8a-a450b5fca577.png)

ProcessorによるActionの実際の実行になります。
圧倒的RxJava感。ちょっと最初見たときはわかりにくかったですが、ofTypeで絞り込んで、それぞれのメンバで処理するみたいです。
つまり今回は `shared.ofType(TasksAction.LoadTasks.class).compose(loadTasksProcessor)`が実行されそう。

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

実際に処理をするloadTasksProcessorはどうなっているかというと以下のようになっています。
mTasksRepository.getTasks()でレポジトリから取得する処理が出てきましたね！
そして結果を`.map(tasks -> TasksResult.LoadTasks.success(tasks, action.filterType()))`でResultに変換します。


```java
    private ObservableTransformer<TasksAction.LoadTasks, TasksResult.LoadTasks> loadTasksProcessor =
            actions -> actions.flatMap(action -> mTasksRepository.getTasks(action.forceUpdate())
                    .toObservable()
                    .map(tasks -> TasksResult.LoadTasks.success(tasks, action.filterType()))
                    .onErrorReturn(TasksResult.LoadTasks::failure)
                    .subscribeOn(mSchedulerProvider.io())
                    .observeOn(mSchedulerProvider.ui())
                    .startWith(TasksResult.LoadTasks.inFlight()));
```

ちなみにTasksResult.LoadTasks.successは以下のような形。AutoValueにも見慣れてきました。

```java
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);
        }
    }
```

### Result -> State
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/41ae776d-f8b0-e239-122d-74a5f7678613.png)

reducerは以下のようになっています。
最後にTasksViewStateをResultを使ってTasksViewStateのBuilderを使って組み立てているようです。ちゃんとエラーとかは考えられていそうです。

```java:TasksViewModel.java
    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();
                    }
                } else if (result instanceof TasksResult.GetLastState) {
                    ....
            };

    private static List<Task> filteredTasks(@NonNull List<Task> tasks,
                                            @NonNull TasksFilterType filterType) {
        List<Task> filteredTasks = new ArrayList<>(tasks.size());
        switch (filterType) {
            case ALL_TASKS:
                filteredTasks.addAll(tasks);
                break;
            case ACTIVE_TASKS:
                for (Task task : tasks) {
                    if (task.isActive()) filteredTasks.add(task);
                }
                break;
            case COMPLETED_TASKS:
                for (Task task : tasks) {
                    if (task.isCompleted()) filteredTasks.add(task);
                }
                break;
        }
        return filteredTasks;
    }

```


### State -> SCREEN
![image.png](https://qiita-image-store.s3.amazonaws.com/0/27388/38669caa-fc05-ace8-821d-d80949d9cfe9.png)

以下のTaskViewStateのObservableはmStatesSubjectによってsubscribeされ、mStatesSubjectはFragmentのメソッドによりSubscribeされます。

```java:TasksViewModel.java
        compose().subscribe(this.mStatesSubject);
```

```java:TasksViewModel.java
    private Observable<TasksViewState> compose() {
        return mIntentsSubject.doOnNext(MviViewModel::logIntent)
                .scan(initialIntentFilter)
                .map(this::actionFromIntent)
                .doOnNext(MviViewModel::logAction)
                .compose(mActionProcessorHolder.actionProcessor)
                .doOnNext(MviViewModel::logResult)
                .scan(TasksViewState.idle(), reducer)
                .doOnNext(MviViewModel::logState);
    }
```

Fragmentのメソッドとなります。

```java:TasksFragment.java
mDisposables.add(mViewModel.states().subscribe(this::render));
```

そして以下のように表示します。シンプル！


```java:TasksFragment.java
    @Override
    public void render(TasksViewState state) {
        mSwipeRefreshLayout.setRefreshing(state.isLoading());
        if (state.error() != null) {
            showLoadingTasksError();
            return;
        }

        if (state.taskActivated()) showMessage(getString(R.string.task_marked_active));

        if (state.taskComplete()) showMessage(getString(R.string.task_marked_complete));

        if (state.completedTasksCleared()) showMessage(getString(R.string.completed_tasks_cleared));

        if (state.tasks().isEmpty()) {
            switch (state.tasksFilterType()) {
                case ACTIVE_TASKS:
                    showNoActiveTasks();
                    break;
                case COMPLETED_TASKS:
                    showNoCompletedTasks();
                    break;
                default:
                    showNoTasks();
                    break;
            }
        } else {
            mListAdapter.replaceData(state.tasks());

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

            switch (state.tasksFilterType()) {
                case ACTIVE_TASKS:
                    showActiveFilterLabel();
                    break;
                case COMPLETED_TASKS:
                    showCompletedFilterLabel();
                    break;
                default:
                    showAllFilterLabel();
                    break;
            }
        }
    }
```


# まとめ(感想)
それぞれ全てActionのようなもので命令を送り、円を描いて？めぐって返ってきます。
FluxではStoreに何かしらの形で状態を保持します。
その上で、一時データ保存領域として、`lgvalle/android-flux-todo-app`ではメンバ変数、`satorufujiwara/kotlin-android-flux`ではDB利用しています。
MVIは状態を持つところがないのが本当にすごいですが、自分がまだIntentとActionの違いなどを理解できていないので、修行が必要そうです。
またMVIで利用されていたStateの作成してUIに反映はとてもシンプルになりそうで良さそうです。
他の発見としてはFluxとMVIで共通していたのは、だいたいちゃんとデータを保存するものの場合はRepositoryがありそうということです。
あとはコードでなく、ちゃんとどういう思想でやっているかなどを理解していきたいですね！