如何更好地使用Kotlin语法糖封装工具类

编程入门 行业动态 更新时间:2024-10-11 21:20:17

如何更好地使用Kotlin<a href=https://www.elefans.com/category/jswz/34/1770552.html style=语法糖封装工具类"/>

如何更好地使用Kotlin语法糖封装工具类


/   今日科技快讯   /

据外媒报道,欧盟委员会周三宣布,已对美国芯片巨头英伟达收购英国芯片设计公司Arm的交易展开正式反竞争调查。英伟达于2020年9月宣布斥资400亿美元从日本软银集团手中收购Arm,并预计可能在18个月内完成交易。

/   作者简介   /

大家周五早上好,顺祝大家周末愉快!

本篇文章来自DylanCai的投稿,文章介绍了使用Kotlin封装一些工具类,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

DylanCai的博客地址:

/   前言   /

在 2019 年 Google I/O 大会上,Google 宣布今后将优先采用 Kotlin 进行 Android 开发,并且也坚守了这一承诺。使用 Kotlin 进行 Android 开发代码更少,可读性更强,并且能和 Java 代码兼容。

我之前学习了一些 Kotlin 的语法糖之后,很想运用到自己整理的工具类上。当时公司只有我学习 Kotlin,所以项目是 Kotlin 和 Java 混编的。在写工具类时突然想到一个问题,我用 Kotlin 写的工具类,调用的结果和原来 Java 工具类得到的结果不一致怎么办。

比如正则,我用 Kotlin 写的和别人用 Java 写的匹配出来不一样,那我不是要兴师问罪。要么就特意保证实现逻辑和 Java 工具类一致,这么做的话为什么不直接调用 Java 工具类呢。所以当时就给公司项目在用的 Java 工具类库 AndroidUtilCode()封装扩展库。

由于绝大部分功能都实现好了,主要做的事是补充没有的功能和设计一套好用的 Kotlin API。设计 API 看似很简单,实际做起来很难。因为 Kotlin 的玩法实在太多了,并且不是用了语法糖就一定会好用,用法骚会带来一定的学习成本,代码可读性可能会更差。个人比较强迫症,在这方面思考了很多,有一些封装经验可以分享给大家。

后来的公司新项目基本是 Kotlin 进行开发,可以不用考虑对 Java 代码的兼容,就着手开始写一个纯 Kotlin 开发、尽可能轻量的 Kotlin 工具类库。得益于之前的很多思考,目前实现的还是比较满意的。

接下来给大家分享个人一些封装 Kotlin 工具类的经验和一个好用的 Kotlin 工具类库。

/   封装思路   /

我看过很多人写的 Kotlin 工具类只是单纯地把原有的 Java 工具类翻译成 Kotlin 语言,这就像当初推出 C++ 后,有些人还是用面向过程的思想写代码。并不是不能用,但是能做得更好用。所以下面介绍的是一些在 Java 不常见的语法糖和一些使用建议,帮助大家更好地在工具类使用这些特性。

Top-Level

这是 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<Drawable>.() -> 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? = nulloverride 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()

为什么叫 Longan ?个人想用个水果名来作为库名,最初想到的是 Guava (石榴),感觉非常合适,但是发现有一个谷歌的同名库,所以换了个也是多子的水果 Longan (龙眼)。

添加依赖:

allprojects {repositories {// ...maven { url '' }}
}
dependencies {implementation 'com.github.DylanCaiCoding.Longan:longan:1.0.0'// 可选implementation 'com.github.DylanCaiCoding.Longan:longan-design:1.0.0'
}

保留和改进了一些 Anko 好用的用法,例如:

startActivity<SomeOtherActivity>("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_selectorR.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<TextView>(R.id.tv_title).setText(titleList[position])findViewById<ImageView>(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<String>()
}

通过 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上焕然一新的小组件

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

更多推荐

如何更好地使用Kotlin语法糖封装工具类

本文发布于:2024-03-23 18:57:59,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1741663.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:语法   如何更好地   工具   Kotlin

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!