MVPやClean Architectureはなんとなくは雰囲気をつかめてきましたが、全くFluxやちょっと話題のMVIがわからないので調べてみました。
lgvalle/android-flux-todo-app ★1,170
flux androidでGitHubで検索すると一番スターが多いリポジトリです。
しかし、2015年に作られたということでRxJavaなどが利用されていないなど、多少古い気がしています。
https://github.com/lgvalle/android-flux-todo-app より
起動してADDボタンを押したときの動きを追ってみます。
View -> Action
まず普通にActivityがあり、そこでイベントリスナのセットなどを行っています。
resetMainInput()
メソッドはただEditTextの中身をsetText("")して空にするものでした。
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)
を呼び出します。(・・・ここでバリデーションでいいの、、?)
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に必要なデータを渡しています。
public void create(String text) {
dispatcher.dispatch(
TodoActions.TODO_CREATE,
TodoActions.KEY_TEXT, text
);
}
Action -> Dispatcher
Dispatcher#dispatchメソッドでは固定の処理が書かれていました。
多分ここは固定で変えないみたい。
一言で言うとActionクラスのインスタンスを作って、それをEventBus(Otto)でイベントを飛ばす感じ。
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クラスは以下のような形。
public class Action {
private final String type;
private final HashMap<String, Object> data;
...
Dispatcher -> Store
TodoStoreクラスはシングルトンのようですね。
todoのリストを持っています。
EventBusからonAction()メソッドが呼び出されます。
自身が持っているタスクのリストに追加して、EventBusにTodoStoreChangeEventを飛ばします。
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);
}
void emitStoreChange() {
dispatcher.emitChange(changeEvent());
}
@Override
StoreChangeEvent changeEvent() {
return new TodoStoreChangeEvent();
}
Store -> View
EventBusでイベントを受け取って、Storeからデータを取り出して使う感じみたいです。
@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 ★202
ふじわらさんのリポジトリです。
TODOではなく、GitHubからリポジトリを取得する内容になっています。
KotlinでRxJavaなどなどモダンなものをたくさん使っています。
いくつか発表されていたりして、勉強になります。
リストの内容が表示されるまでを追ってみます。
説明としては以下の方が良いので、みてみてください。
RxJava + Flux (+ Kotlin)によるAndroidアプリ設計
View -> Action
MainFragmentが表示され、Storeの監視とリフレッシュアクションの処理を行います。
RxJavaのObservableを返すメソッドがMainStoreに生えている感じみたいです。
そしてmainAction.refreshList(screenId)を呼び出しています。
ちなみにscreenIdはhashCode()を呼び出した結果が入っています。
おそらくmainAction.refreshList()を呼ぶとAPI通信を行って、変更が通知されてくると思われるので、そこから見ていきましょう。
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
repository.getRepos()
を呼び出します。repositoryはただRetrofitからAPI通信して取得してくるのみでした。(おそらくRepositoryでキャッシュの実装とかすると思われます。)
fun refreshList(screenId: String) = repository.getRepos("satorufujiwara")
.subscribeOn(Schedulers.io())
.subscribe({
dispatcher.dispatch(screenId, it)
}, {
dispatcher.dispatchError("Couldn't get items... ")
})
Dispatcher -> Store
DispatcherでDBの更新を行っています。
screenIdはハッシュコードで、まずDBから同じidであれば削除します。
そして、DBにinsertしていきます。
markSuccessful()
はsqlbriteのメソッドで特に通知のためのメソッドを呼び出していません。
ではどうやってStoreからViewに通知しているかというと、ここにsqlbriteを使っているようです。sqlbriteを使うとDBの変更をRxJavaのイベントとして流すことができます。
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
MainFragmentでsubscribeしていたのを思い出してください。
dispacherでsqlbriteによる監視が働いて、DBに変更があれば、RxJavaによりViewまで通知されてきます。
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()
}
class MainStore(val dispatcher: MainDispatcher) {
fun errorEvents() = dispatcher.errorEventObservable
fun repos(screenId: String) = dispatcher.repos(screenId)
}
DBの変更の監視を行っている。
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 ★22
Android Architecture BlueprintsのMVI(Model-View-Intent)版を開発しているという話を本人から聞いて、ちょっと読んでみたいと思っていました。(IntentはAndroidのIntentじゃないですよ!)
今回は、フィルターを変更したときの動きを見ていきます(タスク追加は別画面に遷移してしまうので、、)
USER -> EVENT -> INTENT
まず、ClearCompletedTasksIntentを作成し、それをRxJavaのPublisherのmClearCompletedTaskIntentPublisherに流します。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_clear:
mClearCompletedTaskIntentPublisher.onNext(TasksIntent.ClearCompletedTasksIntent.create());
break;
...
AutoValueによってequalsメソッドなどを自動的に作ったクラスのインスタンスを作っているようです。
...
@AutoValue
abstract class ClearCompletedTasksIntent implements TasksIntent {
public static ClearCompletedTasksIntent create() {
return new AutoValue_TasksIntent_ClearCompletedTasksIntent();
}
}
...
mClearCompletedTaskIntentPublisherに流したClearCompletedTasksIntentはどこに行くのかというと、まず、イベントは他のIntentを流すObservableとマージされます。(changeFilterIntent()でmClearCompletedTaskIntentPublisherを返す。)
@Override
public Observable<TasksIntent> intents() {
return Observable.merge(initialIntent(), refreshIntent(), adapterIntents(),
clearCompletedTaskIntent()).mergeWith(changeFilterIntent());
}
で、実際Intentはどこでどう処理されていくのかというと、まずこのintents()で返ってくるものをmViewModelに渡します。
mViewModel.processIntents(intents());
Intent -> Action -> Result -> State
図にVIEW MODELとあるように、ViewModelのクラスの中で、上のすべてを行います。
まずmIntentsSubjectでsubscribeします。
@Override
public void processIntents(Observable<TasksIntent> intents) {
intents.subscribe(mIntentsSubject);
}
そのmIntentsSubjectから以下のようにStateのObservableまで一気に変換しています。
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
actionFromIntent()は以下のようになっており、intentのインスタンスを判定して、Actionに変換しています。今回はFilterしたいので、以下の処理を出しています。
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により自動生成したクラスを利用します)
@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
ProcessorによるActionの実際の実行になります。
圧倒的RxJava感。ちょっと最初見たときはわかりにくかったですが、ofTypeで絞り込んで、それぞれのメンバで処理するみたいです。
つまり今回は shared.ofType(TasksAction.LoadTasks.class).compose(loadTasksProcessor)
が実行されそう。
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に変換します。
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にも見慣れてきました。
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
reducerは以下のようになっています。
最後にTasksViewStateをResultを使ってTasksViewStateのBuilderを使って組み立てているようです。ちゃんとエラーとかは考えられていそうです。
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
以下のTaskViewStateのObservableはmStatesSubjectによってsubscribeされ、mStatesSubjectはFragmentのメソッドによりSubscribeされます。
compose().subscribe(this.mStatesSubject);
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のメソッドとなります。
mDisposables.add(mViewModel.states().subscribe(this::render));
そして以下のように表示します。シンプル!
@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がありそうということです。
あとはコードでなく、ちゃんとどういう思想でやっているかなどを理解していきたいですね!