Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Android MVVM Giải Mã: Chọn Pattern Đúng Cho Clean Architecture

Mục tiêu bài: giải thích tại sao Model-View-ViewModel architectural pattern trình bày separation of concerns khá awkward trong một số tình huống về presentation logic của GUI architecture.

Khám phá 2 biến thể MVVM (không chỉ 1 cách làm), và lý do prefer biến thể này hơn kia dựa trên project requirements.

MVVM vs MVP/MVC?

Câu hỏi thường gặp nhất Sunday Q&A live sessions:

MVVM vs MVP/MVC?

Khi được hỏi, nhấn mạnh: không single GUI architecture works great mọi situation.

Tại sao? Best architecture (hoặc good choice) cho app phụ thuộc mạnh vào requirements.

Suy nghĩ brevemente từ requirements:

  • UI phức tạp đến đâu? UI đơn giản không cần logic phức tạp coordinate, UI phức tạp cần logic extensive và fine-grained control.
  • Quan tâm testing đến mức nào? Nói chung, classes tight coupled với frameworks/OS (đặc biệt user interface) cần extra work test.
  • Mức độ re-usability và abstraction mong muốn? Muốn share backend, domain, presentation logic across platforms?
  • Bạn bản chất pragmatic, perfectionist, lazy, hoặc tất cả ở times/situations khác nhau?

Muốn viết bài chi tiết MVVM works với requirements/concerns trên. Thật không may, một số bạn bị mislead nghĩ chỉ có 1 cách làm MVVM.

Thay vào đó, thảo luận 2 approaches khác nhau cho general idea MVVM, present distinct benefits/disadvantages. Nhưng trước, bắt đầu general idea.

Thou Shalt Not Reference Thy View Classes

Cho bạn không đọc được old English: “Không được reference view classes."

Ngoài dùng tên ViewModel (gây confuse nếu class full của logic), iron-clad rule của MVVM: never reference View từ ViewModel.

Vùng nhầm lẫn đầu từ từ "reference", restate với nhiều jargon levels:

  • ViewModels không possess references (member variables, properties, mutable/immutable fields) đến Views
  • ViewModels không depend trên Views
  • ViewModels không talk directly đến Views

Trên Android platform, lý do rule này không đơn giản là breaking it bad vì ai đó biết software architecture nói bad.

Dùng ViewModel class từ Architecture Components (designed instance persist lâu hơn Fragment/Activity lifecycle khi appropriate), reference View = asking for SERIOUS MEMORY LEAKS.

Vì sao MVVM generally không allow references: goal hypothetically làm View và ViewModel dễ test và viết hơn.

  • Orthers cũng chỉ ra promote reusability ViewModels, nhưng chính nơi này pattern breaks down.
  • Trước khi xem code, note: Mình không dùng LiveData production code. Prefer tự viết Publisher-Subscriber Pattern, nhưng điều dưới áp dụng cho bất kỳ library cho PubSub/Observer Pattern link ViewModel→View.

    Bài có video tutorial covering nhiều ideas tương tự.

    ViewLogic + ViewModel Hay View + ViewModelController?

    Khi nói "break down" phần trước, không có nghĩa pattern literally breaks. Nghĩa là breaks down thành (ít nhất) 2 approaches khác nhau với distinct appearances, benefits, consequences.

    Xem 2 approaches, khi prefer cái này hơn kia.

    Android MVVM Giải Mã: Chọn Pattern Đúng Cho Clean Architecture
    Boromir giải thích MVVM không phải magic wand làm presentation logic biến mất.

    Approach 1: Ưu Tiên Reusable ViewModels

    Theo mình, hầu hết implement MVVM goal promote re-usability ViewModels, reused cho n Views khác nhau (many-to-one ratio).

    Simple terms, 2 ways achieve re-usability:

    • Không reference specific View. Hy vọng không news tại điểm này.
    • Biết ít nhất có thể về chi tiết UI generally

    Điểm 2 nghe vague/counter-intuitive (làm sao biết UI mà không reference?), nên xem code:

    class NoteViewModel(val repo: NoteRepo): ViewModel(){ //Note: may also publish data to View via Databinding, RxJava Observables, other approaches. Although I do not like to use LiveData in back end classes, it works great with Android front end with AAC val noteState: MutableLiveData<Note>() //... fun handleEvent(event: NoteEvent) { when (event) { is NoteEvent.OnStart -> getNote(event.noteId) //... } } private fun getNote(noteId: String){ noteState.value = repo.getNote(noteId) } }

    Ví dụ simplified, điểm là chỉ thứ ViewModel expose publicly (ngoài handleEvent), là simple Note object:

    data class Note(val creationDate:String, val contents:String, val imageUrl: String, val creator: User?)

    Với approach này, ViewModel well and truly decoupled không chỉ specific View, mà cả details, và by extension, presentation logic của bất kỳ View cụ thể.

    Nếu vẫn vague, promise rõ khi describe approach kia.

    Mặc dù heading trước "ViewLogic + ViewModel..." không serious, ý là với rất decoupled/reusable ViewModels, now depend on View itself để figure out render/bind Note object on screen.

    Một số không thích fill View classes với Logic.

    Đây nơi mọi thứ muddy và depend trên project requirements. Không nói fill View classes logic như...

    private fun observeViewModel() { viewModel.notes.observe( viewLifecycleOwner, Observer { notes: List<Note> -> if (notes.isEmpty()) showEmptyState() else showNoteList(notes) } ) //.. }

    ...luôn bad, nhưng classes tight coupled platform (như Fragments) difficult test, và classes có logic là important classes to test!

    In a word, failure apply golden principle good architecture: Separation of concerns.

    Opinion cá nhân: worth apply separation of concerns high degree. But make no mistake plenty cash cow apps viết bởi người không clue về ý nghĩa.

    Anyway, approach tiếp theo, dù có side effects riêng, lại remove presentation logic từ View. Well, most of it.

    Approach 2: Humble View, Control-Freak ViewModel

    Đôi khi không có fine-grained control Views (consequence của prioritize re-usability ViewModels), actually kind of sucks.

    Làm mình less enthusiastic apply previous approach indiscriminately: thường không cần reuse ViewModel.

    Ironically, "too much abstraction" là critique thường của MVP over MVVM.

    Với đó, không simply add reference back ViewModel để regain fine-grained control View. Đó basically MVP + memory leaks (giả sử vẫn dùng ViewModel từ AAC).

    Alternative: build ViewModels contain hầu hết behaviour, state, presentation logic của given View. View vẫn bind ViewModel, nhưng đủ details về View trong ViewModel mà View functions reduced to one-liners (với small exceptions).

    Theo naming conventions Martin Fowler: Passive View/Screen. Tên generally applicable: Humble Object Pattern.

    Để achieve: ViewModel phải có observable field (data binding, Rx, LiveData, whatever) cho mọi control/widget trong View:

    class UserViewModel( val repo: IUserRepository, ){ //Actual data model kept private avoid unwanted tampering private val userState = MutableLiveData<User>() //Control Logic internal val authAttemptState = MutableLiveData<Unit>() internal val startAnimation = MutableLiveData<Unit>() //UI Binding internal val signInStatusText = MutableLiveData<String>() internal val authButtonText = MutableLiveData<String>() internal val satelliteDrawable = MutableLiveData<String>() private fun showErrorState() { signInStatusText.value = LOGIN_ERROR authButtonText.value = SIGN_IN satelliteDrawable.value = ANTENNA_EMPTY } //... }

    Sau đó View vẫn wire up ViewModel, nhưng functions required trở nên trivially simple:

    class LoginView : Fragment() { private lateinit var viewModel: UserViewModel //... //Create and bind to ViewModel override fun onStart() { super.onStart() viewModel = ViewModelProviders.of( //... ).get(UserViewModel::class.java) //start background anim (root_fragment_login.background as AnimationDrawable).startWithFade() setUpClickListeners() observeViewModel() viewModel.handleEvent(LoginEvent.OnStart) } private fun setUpClickListeners() { //... } private fun observeViewModel() { viewModel.signInStatusText.observe( viewLifecycleOwner, Observer { //"it" is the value of the MutableLiveData object, inferred String automatically lbl_login_status_display.text = it } ) viewModel.authButtonText.observe( viewLifecycleOwner, Observer { btn_auth_attempt.text = it } ) viewModel.startAnimation.observe( viewLifecycleOwner, Observer { imv_antenna_animation.setImageResource( resources.getIdentifier(ANTENNA_LOOP, "drawable", activity?.packageName) ) (imv_antenna_animation.drawable as AnimationDrawable).start() } ) viewModel.authAttemptState.observe( viewLifecycleOwner, Observer { startSignInFlow() } ) viewModel.satelliteDrawable.observe( viewLifecycleOwner, Observer { imv_antenna_animation.setImageResource( resources.getIdentifier(it, "drawable", activity?.packageName) ) } ) }

    Full code example tại link.

    Nhận thấy: không reuse ViewModel nơi nào khác. View trở nên sufficiently humble (tùy standards/preferences code coverage), rất dễ viết.

    Đôi khi run vào situations cần half-measure giữa distribution presentation logic giữa Views/ViewModels, không strictly follow approach nào.

    Không advocate approach này hơn kia, mà encourage flexible approach based on requirements.

    Chọn Architecture Dựa Trên Preferences Và Requirements

    Điểm bài: nhìn 2 approaches khác nhau developer có thể take constructing MVVM style GUI architecture trên Android Platform (với carry over platforms khác).

    Thực tế, có thể specific hơn small differences ngay trong 2 approaches.

    • View observe field cho mỗi widget/control, hay observe one field publish single model render entire View anew each time?
    • Có thể avoid one-to-one ViewModels, keep Views as Humble Objects, bằng cách thêm Presenter/Controller vào mix?

    Talk is cheap, strongly advise try learn things in the code để không rely trên người như mình nói làm gì.

    Cuối cùng, 2 elements làm great architecture:

    Thứ nhất: play với several approaches đến khi find one bạn prefer. Best done bằng actually build app (có thể simple) mỗi style, xem gì feels right.

    Thứ hai: hiểu preferences aside, different styles tend emphasize different benefits exchange different deficits. Eventually, pick good choices dựa trên understanding project requirements thay vì blind faith.

    Learn More About Software Architecture:

    Social

    https://www.instagram.com/rkay301/
    https://www.facebook.com/wiseassblog/
    https://twitter.com/wiseass301
    https://wiseassblog.com/