文章目录
- 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_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中已经添加了对retrofit2
的dependencies
//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
才对,但实际情况是
retrofit2
在lib_common
中正常使用, 在lib_home
中无法引入, 想到之前看过implementation
和api
的区别, 发现果然是
的问题, 既然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 与协程,再看数据层处理
上面提到了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_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
这个套路掌握之后,我们也可以自己尝试进行更改,优化.