Example MVVM-ViewModel-LiveData-Retrofit2-RX-Kotlin

Chào các bạn đã một thời gian khá dài mình không viết bài trên blog.
Hôm nay mình sẽ đem đến cho các bạn 1 example về MVVM-ViewModel-LiveData-Retrofit2 trong quá trình mình tìm hiểu với mục đích chính để các bạn mới bắt đầu như mình dễ hiểu hơn
Oke chúng ta sẽ bắt đầu 😀

  1. Khái quát về MVVM (Model – View – ViewModel) Pattern


MVVM được tạo thành từ ba thành phần cốt lõi, mỗi thành phần đều có vai trò riêng :

  • Model – mô hình dữ liệu đối tượng giúp truy xuất và thao tác trên dữ liệu thực sự.
  • View – Xác định cấu trúc, bố trí và sự xuất hiện của một view trên màn hình
  • ViewModel – liên kết giữa View và Model, xử lý view logic
  1. Khái quát về ViewModel- LiveData
    Khi làm việc với android chắc chắn nhiều bạn sẽ gặp phải trường hơp dữ liệu bị mất đi xoay activity vì vòng đời của nó sẽ khởi tạo lại.
    Để giải quyết vấn đề trên 1 cách đơn giản nhất chúng ta sẽ implement:
    – ViewModel là lớp được cung cấp bởi lifecycle. Nó là cầu nối giữa ModelUI và nó khá thông minh khi có thể giữ lại đc dữ liệu cho trường hợp ở trên vì cơ bản nó tách biệt với lớp UI.
    LiveData là một lớp dùng để lưu trữ dữ liệu và cho phép chúng ta có thể theo dõi sự thay đổi của nó và kịp thời cập nhật data lên UI. Và đặc biệt LiveData có thể nhận biết được vòng đời của activies, fragments, services để đảm bảo rằng LiveData chỉ gọi update  khi mà thành phần trên đó còn hoạt động.
  2. Example
    Lưu ý trong example này mình sẽ không sử dụng DataBinding vì một lý do đơn giản là mình chưa dùng nó bao giờ =))))

3.1 Môi Trường
– Ở example này mình sử dụng android studio 3.2 bản mới nhất.
– Các bạn tạo project include Kotlin vào nhé.

3.2 Config project
Các bạn mở file build.gradle lên và update các thư viện như sau:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.mvvmviewmodel.livedata"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        dev {
            buildConfigField "String", "API_URL", '"https://api.github.com"'
        }
    }
}
ext {
    library_version = '28.0.0'
    constrant_layout = '1.1.3'
    retrofit = '2.3.0'
    rx_java = '2.2.1'
    rx_java_adapter = '2.4.0'
    rx_android = '2.1.0'
    retrofit = '2.4.0'
    ok_http = '3.9.0'
    gson = '2.8.5'
    lifecycle_extention = '1.1.1'

}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation "com.android.support:appcompat-v7:$library_version"
    implementation "com.android.support:cardview-v7:$library_version"
    implementation "com.android.support.constraint:constraint-layout:$constrant_layout"
    implementation "android.arch.lifecycle:extensions:$lifecycle_extention"
    //Rx Android
    implementation "io.reactivex.rxjava2:rxandroid:$rx_android"
    implementation "io.reactivex.rxjava2:rxjava:$rx_java"
    //retrofit load api
    implementation "com.squareup.retrofit2:retrofit:$retrofit"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit"
    implementation "com.squareup.retrofit2:adapter-rxjava2:$rx_java_adapter"
    implementation("com.squareup.okhttp3:logging-interceptor:$ok_http") {
        transitive = true
    }
    implementation "com.google.code.gson:gson:$gson"
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

– Các thư viện mình tích hợp sẵn như: retrofit, rx, gson,lifecycle…

3.3 Cấu Trúc Project/Cây thư mục

3.4 Start coding
Example này sẽ có 2 chức năng chính:
+ Get UserInfo
+ Get List Repository show List.

3.4.1 Model
Tương ứng với các chức năng mình sẽ tạo 2 model:
UserEntity.kt

data class UserEntity(var id: Int, var name: String, var lastName: String)

RepositoriesEntity.kt

data class RepositoriesEntity(var name: String, var full_name: String, var html_url: String)

Với kotlin tạo model đúng quá đơn giản và ngắn gọn 😀

3.4.2 Config retrofit để gọi api
Khởi tạo ApiBuilder.kt  

class ApiBuilder() {
    companion object {
        private val apiInterface: ApiInterface? = null
        fun getWebService(): ApiInterface {
            if (apiInterface != null) {
                return apiInterface
            }
            val retrofit = Retrofit.Builder().baseUrl(BuildConfig.API_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .build()
            return retrofit.create(ApiInterface::class.java)
        }
    }
}

Khởi tạo ApiInterface.kt  gồm 2 api kết hợp sử dụng RxJava.

interface ApiInterface {

    @GET("/users/{user}")
    fun getUser(@Path("user") userId: String): Observable<UserEntity>

    @GET("/users/{user}/repos")
    fun getRepositories(@Path("user") userId: String): Observable<List<RepositoriesEntity>>
}

Khởi tạo 1 class BaseResponse.kt để xử lý lỗi api.

data class BaseResponse(var title: String? = null,
                        var message: String? = null,
                        var request_code: String? = null)

3.4.3 Code viewmodel

Để hỗ trợ việc xử lý progress ở đây mình tạo 1 file Event wrapper  Event.kt giúp việc quản lý xự kiện rõ ràng hơn. (Tham Khảo phần cuối bài viết tại đây)

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Tiếp theo khởi tạo class: BaseViewModel.kt class này để các class khác kế thừa một vài hàm cần thiết.

open class BaseViewModel : ViewModel() {
    val eventLoading = MutableLiveData<Event<Boolean>>()
    val eventError = MutableLiveData<Event<BaseResponse>>()
    val eventFailure = MutableLiveData<Event<Throwable>>()

    fun showLoading(value: Boolean) {
        eventLoading.value = Event(value)
    }

    fun showError(baseResponse: BaseResponse) {
        eventError.value = Event(baseResponse)
    }

    fun showFailure(throwable: Throwable) {
        eventFailure.value = Event(throwable)
    }
}

 

Khởi tạo class: UserViewModel.kt  class này fetch data và giao tiếp với UI.
Gồm 2 hàm loadUserInfo () và loadRepositories() để fetch data.
Hàm userResponse trả về 1 LiveData từ loadUserInfo ().
Và hàm repositoriesResponse trả về 1 LiveData từ loadRepositories()

Note: Mình có kết hợp xử dụng RX để code ngắn gọn hơn.

class UserViewModel : BaseViewModel() {
    private val TAG = UserViewModel::class.java.simpleName
    private val userResponse = MutableLiveData<UserEntity>()
    private var repositoriesResponse = MutableLiveData<List<RepositoriesEntity>>()
    private val disposables = CompositeDisposable()

    fun userResponse(): MutableLiveData<UserEntity> {
        return userResponse
    }

    fun repositoriesResponse(): MutableLiveData<List<RepositoriesEntity>> {
        return repositoriesResponse
    }

    fun loadUserInfo(userId: String) {
        disposables.add(ApiBuilder.getWebService().getUser(userId)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showLoading(true) }
                .doFinally { showLoading(false) }
                .subscribe({
                    userResponse.value = it
                }, {
                    showFailure(it)
                }))
    }

    fun loadRepositories(userId: String) {
        ApiBuilder.getWebService().getRepositories(userId)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnSubscribe { showLoading(true) }
                .doFinally { showLoading(false) }
                .subscribe({
                    repositoriesResponse.value = it
                }, {
                    showFailure(it)
                })
    }

    override fun onCleared() {
        disposables.clear()
    }
}

3.4.4 Code trên lớp View

Mình có 1 MainFragment.kt với nhiệm vụ:
+ setup ViewModel
+ gọi api xử lý hiện thị dữ liệu.

class MainFragment : BaseFragment() {

    companion object {
        fun newInstance() = MainFragment()
    }

    private lateinit var viewModel: UserViewModel

    override fun getRootLayoutId(): Int {
        return R.layout.main_fragment
    }

    override fun setupViewModel() {
        if (!this::viewModel.isInitialized) {
            viewModel = ViewModelProviders.of(this).get(UserViewModel::class.java)
            setObserveLive(viewModel)
        }
    }


    override fun setupData(view: View) {
        //1.0  Observe userResponse
        val userObserver = Observer<UserEntity> { userEntity ->
            // Update the UI, in this case, a TextView.
            tv_name_user.text = userEntity!!.name
        }
        viewModel.userResponse().observe(this, userObserver)
        //End 1.0

        //2.0 Observe repositoriesResponse
        val repoObserver = Observer<List<RepositoriesEntity>> {
            val repoName: MutableList<String> = mutableListOf()
            for (repo in it!!) {
                repoName.add(repo.name + "\n" + repo.full_name)
            }
            val adapter = ArrayAdapter(context!!,
                    android.R.layout.simple_list_item_1, android.R.id.text1, repoName)
            list_repositories.adapter = adapter
        }
        viewModel.repositoriesResponse().observe(this, repoObserver)
        //End 2.0

        viewModel.loadUserInfo("nguyenlinhnttu")
        btn_load_data.setOnClickListener {
            viewModel.loadRepositories("nguyenlinhnttu")
        }
    }

}

Mình có thêm hàm: setObserveLive(viewModel)
Hàm này mình dùng để xử lý progress/error/failure trong BaseFragment.

 fun setObserveLive(viewModel: BaseViewModel) {
        viewModel.eventLoading.observe(this, Observer {
            if (it != null) {
                if (it.getContentIfNotHandled() != null) {
                    if (it.peekContent()) {
                        showLoadingDialog()
                    } else {
                        hideLoadingDialog()
                    }
                }
            }
        })
        viewModel.eventError.observe(this, Observer {
            if (it != null) {
                if (it.getContentIfNotHandled() != null) {
                    showRequestError(it.peekContent())
                }
            }
        })
        viewModel.eventFailure.observe(this, Observer {
            if (it != null) {
                if (it.getContentIfNotHandled() != null) {
                    showQuestFailure(it.peekContent())
                }
            }
        })
    }

main_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.MainFragment">

    <Button
        android:id="@+id/btn_load_data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="Get Repositories"
        android:textAllCaps="false" />

    <TextView
        android:id="@+id/tv_name_user"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btn_load_data"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="8dp"
        android:textColor="#f45555"
        android:textSize="18sp" />

    <ListView
        android:id="@+id/list_repositories"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_name_user"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="8dp"
        android:textSize="18sp" />
</RelativeLayout>

MainActivity.kt

class MainActivity : BaseActivity() {

    override fun setupView(savedInstanceState: Bundle?) {
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                    .replace(R.id.container, MainFragment.newInstance())
                    .commitAllowingStateLoss()
        }
    }

    override fun getRootLayoutId(): Int {
        return R.layout.main_activity
    }
}

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.MainActivity" />

2 Class BaseActivityBaseFragment phần code progress bar các bạn check trong git nhé chủ yếu là 3 hàm abstract để dùng chung thôi:

    abstract fun getRootLayoutId(): Int

    abstract fun setupViewModel()

    abstract fun setupData(view: View)

4. Test

Xong xong chúng ta thử test xem nó hoạt động như nào nhé.

  • Các bạn sẽ thấy khi xoay màn hình dữ liệu sẽ không mất đi.

Bài viết còn nhiều thiếu xót, mình rất hoàn nghênh các bạn góp ý để hoàn thiện hơn nhé.

Source GitHub.

Nguyễn Linh

Chia sẻ để cùng tiến bộ...