您现在的位置是:首页 >学无止境 >[Kotlin] 玩Android代码学习之-模块化+Retrofit+协程+viewModel的数据层封装网站首页学无止境

[Kotlin] 玩Android代码学习之-模块化+Retrofit+协程+viewModel的数据层封装

iOSTianNan 2024-08-24 12:01:03
简介[Kotlin] 玩Android代码学习之-模块化+Retrofit+协程+viewModel的数据层封装

1:前言

玩Android APP 源码

学习kotlin,学习Android,最好的方式就是看别人的APP源码, 手头有一份玩安卓APP的kotlin版本,适合学习使用,文中提到的代码都在这款APP的源码里

WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)

本贴的目的

主要目的是基于WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)这份源码, 学习kotlin开发, 顺便记录学习过程中一些相关知识.

1.了解在模块化下,使用Retrofit + 协程 + viewModel, 怎么用完成网络层 , 数据的处理
2. 复盘学习过程,记录思路的变化,加深对语言的理解
3.记录以供后期翻阅(或触类旁通,毕竟好记性不如烂笔头)

参考贴

2: kotlin下的模块化(捎带一嘴)

基本和Java语言下的模块化配置差不多,主要是配置gradle,可以看这个
[Android 模块化配置实践] Java + Gradle7配置模块化实践记录

本项目的模块化截图:
在这里插入图片描述

从项目结构上看, lib_common模块里面,回提供基础的网络层封装, 在lib_home等业务模块中,依赖 lib_common提供的各项功能

//在lib_home中依赖 lib_common
dependencies {
    // 引入模块 lib_common
    implementation project(path: ':lib_common')

}

3:Retrofit+协程+viewModel

乍一看APP源码, 确实有点晕的,所以我们拆分一下,先看我们熟悉的Retrofit部分

其实看完了Retrofit部分,我才发现,Retrofit是结合了协程一起的,所以第一步先尝试使用Retrofit+协程 ,完成简单的网络调用,再去结合viewModel会更加容易,也更符合开发顺序

至于为什么要结合协程?

// 不使用协程,返回值是Call类型,或者比如Java结合RxJava返回FLow
interface IApiServices {
    @GET("getHealthCare")
      fun getAllHealthData(@Query("userId") userId: String):Call<AllHealthBean>
}

// 使用协程,返回值直接就是你定义的数据类型或者Bean,直接就是可以使用
interface IApiServices {
    @GET("getHealthCare")
  suspend  fun getAllHealthData(@Query("userId") userId: String):AllHealthBean
}

总之,Retrofit结合协程, 获取请求结果,使用起来更加方便, 这就有点类似 JS的 async/await

3.1基础网络层搭建

(http请求及数据展示)

先看看APP的接口
https://www.wanandroid.com/banner/json

{"data":[{"desc":"我们支持订阅啦~","id":30,"imagePath":"https://www.wanandroid.com/blogimgs/42da12d8-de56-4439-b40c-eab66c227a4b.png","isVisible":1,"order":2,"title":"我们支持订阅啦~","type":0,"url":"https://www.wanandroid.com/blog/show/3352"},{"desc":"","id":6,"imagePath":"https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png","isVisible":1,"order":1,"title":"我们新增了一个常用导航Tab~","type":1,"url":"https://www.wanandroid.com/navi"},{"desc":"一起来做个App吧","id":10,"imagePath":"https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png","isVisible":1,"order":1,"title":"一起来做个App吧","type":1,"url":"https://www.wanandroid.com/blog/show/2"}],"errorCode":0,"errorMsg":""}

lib_home:Banner

(以上获取banner数据的bean类)

package com.example.lib_home.bean

/**
 * @author: tiannan
 * @time: 2023/6/2.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */
data class Banner(
    val desc: String,
    val id: Int,
    val imagePath: String,
    val isVisible: Int,
    val order: Int,
    val title: String,
    val type: Int,
    val url: String
)

lib_common: BaseResp

( common模块下的Http请求的基本返回类)

http请求 也就是 code, msg,data, 前两个没什么好说的, 都是基本型数据, 唯独data可能是数组,可能是集合,类型不固定
只能在具体的请求数据里,定义出了相对应的Bean,才能说给出一个类型,所以这里的data,理所当然的用到泛型
APP源码中用了 T 作为data属性的泛型类型

class BaseResp<D> {
    var errorCode: Int = -1
    var errorMsg: String = ""
    var data: D? = null
    var responseState: ResponseState? = null //请求状态
    enum class ResponseState {
        REQUEST_START,
        REQUEST_SUCCESS,
        REQUEST_FAILED,
        REQUEST_ERROR
    }
}
// PS : 我自己的练习Demo中, 我把T改成了D,好方便我自己理解, D = data

lib_common:RetrofitManager

common模块下的Retrofit管理类(单例模式)
这个没太多可以说的, Retrofit的使用不是很复杂

package com.example.lib_common.net

import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

/**
 * @author: tiannan
 * @time: 2023/6/1.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */


/**
 * 用object 关键字,单例模式
 */
object RetrofitManager {

    const val BASE_URL = "https://www.wanandroid.com/"

    private lateinit var retrofit: Retrofit

    //init
    init {
        // 日志拦截器
        var loggingInterceptor = HttpLoggingInterceptor {
            Log.d("loggingInterceptor: ", it.toString())
        }.setLevel(HttpLoggingInterceptor.Level.BODY)

        // 配置Retrofit
        // 创建client
        var client: OkHttpClient = OkHttpClient().newBuilder()
            .callTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .followRedirects(false)
            //此处暂时不做cookie支持,后续在添加
            //.cookieJar()
            .addInterceptor(loggingInterceptor)
            .build()

        // 创建 retrofit 实例
        retrofit = Retrofit.Builder()
            .client(client)
            .baseUrl(BASE_URL)
            //添加Gson解析支持
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    }

    /**
     * Retrofit 结合 API泛型 ,创建接口实例并返回
     *
     * 入参: API泛型,代指各模块的 retrofit 接口API ,如HomeApi,MyApi等接口
     * 返回值: API泛型的接口实例,
     *
     * 备注: 由于项目中使用了koin 依赖注入
     * 所以,可以直接把 create 通过koin的module挂载,
     * 然后用到的时候, 直接通过参数注入到其他类中..
     */
    fun <API> create(api: Class<API>): API {
        return retrofit.create(api)
    }

}

lib_home: HomeApi

回到home模块,编写retrofit的Api接口

就拿Home页面的获取Banner接口来举例,我们定义 一个名为HomeApi的interface接口, 使用retrofit风格去编写
这里,我们给fun 添加 suspend, 表示这是挂起函数

package com.example.lib_home.api

import com.example.lib_common.net.BaseResp
import com.example.lib_home.bean.Banner
import retrofit2.http.GET

/**
 * @author: tiannan
 * @time: 2023/6/2.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */
interface HomeApi {

    //首页banner

    @GET("banner/json")
    suspend fun getBanner():BaseResp<List<Banner>>

}

这里插一个经验总结:
在HomeApi的代码内, 想引入 import retrofit2.http.GET,但是失败
按道理说,在lib_common中已经添加了对 retrofit2dependencies

   //oKHttp
    implementation("com.squareup.okhttp3:okhttp:4.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

lib_home模块中,应该可以直接导入 import retrofit2.http.GET才对,但实际情况是
retrofit2lib_common中正常使用, 在lib_home中无法引入, 想到之前看过 implementationapi的区别, 发现果然是 在这里插入图片描述
的问题, 既然lib_common是公共的依赖, 那可以把需要放开的依赖 改为 api
即:

// lib_common
dependencies {
    //oKHttp
    implementation("com.squareup.okhttp3:okhttp:4.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
    //retrofit
    // 此处由于 retrofit2 需要提供给别的模块使用, 故用 api (引用传递)
    // implementation 则代表,只在当前模块下可以, 对外部不可见
    // 所以,如果要开放某个库, 需要改为api (目测这要是 api 关键字为啥叫api的原因)
    api("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

}

简单来说:implementation指令,在A模块中生效, 在引用了A模块的B模块中,B无法访问implementation引入的代码,即 依赖不会传递个B模块, 改为api即可

3.2基础网络层接口测试

截止到目前, 整理一下我们已经实现的

  • lib_home : Banner
  • lib_home: HomeApi
  • lib_common: BaseResp
  • lib_common: RetrofitManager

已经满足我们实现接口请求,我们在lib_test中,进行一下测试
lib_test模块中引入 lib_common和lib_home

// lib_test中
dependencies {
   implementation project(":lib_common")
   implementation project(":lib_home")
}

添加测试代码

//lib_test
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var job: Job = GlobalScope.launch {
            var ress: BaseResp<List<Banner>> =
                RetrofitManager.create(HomeApi::class.java).getBanner()
            Log.d("TestActivity:getBanner", ress.errorMsg + "")
            Log.d("TestActivity:getBanner", ress.errorCode.toString())
            Log.d("TestActivity:getBanner", ress.data.toString())
            Log.d("TestActivity:getBanner", ress.data?.get(0)?.imagePath + "")
        }
    }
}

记得要去项目更目录中gradle.properties中把use_lib_test配置模式修改一下
在这里插入图片描述
(这里我对每个模块做了单独开关,方便灵活切换测试,以前使用一个useLib的tag, 不太灵活)
然后编译运行 lib_test,记得在清单文件中添加网络权限

在这里插入图片描述

测试结果:
在这里插入图片描述
不过,不推荐在lib_test中以com.android.application模式进行测试

1.要给lib_test配置清单文件,创建MainActivity
2.要注意在清单文件中,处理网络层权限等各种配置

所以如果是简单数据测试,用lib_test模块可以的, 要是UI测试的, 还是放app工程
(我这么干是因为跑起来有个lib_test的APP入口好看…好装逼…)

3.3 基础网络层优化-koin依赖注入框架

基础的网络请求有了,按理说数据已经可以获取了,但是既然要学习项目,那肯定要继续深入优化

简单说一下依赖注入

  • 无注入的常规方法
fun abc(){
 val abc = ABC()
 // use abc can do somethind
}
  • 有注入的方法
fun abc(private val abc:ABC){
	// use abc can do somethind
}

总之,在没有依赖的情况下,调用abc函数, 需要调用者自己主动传入 abc对象
那就意味着你必须:

  • 要持有abc对象(强耦合)
  • 要自己提供abc对象或者传递abc
  • 一旦abc需要修改,要修改多处调用

相对的,如果使用依赖注入

class AAA(private val abc:ABC){
	fun abc(){
	this.abc.toString()
	// use abc can do somethind
	}
}

在AAA类的申明中, abc对象是通过注入的方式提供

那有人会问了, 初始化AAA的过程中,不还是要传入abc对象么,这有啥区别?

依赖注入,依赖注入, 既然已经有了注入的概念, 那肯定得有依赖
别的依赖注入框架我还没有学习到, 这里拿koin这个来先回答上面的疑问, 在koin的koinApplication中,会保存有abc实例对象, 所以只要 用koin的方式获取AAA类对象实例, 那么koin会自动把abc对象注入到AAA的实例中去,从而实现了依赖

举列代码就是:

// koin方式获取AAA类的实例 aaa, aaa实例也不需要用常规初始化方法创建,用下面方法即可
val aaa:AAA by inject()

fun test(){
	aaa.abc()
}

如上述代码所示, 可以看到,

  • AAA类的构造函数所需的入参 abc,并不需要开发者手动创建,而是通过koin提供,或者说通过依赖获得
  • 借助koin : AAA对象实例aaa, 也不需要开发者手动实例化,直接通过inject()注入

koin的 无代理、无代码生成、无反射特点, 应该能体会到一些了
(依赖注入框架有很多,但是初学者对koin的上手程度相对友好)

koin的简单理解

Koin是一个依赖注入的框架。其接口可以使用DSL的形式呈现
koin的核心部分

  • KoinApplication - 提供一个容器,用于容纳实例化的对象,便于全局使用
  • Module -是类似配置文件,用于描述注入的内容对象
    koind的通常用法:
    startKoin{}(在 Application或自定义Application类中创建)创建KoinApplication,并且将其挂载到GlobalScope中,便于通过协程使用

koin的简单使用演示

  • startKoin - koinApplication的初始化
//app 工程的application类中初始化 koin, 
// 这里仅先关注 homeModule, 其他的module,类似homeModule
    private fun initKoin() {
        startKoin {
            androidLogger(level = Level.NONE)
            androidContext(this@MyApplication)
            modules(homeModule, projectModule, playgroundModule, myModule, userModule)
        }
    }
  • Module的创建
    homeModule,myModule这里都是对应lib_home lib_my的module
    homeModule举例子
//lib_home
val homeModule = module {

    // 获取ABC的实例, 单例模式,将被koinApplicaion挂载
    // single单例关键字,提供唯一的 ABC实例对象,如 `abc`
    single { ABC() }


    // 注入测试类-测试
    // factory工厂关键字, 每次都创建一个 新的HomeKoinTest实例
    factory {
        //这里, HomeKoinTest类的构造函数 ,需要入参 `abc`实例对象.
        // 我们只需要传入 get(), 就可以了, 这就是依赖注入里面的依赖二字的含义吧
        // koin会帮我们把 上面 ABC()的单例对象通过 get()依赖,注入给HomeKoinTest类
        HomeKoinTest(get())
    }
}
  • ABC类
    ABC类就一个val属性 name, 用于演示
package com.example.lib_home.koin

import android.util.Log

/**
 * @author: tiannan
 * @time: 2023/6/5.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */


/**
 * ABC类,
 * 用于演示 koin
 *
 */
 // lib_home
class ABC {
    val name: String = "abc"

    init {
        Log.d("object_abc", name)
    }
}
  • HomeKoinTest
    (这里我把上面的AAA类,换成了KHomeKoinTest类,应该不影响理解)
package com.example.lib_home.koin

import android.util.Log
import com.example.lib_common.util.ToastUtil

/**
 * HomeKoinTest -lib_home模块下,对koin的测试类
 * 依赖注入了 ABC的实例对象 abc
 */
class HomeKoinTest(private val abc: ABC) {
    fun hi() {
        Log.d("home_hi", "home_hi: " + this.abc.name)
        ToastUtil.showShort("home_hi: " + this.abc.name)
    }
}
  • HomeKoinTest类的引入及测试调用
    // 在MainActivity注入 homeKoinTest, 测试效果
    val homeKoinTest: HomeKoinTest by inject<HomeKoinTest>()
    fun load() {
        homeKoinTest.hi()
    }
  • 测试结果
    在这里插入图片描述

3.4 回到APP源码,解析 真正的homeModule

一路顺下来, 我们也自己定义了homeModule,并且能够简单使用了

从现在开始,可以去 WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)

中的 lib_home/di/HomeModule.kt中看看了

val homeModule = module {
    single { RetrofitManager.getService(HomeApi::class.java) }
    single { HomeRepo(get()) }
    viewModel { HomeViewModel(get()) }
}

经过上文的讲解, 再看源码的 homeModule应该非常好理解

RetrofitManager我们已经在 3.1基础网络层搭建中实现过了
所以
第一行代码

single { RetrofitManager.getService(HomeApi::class.java) }

就是向koinApplication中挂载了 RetrofitManager的实例

那么,谁会向koin依赖它(RetrofitManager)呢?

很显然看谁get()

查看 第二行代码发现

single { HomeRepo(get()) }

发现 HomeRepo类的构造函数,注入了 api: HomeApi,即泛型 API

class HomeRepo(private val api: HomeApi) : BaseRepository() {}

(RetrofitManager的build返回值, 请自行查看3.1中的基础网络层RetrofitManager类)

至于第三行代码
HomeViewModel也被注入依赖(get())

   viewModel { HomeViewModel(get()) }

我们直接查看 HomeViewModel发现

class HomeViewModel(private val repo: HomeRepo) : BaseViewModel() {}

原来HomeViewModel中被注入的依赖是 repo:HomeRepo

再去查看HomeViewModel是怎么用的

    private val homeViewModel: HomeViewModel by viewModel()
    private fun getHomeData() {
        homeViewModel.getBanner()
        homeViewModel.getArticle(0)
    }

很显然

  • HomeAPI是获取网络数据的接口,它负责从服务器获取数据并返回
  • HomeRepo是数据仓库的意思, 它依赖注入了HomeApi,主要负责数据的装载
  • HomeViewModel是VM层,它依赖注入了HomeRepo,主要负责处理业务逻辑,从HomeRepo数据仓库要数据并结合liveData,做数据的绑定等工作

至此,整个Retrofit+协程+koin依赖注入+viewModel的核心逻辑已经梳理完成了
(个人觉得理顺这一套之后,整个APP的业务逻辑层面应该不是阻碍了, lib_home,lib_my等模块都是这样设计的)

3.5 结合 ViewModel 与协程,再看数据层处理

上面提到了HomeRepoHomeViewModel
他们之间的关系也清楚了:HomeApi->HomeRepo->HomeModel
但是不可否认的是,源码中已经封装的很好了,但是对于思路的推导, 还得一步步来

HomeViewModel的简单实现

抛开HomeRepo不谈, 直接让HomeViewModel依赖注入HomeApi,我们可以这样写

class HomeViewModel(private val api: HomeApi) : ViewModel() {

    var bannerList = MutableLiveData<List<Banner>>()
    
    fun getBanner() {
        viewModelScope.launch {
            var res: BaseResp<List<Banner>> = api.getBanner()
            // 对bannerList 赋值
            bannerList.value = res.data
        }
    }
}

HomeFragment里面, 直接就可以进行数据的获取了, 基本上如果要求不高, 整个数据层面的封装就可以到此为止了.

    private val homeViewModel: HomeViewModel by inject<HomeViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        homeViewModel.getBanner()
        homeViewModel.bannerList.observe(this) {
            var banner = it[0]
            ToastUtil.showShort(banner.url)
        }

    }

HomeRepo的加入

为了优化viewmodel对数据的获取,源码中添加了Repo类,来隔离viewmodel与Retrofit,
同时也为了Repo的模块化, 又在lib_common中增加了BaseRepository基类

// 类型别名 netBlock<D>, 为了写代码简约点
typealias netBlock<D> = suspend () -> BaseResp<D>

class BaseRepo {

    /**
     * 数据仓库基类 - load函数
     * 入参:netBlock<D> , 实际为 suspend () -> BaseResp<D> 类型的函数入参 (返回BaseResp<D>数据的挂载函数)
     * 入参:vmData: 类型为  MutableLiveData<D> 的 viewModel 数据, 可以理解为就是用于给VM赋值的
     */
    suspend fun <D> load(block: netBlock<D>, vmData: MutableLiveData<BaseResp<D>>) {

        var result = MutableLiveData<BaseResp<D>>()
        result.value?.responseState = BaseResp.ResponseState.REQUEST_START
        vmData.value = result.value
        try {

            // 执行 网络请求
            result.value = block.invoke()

            // 网络请求状态处理
            when (result.value?.errorCode) {
                Constants.HTTP_SUCCESS -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_SUCCESS
                }

                Constants.HTTP_AUTH_INVALID -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_FAILED
                    ToastUtil.showShort("认证过期,请重新登录!")
                    // TODO: 添加路由跳转到登录页,ARouter未添加
                }

                else -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_FAILED
                    ToastUtil.showShort("code:" + result.value?.errorCode.toString() + " / msg:" + result.value?.errorMsg)
                }
            }

        } catch (e: Exception) {
            when (e) {
                is UnknownHostException,
                is HttpException,
                is ConnectException
                -> {
                    ToastUtil.showShort("网络错误!")
                }
                else -> {
                    ToastUtil.showShort("未知异常!")
                }
            }
            result.value?.responseState = BaseResp.ResponseState.REQUEST_ERROR

        } finally {
            vmData.value = result.value
        }

    }

}

简单来说, BaseRepo就做了两件事


            // 执行 网络请求
            result.value = block.invoke()
            

 finally {
            // 给 viewModel的属性赋值
            vmData.value = result.value
        }

此时,在lib_home模块中, 可以添加 HomeRepo
它依赖注入的 自然是 HomeApi

//lib_home
class HomeRepo(private val api: HomeApi) : BaseRepo() {

    suspend fun getBanner(vmData: MutableLiveData<BaseResp<List<Banner>>>) {
        load({ api.getBanner() }, vmData)
    }

}

HomeViewModel的最终调用

class HomeViewModel(private val repo: HomeRepo) : BaseViewModel() {

    var bannerList = MutableLiveData<BaseResp<List<Banner>>>()

    fun getBanner() {
//        viewModelScope.launch {
//            repo.getBanner(bannerList)
//        }
        
        // or - 通过BaseViewModel 基类 抽取    viewModelScope.launch {}
        launch { repo.getBanner(bannerList) }
    }
}

HomeFragment的调用及测试

(记得要做数据判空…)

    private val homeViewModel: HomeViewModel by inject<HomeViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        homeViewModel.getBanner()
        homeViewModel.bannerList.observe(this) {
            val url = it?.data?.get(0)?.url
            ToastUtil.showShort(url.toString())
        }
    }

在这里插入图片描述
至此,整个数据层的封装已经基本OK
这个套路掌握之后,我们也可以自己尝试进行更改,优化.

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。