MVVM Architecture and base project MVVM use Kotlin

PhamNgocPhi
Chim cu chăm code
Published in
7 min readMar 13, 2020

Trong bài viết này, chúng ta sẽ cùng tìm hiểu về kiến trúc MVVM trong lập trình Android và giới thiệu base project MVVM.

1. Tổng quan về mô hình MVVM

MVVM là viết tắt của Model — View — ViewModel, ở đó view (tức giao diện người dùng) sẽ được cập nhật bởi ViewModel và việc xử lý Logic hoặc trình bày dữ liệu sẽ do Model đảm nhận. Mô hình này sẽ lại bỏ tight coupling giữa các thành phần với nhau. Hiểu đơn giản thì trong kiến trúc này, thằng con sẽ không có một reference trực tiếp nào đến thằng cha, chúng chỉ liên hệ với nhau thông qua observables.

image from blog MindOrks
  • Model: đại diện cho dữ liệu , trạng thái, các logic trên đối tượng. Nó bao gồm cả dữ liệu local hay dữ liệu remote. Model sẽ nhận yêu cầu từ ViewModel, sau khi thực hiện xong, kết quả sẽ không được trả về 1 viewmodel cụ thể nào mà được trả về thông qua hình thức Observable. Tức là model sẽ đóng vai trò là nguồn phát, phát ra kết quả, bất cứ một viewmodel nào đăng ký lắng nghe đều sẽ nhận được kết quả.
  • View: View bao gồm các UI code ( Activity, Fragment), XML. Nó gửi action của user tới viewmodeln nhưng nó sẽ không nhận lại kết quả trực tiếp . Để nhận được kết quả, View cần đăng ký lắng nghe kết quả từ ViewModel.
  • ViewModel: Đây là cầu nối giữa View và Model, nó sẽ không có bất kỳ một reference trực tiếp nào tới View.

2. Giới thiệu về Base project MVVM viết bằng kotlin:

Link project in github:

Sau khi học xong khóa học Kotlin bootcamp link và Developing Android Apps with Kotlin Link, mình đã thử dựng base project MVVM. Mình sẽ mô tả các thành phần và mục đích sử dụng ở dưới đây.

  • dependencies: Trong base project sẽ sử dụng các dependencies dưới đây:
//Data binding// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
// paging for load more
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
// RX
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.18"

// retrofit2
implementation "com.squareup.retrofit2:retrofit:2.7.2"
implementation "com.squareup.retrofit2:converter-gson:2.7.2"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.7.2"
implementation "com.squareup.okhttp3:logging-interceptor:4.4.0"

// dagger
implementation "com.google.dagger:dagger:2.26"
implementation "com.google.dagger:dagger-android:2.26"
implementation "com.google.dagger:dagger-android-support:2.26"
kapt 'com.google.dagger:dagger-compiler:2.26'
kapt 'com.google.dagger:dagger-android-processor:2.26'

// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
kapt 'com.github.bumptech.glide:compiler:4.10.0'

// navigation component
implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
implementation "androidx.navigation:navigation-ui-ktx:2.2.1"

// secure sensitive data but it require min-sdk = 23 (Android 6)
implementation "androidx.security:security-crypto:1.0.0-alpha02"

Trong các dependencies trên, có lẽ DataBindingNavigation Component là 2 thành phần mà một số người sẽ không thích bởi databinding khá phức tạp trong việc báo lỗi còn navigation lại khá khó custom trong các trường hợp luồng di chuyển đặc biệt. Tuy nhiên base project hướng đến việc làm các mock project hay dự án cá nhân nên sẽ không bị áp lực từ phía khách hàng do đó mình vẫn đưa chúng vào để học cũng như tìm hiểu xem chúng có gì mới, ưu nhược điểm của chúng là gì.

Ngoài ra, trong này còn có thêm security-crypto. Đây là một thành phần của jetpack do google phát triển với mục đích mã hóa và bảo mật các dữ liệu nhạy cảm trong app. Mặc dù nó mới đang ở phiên bản alpha 02 và yêu cầu min SDK là 23 nhưng mình vẫn thêm vào với mục đích tìm hiểu xem nó có thể làm được gì, ưu điểm và hạn chế của nó (hiện tại trong base security-crypto đang dùng để mã hóa shared preferences, nơi lưu access token).

Dữ liệu sau khi lưu vào share preference sẽ được mã hóa như dưới đây

  • Project Structure
cấu trúc thư mục trong project
  • Package data: Package này là nơi chứa các class, package dùng để thao tác với dữ liệu. Package model là nơi các model của dự án như dữ liệu để lưu database, dữ liệu call từ api hoặc các model for api request. Package preferences phục vụ cho việc lưu dữ liệu trong shared preferences. Việc lưu trữ dữ liệu sẽ do class EncryptedSharedPreferences đảm nhiệm nên các dữ liệu sẽ được mã hóa khi lưu -> tăng tính bảo mật cho các dữ liệu. Package repository sẽ là nơi thực hiện các logic với data, package này sẽ đảm nhận việc gọi dữ liệu từ api hoặc từ database hoặc kết hợp từ cả 2.
  • Package di: Đây là package phục vụ dependency injection sử dụng Dagger 2. Mình xin phép không nói chi tiết về package này do chưa nắm rõ về Dagger 2, code trong phần này được convert từ java -> kotlin, mà phần java trước đây do a KienDD làm 😁 😁. Trong package mọi người chỉ cần chú ý đến 2 class là ActivityBindingModule ViewModelModule.
// thêm đoạn code này vào ActivityBindingModule mỗi khi thêm     activity hoặc fragment mới
// đổi tên function và kiểu trả về theo tên activity hoặc fragment
@ContributesAndroidInjector
abstract fun bindMainActivity(): MainActivity
------------------------------------------------------------// thêm đoạn code này vào ViewModelModule mỗi khi thêm viewmodel mới
// đổi tên function và kiểu trả về theo tên viewmodel
// khi tạo mới viewmodel, viewmodel bắt buộc phải có @Inject constructor() như mainviewmodel
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(mainViewModel: MainViewModel): ViewModel
  • Package ui: Đây là package chứa toàn bộ các màn hình, các thành phần giao diện trong app. Package base là nơi chứa các thành phần base của ui như BaseActivity, BaseFragment, BaseViewModel… Package binding là nơi custom các thuộc tính của view để tận dụng khả năng của data binding, đưa 1 vài logic đơn giản hoặc bind data lên file layout (tham khảo thêm tại đây). Package custom sẽ chứa các layout custom như custom loading hoặc custom dialog…Package paging sẽ chứa các class datasource và datasource factory để load more sử dụng thư viện paging.
  • Package utils chứa các constant, enum, typedef và class hỗ trợ cho toàn app.

3. Một số điểm đặc biệt trong project

  • Mỗi fragment sẽ bao gồm 2 ViewModel gồm 1 Viewmodel có vòng đời gắn với fragment đó còn 1 cái là SharedViewModel có vòng đời gắn với activity. SharedViewModel sẽ không bị hủy khi bất kỳ fragment nào bị destroy, nó sẽ bị hủy chỉ khi activity bị hủy. Do Navigation không có cơ chế truyền dữ liệu về màn hình cũ khi back nên SharedViewModel sẽ đảm nhận việc này, hoặc đơn giản mọi người cũng có thể dùng shared viewmodel cho việc truyền data giữa các fragment (tuy nhiên việc này có thể làm tăng độ phúc tạp cho fragment).
  • Mỗi ViewModel sẽ có 1 SingleLiveEvent được gọi là viewstate. Live data này sẽ chịu trách nhiệm thông báo cho view các event thông thường như show loading, hide loading, show error… Nếu mọi người đã từng làm việc với RxJava hẳn sẽ quen với đoạn code sau:

Thông thường trong trường hợp này, để thông báo đến view, mọi người sẽ lựa chọn cách wrap trạng thái vào chung với dữ liệu như class dưới đây:

public class ResponseList<T> {
private int status; // we will set LOADING, ERROR, SUCCESS

@Nullable
private List<T> data;

@Nullable
private Throwable error;

}

Tuy nhiên sau khi làm một vài dự án với cách này, mình thấy nếu fragment chỉ có 1 vài đoạn gọi Rx và lắng nghe thì cách này sẽ ổn tuy nhiên khi màn hình có nhiều dữ liệu cần lắng nghe trạng thái thì việc dùng cách này sẽ làm cho fragment trở nên phúc tạp và nhiều đoạn code thừa và có thể sẽ không tận dụng được việc binding data trực tiếp lên view(?!). Do đó thay vì việc gộp trạng thái dữ liệu, chúng ta sẽ dùng 1 SingleLiveData để làm việc này. Khi đó việc call api hoặc các hàm xử lý Rx trong viewmodel sẽ như sau:

Khi đó việc lắng nghe các trạng thái sẽ được thực hiện như sau

handle common state in base fragment

Summary and next

Trên đây là mô tả về project base mà mình đã viết trong quá trình học kotlin. Do cũng mới bắt đầu học kotlin và kinh nghiệm làm việc chưa nhiều nên rất mong mọi người góp ý thêm.

Mọi người có thể checkout sang nhánh mock_develop để xem cách tạo mới fragment, cách call api. Ở nhánh này mình đã tạo mới màn hình List Category và thực hiện call api rồi đổ dữ liệu lên recycler view

Ngoài ra, Do dagger khá là khó học và làm việc nên mình có thử thay thế dagger bằng koin, mọi người có thể checkout sang nhánh replace_dagger_by_koin để xem.

Thanks and happy coding!

--

--