Phát triển Android hiện đại với Kotlin (Phần 2)

Đây là phần thứ hai của series bài viết Phát triển Android hiện đại với Kotlin.

Thật khó để tìm thấy một dự án bao gồm tất cả những công nghệ mới mẻ nhất trong phát triển Android. Trong bài này, chúng ta sẽ được tìm hiểu những điều mới mẻ đó: 0 . Android Studio 3, beta 1 1 . Kotlin language 2 . Build Variants 3 . ConstraintLayout 4 . Data binding library 5. MVVM architecture + repository pattern ( with mappers) + Android Manager Wrappers 6 . RxJava2 and how it helps us in architecture 7 . Dagger 2.11, what is Dependency Injection, why you should use it. 8 . Retrofit (with Rx Java2) 9 . Room (with Rx Java2)

MVVM architecture + repository pattern + Android Manager Wrappers

Sơ lược về Architecture trong Android

Trong một thời gian dài, các nhà phát triển Android không có bất kỳ kiến trúc nào trong các dự án của họ. Trong ba năm qua, kiến trúc ứng dụng mới được thảo luận rộng rãi trong cộng đồng phát triển Android. Thời gian của God Activity đã qua và Google đã publish repo Android Architecture Blueprints với rất nhiều sample và hướng dẫn về các cách tiếp cận kiến trúc khác nhau. Cuối cùng, tại Google I/O 17, họ đã giới thiệu các thành phần kiến trúc Android (Android Architecture Components), một tập hợp các lib để giúp code của chúng ta được rõ ràng và tốt hơn. Bạn có thể sử dụng tất cả hoặc một trong số các Component, dù tôi thấy tất cả chúng đều thực sự hữu ích. Có hai mẫu kiến trúc chính:

  • MVP
  • MVVM Rất khó để nói rằng mẫu nào sẽ tốt hơn. Bạn có thể thử sử dụng cả hai và quyết định lựa chọn. Tôi thích MVVM sử dụng các component nhận biết lifecycle và trong bài viết này, tôi sẽ đề cập đến nó.

Mô hình MVVM là gì?

MVVM là một mô hình kiến trúc (architectural pattern). Nó là viết tắt của Model-View-ViewModel. Trong đó, ViewModel là thành phần ở giữa, kết nối ViewModel.

View là tên trừu tượng cho Activity, Fragment hoặc bất kỳ một Android Custom View. Một lưu ý quan trọng là không nhầm lẫn View với Android View. View không nên chứa bất kỳ một logic nào ở trong. View không được chứa data. Nó nên chứa một tham chiếu tới thể nghiệm của ViewModel và tất cả data mà nó cần sẽ được lấy từ ViewModel. Ngoài ra, View nên observe những data đó và layout nên được cập nhật mỗi khi data từ ViewModel thay đổi. Tóm lại, View có trách nhiệm: hiển thị layout tương ứng với những data và trạng thái khác nhau.

ViewModel là tên trừu tượng cho class mà giữ data và chứa logic lấy data và cung cấp cho View hiển thị. ViewModel giữ trạng thái hiện tại. ViewModel có tham chiếu tới một hoặc nhiều thể nghiệm Model và tất cả data được lấy từ chúng. Tuy nhiên, ViewModel không biết được data được lấy từ database hay server từ xa. Hơn nữa, ViewModel không có tham chiếu tới bất kỳ thể nghiệm nào của View và cũng không biết về Android framework.

Model là tên trừu tượng cho layer - nơi chúng ta chuẩn bị data cho ViewModel. Nó là class - nơi chúng ta lấy data từ server từ xa và cache vào bộ nhớ hoặc lưu nó trong local database. Lưu ý rằng nó không giống các class model như: User, Car, Square... chỉ chứa data. Thông thường, nó cài đặt theo mô hình Repository, chúng ta sẽ tìm hiểu cụ thể ở dưới. Model không chứa tham chiếu tới ViewModel.

MVVM, nếu implement đúng, là một cách tuyệt vời để tách biệt code của bạn và làm cho nó dễ test hơn. Nó giúp chúng ta tuân theo nguyên tắc SOLID, vì vậy code của chúng ta sẽ dễ dàng maintain hơn.

Code Example

Chúng ta sẽ tìm hiểu cách hoạt động thông qua ví dụ rất đơn giản.

Đầu tiên, tạo một Model đơn giản return về chuỗi String:

class RepoModel {

    fun refreshData() : String {
        return "Some new data"
    }
}

Thông thường, get data là một lời gọi không đồng bộ (async), vì vậy chúng ta phải đợi nó. Để mô phỏng điều đó, chúng ta thay đổi code như sau:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

Bây giờ, hãy tạo ViewModel:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false
}

Như bạn thấy, có một thể nghiệm của RepoModel, text sẽ hiển thị và biến boolean isLoading để giữ giá trị trạng thái hiện tại. Bây giờ, tạo fun refresh để chịu trách nhiệm get data:

class MainViewModel {
    ...

    val onDataReadyCallback = object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            isLoading.set(false)
            text.set(data)
        }
    }

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(onDataReadyCallback)
    }
}

Với Kotlin, bạn có thể sử dụng object expression để thay thế như sau:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false

    fun refresh() {
        repoModel.refreshData( object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            text = data
        })
    }
}

Khi gọi refresh, chúng ta thay đổi view với trạng thái mới, và mỗi khi lấy được data isLoading được set false.

Ngoài ra, chúng ta nên sử dụng text là biến kiểu ObservableField<String>isLoading là kiểu ObservableField<Boolean>. ObservableField là class của thư viện Data Binding mà chúng ta có thể sử dụng thay vì phải tạo đối tượng Observable.

class MainViewModel {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField<String>()

    val isLoading = ObservableField<Boolean>()

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(object : OnDataReadyCallback {
            override fun onDataReady(data: String) {
                isLoading.set(false)
                text.set(data)
            }
        })
    }
}

Lưu ý rằng chúng ta sử dụng val thay cho var vì chỉ thay đổi giá trị trong field chứ không phải thay đổi chính field đó. Và nếu muốn khởi tạo nó, chúng ta có thể khai báo:

val text = ObservableField("old data")
 val isLoading = ObservableField(false)

Bây giờ, chúng ta thay đổi layout để observe textisLoading. Đầu tiên, chúng ta bind MainViewModel thay vì Repository:

<data>
    <variable
        name="viewModel"
        type="me.fleka.modernandroidapp.MainViewModel" />
</data>

Sau đó:

  • để TextView observe text từ thể nghiệm của MainViewModel
  • thêm xử lý ProgressBar chỉ hiển thị (visible) khi isLoading = true
  • thêm xử lý khi click vào Button sẽ gọi function refresh của MainViewModelButton chỉ có thể click khi isLoading = false
...

        <TextView
            android:id="@+id/repository_name"
            android:text="@{viewModel.text}"
            ...
            />

        ...
        <ProgressBar
            android:id="@+id/loading"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            ...
            />

        <Button
            android:id="@+id/refresh_button"
            android:onClick="@{() -> viewModel.refresh()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            />
...

Để run đoạn code trên với các giá trị View.VISIBLEView.GONE, chúng ta cần import View:

<data>
        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
</data>

Bây giờ, chúng ta cần binding với Activity:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}

Như vậy, chúng ta đã hoàn thành ví dụ đơn giản nhất về MVVM