Архитектура приложения Android - MVVM или MVC? - PullRequest
35 голосов
/ 14 декабря 2011

У меня есть проект 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

Надеюсь, я был достаточно ясен и не слишком много просил, я просто хочу убедиться в структуре моего приложения, прежде чем мой код превратится в монстра-спагетти.

Спасибо!

Ответы [ 2 ]

35 голосов
/ 15 декабря 2011

Прежде всего, Android не заставляет вас использовать какую-либо архитектуру.Мало того, но это также делает несколько трудным пытаться следовать любому.Это потребует от вас умного разработчика, чтобы избежать создания базы кода для спагетти:)

Вы можете попытаться вписаться в любой шаблон, который вам нравится и вам нравится.Я считаю, что лучший подход каким-то образом проникнет в вашу душу, когда вы будете разрабатывать все больше и больше приложений (извините, но, как всегда, вам придется совершать много ошибок, пока вы не начнете делать это правильно).

Что касается шаблонов, которые вы знаете, позвольте мне сделать что-то не так: я смешаю три различных шаблона, чтобы вы почувствовали, что делает то, что в Android.Я считаю, что Presenter / ModelView должен быть где-то во фрагменте или в действии.Адаптеры могут иногда выполнять эту работу, поскольку они заботятся о входах в списки.Вероятно, деятельность должна работать как контроллеры тоже.Модели должны быть обычными java-файлами, тогда как представление должно включать ресурсы макета и некоторые пользовательские компоненты, которые вам, возможно, придется реализовать.


Я могу дать вам несколько советов. Это вики-ответ сообщества , поэтому, надеюсь, другие люди могут предложить другие предложения.

Организация файлов

Я думаю, что в основном есть две разумные возможности:

  • организовать все по типу - создать папку для всех действий, другую папку для всех адаптеров, другую папку для всех фрагментов и т. Д.
  • организовать все по домен (может быть, не лучшее слово).Это будет означать, что все, что связано с «ViewPost», будет находиться внутри одной и той же папки - действие, фрагмент, адаптеры и т. Д. Все, что связано с «ViewPost», будет находиться в другой папке.То же самое для "EditPost" и т. Д. Я предполагаю, что действия будут требовать создания папок, которые вы создадите, и тогда будет несколько более общих папок для базовых классов, например.

Лично я был толькоучаствует в проектах, использующих первый подход, но я действительно хотел бы попробовать позже, так как считаю, что это может сделать вещи более организованными.Я не вижу никакого преимущества в том, чтобы иметь папку с 30 несвязанными файлами, но это то, что я получаю при первом подходе.

Именование

  • При создании макетов и стилей всегда называйте (или идентифицируйте их)) с использованием префикса для действия (/ фрагмента), в котором они используются.

Таким образом, все строки, стили, идентификаторы, используемые в контексте «ViewPost», должны начинаться с «@ id / view_post_heading»(для текстового обзора, например), "@ style / view_post_heading_style", "@ string / view_post_greeting".

Это оптимизирует автозаполнение, организацию, избежание коллизии имен и т. д.

Базовые классы

Я думаю, вы захотите использовать базовые классы практически для всего, что вы делаете: адаптеры, действия, фрагменты, службы и т. Д. Они могут быть полезны, по крайней мере, для целей отладки, чтобы вы знали, какие события происходят во всехваша деятельность.

Общие вопросы

  • Я никогда не использую анонимные занятия - они ужасны и отвлекут ваше внимание, когда вы пытаетесьd код
  • Иногда я предпочитаю использовать внутренние классы (по сравнению с созданием выделенного класса) - если класс не будет использоваться где-либо еще (и он небольшой), я думаю, что это очень удобно.
  • Подумайте о своей системе ведения журнала с самого начала - вы можете использовать систему ведения журнала android, но эффективно ее использовать!
0 голосов
/ 19 мая 2018

Я думаю, что было бы более полезно объяснить MVVM в Android на примере. Полная статья вместе с информацией о репозитории GitHub: здесь для получения дополнительной информации.

Давайте предположим, что тот же самый пример приложения-ролика, представленный в первой части этой серии. Пользователь вводит поисковый запрос для фильма и нажимает кнопку «НАЙТИ», на основе которого приложение ищет список фильмов, включающих этот поисковый термин, и показывает их. Нажатие на каждый фильм в списке затем показывает его детали.

enter image description here

Теперь я объясню, как это приложение реализовано в MVVM, за которым следует полное приложение для Android, которое доступно на моей странице 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)
}

Когда ответ приходит из Модели, метод onSuccess наблюдателя RxJava несет успешный результат, но поскольку ViewModel является независимым от View, он не имеет или не использует какой-либо экземпляр View для передачи результата для показа. Вместо этого он вызывает событие в resultListObservable, вызывая resultListObservable.onNext (fetchItemTextFrom (t)), который наблюдается представлением:

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

Таким образом, наблюдаемая играет роль посредника между View и ViewModel:

  • ViewModel вызывает событие в своей наблюдаемой
  • Просмотр обновлений пользовательского интерфейса путем подписки на наблюдаемую ViewModel

Вот полный код для просмотра. В этом примере 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>>
    }

}

Полная статья здесь

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...