Phát triển Android hiện đại với Kotlin (Phần 3)
Bài đăng này đã không được cập nhật trong 6 năm
Giới thiệu về các thành phần nhận biết vòng đời (Lifecycle-aware components)
Rất nhiều developer gặp phải vấn đề liên quan đến vòng đời trong Android, vì vậy Google đã tạo ra một library để hỗ trợ chúng ta giải quyết vấn đề này. Class ViewModel là một trong số chúng. Nó là class mà tất cả các ViewModel (trong mô hình MVVM) của chúng ta nên extend.
Chúng ta hãy tạo class MainViewModel extend từ ViewModel của Lifecycle-aware component. Đầu tiên, chúng ta add thư viện Lifecycle-aware component vào file build.gradle:
dependencies {
...
implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}
Bây giờ, extend MainViewModel từ ViewModel:
package me.fleka.modernandroidapp
import android.arch.lifecycle.ViewModel
class MainViewModel : ViewModel() {
...
}
Và trong function onCreate() của MainActivity, chúng ta có:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.executePendingBindings()
}
}
Lưu ý rằng, chúng ta không tạo một instance mới của MainViewModel. Chúng ta lấy nó từ ViewModelProviders. ViewModelProviders là lớp utility có method cho phép lấy ra ViewModelProvider. Vì vậy, nếu gọi ViewModelProviders.of(this) trong Activity thì ViewModel sẽ sống cho đến khi Activity đó còn sống (bị destroy và không recreate lại). Tương tự, nếu bạn gọi nó trong Fragment thì ViewModel của bạn sẽ sống cho đến khi Fragment đó còn sống. Xem biểu đồ sau:
ViewModelProvider có trách nhiệm tạo instance mới nếu nó được gọi lần đầu tiên và return lại instance cũ mỗi khi Activity/Fragment được recreated. Quan sát điều xảy ra khi chúng ta xoay màn hình: Trước khi sử dụng ViewModelProvider: Khi xoay màn hình, dữ liệu cũ bị hiển thị lại.
Sau khi lấy ViewModel từ ViewModelProvider: Dữ liệu là như nhau trước và sau khi xoay màn hình.
Ví dụ fetch danh sách Github Repository và hiển thị nó
Để thực hiện nó, chúng ta thêm function getRepositories mà sẽ return lại danh sách repositories giả lập:
class RepoModel {
fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
}
fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
var arrayList = ArrayList<Repository>()
arrayList.add(Repository("First", "Owner 1", 100 , false))
arrayList.add(Repository("Second", "Owner 2", 30 , true))
arrayList.add(Repository("Third", "Owner 3", 430 , false))
Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)
}
}
interface OnDataReadyCallback {
fun onDataReady(data : String)
}
interface OnRepositoryReadyCallback {
fun onDataReady(data : ArrayList<Repository>)
}
Ngoài ra, chúng ta nên có thêm function trong MainViewModel mà sẽ gọi đến getRepositories từ RepoModel:
class MainViewModel : ViewModel() {
...
var repositories = ArrayList<Repository>()
fun refresh(){
...
}
fun loadRepositories(){
isLoading.set(true)
repoModel.getRepositories(object : OnRepositoryReadyCallback{
override fun onDataReady(data: ArrayList<Repository>) {
isLoading.set(false)
repositories = data
}
})
}
}
Và khi kết thúc, chúng ta sẽ hiển thị các repositories trong RecyclerView. Để làm được điều đó, chúng ta phải:
- tạo layout rv_item_repository.xml
- add RecyclerView vào layout activity_main.xml
- tạo RepositoryRecyclerViewAdapter
- set adapter cho recyclerview
Để tạo rv_item_repository.xml, chúng ta sử dụng thư viện CardView:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.view.View" />
<variable
name="repository"
type="me.fleka.modernandroidapp.uimodels.Repository" />
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/repository_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryName}"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.083"
tools:text="Modern Android App" />
<TextView
android:id="@+id/repository_has_issues"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@string/has_issues"
android:textStyle="bold"
android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="@+id/repository_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/repository_name"
app:layout_constraintTop_toTopOf="@+id/repository_name"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="@+id/repository_owner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:text="@{repository.repositoryOwner}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_name"
app:layout_constraintVertical_bias="0.0"
tools:text="Mladen Rakonjac" />
<TextView
android:id="@+id/number_of_starts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{String.valueOf(repository.numberOfStars)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/repository_owner"
app:layout_constraintVertical_bias="0.0"
tools:text="0 stars" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</layout>
Bước tiếp theo, thêm RecyclerView vào activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="me.fleka.modernandroidapp.MainViewModel" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="me.fleka.modernandroidapp.MainActivity">
<ProgressBar
android:id="@+id/loading"
android:layout_width="48dp"
android:layout_height="48dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="@+id/repository_rv"
android:layout_width="0dp"
android:layout_height="0dp"
android:indeterminate="true"
android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toTopOf="@+id/refresh_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/rv_item_repository" />
<Button
android:id="@+id/refresh_button"
android:layout_width="160dp"
android:layout_height="40dp"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:onClick="@{() -> viewModel.loadRepositories()}"
android:clickable="@{viewModel.isLoading ? false : true}"
android:text="Refresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
</android.support.constraint.ConstraintLayout>
</layout>
Bây giờ, chúng ta add Adapter cho RecyclerView:
class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
private var listener: OnItemClickListener)
: RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int)
= holder.bind(items[position], listener)
override fun getItemCount(): Int = items.size
interface OnItemClickListener {
fun onItemClick(position: Int)
}
class ViewHolder(private var binding: RvItemRepositoryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(repo: Repository, listener: OnItemClickListener?) {
binding.repository = repo
if (listener != null) {
binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
}
binding.executePendingBindings()
}
}
}
Cuối cùng, chúng ta set adapter cho recyclerView trong MainActivity:
class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
binding.executePendingBindings()
binding.repositoryRv.layoutManager = LinearLayoutManager(this)
binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)
}
override fun onItemClick(position: Int) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
Ví dụ trên sẽ hiển thị danh sách Repositories nhờ vào RecyclerView.
All rights reserved