文章目录
- 1:前言
- 玩Android APP 源码
- 本贴的目的
- 参考贴
- 2: kotlin下的模块化(捎带一嘴)
- 3:Retrofit+协程+viewModel
- 3.1基础网络层搭建
- `lib_home:Banner`
- `lib_common: BaseResp `
- `lib_common:RetrofitManager`
- `lib_home: HomeApi `
- 3.2基础网络层接口测试
- 3.3 基础网络层优化-koin依赖注入框架
- 简单说一下`依赖注入`
- koin的简单理解
- koin的简单使用演示
- 3.4 回到APP源码,解析 真正的homeModule
- 3.5 结合 ViewModel 与协程,再看数据层处理
- `HomeViewModel`的简单实现
- `HomeRepo`的加入
- `HomeViewModel`的最终调用
- `HomeFragment`的调用及测试
1:前言
玩Android APP 源码
学习kotlin,学习Android,最好的方式就是看别人的APP源码, 手头有一份玩安卓APP的kotlin版本,适合学习使用,文中提到的代码都在这款APP的源码里
WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)
本贴的目的
主要目的是基于WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)这份源码, 学习kotlin开发, 顺便记录学习过程中一些相关知识.
1.了解在模块化下,使用Retrofit + 协程 + viewModel, 怎么用完成网络层 , 数据的处理
2. 复盘学习过程,记录思路的变化,加深对语言的理解
3.记录以供后期翻阅(或触类旁通,毕竟好记性不如烂笔头)
参考贴
- Kotlin retrofit 协程 最简单的使用(一)
- 扒一扒Koin
- Koin简单使用
2: kotlin下的模块化(捎带一嘴)
基本和Java语言下的模块化配置差不多,主要是配置gradle,可以看这个
[Android 模块化配置实践] Java + Gradle7配置模块化实践记录
本项目的模块化截图:
从项目结构上看, lib_common模块里面,回提供基础的网络层封装, 在lib_home等业务模块中,依赖 lib_common提供的各项功能
//在lib_home中依赖 lib_common
dependencies {// 引入模块 lib_commonimplementation 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 = -1var errorMsg: String = ""var data: D? = nullvar 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.netimport 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//initinit {// 日志拦截器var loggingInterceptor = HttpLoggingInterceptor {Log.d("loggingInterceptor: ", it.toString())}.setLevel(HttpLoggingInterceptor.Level.BODY)// 配置Retrofit// 创建clientvar 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.apiimport 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中已经添加了对retrofit2
的dependencies
//oKHttpimplementation("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
才对,但实际情况是
retrofit2
在lib_common
中正常使用, 在lib_home
中无法引入, 想到之前看过implementation
和api
的区别, 发现果然是
的问题, 既然lib_common
是公共的依赖, 那可以把需要放开的依赖 改为api
即:
// lib_common
dependencies {//oKHttpimplementation("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,类似homeModuleprivate 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.koinimport 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.koinimport 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 与协程,再看数据层处理
上面提到了HomeRepo
与HomeViewModel
他们之间的关系也清楚了: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_STARTvmData.value = result.valuetry {// 执行 网络请求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_FAILEDToastUtil.showShort("认证过期,请重新登录!")// TODO: 添加路由跳转到登录页,ARouter未添加}else -> {result.value?.responseState = BaseResp.ResponseState.REQUEST_FAILEDToastUtil.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)?.urlToastUtil.showShort(url.toString())}}
至此,整个数据层的封装已经基本OK
这个套路掌握之后,我们也可以自己尝试进行更改,优化.