Kotlin-first but not kotlin-must
谷歌在 I/O 大会上宣布,Kotlin 编程语言现在是 Android 应用程序开发人员的首选语言后,有更多的安卓程序投入Kotlin的怀抱。
Kotlin的语法糖更加提高了开发的效率,加快了开发速度,使开发工作变得有趣,也让我们有更多时间写注释了(笑)。但是其实对于Kotlin和Java在Android开发上的选择,个人觉得这个除了开发人员对语言的喜好的,同时也会应该到各自语言的魅力和特点,甚至项目的需求以及后续维护等等各个因素,没有绝对的选择的。我们要做到的是放大不同语言优点并加以拓展,不是一味只选择某个语言,语言不是问题,用的那个人怎么用才是关键。
Kotlin的DSL
一、从TextWatcher和 TabLayout.OnTabSelectedListener的优化开始
先说说语法糖,例如下面的代码:
infix fun MutableLiveData.post(newValue:T){
this.postValue(newValue)
}
infix fun MutableLiveData.set(newValue:T){
this.value = newValue
}
通过 infix 定义 中缀符号 使得操作LiveData的set和post更加好看
//初始化
private val pagerNumber = MutableLiveData()
......................省略无关内容.............................
//执行postValue操作
pagerNumber post 0
又好像kotlin中的Iterable
public inline fun Iterable.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
那我们不如改装一下,如下:
//定义
inline fun Iterable.forEachBreak(action: (T) -> Boolean ){
kotlin.run breaking@{
for (element in this)
if(!action(element)){
return@breaking
}
}
}
//应用
members.forEachBreak { call->
// true or false 控制适当地方的return值,跳出循环
true
}
回到今天的主题,Kotlin 的DSL更加语法糖升华,好像语法糖plus+。今天是打算分享一下我开发时候对应DSL的运用,如何利用DSL使得的开发变得有趣的。
先看看下面代码
edittext.textWatcher {
afterTextChanged {
if(!isNullOrEmpty()){
button.visibility = View.VISIBLE
}else{
button.visibility = View.INVISIBLE
}
}
}
第一眼看上去,是不是很熟悉呢。不就一个EditText实现addTextChangedListener的方法,然后afterTextChanged里面执行操作button是否出现吗?但是你再认真看看,代码是不是简单了很多,好像少了什么,这时候应该有人会留意到那个大括号了吧。这个就是DSL,里面就一个afterTextChanged?,其实如果刚刚开始用Kotlin的开发会这样想到,那我们建一个XXXX类继承一下TextWatcher然后
edittext.addTextChangedListener(object:XXXX){
....................override相应方法................
}
其实这种写法有错吗?当然没有,但是不美观。也不太符合Kotlin的编码习惯。
那不如我直接POST代码出来,想让你们看看内部封装吧。
fun EditText.textWatcher(textWatch: SimpleTextWatcher.() -> Unit) {
val simpleTextWatcher = SimpleTextWatcher(this)
textWatch.invoke(simpleTextWatcher)
}
class SimpleTextWatcher(var view: EditText) {
private var afterText: (Editable?.() -> Unit)? = null
fun afterTextChanged(afterText: (Editable?.() -> Unit)) {
this.afterText = afterText
}
private var beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null
fun beforeTextChanged(beforeText: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)) {
this.beforeText = beforeText
}
private var onTextChanged: ((s: CharSequence?,
start: Int, before: Int, count: Int) -> Unit)? = null
fun onTextChanged(onTextChanged: ((s: CharSequence?,
start: Int, before: Int, count: Int) -> Unit)) {
this.onTextChanged = onTextChanged
}
init {
view.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
afterText?.invoke(s)
}
override fun beforeTextChanged(s: CharSequence?,
start: Int, count: Int, after: Int) {
beforeText?.invoke(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?,
start: Int, before: Int, count: Int) {
onTextChanged?.invoke(s, start, before, count)
}
})
}
}
代码有点长,当然我知道上面代码还能继续优化,也欢迎大家提供意见。但是通过这样的封装,代码风格更加简便。以此类推,我们也可以对TabLayout的addOnTabSelectedListener进一步封装
//封装
fun TabLayout.onTabSelected(tabSelect: TabSelect.() -> Unit) {
tabSelect.invoke(TabSelect(this))
}
class TabSelect(tab: TabLayout) {
private var tabReselected: ((tab: TabLayout.Tab) -> Unit)? = null
private var tabUnselected: ((tab: TabLayout.Tab) -> Unit)? = null
private var tabSelected: ((tab: TabLayout.Tab) -> Unit)? = null
fun onTabReselected(tabReselected: (TabLayout.Tab.() -> Unit)) {
this.tabReselected = tabReselected
}
fun onTabUnselected(tabUnselected: (TabLayout.Tab.() -> Unit)) {
this.tabUnselected = tabUnselected
}
fun onTabSelected(tabSelected: (TabLayout.Tab.() -> Unit)) {
this.tabSelected = tabSelected
}
init {
tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.apply { tabReselected?.invoke(tab) }
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
tab?.apply { tabUnselected?.invoke(tab) }
}
override fun onTabSelected(tab: TabLayout.Tab?) {
tab?.apply { tabSelected?.invoke(tab) }
}
})
}
}
//使用
tab.onTabSelected {
onTabSelected {
pos = position
}
}
我们其实还有更多这样的方法可以这样封装,达到更加方便。
二、DSL运用升级
我们还是先看看代码:
//在Application内进行执行,当然还是那个句:
//Application不要有太多的复杂耗时任务,我只是举个一个可以运用的地方而已,保证初始化成功。
//RetroHttp就是一个Retrofit Client封装类
//createApi(tClass)还是那个Retrofit.create(tClass)
startInit {
modules(Module{
single{ RetroHttp.createApi(auth::class.java) }
}
}
//然后我们在某个Repository内初始化 interface auth 这个接口类
val api : auth by inject()
是不是很简单呢,我们只要inject方法就能把这个接口初始化成功了。
先别那么快否定我,这样写实不实用,因为初始化这些接口,对于每个安卓开发都再熟悉不过了,方法一大堆。今天我们是对DSL的进一步学习,把思路拓宽。
这个startInit内部长这样的
fun startInit(component: Components.()->Unit){
component.invoke(Components.get())
}
class Components {
companion object{
private val entry = ArrayMap()
private val module = ArrayList()
private val instant by lazy { Components() }
fun get() = instant
fun getEntry() = entry
}
fun modules(vararg modules: Module){
module.addAll(modules)
}
}
inline fun get(name: String = T::class.java.name) : T{
return Components.getEntry()[name] as T
}
inline fun inject(name: String = T::class.java.name) : Lazy {
return lazy { Components.getEntry()[name] as T }
}
class Module(component: Component.() -> Unit){
init {
component.invoke(Component())
}
}
class Component{
inline fun single(noinline single: Component.()->T){
val name = T::class.java.name
Components.getEntry()[name] = single()
}
}
这个reified关键字起到的作用很核心,可以简化模板代码,编译器可以自动推断类型
例如:
//定义
inline fun startActivity(bundle: Bundle? = null) {
val intent = Intent(this, T::class.java)
if (bundle != null) {
intent.putExtras(bundle)
}
startActivity(intent)
}
//利用
startActivity()
能直接通过这个reified 拿到泛型的类型,对于Kotlin这种很注重泛型的语言尤其出色,加上inline进一步节省调用开销。通过startInit方法我们现在可以更加优雅处理类的构造函数初始化。
实用大升级
在平时开发中,DSL除了应用在一些普通方法上,我们其实还可以拓展到一些常用类的封装,例如DialogFragment。DialogFragment其实对于安卓的开发人员来说,都不是一个陌生的类。
方法 — 、可以覆写其 onCreateDialog 利用AlertDialog或者Dialog创建出Dialog。
方法 二、 覆写其 onCreateView 使用定义的xml布局文件展示Dialog。
那我们的DSL可以怎么进一步优化使用过程,接下来分享一下我的处理:
一、先提取常用配置,减少重复代码的书写
我们平时在书写DialogFrament时候都会有不少的模板代码,几乎三四个DialogFramgent 里面都有好几十行是一模一样的基础设置,那我们先定义一个注解
@Target(AnnotationTarget.CLASS)
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class WindowParam(val gravity:Int = Gravity.CENTER,val outSideCanceled:Boolean = true, val noAnim : Boolean = false,
val animRes :Int = -1,val canceled:Boolean = true,
val dimAmount :Float = -1f)
// gravity 是dialog中的Window的 setGravity(gravity)方法
//outSideCanceled 是 dialog.setCanceledOnTouchOutside(outSideCanceled)
//canceled 是 dialog.setCancelable(canceled)
//noAnim 是指是否使用进场出场动画
//animRes是我们的进场出场动画资源
//dimAmount 是window的setDimAmount(dimAmount) 用于控制弹窗后灰色蒙版的透明度
二、引入DefaultLifecycleObserver
引入这个对于DialogFragment是为方便获取到它依附的Activity的生命周期,能在适当的地方进行适当操作
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context
if(context is AppCompatActivity){
init(context)
}else if(context is LifecycleOwner){
init(context)
}
}
private var owner : LifecycleOwner? = null
private fun init(owner: LifecycleOwner){
this.owner = owner
this.owner?.lifecycle?.addObserver(this)
}
在onAttach方法的地方获取LifecycleOwner,进行生命周期的监听。
三、引入DSL语法
在我这个封装中,我是覆写onCreateDialog这个方法,由于封装内容很多,我先贴出代码在慢慢一步步讲
abstract class SimpleDialogFragment : DialogFragment(),DefaultLifecycleObserver {
lateinit var activity : Context
private var onCreate :(()->Int)? = null
private var onWindow :((window:Window)->Unit)? = null
private var onView :((view:View)->Unit)? = null
abstract fun build(savedInstanceState: Bundle?)
private var owner : LifecycleOwner? = null
private fun init(owner: LifecycleOwner){
this.owner = owner
this.owner?.lifecycle?.addObserver(this)
}
override fun onStop(owner: LifecycleOwner) {
dialog?.apply {
dismissAllowingStateLoss()
}
}
override fun onDestroy(owner: LifecycleOwner) {
if(this.owner != null){
dismissAllowingStateLoss()
this.owner?.lifecycle?.removeObserver(this)
}
}
fun buildDialog(onCreate :(()->Int)) : SimpleDialogFragment{
this.onCreate = onCreate
return this
}
fun onWindow(onWindow :((window:Window)->Unit)) : SimpleDialogFragment{
this.onWindow = onWindow
return this
}
fun View.onBindingView(onBindingView :((binding : T?)->Unit)){
onBindingView.invoke(DataBindingUtil.bind(this))
}
fun onView(onView :((view:View)->Unit)) : SimpleDialogFragment{
this.onView = onView
return this
}
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context
if(context is AppCompatActivity){
init(context)
}else if(context is LifecycleOwner){
init(context)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
build(savedInstanceState)
val viewId = onCreate?.invoke()
if(viewId!= null){
val view = View.inflate(activity, viewId, null)
val param = javaClass.getAnnotation(WindowParam::class.java)!!
val gravity = param.gravity
val outSideCanceled = param.outSideCanceled
val canceled = param.canceled
val dimAmount = param.dimAmount
val noAnim = param.noAnim
val dialog = Dialog(activity)
dialog.setContentView(view)
dialog.setCanceledOnTouchOutside(outSideCanceled)
dialog.setCancelable(canceled)
val window = dialog.window
val dm = DisplayMetrics()
window?.apply {
windowManager.defaultDisplay.getMetrics(dm)
setLayout(dm.widthPixels, window.attributes.height)
setBackgroundDrawable(ColorDrawable(0x00000000))
setGravity(gravity)
if(!noAnim){
setWindowAnimations(R.style.LeftRightAnim)
}
if(dimAmount!=-1f){
setDimAmount(dimAmount)
}
onWindow?.invoke(this)
}
view?.apply {
onView?.invoke(this)
}
return dialog
}
return super.onCreateDialog(savedInstanceState)
}
override fun dismiss() {
dialog?.apply {
if(isShowing){
if(getActivity()!=null){
super.dismiss()
}
}
}
}
override fun show(manager: FragmentManager, tag: String?) {
try {
if(!isAdded){
val transaction = manager.beginTransaction()
transaction.add(this, tag)
transaction.commitAllowingStateLoss()
transaction.show(this)
}
}catch (e: Exception){
Log.e("DialogFragment","${e.message}")
}
}
}
用的时候变成了这样,如下:
@WindowParam(gravity = Gravity.BOTTOM,animRes = R.style.BottomTopAnim)
class BottomDialog : SimpleDialogFragment() {
override fun build(savedInstanceState: Bundle?) {
buildDialog {
R.layout.XXXXXXXX
}
onView {
it.onBindingView { binding ->
binding?.apply {
//do something
}
}
}
}
相对的代码量少了,把基本的内容都封装起来,
一、buildDialog 方法,利用onCreate :(()->Int) 这个Int的返回值,即该方法大括号的最后一行,代表返回值的特性,把layoutId设置进去,当然你也可以采用在上面注解的地方,添加也可以。
二、onWindow方法,该方法可以拿到dialog.window的对象,利用该对象,我们可以再进一步进行配置
三、onView方法,该方法能拿到layout的view对象,这里提一个特别的地方
平时开发可以留一下在不使用databinding情况下,Dialog中使用kotlin了
apply plugin: 'kotlin-android-extensions'这个配置可以拿到view的id,但是有一点需要注意引入的时候 xxxxxx.* 和 xxxxx.view.* (xxxxx即你的布局名字)是有区别的,各位可以留意一下。
四、onBindingView是我特意留的一个databinding的方法,方便使用databinding的朋友。
结合协程实现倒计时功能
我们平时实现倒计时功能都会用到RxJava,CountDownTimer,Timer+TimerTask,线程,今天借此利用线程的方案,即Kotlin中的协程,废话不说先放代码:
fun LifecycleOwner.counter(dispatcher: CoroutineContext,start:Int, end:Int, delay:Long, onProgress:((value:Int)->Unit),onFinish: (()->Unit)?= null){
val out = flow {
for (i in start..end) {
emit(i)
delay(delay)
}
}
lifecycleScope.launchWhenStarted {
withContext(dispatcher) {
out.collect {
onProgress.invoke(it)
}
onFinish?.invoke()
}
}
}
//使用
counter(Dispatchers.Main,1,3,1000,{
//倒计时过程
}){
//完成倒计时
}
利用了携程中的flow方法,进一步的优化了采用线程方案的倒计时。
小结
这篇文章也是我第二篇分享文章,因为个人很少写文章和博客,也不是懒不懒的问题,其实就是有个感觉,觉得自己学习知识学了一段时间,是不是应该做个分享,多一些交流,让自己的思路更加拓展。
个人的github地址:https://github.com/ShowMeThe
也分享一下,无聊时候写的一个基于ViewPager2的轮播图:https://github.com/ShowMeThe/BannerView
有问题也可以留个言,交流一下。