Day 11: 回到原生环境!在Android上展示Ktor资料!

Keyword: Android ViewModel,Coroutine,LiveData,RecyclerView
到Day11使用Ktor进行网路请求并且显示在Android画面的Code放在
KMMDay11


有了shared内的资料,我们就要真正的来使用这些资料了

建立DataRepository

先在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的机率就更低.

使用Coroutine与ViewModel

在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"/>

在ViewModel中进行网路请求

在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()
        }
    }
}

使用RecyclerView显示资料

来到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()
        })
    }
}

执行的结果如下

https://github.com/officeyuli/itHome2021/raw/main/day11/recyclerView.jpg

明天将会来将Ktor的网路资料显示在iOS画面上


<<:  Day00 - 开始之前

>>:  Day 1 前言 - 我是谁、我在哪里、我要做什麽

AI ninja project [day 16] 文字处理 -- 回归

我们已经有了语音转文字的技术, 那我们也能将文字进行向量化。 那我们是否能收集客服人员顾客的回答, ...

30天学会C语言: Day 20-元元元运算子

一二三元 什麽叫做三元运算子?有三元运算子那有没有一元和二元运算子? 三元运算子就是运算元有三个的运...

我们的基因体时代-AI, Data和生物资讯 Day23- 基因注释资料在Bioconductor中视觉化之呈现:Gviz

上一篇我们的基因体时代-AI, Data和生物资讯 Day22- 基因注释资料在Bioconduct...

安全工程101

系统工程是一门应用知识来创建或获取一个系统的学科,该系统由相互关联的元素组成,这些元素在整个系统开...

AE-Lightning 雷电云特效4-Day26

终於倒数胜4天了!! 接续昨天的练习 1.在light comp加入一个Solid Composit...