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画面上
我们已经有了语音转文字的技术, 那我们也能将文字进行向量化。 那我们是否能收集客服人员顾客的回答, ...
一二三元 什麽叫做三元运算子?有三元运算子那有没有一元和二元运算子? 三元运算子就是运算元有三个的运...
上一篇我们的基因体时代-AI, Data和生物资讯 Day22- 基因注释资料在Bioconduct...
系统工程是一门应用知识来创建或获取一个系统的学科,该系统由相互关联的元素组成,这些元素在整个系统开...
终於倒数胜4天了!! 接续昨天的练习 1.在light comp加入一个Solid Composit...