【Android】 MVVM實作 - 使用ViewModel, LiveData, DataBinding
今天要利用Youbike場站資訊的OpenData來練習MVVM的架構,主要功能就是每30秒去抓一次資料然後顯示出來。這邊會拆分成幾個步驟來做,首先會先把資料的部分處理好,也就是Call API的部分;再來會實作LiveData及DataBinding的功能,那就來看看該怎麼做吧!
資料來源
這次使用的是由台北市政府提供的「YouBike臺北市公共自行車即時資訊」
API名稱:YouBike臺北市公共自行車即時資訊
API網址:https://tcgbusfs.blob.core.windows.net/blobyoubike/YouBikeTP.json
引入套件
由於會使用到KAPT(Kotlin的Annotation Processing),DataBinding,anko,recyclerView及lambda的寫法,所以需要對Gradle檔案做些修改。
KAPT
apply plugin: 'kotlin-kapt'
android {
kapt {
generateStubs = true
}
}
dependencies {
implementation 'androidx.activity:activity-ktx:1.1.0'
}
DataBinding
android {
dataBinding {
enabled true
}
}
dependencies {
kapt 'com.android.databinding:compiler:4.0.0'
}
Anko
dependencies {
implementation 'org.jetbrains.anko:anko:0.10.8'
}
RecyclerView
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.1.0'
}
Lambda
android {
kotlinOptions {
jvmTarget = '1.8'
}
}
資料抓取
我們先來看API抓回來的資料結構,並依這個資料結構建立對應的data class「StationData」
{
"retCode":1,
"retVal":{
"0001":{
"sno":"0001",
"sna":"捷運市政府站(3號出口)",
"tot":"180",
"sbi":"84",
"sarea":"信義區",
"mday":"20200604160144",
"lat":"25.0408578889",
"lng":"121.567904444",
"ar":"忠孝東路/松仁路(東南側)",
"sareaen":"Xinyi Dist.",
"snaen":"MRT Taipei City Hall Stataion(Exit 3)-2",
"aren":"The S.W. side of Road Zhongxiao East Road & Road Chung Yan.",
"bemp":"83",
"act":"1"
}
}
}
我們需要用到的資料是retVal裡面的jsonObject,所以我們可以把StationData設計成這樣
data class StationData(
val sno: String? = null,
val sna: String? = null,
val tot: String? = null,
val sbi: String? = null,
val sarea: String? = null,
val mday: String? = null,
val lat: String? = null,
val lng: String? = null,
val ar: String? = null,
val sareaen: String? = null,
val snaen: String? = null,
val aren: String? = null,
val bemp: String? = null,
val act: String? = null
)
接著就可以開始著手接資料了,我們先建立一個對應MainActivity的MainViewModel,在這個ViewModel裡面我們會宣告一個LiveData形式的stationList,然後會有一個getBikeDataFromServer的函示去抓取資料並轉換成StationData的類別存放進stationList裡,最後會有一個startRetrieveData的函示讓他每30秒觸發getBikeDataFromServer一次去更新資料。
由於會使用到網路,所以要記得先到AndroidManifest.xml裡去新增網路的權限
<uses-permission android:name="android.permission.INTERNET"/>
class MainViewModel : ViewModel() {
var stationList = MutableLiveData<List<StationData>>()
var delay = 30 * 1000L
fun startRetrieveData() {
Timer().schedule(timerTask{
getBikeDataFromServer()
}, 0, delay) }
private fun getBikeDataFromServer() {
doAsync {
val tempList = mutableListOf<StationData>()
val url = "https://tcgbusfs.blob.core.windows.net/blobyoubike/YouBikeTP.json"
val jsonStr = URL(url).readText()
val jsonObj = JSONObject(jsonStr).getJSONObject("retVal")
jsonObj.keys().forEach {
val stationObj = jsonObj.getJSONObject(it)
tempList.add(
StationData(
stationObj.getString("sno"),
stationObj.getString("sna"),
stationObj.getString("tot"),
stationObj.getString("sbi"),
stationObj.getString("sarea"),
stationObj.getString("mday"),
stationObj.getString("lat"),
stationObj.getString("lng"),
stationObj.getString("ar"),
stationObj.getString("sareaen"),
stationObj.getString("snaen"),
stationObj.getString("aren"),
stationObj.getString("bemp"),
stationObj.getString("act")
)
)
}
stationList.postValue(tempList)
}
}
}
資料呈現
接著我們來處理資料呈現的部分,這次我們僅簡單利用RecyclerView以列表的方式來呈現資料,所以先到activity_main.xml裡去新增RecyclerView。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_recycler" />
</androidx.constraintlayout.widget.ConstraintLayout>
再來接著處理,RecyclerView裡面的item_recycler.xml。在這裡我們會同時採用DataBinding的技術,如此一來資料就會自動對應到Model裡的屬性。要使用DataBinding時,layout的最外層必須是<layout>,而裡面需要含有一個<data>的區塊。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tool="http://schemas.android.com/tools">
<data>
<import type="com.jeremyhuang.mvvmsample.model.StationData" />
<variable name="item" type="StationData" />
</data>
</layout>
再來我們就可以開始設計畫面,然後在需要綁定資料的地方以@{`場站名稱: ` + item.sna}
的方式來綁定,所以最後應該會長成這樣:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tool="http://schemas.android.com/tools">
<data>
<import type="com.jeremyhuang.mvvmsample.model.StationData" />
<variable name="item" type="StationData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_50"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".5" />
<TextView
android:id="@+id/tv_station_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`場站名稱: ` + item.sna}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tool:text="場站名稱" />
<TextView
android:id="@+id/tv_addr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`地址: ` + item.ar}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_station_name"
tool:text="地" />
<TextView
android:id="@+id/tv_bike_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`場站目前車輛數量: ` + item.sbi}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_addr"
tool:text="場站目前車輛數量" />
<TextView
android:id="@+id/tv_park_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`空位數量: ` + item.bemp}"
app:layout_constraintStart_toStartOf="@id/guide_50"
app:layout_constraintTop_toBottomOf="@+id/tv_addr"
tool:text="空位數量" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:background="@color/colorPrimaryDark"
app:layout_constraintTop_toBottomOf="@id/tv_park_count" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
接著我們開始著手處理RecyclerView的Adapter,因為使用DataBinding所以在ViewHolder裡接收的不是View而是ViewDataBinding,而在onBindViewHolder裡則是直接把資料的類別傳給View,所以我們的Adapter便會是這樣
class StationAdapter(
private var context: Context, private var list: List<StationData>):
RecyclerView.Adapter<StationAdapter.ViewHolder>(){
inner class ViewHolder(var dataBinding: ViewDataBinding) :
RecyclerView.ViewHolder(dataBinding.root)
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.item_recycler, parent, false
)
)
}
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding: ViewDataBinding = holder.dataBinding
binding.setVariable(BR.item, list[position])
}
}
剩下的事情就簡單了,我們只需要把RecyclerView設定好,把LiveData的觀察設定好,然後觸發startRetrieveData讓他開始自動去抓取資料即可。我們分別利用initView及initData兩個函式來處理前兩件事。
private fun initView() {
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.VERTICAL
recycler_view.layoutManager = layoutManager
}
private fun initData() {
model.stationList.observe(this, Observer {
recycler_view.adapter = StationAdapter(this@MainActivity, it)
})
}
最後我們在onCreate的最後面呼叫model.startRetrieveData()
來開始抓取資料,以下是整個MainActivity的程式:
class MainActivity : AppCompatActivity() {
private val model: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
initData()
model.startRetrieveData()
}
private fun initView() {
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.VERTICAL
recycler_view.layoutManager = layoutManager
}
private fun initData() {
model.stationList.observe(this, Observer {
recycler_view.adapter =
StationAdapter(this@MainActivity, it)
})
}
class StationAdapter(
private var context: Context,
private var list: List<StationData>):
RecyclerView.Adapter<StationAdapter.ViewHolder>(){
inner class ViewHolder(var dataBinding: ViewDataBinding) :
RecyclerView.ViewHolder(dataBinding.root)
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int):
ViewHolder {
return ViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.item_recycler, parent, false)
)
}
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(
holder: ViewHolder, position: Int) {
val binding: ViewDataBinding = holder.dataBinding
binding.setVariable(BR.item, list[position])
}
}
}
這時候執行App應該就可以看到資料跑出來了,並且每30秒會刷新一次。