Keyword: Android ViewModel,Coroutine,LiveData,RecyclerView
到Day11使用Ktor進行網路請求並且顯示在Android畫面的Code放在
KMMDay11
有了shared內的資料,我們就要真正的來使用這些資料了
先在shared/commonMain/model/的路徑下建立一個DataRepository的class,當成shared與雙平台交互的地方.
這層Respository雖然不是必要的,Android與iOS可以直接呼叫剛剛建立好的CafeApiImpl來使用其中的方法進行網路請求.但還是推薦額外建立這層Repositroy.
這層Repositroy讓Android或iOS平台和資料的來源隔離,雙平台並不需要知道資料從哪裡來的,從Api拉取或是從本地DB庫撈出來對於使用的平台根本一點都不重要.我只要這個資料能夠使用就好.
class DataRepository {
private val ktorApi: CafeApi by lazy { CafeApiImpl() }
suspend fun fetchCafesFromNetwork(cityName: String): List<CafeResponseItem> {
try {
return ktorApi.fetchCafeFromApi(cityName)
} catch (e: Exception) {
println(e.message)
}
return listOf()
}
}
撰寫測試的時候,也能根據情境切換不同的資料源,而對於使用資料的那方毫無影響.再配合上依賴注入,就能更加解耦,在多平台的專案中,解耦程度越高,修改越容易,伴隨產生side effect的機率就更低.
在DataRepository內的fetchCafesFromNetwork是一個suspend function,意味著這個function需要運行在一個coroutine的環境內.
昨天我們在gradle(shared)已經加入了coroutine的依賴,因此目前在shared模組內是可以使用coroutine的,但是在androidApp的模組內還沒有,所以會發生錯誤.
另外我們在Android平台,今天還會使用到ViewModel與Ktor等等的組件,也一起加入到androidApp的gradle中
記得加入DSL來管理新的依賴.
//在gradle(android)中 添加以下依賴
dependencies {
...
implementation(Develop.Ktor.androidCore)
implementation(Develop.Coroutines.common)
implementation(Develop.Coroutines.android)
implementation(Develop.AndroidX.lifecycle_runtime)
implementation(Develop.AndroidX.lifecycle_viewmodel)
implementation(Develop.AndroidX.lifecycle_viewmodel_extensions)
...
}
//在Dependencies.kts中 添加依賴版本管理
object Versions{
val ktor = "1.6.3"
val coroutines = "1.5.0-native-mt"
val serialization_version = "1.5.21"
object AndroidX {
val core = "1.6.0"
val lifecycle = "2.4.0-alpha02"
val test = "1.3.0"
val test_ext = "1.1.2"
}
}
object Develop{
object Ktor{
...
val androidCore = "io.ktor:ktor-client-okhttp:${Versions.ktor}"
...
}
object Coroutines{
val common = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
val test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}"
}
object AndroidX {
val core_ktx = "androidx.core:core-ktx:${Versions.AndroidX.core}"
val lifecycle_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.AndroidX.lifecycle}"
val lifecycle_viewmodel = "androidx.lifecycle:lifecycle-viewmodel:${Versions.AndroidX.lifecycle}"
val lifecycle_viewmodel_extensions = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.AndroidX.lifecycle}"
}
}
之後記得有新的依賴也要到這邊來修改.
然後因為我們將要使用網路請求,所以需要在AndroidManifest中註冊權限,告訴Android系統我們將會進行網路請求
<uses-permission android:name="android.permission.INTERNET"/>
在androidApp模組中,建立一個MainViewModel,繼承Google的ViewModel組件.
在其中加入之前建立好的DataRepository當作資料元,再加入MutableLiveData<List>,最後再補上一個根據MutableLiveData變化的LiveData<List>
class MainViewModel : ViewModel() {
private val dataRepository: DataRepository = DataRepository()
private val cafeList = MutableLiveData<List<CafeResponseItem>>()
val cafeListLiveData: LiveData<List<CafeResponseItem>> = Transformations.map(cafeList) { it }
}
注意兩個LiveData中,普通的LiveData是public的,而Mutable的是private的.這是為了讓View層不能去修改其中的資料,而專職於顯示的部分.由架構上限制了修改的可能性.記得前面提過的單一職責原則嘛?
最後寫下一個進行網路請求的方法,由於拉取網路資料是suspend方法所以需要放在coroutine中執行,然後將回傳的資料再設置回LiveData
完整的ViewModel如下
class MainViewModel : ViewModel() {
private val dataRepository: DataRepository = DataRepository()
private val cafeList = MutableLiveData<List<CafeResponseItem>>()
val cafeListLiveData: LiveData<List<CafeResponseItem>> = Transformations.map(cafeList) { it }
fun fetchCafeData(city: String = "") {
viewModelScope.launch() {
val result = async { dataRepository.fetchCafesFromNetwork(city) }
cafeList.value = result.await()
}
}
}
來到androidApp 模組內的 MainActivity,先把範例的程式碼刪掉,然後加入ViewModel.
class MainActivity : AppCompatActivity() {
private lateinit var viewModel : MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
}
}
之後使用ViewModel進行網路請求,並且監聽LiveData
class MainActivity : AppCompatActivity() {
private lateinit var viewModel : MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
viewModel.fetchCafeData("taipei")
viewModel.cafeListLiveData.observe(this, Observer { it ->
//將資料設定到RecyclerView內
})
}
}
接下來就是Android工程師熟悉的使用RecyclerView顯示畫面
修改activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_cafeList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
加入item_cafe.xml給RecyclerView顯示
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_cafename"
android:layout_width="150dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="14sp"
tools:text="店名" />
<TextView
android:id="@+id/tv_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/tv_cafename"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="14sp"
tools:text="地址"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
顯示邏輯用的ViewHolder
class CafeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val name :TextView = itemView.findViewById(R.id.tv_cafename)
private val address :TextView = itemView.findViewById(R.id.tv_address)
fun bind(cafe: CafeResponseItem) {
this.name.text = cafe.name
this.address.text = cafe.address
}
}
用來轉換資料與顯示的adapter
class CafeAdapter : RecyclerView.Adapter<CafeViewHolder>() {
var cafeList = listOf<CafeResponseItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CafeViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_cafe, parent, false)
return CafeViewHolder(view)
}
override fun onBindViewHolder(holder: CafeViewHolder, position: Int) {
val cafe:CafeResponseItem = cafeList[position]
holder.bind(cafe)
}
override fun getItemCount() = cafeList.size
}
然後讓MainActivity 使用,並且將監聽LiveData的值傳入adapter中顯示,修改後的MainActivity如下
class MainActivity : AppCompatActivity() {
private lateinit var cafeRecyclerView : RecyclerView
private lateinit var viewModel : MainViewModel
private val adapter :CafeAdapter by lazy { CafeAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
cafeRecyclerView = findViewById(R.id.rv_cafeList)
cafeRecyclerView.adapter = adapter
cafeRecyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
cafeRecyclerView.addItemDecoration(DividerItemDecoration(this,DividerItemDecoration.VERTICAL))
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
viewModel.fetchCafeData("taipei")
viewModel.cafeListLiveData.observe(this, Observer {
adapter.cafeList = it
adapter.notifyDataSetChanged()
})
}
}
執行的結果如下
明天將會來將Ktor的網路資料顯示在iOS畫面上