KCF Labo Blog

KDDI Commerce Forwardの開発ブログ

MVVM+Fluxを試してみた

こんにちは。

現在、iOSAndroidの開発を行なっている高橋(@KoH_1011)です。

Wowma!アプリではMVVMのアーキテクチャを使用しています。

ただ、最近Fluxもいいよ!という声をよく聞くので、現在採用しているMVVMとFluxを合わせた実装を試してみたいと思います。

この記事を読むことで以下のことがわかる。というのを目標に書いていきます。

・Fluxについてざっくり理解できる

・Fluxのコードのイメージができる

Fluxとは

FluxとはFacebookが提唱したUIを持ったアプリを作るためのアーキテクチャです。

公式では以下のように説明されています。

Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction. As we go through this guide we'll talk about the different pieces of a Flux application and show how they form unidirectional cycles that data can flow through. Fluxは、アプリケーションのデータフローを管理するためのパターンです。最も重要な概念は、データが一方向に流れることです。このガイドでは、Fluxアプリケーションのさまざまな部分について説明し、データが流れることができる単方向サイクルをどのように形成するかを示します。

Fluxは以下の図でよく説明されています。

f:id:kosuke_1011:20180608162133p:plain

図でわかるようにデータが単一方向に流れるので非常に見通しがよくなるほか、「データはすべて Action を介して更新する」という制約があるため、 View の更新状態を予測しやすくなります。

それぞれの責務を説明すると、

・Dispatcher:発火された ActionStore に通知するもの

・Store:dispatcher 経由できた Action を格納するもの

・Action:View 等から発火されたイベントを dispatcher に通知するもの

・View(ViewController): Store のデータを反映するもの。イベントを Action に通知するもの

開発環境

Xcode:9.4

・Swift:4.1

仕様

QiitaAPIを使って以下のことを実現させていきます。

ViewController に記事がリスト表示されている。

DetailViewController にお気に入り状態が表示されている。

ViewControllerDetailViewController のお気に入り状態が同期されている。

使用するライブラリ

まずは使用するライブラリを取り込みます。

使用するライブラリは実際にWowma!アプリでも使用している以下のものを使っていきます。

・APIKit

・Himotoki

・RxSwift

Model

Himotoki を使って Model の生成をしていきます。

今回はお気に入り状態の更新を見ていきたいので、 titlefavorite の2つを定義しています。

Request

APIKit を使って Request の生成をしていきます。

リクエスト先は (https://qiita.com/api/v2) になります。

このAPIで新着記事一覧を取得できます。

Flux

Fluxには以下の要素が必要なので、実装していきます。

・Action

・Dispatcher

・Store

Action

まずは Action の実装をしていきます。

今回の仕様は

ViewController に記事がリスト表示されている。

DetailViewController にお気に入り状態が表示されている。

ViewControllerDetailViewController のお気に入り状態が同期されている。

なので、 Action で実装するイベントは以下の2つになります。

① 一覧の取得

② お気に入り状態の更新

まずは

① 一覧の取得

①一覧の取得 では、実際にリクエストを投げてそのレスポンス [Article]dispatch します。 エラーの場合も dispatch する必要がありますが、今回は割愛します。

dispatch の処理は後ほど Dispatcher の箇所で実装していきます。

実際にリクエストを投げる処理を実装していきます。

func load() {   
    let request = ArticleRequest()
    Session.send(request) { (result: Result<ArticleRequest.Response, SessionTaskError>) in
        switch result {
        case .success(let articles):
            // 一覧の取得
        case .failure(let error):
            // エラー
        }
    }
}

上記の処理でレスポンス [Article] を取得できました。

次に取得したレスポンス [Article] を実際に dispatch していきます。

後ほど実装しますが、 dispatcher が必要になるので、初期化メソッドで dispatcher を生成します。

private let dispatcher: ArticleDispatcher

init(dispatcher: ArticleDispatcher = .shared) {
    self.dispatcher = dispatcher
}

あとは後ほど Dispatcher で実装する dispatch メソッドに値を渡すだけです。

self.dispatcher.dispatch(obj: articles)

② お気に入り状態の更新

大枠は上記の実装で終えているので、 dispatch する関数だけ追加します。

お気に入り状態の更新で必要な情報は Article なのでこれを引数にして dispatch します。

こんな感じ。

func update(article: Article) {
    self.dispatcher.dispatch(obj: article)
}

以上で Action の実装は終わりです。

Dispatcher

続いて Dispatcher を実装していきます。

Action から来るイベントは以下の2つになるので、①の関数と②の関数を実装していきます。

① 一覧の取得

② お気に入り状態の更新

先ほど Action の実装で出てきた dispatch をここで実装します。

func dispatch(obj: [Article]) {
    self.articles.onNext(obj)
}

func dispatch(obj: Article) {
    self.article.onNext(obj)
}

dispatch する関数ができたので、これを Store に通知する仕組みを実装していきます。今回は通知する仕組みに PublishSubject を使用したいと思います。こんな感じですね。

let articles = PublishSubject<[Article]>()
let article = PublishSubject<Article>()

また、複数インスタンスだとデータを受け取った受け取ってないということが起きてしまうので、 singleton で実装します。

static let shared = ArticleDispatcher()

Store

続いて Store を実装していきます。

Store で必要な情報は Dispatcherdispatch した PublishSubject を購読して処理に移すことです。

また、購読した値を View に反映させる責務もあるのでその実装もしていきます。

購読するものは Dispatcher で実装した articlesarticle になるので、まずは初期化メソッドにて dispatcher を受け取ります。

required init(dispatcher: ArticleDispatcher = .shared) {

    super.init(dispatcher: dispatcher)
}

dispatcher を受け取ったらそれをもとに購読をしていきます。

/// dispatcherのarticlesの購読
dispatcher.articles.subscribe(onNext: { [weak self] (articles) in
    // 処理
}).disposed(by: disposeBag)

/// dispatcherのarticleの購読
dispatcher.article.subscribe(onNext: { [weak self] (article) in
    // 処理
}).disposed(by: disposeBag)

次に購読したあとの処理を実装していきます。

必要な実装としては以下になります。

・articlesの更新処理

・お気に入り状態を更新する処理

更新処理の実装をする前に View の更新に必要な articlesarticle を定義します。

BehaviorRelay についてはこちらの記事に概要が記載されています。

private(set) var articles = BehaviorRelay<[Article]>(value: [])
private(set) var article = BehaviorRelay<Article>(value: Article())

定義をしたら実際の更新処理を書いていきます。

articles の更新は一覧の更新になるので、値を特に変更せずにそのまま更新します。

self?.articles.accept(articles)

article の更新はお気に入り状態の更新になるので、詳細用の article と 一覧用の articles の両方を更新する必要があります。 また、更新する際にお気に入り状態も更新します。 お気に入り状態を更新する際は ID 等で比較して更新するのがベターですが、今回は比較できるものが title しかないので、 title の一致でお気に入り状態を更新します。

guard var articles = self?.articles.value else { return }
articles.enumerated().forEach { (index, value) in
    if value.title == article.title {
        articles[index].favorite = !articles[index].favorite
        self?.article.accept(articles[index])
    }
}
self?.articles.accept(articles)

以上で Store の実装は終わりです。

TopView

Flux の肝となる部分の実装が終わったので、 Flux を用いながら MVVM の実装をしていきます。

まずはトップの記事一覧で使う ViewModel を実装します。

ViewModel では Action の実行と Store を購読します。

Action の実行は簡単ですね。

そのまま呼ぶだけです。

func load() {
    self.action.load()
}

func update(article: Article) {
    self.action.update(article: article)
}

Store の購読も簡単ですね。

こんな感じです。

self.store.articles
    .asObservable()
    .bind(to: _articles)
    .disposed(by: disposeBag)

bind してる先は BehaviorRelay を定義してあります。

private(set) var articles = BehaviorRelay<[Article]>(value: [])

ここまでの流れをみるとやりたいことは1つですね。

articlesTopViewController で購読させます。

ViewController では以下のように購読させて自前で作成した TopDataSourcebind します。

長くなりましたが、これで記事の一覧を表示することができました。

お気に入りもできます。

viewModel.articles
    .asObservable()
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

記事一覧

f:id:kosuke_1011:20180621150131g:plain

DetailView

記事の一覧の実装が終わったので、あとは詳細画面の実装をしていきます。

Flux の部分は先ほどの実装で終わってるので詳細用の ViewModel を実装します。

先ほどの ViewModel の実装と同じように Action の実行と Store を購読していきます。

Action の実行は例によって実行するだけになります。

func update(article: Article) {
    self.action.update(article: article)
}

Store の購読も先ほどとほぼ同じです。

self.store.article
    .asObservable()
    .bind(to: article)
    .disposed(by: disposeBag)

ここでも bind をしていますが、 bind している先は先ほどと同じ BehaviorRelay です。

private(set) var article = BehaviorRelay<Article>(value: Article())

ここも上と同じですね。

articleDetailViewController に購読させます。

DetailViewController では以下のように購読させて自身の articlebind します。

これで詳細画面を表示することができました。

詳細

f:id:kosuke_1011:20180621150151g:plain

お気に入りの同期

TopViewControllerお気に入りした情報DetailViewController でも反映されます。

ただ、このままだと DetailViewController で変更した お気に入り情報TopViewController に反映されません。

なので、 DetailViewControllerお気に入り情報 の変更を先ほど ViewModel で実装した update に投げてみましょう。

まずは、お気に入りボタンのイベントを取得する必要があるので、以下のように subscribe します。

favoriteButton.rx.tap
    .subscribe { [weak self] _ in
        // 処理
    }.disposed(by: disposeBag)

これで subscribe できたので、あとは ViewModelupdate を呼ぶだけです。

guard let article = self?.viewModel.article.value else { return }
self?.viewModel.update(article: article)

これで、お気に入り状態は TopViewControllerDetailViewController で同期されるようになりました。

トップから詳細へ遷移

f:id:kosuke_1011:20180621150426g:plain

詳細からトップへ戻る

f:id:kosuke_1011:20180621150441g:plain

やってみて

MVVM+Fluxのメリット

・データが一方向にしか流れない点

・各 class の責務が明確なので複数人で実装をしてもそこまでブレない

MVVM+Fluxのデメリット

・Rxの知識が必要

・習得までのハードル

最後に

Wowma!のアプリ開発では Flux を採用していませんが、これを機に少しづつ採用してもいいかもと思いました。

反響があれば今回実装したリポジトリを別途公開しようと思います。

次回は Flux のライブラリの ReactorKit について書きたいと思います。

また、アプリ開発エンジニアを絶賛募集中ですので、興味を持たれた方は話だけでもしましょう。

hrmos.co