web-dev-qa-db-ja.com

Androidアプリケーションアーキテクチャ-MVVMまたはMVC?

Androidプロジェクトに取り組み始めています。その構造をできるだけ堅牢にしたいと考えています。

私はWPF MVVMのバックグラウンドから来ており、Androidアプリケーションアーキテクチャについて少し読んでいますが、どのアーキテクチャを使用するべきかについて明確な答えを見つけることができませんでした。

一部の人々はMVVMの使用を提案しました- http://vladnevzorov.com/2011/04/30/Android-application-architecture-part-ii-architectural-styles-and-patterns/

他の人たちはMVCの使用を提案しましたが、それをどのように実装するべきかを正確に指定していませんでした。

私が言ったように、私はWPF-MVVMのバックグラウンドから来ているので、私が理解している限り、Androidではデフォルトでサポートされていないバインディングに大きく依存しています。

サードパーティのソリューションがあるようです- http://code.google.com/p/Android-binding/ しかし、それを信頼するかどうかはわかりません。開発が停止し、将来のAPIなどでサポートされなくなるとしたらどうでしょう。

基本的に私が探しているのは、アプリケーションの構造を構築するためのベストプラクティスを教えてくれる完全なチュートリアルです。フォルダーやクラスの構造など。完全なチュートリアルを見つけることができなかっただけで、Googleがそのようなチュートリアルを開発者に提供することを期待していました。この種類のドキュメントが技術的な側面を十分に処理するとは思いません- http://developer.Android.com/guide/topics/fundamentals.html

私が十分に明確で、あまり多くを求めていないことを願っています。コードがスパゲッティモンスターになる前に、アプリケーションの構造を確認したいだけです。

ありがとう!

35
Dror

まず、Androidを使用すると、任意のアーキテクチャを使用する必要がなくなります。それだけでなく、どのアーキテクチャにも従おうとするのがやや難しくなります。これには、賢い開発者である必要がありますスパゲッティコードベースの作成を避けるために:)

あなたはあなたが知っているそしてあなたが好きなどんなパターンにも合うように試みることができます。アプリケーションを開発するにつれて、最良のアプローチは何らかの形であなたの内臓に入ることがわかります(申し訳ありませんが、いつものように、正しく実行し始めるまでに多くの間違いを犯す必要があります)。

あなたが知っているパターンについて、私が何か間違ったことをさせてください:私は3つの異なるパターンを混ぜて、Androidで何が行われているのかを感じてもらいます。 Presenter/ModelViewはフラグメントまたはアクティビティのどこかにあるはずです。アダプターは、リスト内の入力を処理するため、この作業を行うことがあります。おそらくアクティビティもコントローラーのように機能するはずです。モデルは通常のJavaファイルである必要がありますが、ビューはレイアウトリソースと、実装する必要がある一部のカスタムコンポーネントを配置する必要があります。


ヒントをいくつかあげましょう。 これはコミュニティwikiの回答ですですから、他の人が他の提案を含めることを望みます。

ファイル編成

主に2つの賢明な可能性があると思います。

  • typeですべてを整理-すべてのアクティビティ用のフォルダー、すべてのアダプター用の別のフォルダー、すべてのフラグメント用の別のフォルダーなどを作成します
  • domainですべてを整理します(おそらく最高のWordではありません)。これは、「ViewPost」に関連するすべてが同じフォルダ内にあることを意味します-アクティビティ、フラグメント、アダプタなど。「ViewPost」に関連するすべてが別のフォルダにあります。 「EditPost」などについても同じです。アクティビティでは、作成するフォルダを義務付け、その後、たとえば基本クラス用にさらにいくつかの汎用的なフォルダが必要になると思います。

個人的には、最初のアプローチを使用してプロジェクトに携わってきただけですが、物事をより組織化できると信じているので、実際に後者を試してみたいと思います。 30個の関連のないファイルを含むフォルダを作成してもメリットはありませんが、それが最初のアプローチで得られるものです。

ネーミング

  • レイアウトとスタイルを作成するときは、常にそれらが使用されるアクティビティ(/フラグメント)のプレフィックスを使用して名前を付ける(またはそれらを識別する)。

したがって、「ViewPost」のコンテキストで使用されるすべての文字列、スタイル、IDは、「@ id/view_post_heading」(たとえば、テキストビューの場合)、「@ style/view_post_heading_style」、「@ string/view_post_greeting」で始まる必要があります。

これにより、オートコンプリート、編成が最適化され、名前の衝突などが回避されます。

基本クラス

アダプタ、アクティビティ、フラグメント、サービスなど、ほとんどすべての場合に基本クラスを使用する必要があると思います。これらは、少なくともデバッグ目的で役立ち、すべてのアクティビティで発生しているイベントを知ることができます。

一般的な

  • 匿名クラスは決して使用しません-これらは醜く、コードを読み込もうとすると注意をそらします
  • (専用クラスを作成する場合と比較して)内部クラスを使用することを好む場合があります。クラスが他の場所で使用されない場合(それが小さい場合)、これは非常に便利だと思います。
  • 最初からロギングシステムについて考えてください。Androidのロギングシステムを使用できますが、それをうまく利用できます。
35
Pedro Loureiro

Android=例を使用してMVVMを説明する方が役立つと思います。GitHubリポジトリ情報を含む完全な記事は、詳細については here です。

このシリーズの最初のパートで紹介した同じベンチマーク映画アプリの例を考えてみましょう。ユーザーが映画の検索語句を入力して[検索]ボタンを押すと、アプリはその検索語句を含む映画のリストを検索して表示します。リストの各映画をクリックすると、その詳細が表示されます。

enter image description here

次に、このアプリがMVVMにどのように実装されているかを説明し、続いて完全なAndroidアプリ、 my GitHubページ で入手できます)を説明します。

ユーザーがビューの[検索]ボタンをクリックすると、引数として検索用語を使用して、ViewModelからメソッドが呼び出されます。

    main_activity_button.setOnClickListener({
        showProgressBar()
        mMainViewModel.findAddress(main_activity_editText.text.toString())
    })

次に、ViewModelはモデルからfindAddressメソッドを呼び出して、映画名を検索します。

fun findAddress(address: String) {
    val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
        override fun onSuccess(t: List<MainModel.ResultEntity>) {
            entityList = t
            resultListObservable.onNext(fetchItemTextFrom(t))
        }

        override fun onError(e: Throwable) {
            resultListErrorObservable.onNext(e as HttpException)
        }
    })
    compositeDisposable.add(disposable)
}

応答がモデルからのものである場合、RxJavaオブザーバーのonSuccessメソッドは成功した結果を運びますが、ViewModelはビューにとらわれないため、表示するために結果を渡すためのビューインスタンスを持たないか、使用しません。代わりに、ビューで監視されるresultListObservable.onNext(fetchItemTextFrom(t))を呼び出すことにより、resultListObservableでイベントをトリガーします。

mMainViewModel.resultListObservable.subscribe({
    hideProgressBar()
    updateMovieList(it)
})

したがって、オブザーバブルはViewとViewModelの間で仲介役を果たします。

  • ViewModelはそのオブザーバブルでイベントをトリガーします
  • Viewは、ViewModelのオブザーバブルをサブスクライブすることにより、UIを更新します

これがビューの完全なコードです。この例では、ViewはActivityクラスですが、Fragmentも同様に使用できます。

class MainActivity : AppCompatActivity() {

    private lateinit var mMainViewModel: MainViewModel
    private lateinit var addressAdapter: AddressAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mMainViewModel = MainViewModel(MainModel())
        loadView()
        respondToClicks()
        listenToObservables()
    }

    private fun listenToObservables() {
        mMainViewModel.itemObservable.subscribe(Consumer { goToDetailActivity(it) })
        mMainViewModel.resultListObservable.subscribe(Consumer {
            hideProgressBar()
            updateMovieList(it)
        })
        mMainViewModel.resultListErrorObservable.subscribe(Consumer {
            hideProgressBar()
            showErrorMessage(it.message())
        })
    }

    private fun loadView() {
        setContentView(R.layout.activity_main)
        addressAdapter = AddressAdapter()
        main_activity_recyclerView.adapter = addressAdapter
    }

    private fun respondToClicks() {
        main_activity_button.setOnClickListener({
            showProgressBar()
            mMainViewModel.findAddress(main_activity_editText.text.toString())
        })
        addressAdapter setItemClickMethod {
            mMainViewModel.doOnItemClick(it)
        }
    }

    fun showProgressBar() {
        main_activity_progress_bar.visibility = View.VISIBLE
    }

    fun hideProgressBar() {
        main_activity_progress_bar.visibility = View.GONE
    }

    fun showErrorMessage(errorMsg: String) {
        Toast.makeText(this, "Error retrieving data: $errorMsg", Toast.LENGTH_SHORT).show()
    }

    override fun onStop() {
        super.onStop()
        mMainViewModel.cancelNetworkConnections()
    }

    fun updateMovieList(t: List<String>) {
        addressAdapter.updateList(t)
        addressAdapter.notifyDataSetChanged()
    }

    fun goToDetailActivity(item: MainModel.ResultEntity) {
        var bundle = Bundle()
        bundle.putString(DetailActivity.Constants.RATING, item.rating)
        bundle.putString(DetailActivity.Constants.TITLE, item.title)
        bundle.putString(DetailActivity.Constants.YEAR, item.year)
        bundle.putString(DetailActivity.Constants.DATE, item.date)
        var intent = Intent(this, DetailActivity::class.Java)
        intent.putExtras(bundle)
        startActivity(intent)
    }

    class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() {
        var mList: List<String> = arrayListOf()
        private lateinit var mOnClick: (position: Int) -> Unit

        override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder {
            val view = LayoutInflater.from(parent!!.context).inflate(R.layout.item, parent, false)
            return Holder(view)
        }

        override fun onBindViewHolder(holder: Holder, position: Int) {
            holder.itemView.item_textView.text = mList[position]
            holder.itemView.setOnClickListener { mOnClick(position) }
        }

        override fun getItemCount(): Int {
            return mList.size
        }

        infix fun setItemClickMethod(onClick: (position: Int) -> Unit) {
            this.mOnClick = onClick
        }

        fun updateList(list: List<String>) {
            mList = list
        }

        class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView)
    }

}

これがViewModelです:

class MainViewModel() {
    lateinit var resultListObservable: PublishSubject<List<String>>
    lateinit var resultListErrorObservable: PublishSubject<HttpException>
    lateinit var itemObservable: PublishSubject<MainModel.ResultEntity>
    private lateinit var entityList: List<MainModel.ResultEntity>
    private val compositeDisposable: CompositeDisposable = CompositeDisposable()
    private lateinit var mainModel: MainModel
    private val schedulersWrapper = SchedulersWrapper()

    constructor(mMainModel: MainModel) : this() {
        mainModel = mMainModel
        resultListObservable = PublishSubject.create()
        resultListErrorObservable = PublishSubject.create()
        itemObservable = PublishSubject.create()
    }

    fun findAddress(address: String) {
        val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() {
            override fun onSuccess(t: List<MainModel.ResultEntity>) {
                entityList = t
                resultListObservable.onNext(fetchItemTextFrom(t))
            }

            override fun onError(e: Throwable) {
                resultListErrorObservable.onNext(e as HttpException)
            }
        })
        compositeDisposable.add(disposable)
    }

    fun cancelNetworkConnections() {
        compositeDisposable.clear()
    }

    private fun fetchItemTextFrom(it: List<MainModel.ResultEntity>): ArrayList<String> {
        val li = arrayListOf<String>()
        for (resultEntity in it) {
            li.add("${resultEntity.year}: ${resultEntity.title}")
        }
        return li
    }

    fun doOnItemClick(position: Int) {
        itemObservable.onNext(entityList[position])
    }
}

そして最後にモデル:

class MainModel {
    private var mRetrofit: Retrofit? = null

    fun fetchAddress(address: String): Single<List<MainModel.ResultEntity>>? {
        return getRetrofit()?.create(MainModel.AddressService::class.Java)?.fetchLocationFromServer(address)
    }

    private fun getRetrofit(): Retrofit? {
        if (mRetrofit == null) {
            val loggingInterceptor = HttpLoggingInterceptor()
            loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
            val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build()
            mRetrofit = Retrofit.Builder().baseUrl("http://bechdeltest.com/api/v1/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build()
        }
        return mRetrofit
    }

    class ResultEntity(val title: String, val rating: String, val date: String, val year: String)
    interface AddressService {
        @GET("getMoviesByTitle")
        fun fetchLocationFromServer(@Query("title") title: String): Single<List<ResultEntity>>
    }

}

完全な記事 ここ

0
Ali Nem