更新しました (2017/4/12)
この記事を書いたときから大分時間が経ったので、最新版をこちらに書き直しました。
基本的な階層構造
AndroidでMVCを適用しようとすると、Activity/FragmentがViewとController両方の責務を担っていて綺麗にVとCを分離できず、もやっとしてしました。そこで、色々な資料やサンプルを見たところ、 http://www.slideshare.net/mokemokechicken/iosandroidmodel にある階層構造が一番しっくり来たのでこれをベースにパッケージ構造を考えることにします。
ユーザー操作に近い順に、以下の3階層に分類されます。
- プレゼンター層
- UIの制御やユーザーの入力に対する制御を扱う層。MVCでいうとVCにあたる。
- ビジネス層
- ビジネスロジックを扱う層。MVCでいうとMのうち抽象度の高い部分。
- データ層
- データリソース(データベースやサーバーAPIなど)へのアクセスを行う層。MVCでいうとMのうち抽象度の低い部分。
パッケージ構造
基本的な階層構造をもとに、もう少し細かい単位で分けたものをパッケージ構造に一致させます。
com.example.myapp
|
| (プレゼンター層)
|- activities
|- fragments
|- views
|
| (ビジネス層)
|- usecases
|- entities
|
| (データ層)
|- network
|- storage
または、階層を強く意識させるなら、一段階掘ってこういう階層構造もありかと(やりすぎ?)
com.example.myapp
|
|- presenters
| |- activities
| |- fragments
| |- views
|
|- business
| |- usecases
| |- entities
|
|- data
|- network
|- storage
各パッケージ
activities
Activityが入るパッケージです。Activityが担うべき責務としては、
- Fragmentの追加・変更・削除
- ActionBar, Menu, Drawerの管理
- 他のActivityへの遷移
などがあります。ActionBar以外の画面内のビューについては、Fragmentにやらせるので触らないようにします。
fragments
Fragmentが入るパッケージです。Fragmentが担うべき責務としては、
- UIの制御(ActionBarは除く)
- ユーザーの操作やライフサイクルに合わせて、Model(後述するusecases)を呼び出す。また、処理完了時の通知を受け取ってUIに反映する
があります。
なお、Fragment in Fragmentはライフサイクルがややこしくなるので避けるのがベターだと思っています(Fragment in ViewPager in Fragmentとかは除く)。複数のFragmentで共通して利用したい部品は、後述するviewsパッケージにカスタムビューを作ります。
views
カスタムビューが入るパッケージです。色々なFragmentで共通して利用するViewやView周りのロジックを共通化します。
usecases
ビュー(Activity, Fragment, etc)が直接呼ぶビジネスロジックをここに入れます。Twitterクライアントアプリを例にすると、
- ログイン済みかどうかをチェックする
- ツイートする
- 自分のタイムラインを取得する
などが挙げられます。
内部では後述するnetwork/storageパッケージのクラスを使ってデータの取得や更新を行います。自分のタイムラインをRESTful APIを叩いてサーバーから取得するかSQLiteのキャッシュを使うかなどはビューが知るところではないので、usecasesクラスのAPIでは抽象化します。
/**
* Twitter関連のビジネスロジック
*/
public class TwitterUseCase {
/**
* タイムライン取得時にプレゼンテーション層に返すイベント
*/
public class TimeLineEvent {
public final List<Tweet> tweets;
public TimeLineEvent(List<Tweet> tweets) { this.tweets = tweets; }
}
/**
* タイムラインを取得する
* これを呼ぶクラス(Fragment)は、中でどんなデータリソースにアクセスしてるかは知らない。
*/
public void getTimeline() {
// ローカルキャッシュから取得
List<Tweet> tweets = LocalTweetCache.getAll();
if (tweet != null) {
EventBus.getDefault().post(new TimeLineEvent(tweets));
return;
}
// キャッシュから取れなかった場合、APIから取得
ApiClient apiClient = new ApiClient();
apiClient.getTimeLine(new TimeLineListener() {
@Override
public void onSuccess(List<Tweet> tweets) {
EventBus.getDefault().post(new TimeLineEvent(tweets));
}
});
}
}
http://www.slideshare.net/mokemokechicken/iosandroidmodel にある通り、非同期処理を行った後のビューへの結果の返し方は、通知を使います。ライブラリには EventBus を使います。ビューのライフサイクルは短いのですが(ユーザーの操作によって簡単に破棄されるので)、EventBusでは通知先が死んでいても気にせずにイベントをpostできて楽です。
あとusecasesという名前はいまいちしっくり来てないので、他にいい名前がないか考えています。modelsだとカバーする範囲が広すぎるし、managersもなあ。。。
entities
データそのものを表すPOJOが入るパッケージです。データの取得や保存は後述するstorageやnetパッケージのクラスにやらせるので、純粋なデータを表すだけのクラスにします。
データ層でデータベースやRESTful APIから取得した生のデータからEntityに変換され、そのままビジネス層, プレゼンテーション層へと渡っていきます。
public class Tweet {
public long id;
public User user;
public String body;
public Date createdAt;
}
network
RESTful APIのクライアントなど、ネットワーク関連のクラスが入るパッケージです。APIクライアントのやるべきことを時系列に並べると、
- 設定やパラメータからHTTPリクエストを組み立てる
- HTTPリクエストを発行する
- HTTPレスポンス(主にJSON文字列)をパースし、Entityに変換する
- 呼び出し元のスレッドで、Entityを渡したコールバックを実行する
という流れになります。自分は内部実装には Volley + Gson をよく使います。
storage
データベース(SQLite, SharedPreferences, ローカルファイル etc)関連のクラスが入るパッケージです。
- データベースからEntityを取得
- Entityをデータベースに保存
などを行います。
内部では ActiveAndroid などのORMapperやContentProvierを使用してデータベースへのアクセスを行います。
最後に
こんな感じでパッケージを分けておくと、クラスを設計するときにパッケージの粒度に従って設計すればよいので、複数メンバーで作業していても抽象化のレベルを統一でき、楽にクラスの責務を分離できるのではないかと思っています。
開発が進むにつれてアプリが大きくなっていくと他のアーキテクチャや設計に変えるかもしれませんが、今のところはこれくらいがちょうどいいような気がしています。
参考資料
-
http://suzukaze.hatenablog.jp/entry/2015/02/22/221433
- 定番のベストプラクティス。
-
http://www.slideshare.net/mokemokechicken/iosandroidmodel
- 大変参考になった資料。モデルの設計とか。
-
http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
- これもとてもいい資料。階層分けなど大体同じです。サンプルは https://github.com/android10/Android-CleanArchitecture
-
https://github.com/antoniolg/androidmvp
- MVPアーキテクチャの例。Viewの抽象化をするとこが冗長な気がしてやめた。