Androidのパッケージ構成について考えてみた

  • 133
    いいね
  • 0
    コメント

更新しました (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では抽象化します。

TwitterUseCase.java
/**
 * 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に変換され、そのままビジネス層, プレゼンテーション層へと渡っていきます。

Tweet.java
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を使用してデータベースへのアクセスを行います。

最後に

こんな感じでパッケージを分けておくと、クラスを設計するときにパッケージの粒度に従って設計すればよいので、複数メンバーで作業していても抽象化のレベルを統一でき、楽にクラスの責務を分離できるのではないかと思っています。

開発が進むにつれてアプリが大きくなっていくと他のアーキテクチャや設計に変えるかもしれませんが、今のところはこれくらいがちょうどいいような気がしています。

参考資料