一番最新のやつはこっち
前回はこちら
- 前のプロジェクトで開発していたアプリをリリースしてからはAndroidから離れていたのですが、会社変えてまた触ることになったので思い出す意味でも再考します。
- 前回は出来るだけ純正のAPIだけで考えたのですが、今回はライブラリも使いながらもう少し手を入れてみたり。
今一度ポイント
- 基本的にModelは死なない + 通知で変更を伝える役割を持つ
- ViewやVCでAPIを呼ぶのはユーザーの操作に影響されやすいからやめれ
変えたところ
- Android Best Practiceを参考にパッケージ構成を整理
以下のライブラリ群をGradleに追加。AsyncTaskLoader周りはokHttpとRxを使い、Observer周りはottoのEventBusを使って通知のやり取りをする。
- okHttp
- otto
- rxJava
- rxAndroid
こちらも参考に。たぶん前の投稿のスライドと同じ所属の方の作。ありがたや。
- 今、Kotlin楽しい感じなので、Kotlinで書きなおしてます。考え方は変わらないのでJavaに置き換えて読むとよいかと。
パッケージ構成
パッケージ名 | 役割 |
---|---|
entities | 各ドメインで扱うデータクラスを配置 |
events | EventBusを通して発火するイベントクラスを配置 |
fragments | Fragmentを配置 |
managers | 多様なデータを扱うコントローラを配置 |
models | 各ドメインモデルを配置 |
network | 外部との通信が発生するモジュールを配置 |
utils | ユーティリティー群 |
views | Viewに関連するモジュールを配置 |
コード
package me.rei_m.kotlinmvcsample.managers import java.util.HashMap /** * 各Modelのインスタンスを管理する */ public class ModelLocator private constructor() { companion object { private val showcase = HashMap<Tag, Any>(); public enum class Tag { ATND, } public fun register(tag: Tag, model: Any): Unit { showcase.put(tag, model) } public fun get(tag: Tag): Any = showcase[tag]!! } }
- アプリ起動時にAtndApiのモデルをセットしておく
// ModelLocatorにModelの参照を登録
ModelLocator.register(ModelLocator.Companion.Tag.ATND, AtndModel());
- entities/AtndEntity
- Kotlinのデータクラスを使って作成
package me.rei_m.kotlinmvcsample.entities import java.io.Serializable /** * Atndのイベント情報を保持するデータクラス. */ public data class AtndEventEntity(val id: Int, val title: String) : Serializable
- network/AtndApi
- APIとの通信部分を作成。さんぷるなのでキーワードは固定。
package me.rei_m.kotlinmvcsample.network import com.squareup.okhttp.CacheControl import com.squareup.okhttp.HttpUrl import com.squareup.okhttp.OkHttpClient import com.squareup.okhttp.Request import me.rei_m.kotlinmvcsample.entities.AtndEventEntity import org.json.JSONObject import rx.Observable import java.net.HttpURLConnection /** * Atnd Apiへの接続部分を定義する. */ public final class AtndApi private constructor() { companion object { /** * Atnd APIへのリクエストを行いAtndEventのEntityを返すObservableを作成する. */ public fun request(): Observable<AtndEventEntity> { return Observable.create({ t -> // リクエストのURLを作成. val url = HttpUrl.Builder() .scheme("https") .host("api.atnd.org") .addPathSegment("events") .addQueryParameter("keyword_or", "google,cloud") .addQueryParameter("format", "json") .build() val request = Request.Builder() .url(url) .cacheControl(CacheControl.FORCE_NETWORK) .build() // リクエスト開始. val response = OkHttpClient().newCall(request).execute() if (response.code() == HttpURLConnection.HTTP_OK) { // 正常に取得できたら、パースしてonNextでObserverに流す. val responseJson = JSONObject(response.body().string()); val eventCount = responseJson.getInt("results_returned") if (0 < eventCount) { val events = responseJson.getJSONArray("events") for (i in 0..eventCount - 1) { val e = events.getJSONObject(i).getJSONObject("event"); t.onNext(AtndEventEntity(e.getInt("event_id"), e.getString("title"))) } } } else { // エラーの場合はレスポンスコードをThrowableにつめて投げる t.onError(Throwable(response.code().toString())) } t.onCompleted() }) } } }
- models/AtndModel
package me.rei_m.kotlinmvcsample.models import me.rei_m.kotlinmvcsample.entities.AtndEventEntity import me.rei_m.kotlinmvcsample.events.AtndLoadedEvent import me.rei_m.kotlinmvcsample.events.EventBusHolder import me.rei_m.kotlinmvcsample.network.AtndApi import rx.Observer import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import java.util.* import me.rei_m.kotlinmvcsample.events.AtndLoadedEvent.Companion.Type as EventType /** * Atndに関する情報を管理するドメインモデル. */ public class AtndModel { /** API読込中など処理中か判定するフラグ */ public var isBusy = false private set /** アテンドのイベント情報を保持するリスト */ public val atndEventList = ArrayList<AtndEventEntity>() /** * AtndのAPIからイベント情報を取得し、内部に保持する. */ public fun fetch() { // ビジー状態の場合は処理終了. if (isBusy) { return } // ビジー状態にする isBusy = true // APIリクエストのオブザーバーを作成 val observer = object : Observer<AtndEventEntity> { override fun onNext(t: AtndEventEntity?) { // リストに追加する. atndEventList.add(t!!) } override fun onCompleted() { // onCompletedを受け取ったら完了のLoadedEventを発火する EventBusHolder.EVENT_BUS.post(AtndLoadedEvent(EventType.COMPLETE)) } override fun onError(e: Throwable?) { // onErrorを受け取ったらエラーのLoadedEventを発火する EventBusHolder.EVENT_BUS.post(AtndLoadedEvent(EventType.ERROR)) } } // AtndのAPIにリクエストを飛ばしてModelへの配信を開始する AtndApi.request() .onBackpressureBuffer() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .finallyDo({ // 必ずビジー状態を解除する. isBusy = false }) .subscribe(observer) } }
- fragments/AtndEventListFragment
package me.rei_m.kotlinmvcsample.fragments import android.os.Bundle import android.support.v4.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ListView import com.squareup.otto.Subscribe import me.rei_m.kotlinmvcsample.R import me.rei_m.kotlinmvcsample.entities.AtndEventEntity import me.rei_m.kotlinmvcsample.events.AtndEventClickEvent import me.rei_m.kotlinmvcsample.events.AtndLoadedEvent import me.rei_m.kotlinmvcsample.events.EventBusHolder import me.rei_m.kotlinmvcsample.managers.ModelLocator import me.rei_m.kotlinmvcsample.models.AtndModel import me.rei_m.kotlinmvcsample.views.adaptors.AtndEventListAdapter import me.rei_m.kotlinmvcsample.events.AtndLoadedEvent.Companion.Type as EventType import me.rei_m.kotlinmvcsample.managers.ModelLocator.Companion.Tag as ModelTag /** * AtndEventをリスト表示するFragment. */ public class AtndEventListFragment : Fragment() { /** ListViewのアダプター */ private var mListAdapter: AtndEventListAdapter? = null companion object { /** * ファクトリ. */ fun newInstance(): AtndEventListFragment { return AtndEventListFragment() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mListAdapter = AtndEventListAdapter(activity, R.layout.list_item_atnd_event) } override fun onDestroy() { super.onDestroy() mListAdapter = null } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater!!.inflate(R.layout.fragment_atnd_event_list, container, false) val listView = view.findViewById(R.id.list_atnd_event) as ListView listView.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> // リスト内の項目がクリックされたらクリックイベントを発火. val atndEventEntity = parent?.adapter?.getItem(position) as AtndEventEntity EventBusHolder.EVENT_BUS.post(AtndEventClickEvent(atndEventEntity)) } // リストビューにアダプターをセット listView.adapter = mListAdapter return view } override fun onResume() { super.onResume() // EventBus登録 EventBusHolder.EVENT_BUS.register(this); // AtndModelとリストビューに表示中のイベント数を取得 val atndModel = ModelLocator.get(ModelTag.ATND) as AtndModel val displayedCount = mListAdapter?.count!! if (displayedCount != atndModel.atndEventList.size) { // 表示済の件数とModel内で保持している件数をチェックし、 // 差分があれば未表示のイベントがあるのでリストに表示する mListAdapter?.clear() mListAdapter?.addAll(atndModel.atndEventList) mListAdapter?.notifyDataSetChanged() } else if (displayedCount === 0) { // 1件も表示していなければイベント情報を取得する atndModel.fetch() } } override fun onPause() { super.onPause() // EventBus登録解除 EventBusHolder.EVENT_BUS.unregister(this); } @Subscribe @SuppressWarnings("unused") public fun onAtndEventLoaded(event: AtndLoadedEvent) { // ModelからLoad完了のイベントを受け取った時の処理 when (event.type) { EventType.COMPLETE -> { // 読み込みが正常に完了したらModel内のイベント情報をアダプターにセットして再描画する val atndModel = ModelLocator.get(ModelTag.ATND) as AtndModel mListAdapter?.clear() mListAdapter?.addAll(atndModel.atndEventList) mListAdapter?.notifyDataSetChanged() } EventType.ERROR -> { // エラー表示する } } } }
動かしてみる
- 今回のコードはこちらに
GitHub - rei-m/Kotlin_mvc_sample
- Retrofitとか使うともっと短く書けるのかな。まだまだAndroid歴浅いのでなんとも修行足りない感。
- ちゃんとテストを書かないとというところがまだ考えきれてないのでその辺もやらないとですね。Stateパターンとかも取り入れてみたら規模がでかくなったら幸せになる気も。しばらく触りそうなので色々試してみます。