MVVM with Android

by Cao Ngọc Hiệp
224 views

Ở bài viết trước mình có nó qua của hạn chế chủa MVP so với MVVM. Nên ở bài viết này mình sẽ tìm hiểu kĩ về mô hình MVVM để biết lý do chọn MVVM hơn là MVP.

Content

  • Liva Data là gì?
  • DataBinding là gì?
  • MVVM là gì?
  • Demo MVVM
  • Kết Luận

Live Data là gì?

LiveData là observable data holder class. Không giống như các observables khác, nó nhận thức được vòng đời, tức là nhận nhận thức được vòng đời của các components như là activity, fragment hay services. Điều này có nghĩa là nó chỉ cập nhật các component observers khi chúng ở trạng thái vòng đời hoạt động.

DataBinding là gì?

Là library hỗ trợ cho phép chúng ta liên kết với các thành phần UI của mình từ layout với data source trong ứng dụng của chúng ta bằng cách khai báo thay vì lập trình.

Set up:

android {
  ...
  dataBinding {
    enabled = true
  }
}

MVVM là gì?

Mô hình MVVM

MVVM gồm 3 phần:

Model: Cũng tương tự như trong mô hình MVC. Model là các đối tượng giúp truy xuất và thao tác trên dữ liệu thực sự.

View: là thành phần UI đại diện cho trạng thái hiện tại cảu thông tin mà người dùng có thể nhìn thấy.

ViewModel:

  • ViewModel nằm giữa Model View, có tác dụng là cầu nối để giao tiếp giữa Model View.
  • ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
  • Đây là phần khác biệt của MVVM so với MVP.
  • ViewModel và View được kết thông thông qua Databiding và observable Livedata,

Demo MVVM

Model layer:

Ví dụ về cách triển khai 1 Model:

data class User(
    var username: String,
    var password: String
)

ViewModel layer:

Ví dụ về cách triển khai 1 ViewModel:

class LoginViewModel(
    private val userRepository: IUserRepository,
    private val dispatcher: CoroutineDispatcher = Dispatchers.Main
) : ViewModel() {

    private val _loginResult = MutableLiveData<Resource<Status>>()
    val loginResult: LiveData<Resource<Status>>
        get() = _loginResult

    private val _stateLoading = MutableLiveData<Resource<Boolean>>()
    val stateLoading: LiveData<Resource<Boolean>>
        get() = _stateLoading

    private var listDataUser = listOf<User>()

    fun clickLogin(inputUser: User) {
        viewModelScope.launch(dispatcher) {
            listDataUser = userRepository.getUserList()
            _stateLoading.value = Resource.loading(true)
            val invalid = checkInvalidInput(inputUser)
            if (invalid) {
                _loginResult.postValue(Resource.invalid(Status.INVALID, Status.INVALID.value))
                _stateLoading.value = Resource.loading(false)
            } else {
                checkLogin(inputUser)
            }
        }
    }

    private suspend fun checkInvalidInput(inputUser: User) =
        userRepository.checkInvalidInput(inputUser)

    private suspend fun checkLogin(inputUser: User) {
        val isSuccess = userRepository.loginResult(inputUser, listDataUser)
        if (isSuccess) {
            _loginResult.postValue(Resource.success(Status.SUCCESS, Status.SUCCESS.value))
        } else {
            _loginResult.postValue(Resource.error(Status.ERROR, Status.ERROR.value))
        }
        _stateLoading.value = Resource.loading(false)
    }

    class Factory : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            return LoginViewModel(Injector.userRepository) as T
        }
    }
}
  1. Thuộc tính userRepository để lấy dữ liệu từ data.
  2. Hàm checkInvalidInput dùng để check dữ liệu nhập vào null hay không.
  3. Hàm checkLogin check dữ liệu nhập vào có trùng với data không.
  4. Thuộc tính _loginResult trả về Success nếu đúng dữ liệu, Error là sai còn Invalid là dữ liệu null.

View layer:

Ví dụ về cách triển khai 1 View:

Code file .xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvLogin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginBottom="36dp"
            android:text="@string/login_text_view"
            android:textSize="@dimen/login_text_view_size" />

        <EditText
            android:id="@+id/edtUserName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawableStart="@drawable/ic_baseline_person"
            android:drawablePadding="6sp"
            android:hint="@string/user_name_text_view"
            android:importantForAutofill="no"
            tools:ignore="TextFields" />

        <EditText
            android:id="@+id/edtPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:drawableStart="@drawable/ic_pass"
            android:drawablePadding="6sp"
            android:hint="@string/pass_word_text_view"
            android:importantForAutofill="no"
            tools:ignore="TextFields" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:gravity="center">

            <Button
                android:id="@+id/btnLogin"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="40dp"
                android:text="@string/login_button"
                tools:ignore="ButtonStyle" />

            <Button
                android:id="@+id/btnClear"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/clear_button"
                tools:ignore="ButtonStyle" />

        </LinearLayout>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="@string/information_text_view" />


    </LinearLayout>

    <ProgressBar
        android:id="@+id/prbLoading"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="visible" />

</androidx.constraintlayout.widget.ConstraintLayout>

Code file .kt

class LoginFragment : BaseFragment<FragmentLoginBinding>(), View.OnClickListener {

    private val loginViewModel: LoginViewModel by activityViewModels { LoginViewModel.Factory() }

    override fun createBinding(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): FragmentLoginBinding {
        return FragmentLoginBinding.inflate(inflater, container, false)
    }

    override fun observeLiveData() {
        loginViewModel.loginResult.observeNotNull(this) { resultMode ->
            when (resultMode.status) {
                Status.INVALID -> resultMode.message?.let {
                    MessageDialog(it).show(
                        requireActivity().supportFragmentManager,
                        MessageDialog::class.java.name
                    )
                }

                Status.SUCCESS -> {
                    val action = LoginFragmentDirections.actionLoginFragmentToHomeFragment()
                    findNavController().navigate(action)
                }

                Status.ERROR -> resultMode.message?.let {
                    MessageDialog(it).show(
                        requireActivity().supportFragmentManager,
                        MessageDialog::class.java.name
                    )
                }

                else -> {
                    // do nothing
                }
            }
        }

        loginViewModel.stateLoading.observeNotNull(this) { stateLoading ->
            binding.prbLoading.visibility = if (stateLoading.data == true) {
                View.VISIBLE
            } else {
                View.GONE
            }
        }
    }

    override fun initAction() {
        binding.btnLogin.setOnClickListener(this)
        binding.btnClear.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when (v) {
            binding.btnLogin -> {
                handleClickLogin()
            }

            binding.btnClear -> {
                clearInputData()
            }
        }
    }

    private fun handleClickLogin() {
        val username = binding.edtUserName.text.toString()
        val password = binding.edtPassword.text.toString()
        val inputUser = User(username = username, password = password)

        loginViewModel.clickLogin(inputUser)
    }

Kết Luận

Cũng giống như MVP, MVVM thực hiện abtract trạng thái và thể hiện của View, cho phép chúng ta phân tách rõ ràng việc phát triển giao diện với xử lý business logic. MVVM đã kế thừa những ưu điểm vốn có của MVP, kết hợp với những lợi thế của data binding đem đến một pattern có khả năng phân chia các thành phần với từng chức năng riêng biệt, dễ dàng trong việc maintain, redesign. MVVM cũng đem lại khả năng test rất dễ dàng.

Trên đây là 1 số chia sẻ về MVVM. Nếu có gì chưa chính xác hoặc còn thiếu thì rất mong nhận được góp ý từ mọi người. Cảm ơn các bạn đã theo dõi bài viết!

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.