/ 今日科技快讯 /
据外媒报道,欧盟委员会周三宣布,已对美国芯片巨头英伟达收购英国芯片设计公司Arm的交易展开正式反竞争调查。英伟达于2020年9月宣布斥资400亿美元从日本软银集团手中收购Arm,并预计可能在18个月内完成交易。
/ 作者简介 /
大家周五早上好,顺祝大家周末愉快!
本篇文章来自DylanCai的投稿,文章介绍了使用Kotlin封装一些工具类,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
DylanCai的博客地址:
https://juejin.cn/user/4195392100243000/posts
/ 前言 /
在 2019 年 Google I/O 大会上,Google 宣布今后将优先采用 Kotlin 进行 Android 开发,并且也坚守了这一承诺。使用 Kotlin 进行 Android 开发代码更少,可读性更强,并且能和 Java 代码兼容。
我之前学习了一些 Kotlin 的语法糖之后,很想运用到自己整理的工具类上。当时公司只有我学习 Kotlin,所以项目是 Kotlin 和 Java 混编的。在写工具类时突然想到一个问题,我用 Kotlin 写的工具类,调用的结果和原来 Java 工具类得到的结果不一致怎么办。
比如正则,我用 Kotlin 写的和别人用 Java 写的匹配出来不一样,那我不是要兴师问罪。要么就特意保证实现逻辑和 Java 工具类一致,这么做的话为什么不直接调用 Java 工具类呢。所以当时就给公司项目在用的 Java 工具类库 AndroidUtilCode(https://github.com/Blankj/AndroidUtilCode)封装扩展库。
由于绝大部分功能都实现好了,主要做的事是补充没有的功能和设计一套好用的 Kotlin API。设计 API 看似很简单,实际做起来很难。因为 Kotlin 的玩法实在太多了,并且不是用了语法糖就一定会好用,用法骚会带来一定的学习成本,代码可读性可能会更差。个人比较强迫症,在这方面思考了很多,有一些封装经验可以分享给大家。
后来的公司新项目基本是 Kotlin 进行开发,可以不用考虑对 Java 代码的兼容,就着手开始写一个纯 Kotlin 开发、尽可能轻量的 Kotlin 工具类库。得益于之前的很多思考,目前实现的还是比较满意的。
接下来给大家分享个人一些封装 Kotlin 工具类的经验和一个好用的 Kotlin 工具类库。
/ 封装思路 /
我看过很多人写的 Kotlin 工具类只是单纯地把原有的 Java 工具类翻译成 Kotlin 语言,这就像当初推出 C++ 后,有些人还是用面向过程的思想写代码。并不是不能用,但是能做得更好用。所以下面介绍的是一些在 Java 不常见的语法糖和一些使用建议,帮助大家更好地在工具类使用这些特性。
这是 Kotlin 和 Java 一个比较大的差异,Java 的属性和方法都需要写在类里的,而 Kotlin 有 top-level property 顶级属性和 top-level function 顶级函数,可以把方法和属性写在类外面。top-level 顾名思义是最高级别的,可以理解为是全局的,在别的类里是能直接调用到顶级属性或顶级方法。
有什么用呢?比如获取 Application 对象,Java 工具类是调用 AppUtils.getApplication() 来获取,而 Kotlin 工具类可以直接获取 application 属性,能在任何的地方随时获取一个 application 属性是非常爽的事情。
我们能直接获取一个 application 属性的话,何必调用 AppUtils.getApplication() 呢。绝大多数情况用 Kotlin 写一个 XXXUtils 去调用静态方法都是多此一举,明明写成顶级属性或顶级方法会更好用。
这虽然是一个很简单的特性,但是也有地方要注意一下,就是命名要把功能描述清楚,个人认为很重要。比如之前写了好一个沉浸式状态栏的功能,用法如下:
StatusBarUtils.immerse(this)
把方法移到类的外面就能变成顶级方法:
immerse(this)
有些人可能这么改完就算了,但这是全局方法,别人调用一个全局的沉浸方法会很疑惑这是要沉浸什么东西。之前能用“沉浸”的单词作为方法名是因为工具类名也具有信息,可以结合工具类的名称推导出是要沉浸状态栏。所以最好改成:
immerseStatusBar(this)
顶级方法或顶级属性的命名要把功能描述清楚,因为这是能全局调用的,不要只是单纯地把原有 Java 工具类的类名给去掉。
扩展
Kotlin 可以很方便的扩展一个已经存在的类,为它添加额外的方法或属性,无需继承类或者使用装饰者模式。
我们可以进一步优化上面沉浸状态栏的用法,把方法改成 Activity 的扩展方法。在方法前面增加一个接收者:
fun Activity.immerseStatusBar() {
...
}
在方法内能用 this 获取到 Activity 对象,所以原本的 Activity 参数就可以去掉了。这样就可以在 Activity 调用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
immerseStatusBar()
}
}
Java 想实现这个用法,需要在 Activity 基类里写一个 immerseStatusBar() 方法,而 Kotlin 能直接用扩展实现,无需写基类。
其实这也是个很简单的特性,不过个人也有些注意事项给到大家:
给 Any 或者常见的基础数据类型进行扩展要慎重。
用法尽量符合原来的使用习惯或直觉,不要差异过大。
什么意思呢?通过扩展能玩出一些骚操作,但并不是什么用法都合适。比如我刚开始接触扩展时,什么功能都想用扩展来封装,看到打印日志要传两个参数挺麻烦的,就用扩展函数来减少一个参数,给 String 增加一个打印的扩展方法:
"Downloaded progress is $progress".logd("download")
用法确实很骚,但是用了一段时间后觉得并不好用。用法与原来的打印日志用法差异过大,写得很别扭。要读到末尾才知道是打印日志,代码阅读性变差了,String 比较长的话可能没反应过来这行是用来打印日志的。而且在调用字符串的方法时会弹出一个很让人疑惑的代码提示。
还有看过别人给 Int 增加一个扩展属性 drawableRes 获取 Drawable,也是有类似的问题。
val drawable = R.drawable.ic_back_icon_black.drawableRes
这两个例子在功能上都是没问题的,但是用法差异太大会降低代码阅读性,需要不少时间来适应。还给常见的类型增加了奇怪的方法联想,个人是不提倡的。用法骚并不代表着好用,不要为了用语法糖而用语法糖。
当然也有提倡的骚用法,比如给 Int 增加 dp 属性,将 dp 转为 px,用法如下:
paint.strokeWidth = 1.dp
虽然用法也是很大差异,但是符合直觉。我们读这行代码能很容易想到是给属性设置了 1 dp 的长度,代码可读性反而更好了,这种用法是提倡的。
高阶函数
高阶函数是将函数用作参数或返回值的函数。多数人用高阶函数是用于事件回调,其实高阶函数还能很方便地实现 DSL 用法。比如 Anko Layout 的 DSL:
verticalLayout {
editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
这样的 DSL 用法比链式调用舒服一些,而且能分层级,这是链式调用不好实现的。
那要怎么运用呢?其实有可选的配置都是可以考虑使用的,最常见的是建造者模式,比如我们很熟悉的 Glide:
Glide.with(context)
.load(url)
.placeholder(placeholder)
.fitCenter()
.into(imageView)
我们稍微来封装一下:
fun ImageView.load(url: String?, block: RequestBuilder.() -> Unit) =
Glide.with(context).load(url).apply(block).into(this)
就这么简单地封装就可以把链式调用转为 DSL 用法。
imageView.load(url) {
placeholder(placeholder)
fitCenter()
}
这样用法就和 Coil 一样了,不过还有些黄色警告需要处理,所以个人建议直接用 Coil。DSL 用法比链式调用更简洁舒服一点,还能实现多级嵌套。
属性委托
属性委托是通过 by 关键字将属性的 get、set 方法委托给 by 后面的表达式。比如:
private val viewModel: LoginViewModel by viewModels()
这是官方的 ViewModel 委托用法,获取 viewModel 属性时会通过 ViewModelProvider 去获得 ViewModel 实例。使用委托后我们不用管如何获得 ViewModel 了,可以专注于写逻辑代码。
有的人可能学习过属性委托,但是不知道怎么运用。其实我们在通过某种方式获取或设置属性时,就可以考虑一下属性委托合不合适。比如通过 intent 获取传递的值:
private var id: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val id = intent.getStringExtra("id")
}
这里可以通过属性委托来简化代码:
private val id: String? by intentExtras("id")
属性委托能让我们不用管如何获得和设置属性,代码更加简洁,是个不错的语法糖,可以多思考一下是否适合用属性委托。
/ 其他经验 /
比如显示隐藏需要调用 view.visibily = View.GONE 略显繁琐的,所以有些人会封装扩展函数 view.visible()、view.invisible()、view.gone() 快速实现显示隐藏。
用起来确实比之前方便了一些,但是还有优化空间,显示隐藏经常是有个判断操作的,比如:
if (isShowed) {
view.visible()
} else {
view.gone()
}
每次都要这么判断稍显麻烦,所以更优地封装方式是增加一个 view.isVisible 的 Boolean 值的扩展属性,这样就能优化成下面的用法:
view.isVisible = isShowed
这个扩展属性不仅能用于修改显示隐藏状态,还能判断当前是否在布局上显示,用起来更加方便。
不过在封装完调用该扩展属性时,你会发现有重名的属性需要选择用哪一个,仔细一看原来官方的 core-ktx 库已经实现这个扩展属性,我们没必要再重复造轮子。
所以封装工具类之前最好先了解一下 Android KTX 库和 Kotlin 的标准库有没实现相同的功能,我们封装的工具类的定位应该是对没有的功能进行补充。重复造轮子没有意义,而且造出来的轮子可能还不如官方的。
上面说了我们应该是补充官方库没有的功能,那么设计用法时也建议参考一下官方库的命名和用法。
比如带参数的创建操作,官方通常会用 listOf()、mapOf() 等 xxxOf() 的命名,建议与官方统一,不建议用 createXXX() 或者 newXXX() 等命名。
还有监听事件的方法命名有些人喜欢命名为 onXXX,比如:
btnLogin.onClick {
// ...
}
这样直接用介词开头很奇怪,一般方法名是动词开头。所以个人建议参考官方的命名 doOnXXX,例如:
view.doOnAttach {
// ...
}
与官方库的命名规则进行统一的好处是不容易产生歧义,而且别人可能会根据以往的使用习惯,去猜想你的工具类会不会有某个功能。比如想看下有没有某个监听事件,可能会先敲个 do 看下有没对应功能方法的联想。所以个人建议不要增加太多个人的命名规则,多参考学习一下官方库的命名和用法。
/ 最终方案 /
上述的经验主要是分享给一些自己有在写 Kotlin 工具类的小伙伴,而更多的人是不太会写的,所以这里分享一个我个人打磨了很久的 Kotlin 工具类库:
Longan(https://github.com/DylanCaiCoding/Longan)
为什么叫 Longan ?个人想用个水果名来作为库名,最初想到的是 Guava (石榴),感觉非常合适,但是发现有一个谷歌的同名库,所以换了个也是多子的水果 Longan (龙眼)。
添加依赖:
allprojects {
repositories {
// ...
maven { url 'https://www.jitpack.io' }
}
}
dependencies {
implementation 'com.github.DylanCaiCoding.Longan:longan:1.0.0'
// 可选
implementation 'com.github.DylanCaiCoding.Longan:longan-design:1.0.0'
}
保留和改进了一些 Anko 好用的用法,例如:
startActivity("id" to 5)
logDebug(5)
toast("Hi there!")
snackbar(R.string.message)
alert("Hi, I'm Roy", "Have you tried turning it off and on again?")
还有很多开发常用的功能,比如下面的一些用法:
在需要 Context 或 Activity 的时候,可直接获取 application 或 topActivity 属性。
用较少的代码实现 TabLayout + ViewPager2 的自定义样式的底部导航栏:
private val titleList = listOf(R.string.home, R.string.shop, R.string.mine)
private val iconList = listOf(
R.drawable.bottom_tab_home_selector
R.drawable.bottom_tab_shop_selector,
R.drawable.bottom_tab_mine_selector
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
viewPager2.adapter = FragmentStateAdapter(HomeFragment(), ShopFragment(), MineFragment())
tabLayout.setupWithViewPager2(viewPager2, enableScroll = false) { tab, position ->
tab.setCustomView(R.layout.layout_bottom_tab) {
findViewById(R.id.tv_title).setText(titleList[position])
findViewById(R.id.iv_icon).apply {
setImageResource(iconList[position])
contentDescription = getString(titleList[position])
}
}
}
}
创建带参数的 Fragment,在 Fragment 内通过属性委托获取参数:
class SomeFragment : Fragment() {
private val viewModel: SomeViewModel by viewModels()
private val id: String by safeArguments(KEY_ID)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//...
viewModel.loadData(id)
}
companion object {
fun newInstance(id: String) = SomeFragment().withArguments(KEY_ID to id)
}
}
val fragment = SomeFragment.newInstance(id)
一行代码实现双击返回键退出 App 或者点击返回键不退出 App 回到桌面:
pressBackTwiceToExitApp("再次点击退出应用")
// pressBackToNotExitApp()
实现沉浸式状态栏,并且给标题栏的顶边距增加状态栏高度,可以适配刘海水滴屏:
immerseStatusBar()
toolbar.addStatusBarHeightToMarginTop()
// toolbar.addStatusBarHeightToPaddingTop()
快速实现获取验证码的倒计时:
btnSendCode.startCountDown(this,
onTick = {
text = "${it}秒"
},
onFinish = {
text = "获取验证码"
})
设置按钮在输入框有内容时才能点击:
btnLogin.enableWhenOtherTextNotEmpty(edtAccount, edtPwd)
点击事件可以设置的点击间隔,防止一段时间内重复点击:
btnLogin.doOnClick(clickIntervals = 500) {
// ...
}
简化自定义控件获取自定义属性:
withStyledAttrs(attrs, R.styleable.CustomView) {
textSize = getDimension(R.styleable.CustomView_textSize, 12.sp)
textColor = getColor(R.styleable.CustomView_textColor, getCompatColor(R.color.text_normal))
icon = getDrawable(R.styleable.CustomView_icon) ?: getCompatDrawable(R.drawable.default_icon)
iconSize = getDimension(R.styleable.CustomView_iconSize, 30.dp)
}
自定义控件绘制居中或者垂直居中的文字:
canvas.drawCenterText(text, centerX, centerY, paint)
canvas.drawCenterVerticalText(text, centerX, centerY, paint)
切换到主线程,用法与 thread {...} 保持了统一:
mainThread {
// ...
}
监听生命周期操作:
lifecycleOwner.doOnLifecycle(
onCreate = {
// ...
},
onDestroy = {
// ...
}
)
在 RecyclerView 数据为空的时候自动显示一个空布局:
recyclerView.setEmptyView(this, emptyView)
RecyclerView 的 smoothScrollToPosition() 方法是滑动到 item 可见,如果从上往下滑会停在底部,一般不符合需求。所以增加了个始终滑动到顶部位置的扩展方法。
recyclerView.smoothScrollToStartPosition(position)
每次判断 TextView 文本是否不为空要写 textView.text.toString().isNotEmpty() 特别长,对此进行了简化:
if (textView.isTextNotEmpty()) {
// ...
}
消息事件传递推荐 KunMinX 大佬的方案,用共享 ViewModel 持有的 LiveData 进行分发,避免消息推送难以溯源、消息同步不可靠不一致等问题。由于 LiveData 存在依赖倒灌的问题,一般会自行封装 EventLiveData 用于事件的场景。但是不考虑 Java 的话,直接用协程的 SharedFlow 就行。
class SharedViewModel : ViewModel() {
val saveNameEvent = MutableSharedFlow()
}
通过 by applicationViewModels() 获取 Application 级别的 ViewModel,实现共享 ViewModel:
private val sharedViewModel: SharedViewModel by applicationViewModels()
// 发送事件
sharedViewModel.saveNameEvent.tryEmit(name)
// 监听事件,提供了类似 LiveData 的 observe 用法,简化 collect 的代码
sharedViewModel.saveNameEvent.launchAndCollectIn(this) {
finish()
}
还有很多好用的 API,比如 Android 10 分区存储适配需要增删查改媒体文件的 uri,能简化很多代码,这里就不一一介绍了。更多的用法请查看 GitHub
目前已有超过 300 个常用方法或属性,可以大大提高开发效率。
个人会长期维护,有任何问题都可以提 issues,我会尽快去处理。有什么想要的功能也可以提。
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
带倒计时RecyclerView的设计心路历程
Android 12上焕然一新的小组件
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注