写在前面:搭建此项目 目的为了学习Kotlin 以及相关第三方库(用到的协程都是最基础的),公司项目一直为Java ,没有Kotlin 实战的机会,并且一直在开发h5 php 抽不开身系统的学习Kotlin 碰巧另一项目组需要开发新App,这才有了Kotlin实战的机会,搭建了一个简单的基础框架用于后续开发快速使用。 此项目前3个页面为此次项目第一版UI,最后一个页面
Profile
演示了 请求权限、请求相册、模拟Token失效、Coil的使用
- 网络 :Retrofit + Okhttp + Coroutines
- UI绑定 :ViewBinding
- 数据解析 :Moshi
- 消息通知 :LifeEventBus
- 图片加载 :coil
- 屏幕适配 : 屏幕适配终结者
- Permission : PermissionX
- ImageCrop : UCrop
- ImageChoose : LPhotoPicker
- Data storage : MMKV
- 依赖注入 : Hilt
项目中用了自定义阴影布局,如果未运行 可能xml中不显示具体效果,运行后在看即可查看效果
运行项目后 Login页面无需输入账号密码,点击登陆按钮即可跳转首页
项目使用了BlankJ 的 AdaptScreenUtils
,遂项目中 xml 开发都是使用pt为单位,且前3个Fragment UI 均为公司项目第一版UI
按照AdaptScreenUtils
的使用,在BaseActivity 中对宽度进行了配置,所以,在查看布局xml的时候 在布局 split 页面下,
勾选创建的Virtual Devices,创建Virtual Devices 尺寸参考下图
项目对应的一键生成页面插件,项目地址 TinMVVM,编译好的插件在此项目 template
文件夹下,导入AS插件即可使用
AndroidStudio Chipmunk 2021.2.1 版本 选择 'TinMVVM-1.1.0.jar'
AndroidStudio Arctic Fox | 2020.3.1 版本以及以下版本 选择 'TinMVVM-1.0.75.jar'
说几点使用事项
- 点击
app
或者 包名 右键 - New - other - 使用的时候
Generate Activity
默认为勾选状态,如果不想生成Act ,需要先去掉Generate Activity
的勾选, 在勾选Generate Fragment
,别问为啥,问就是你在这给我刷bug呢 ?我也想做联动啥的,但是插件代码拿不到值变化监听。 - 填写
Activity Package Name
或者Fragment Package Name
的时候 ,我测试过二级目录,比如ui.xxx
,结果发现不会合并,1.0.74 版本修复此问题 但是必须使用/,PS:ui/xxx
,如果你依旧使用ui.xxx
我也不拦你。 创建后 会直接在ui 文件夹下生成对应的文件。 - 生成Act时 会在项目清单文件 AndroidManifest 创建对应Act标签,但是属性有点少,需要自己在编辑添加。
- 插件是给自己使用的,如果你作为开发像用户那样在这里测试插件,那你还做什么开发啊。我也想过如何解决,但是编写插件代码没有方法支持啊。我也很无奈。 编写插件目的就是为了自己的使用,知道怎么用 何必考虑那么复杂情况呢 ?
03-12-2021 添加Hilt 官方文档请看英语版,切换页面语言为中文 会发现依赖不是最新版,和当前项目依赖不一样。 如果英语版看不懂,推荐 看一遍郭霖 的文章 带你玩转Hilt和依赖注入 学习了解一下什么是Hilt ,然后再在官网中查看一些更新的注解 具体为 :作用域 Scope 以及最新的 配合ViewModel使用
此类下进行统一网络配置,包括Retrofit、OkHttp以及OkHttp涉及到的日志打印配置、头信息等拦截器配置项
Project - build.gradle apply 'config.gradle'
app - build.gradle use 'config.gradle'
项目依赖了BlankJ 的 AndroidUtilCode,基本涵盖了所有相关Utils。
- base : 相关基类配置
- entity : 实体类
- network : 网络配置
- ui : 页面
- modelfolder : 模块名文件夹
- modelAct
- modelViewModel
- modelViewModelFactory
- modelfolder : 模块名文件夹
- utils : 自写utils
- widget : 自定义view
use CommonLiveBusEvent
(app - package - base - CommonLiveBusEvent)
Profile 中测试Token 失效,在Retrofit + OKHttp 的 TokenInterceptor 中有展示 ,MainActivity 中接收
// post
LiveEventBus.get(CommonLiveBusEvent::class.java)
.post(CommonLiveBusEvent(CommonLiveBusEvent.market_select_currency,"1"))
// observer
LiveEventBus.get(CommonLiveBusEvent::class.java)
.observe(this@WatchListFragment){
it.mEventObject as String
ToastUtils.showLong("收到的消息是${it.mEventObject}")
}
use CoilDownloadImage.kt
(app - package - base - CoilDownloadImage) - downloadImage
method
需要注意 android R(11 version_sdk = 30) 申请读写权限时 使用 MANAGE_EXTERNAL_STORAGE
,并注意,这个权限
属于特殊权限,使用PermissionX时不会显示在普通权限列表中
个人建议还是判断一下版本号
val requestList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
arrayListOf(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
} else {
arrayListOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
PermissionX.init(this)
.permissions(requestList)
.explainReasonBeforeRequest()
.onExplainRequestReason { scope, deniedList ->
val message = "Industry needs following permissions to continue"
scope.showRequestReasonDialog(deniedList, message, "I See", "No")
}
.onForwardToSettings { scope, deniedList ->
val message = "You following permissions to continue"
scope.showForwardToSettingsDialog(deniedList, message, "I See", "No")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
...
}
}
custom Engine - LCoilEngine
LPhotoHelper.Builder()
.maxChooseCount(1)
.columnsNumber(3)
.imageType(LPPImageType.ofAll())
.imageEngine(LCoilEngine())
.build()
.start(this, CHOOSE_PHOTO_REQUEST)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when (requestCode) {
CHOOSE_PHOTO_REQUEST -> {
val selectedPhotos = LPhotoHelper.getSelectedPhotos(data)
if (selectedPhotos.size == 1) {
val mFilePath = PathUtils.getInternalAppCachePath() + "/${System.currentTimeMillis()}.jpg"
val createOrExistsFile =
FileUtils.createOrExistsFile(mFilePath)
if (createOrExistsFile) {
val file2Uri =
Uri.fromFile(FileUtils.getFileByPath(mFilePath))
UCrop.of(selectedPhotos[0], file2Uri)
.withAspectRatio(1f, 1f)
.withMaxResultSize(800, 800)
.start(this)
}
}
}
UCrop.REQUEST_CROP -> {
data?.let { it ->
val output = UCrop.getOutput(it)
// 下载图片 并保存到相册中
downloadImage(output, onSuccess = {
ImageUtils.save2Album(it, Bitmap.CompressFormat.JPEG)
})
}
}
}
} else if (resultCode == UCrop.RESULT_ERROR) {
data?.let {
val output: Throwable? = UCrop.getError(it)
LogUtils.eTag("JiaYang", output.toString())
}
}
}