Computer >> Máy Tính >  >> Hệ thống >> Android

Cách sử dụng Model-View-ViewModel trên Android Giống như một chuyên gia

Mục tiêu của tôi trong bài viết này là giải thích lý do tại sao mẫu kiến ​​trúc Model-View-ViewModel lại thể hiện sự tách biệt rất khó hiểu về các mối quan tâm trong một số tình huống liên quan đến logic trình bày của kiến ​​trúc GUI.

Chúng ta sẽ khám phá hai biến thể của MVVM (có không chỉ một cách để làm điều đó) và lý do tại sao bạn có thể thích một biến thể này hơn một biến thể khác, dựa trên các yêu cầu của dự án.

MVVM so với MVP / MVC?

Rất có thể câu hỏi phổ biến nhất mà tôi được hỏi trong các phiên Hỏi và Đáp trực tiếp vào Chủ nhật của mình, đại loại là:

MVVM so với MVP / MVC?

Bất cứ khi nào tôi được hỏi câu hỏi này, tôi nhanh chóng nhấn mạnh ý tưởng rằng không có kiến ​​trúc GUI duy nhất nào hoạt động tốt trong mọi tình huống.

Tại sao, bạn có thể hỏi? Kiến trúc tốt nhất (hoặc ít nhất là một lựa chọn tốt) cho một ứng dụng nhất định phụ thuộc rất nhiều vào các yêu cầu hiện có.

Hãy để chúng tôi suy nghĩ ngắn gọn về những gì từ yêu cầu này thực sự có nghĩa là:

  • Giao diện người dùng của bạn phức tạp đến mức nào? Giao diện người dùng đơn giản thường không yêu cầu logic phức tạp để điều phối nó, trong khi giao diện người dùng phức tạp có thể yêu cầu logic mở rộng và kiểm soát chi tiết để hoạt động trơn tru.
  • Bạn quan tâm đến mức độ nào về việc kiểm tra? Nói chung, các lớp được kết hợp chặt chẽ với các khuôn khổ và hệ điều hành (đặc biệt là giao diện người dùng ) yêu cầu làm việc thêm để kiểm tra.
  • Bạn muốn thúc đẩy khả năng tái sử dụng và tính trừu tượng đến mức nào? Điều gì sẽ xảy ra nếu bạn muốn chia sẻ back end, miền và thậm chí logic trình bày của ứng dụng của mình trên các nền tảng khác nhau?
  • Bản chất bạn có thực dụng không , người cầu toàn , lười biếng hoặc tất cả những điều trên vào những thời điểm khác nhau, trong những tình huống khác nhau?

Tôi rất thích viết một bài báo trong đó tôi thảo luận rất chi tiết về cách thức hoạt động của MVVM đối với các yêu cầu và mối quan tâm được liệt kê ở trên. Thật không may, một số bạn có thể đã nhầm lẫn khi nghĩ rằng chỉ có một cách để thực hiện MVVM.

Thay vào đó, tôi sẽ thảo luận về hai cách tiếp cận khác nhau đối với ý tưởng chung về MVVM mang lại những lợi ích và bất lợi rất khác biệt. Nhưng trước tiên, chúng ta hãy bắt đầu với ý tưởng chung.

Bạn không nên tham khảo các lớp xem của mình

Dành cho những người bạn của tôi, những người không thể đọc tiếng Anh cũ: Bạn không thể tham chiếu các lớp chế độ xem . "

Ngoài việc sử dụng tên ViewModel (tự nó gây nhầm lẫn nếu lớp có đầy đủ logic ), quy tắc sắt đá duy nhất của kiến ​​trúc MVVM là bạn không bao giờ được tham chiếu Chế độ xem từ ViewModel.

Bây giờ, khu vực nhầm lẫn đầu tiên có thể phát sinh từ từ “tham chiếu” này, mà tôi sẽ trình bày lại bằng cách sử dụng một số cấp độ biệt ngữ khác nhau:

  • ViewModels của bạn không được sở hữu bất kỳ tham chiếu nào (biến thành viên, thuộc tính, trường có thể thay đổi / không thay đổi) đến bất kỳ Chế độ xem nào
  • ViewModels của bạn có thể không phụ thuộc vào bất kỳ Chế độ xem nào
  • ViewModels của bạn có thể không nói chuyện trực tiếp với View của bạn

Bây giờ, trên nền tảng Android, lý do cho quy tắc này không chỉ đơn giản là việc phá vỡ nó là xấu bởi vì một người nào đó dường như biết về kiến ​​trúc phần mềm đã nói với bạn rằng nó là xấu.

Khi sử dụng lớp ViewModel từ Thành phần Kiến trúc (được thiết kế để có phiên bản tồn tại dài hơn vòng đời của Phân đoạn / Hoạt động khi thích hợp ), tham chiếu đến một Chế độ xem đang yêu cầu LÁI BỘ NHỚ NGHIÊM TRỌNG .

Về lý do tại sao MVVM nói chung không cho phép các tham chiếu như vậy, mục tiêu là theo giả thuyết để làm cho cả View và ViewModel dễ kiểm tra và viết hơn.

Những người khác cũng có thể chỉ ra rằng nó thúc đẩy khả năng tái sử dụng của ViewModels, nhưng đây chính xác là nơi mọi thứ trở nên phá vỡ với mẫu này .

Trước khi chúng tôi xem mã, xin lưu ý rằng Cá nhân tôi không sử dụng LiveData trong mã sản xuất của riêng tôi. Ngày nay, tôi thích viết Mẫu nhà xuất bản-Người đăng ký của riêng mình, nhưng những gì tôi nói bên dưới áp dụng cho bất kỳ thư viện nào cho phép liên kết Mẫu PubSub / Người quan sát từ ViewModel đến Chế độ xem.

Bài viết này có kèm theo một video hướng dẫn bao gồm nhiều ý tưởng giống nhau ở đây:

ViewLogic + ViewModel hoặc View + ViewModelController?

Khi tôi nói "phá vỡ" trong phần trước, tôi không có ý nói rằng mô hình phá vỡ theo nghĩa đen. Ý tôi là nó chia thành (ít nhất) hai cách tiếp cận khác nhau có hình thức, lợi ích và hậu quả rất riêng biệt.

Hãy để chúng tôi xem xét hai cách tiếp cận này và khi bạn có thể thích cách này hơn cách khác.

Cách sử dụng Model-View-ViewModel trên Android Giống như một chuyên gia
Boromir giải thích rằng MVVM không phải là cây đũa thần có thể làm biến mất logic trình bày ứng dụng của bạn.

Phương pháp tiếp cận đầu tiên:Ưu tiên các mô hình View có thể tái sử dụng

Theo như tôi có thể nói, hầu hết những người triển khai MVVM đều coi mục tiêu đó là thúc đẩy khả năng tái sử dụng của ViewModels, để chúng có thể được sử dụng lại cho n số lượt xem khác nhau (tỷ lệ nhiều đối một).

Nói một cách dễ hiểu, có hai cách bạn có thể đạt được khả năng tái sử dụng này:

  • Bằng cách không tham chiếu đến một Chế độ xem cụ thể. Hy vọng rằng đây không phải là tin tức đối với bạn vào thời điểm này.
  • Bằng cách biết càng ít càng tốt về chi tiết của Giao diện người dùng nói chung

Điểm thứ hai nghe có vẻ mơ hồ hoặc phản trực quan (làm thế nào nó có thể biết bất cứ điều gì về một cái gì đó mà nó không tham chiếu?), Vì vậy tôi nghĩ đã đến lúc xem một số mã:

class NoteViewModel(val repo: NoteRepo): ViewModel(){
    //Note: you may also publish data to the View via Databinding, RxJava Observables, and 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)
    }
}

Trong khi đây là một ví dụ rất đơn giản, vấn đề là thứ duy nhất mà ViewModel cụ thể này hiển thị công khai (ngoài hàm handleEvent), là một đối tượng Note đơn giản:

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

Với cách tiếp cận cụ thể này, ViewModel được tách rời tốt và thực sự khỏi không chỉ một Chế độ xem cụ thể mà còn cả các chi tiết và theo cách mở rộng, logic trình bày của bất kỳ Chế độ xem cụ thể nào.

Nếu những gì tôi đang nói vẫn còn mơ hồ, tôi hứa sẽ rõ ràng khi tôi mô tả cách tiếp cận khác.

Mặc dù tiêu đề trước đó của tôi, “ ViewLogic + ViewModel… ”Không có nghĩa là được sử dụng hoặc thực hiện nghiêm túc, ý tôi là bằng cách có các ViewModels rất được tách rời và có thể sử dụng lại, chúng ta hiện đang phụ thuộc vào chính View để thực hiện công việc tìm ra cách hiển thị / liên kết đối tượng Note này trên màn hình.

Một số người trong chúng ta không thích điền các lớp Xem bằng Logic.

Đây là lúc mọi thứ trở nên rất khó khăn và phụ thuộc vào yêu cầu của dự án . Tôi không nói rằng điền các lớp View bằng logic như…:

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

… Là luôn luôn một điều tồi tệ, nhưng các lớp liên kết chặt chẽ với nền tảng (như Fragment) rất khó kiểm tra và các lớp có logic trong đó là các lớp quan trọng nhất để kiểm tra!

Nói một cách ngắn gọn, thật là thất bại nếu áp dụng những gì tôi coi là nguyên tắc vàng của bất kỳ công trình kiến ​​trúc tốt nào: Tách biệt các mối quan tâm .

Ý kiến ​​cá nhân của tôi cho rằng việc áp dụng tách biệt các mối quan tâm ở mức độ rất cao là rất đáng. Nhưng đừng nhầm rằng rất nhiều ứng dụng kiếm tiền đã được viết bởi những người không có manh mối mờ nhạt nhất về ý nghĩa của điều đó.

Trong mọi trường hợp, cách tiếp cận mà chúng ta sẽ thảo luận tiếp theo, mặc dù có tác dụng phụ riêng của nó , một lần nữa xóa logic bản trình bày khỏi Chế độ xem.

Chà, dù sao thì hầu hết đều vậy.

Phương pháp tiếp cận thứ hai:Humble View, Control-Freak ViewModel

Đôi khi không có quyền kiểm soát chi tiết đối với Chế độ xem của bạn (đó là hệ quả của việc ưu tiên khả năng tái sử dụng của các Mô hình xem), thực sự là một điều tệ hại.

Để khiến tôi không còn nhiệt tình áp dụng cách tiếp cận trước đó một cách bừa bãi, tôi thấy rằng tôi thường xuyên không cần sử dụng lại ViewModel .

Trớ trêu thay, "trừu tượng quá nhiều" là một chỉ trích phổ biến về MVP so với MVVM.

Như đã nói, người ta không thể chỉ cần thêm một tham chiếu trở lại ViewModel để lấy lại quyền kiểm soát chi tiết này đối với Chế độ xem. Về cơ bản, đó chỉ là MVP + rò rỉ bộ nhớ (giả sử bạn vẫn đang sử dụng ViewModel từ AAC).

Sau đó, giải pháp thay thế là tạo các ViewModels của bạn sao cho chúng chứa hầu hết các hành vi , tiểu bang logic trình bày của một Chế độ xem nhất định. Tất nhiên, Chế độ xem vẫn phải liên kết với ViewModel, nhưng đủ thông tin chi tiết về Chế độ xem có trong ViewModel để các chức năng của Chế độ xem được giảm xuống một lớp lót (với một số ngoại lệ nhỏ).

Trong quy ước đặt tên của Martin Fowler, điều này được gọi là Chế độ xem thụ động / Màn hình. Tên áp dụng chung hơn cho phương pháp này là Mẫu đối tượng khiêm tốn .

Để đạt được điều này, về cơ bản bạn phải có ViewModel của mình sở hữu một trường có thể quan sát được (tuy nhiên bạn đạt được điều đó - ràng buộc dữ liệu, Rx, LiveData, bất cứ điều gì) cho mọi điều khiển hoặc tiện ích con có trong Chế độ xem:

class UserViewModel(
    val repo: IUserRepository,
){

    //The actual data model is kept private to 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 đó, Chế độ xem sẽ vẫn cần tự kết nối với ViewModel, nhưng các chức năng cần thiết để làm như vậy trở nên đơn giản một cách đáng kể khi viết:

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, which is inferred to be a 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)
                )
            }
        )
    }

Bạn có thể tìm thấy mã đầy đủ cho ví dụ này tại đây.

Như bạn có thể đã nhận thấy, chúng tôi có thể sẽ không sử dụng lại ViewModel này ở bất kỳ nơi nào khác . Ngoài ra, Chế độ xem của chúng tôi đã trở nên đủ khiêm tốn (tùy thuộc vào tiêu chuẩn và sở thích của bạn đối với phạm vi mã) và rất dễ viết.

Đôi khi bạn sẽ gặp phải những tình huống mà bạn phải tìm ra một số biện pháp phân nửa giữa sự phân bố của logic trình bày giữa Chế độ xem và Mô hình xem, không tuân thủ nghiêm ngặt một trong hai cách tiếp cận này.

Tôi không ủng hộ cách tiếp cận này hơn cách tiếp cận khác, mà khuyến khích bạn linh hoạt trong cách tiếp cận của mình, dựa trên các yêu cầu hiện có.

Chọn kiến ​​trúc của bạn dựa trên sở thích và yêu cầu

Mục đích của bài viết này là xem xét hai cách tiếp cận khác nhau mà nhà phát triển có thể thực hiện để xây dựng kiến ​​trúc GUI kiểu MVVM trên Nền tảng Android (với một số chuyển sang các nền tảng khác).

Trên thực tế, chúng tôi có thể tìm hiểu cụ thể hơn về những khác biệt nhỏ ngay cả trong hai cách tiếp cận này.

  • Chế độ xem có nên quan sát một trường cho mọi tiện ích / điều khiển riêng lẻ mà nó sở hữu hay nó quan sát một trường xuất bản một mô hình duy nhất để hiển thị lại toàn bộ Chế độ xem mỗi lần?
  • Có lẽ chúng tôi có thể tránh phải tạo các ViewModels của mình là 1-1, trong khi vẫn giữ Chế độ xem của chúng tôi là Đối tượng khiêm tốn, đơn giản bằng cách thêm một cái gì đó như Người trình bày hoặc Bộ điều khiển vào hỗn hợp?

Nói chuyện là rẻ và tôi thực sự khuyên bạn nên thử và học những điều này trong mã để bạn không cần phải dựa vào những người như tôi để nói cho bạn biết phải làm gì.

Cuối cùng, tôi nghĩ rằng hai yếu tố tạo nên một công trình kiến ​​trúc tuyệt vời cần phải xem xét sau:

Trước hết, hãy chơi với một số phương pháp cho đến khi bạn tìm thấy phương pháp mà bạn thích . Điều này được thực hiện tốt nhất bằng cách thực sự xây dựng một ứng dụng (có thể đơn giản) theo từng phong cách và xem điều gì cảm thấy phù hợp .

Thứ hai, hiểu rằng các sở thích sang một bên, các phong cách khác nhau sẽ có xu hướng nhấn mạnh các lợi ích khác nhau để đổi lấy các khoản thâm hụt khác nhau. Cuối cùng, bạn sẽ có thể chọn những lựa chọn tốt dựa trên sự hiểu biết của bạn về các yêu cầu của dự án chứ không phải là niềm tin mù quáng .

Tìm hiểu Thêm về Kiến trúc Phần mềm:

Xã hội

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