高级特性
@Parcelize
我们经常使用 Parceable 这个接口,但是使用这个接口存在两个非常操蛋的地方。
需要实现 writeToParcel() 方法和 createFromParcel() 方法,这一个手写非常操蛋,还好有 as 插件可以支持快速生成。
在该类添加字段之后,需要手动或者再一次生成 Parceable 相关的实现方法和抽象类。
这种操作极度不友好。
在 kotlin 这里,你可以消除这些烦恼。代码如下:
@Parcelize
data class UserInfo(var name: String, var age: Int) : Parcelable {
}
再此之前你需要在该 module 下的 build.gradle 文件中的 Android 标签下,添加如下代码:
android {
androidExtensions{
experimental = true
}
}
通过decompile 该kt 文件的字节码,我们可以知道,实际上这个注解,帮助我们实现了 Parcelable 接口的相关内容。
Delegation 代理
代理和代理属性,代理是类似继承的一种方式,同时也是继承更好的替代方案。kotlin 实现类似多继承的功能,也就是代理,简单的 demo 如下:
//被代理的 接口A
interface Base {
val mDefaultMsg: String
fun printX()
fun printMessage(contenc: String)
}
class BaseImpl : Base {
override val mDefaultMsg: String
get() = "mDefaultMsg"
override fun printX() {}
override fun printMessage(contenc: String) {
}
}
//被代理的接口B
interface IPresnter
class BasePresenter : IPresnter
open class Item
open class HomeItem
//代理类
class Dervied(b: Base, basePresenter: BasePresenter) : Base by b, Item(), IPresnter by basePresenter {
override fun printX() {
Log.i("Dervied", "printX")
}
}
总结:
<Dervied Class Name>: <Delagate Inferface Name> by <Delagate Class SingTone>
也就是 代理类 :被代理接口 by 接口实例
那么有人问,这里和 java 的接口继承有什么不同呢?这里是拓展了,接口继承的形式,java 继承类是单继承的,可以实现多个接口,但是接口一般是空的。所 kotlin 拓展的继承,是一种偏向组合的思想,而不是继承。假设这样一个场景,你需要继承多个具有Base 功能的类,那么在 java 上,是无法实现的,但是在kotlin 可以使用代理实现。(例举 关注逻辑的实现)
Decompile 之后发现:
public static final class Dervied extends DelategeTest.Item implements DelategeTest.Base, DelategeTest.IPresnter {
// $FF: synthetic field
private final DelategeTest.Base $delegate_0;
// $FF: synthetic field
private final DelategeTest.BasePresenter $delegate_1;
public void printX() {
Log.i("Dervied", "printX");
}
public Dervied(@NotNull DelategeTest.Base b, @NotNull DelategeTest.BasePresenter basePresenter) {
Intrinsics.checkParameterIsNotNull(b, "b");
Intrinsics.checkParameterIsNotNull(basePresenter, "basePresenter");
super();
this.$delegate_0 = b;
this.$delegate_1 = basePresenter;
}
@NotNull
public String getMDefaultMsg() {
return this.$delegate_0.getMDefaultMsg();
}
public void printMessage(@NotNull String contenc) {
Intrinsics.checkParameterIsNotNull(contenc, "contenc");
this.$delegate_0.printMessage(contenc);
}
}
所以实际上,这是一种类似继承的关系,但代理模式是比继承更解耦的设计模式。
我们可以给我们的功能设计许多的类似工具方法或者工具类的代理对象类,例如实现主播关注的类,实现请求用户信息的类,然后再需要实现这个功能的地方,使用代理实现,这样就可以节省很多代码,同时也达到了高度解耦。
Delegated Properties 代理属性
怎么理解代理属性这样一种东西呢?我们可能需要实现各种各样的特效的属性,例如延迟初始化,kotlin 拓展了常用的几种属性。
lazy 属性 :其值只在首次访问时计算;
observable 属性:监听器会收到有关此属性变更的通知;
映射 属性:把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中
标准代理属性-by lazy block
这是委托属性的一种,可以实现延迟加载,并且提供多种方式,控制线程安全,其使用语法:
val/var <property name>: <Type> by <expression>
例如一个简单的例子:
val mMsg: String by lazy {
"kk" + "oo"
}
mMsg 会在第一次访问的时候,执行初始化,然后被赋值。
其代码实现在 Lazy.kt 文件中,源码如下:
@kotlin.jvm.JvmVersion
public fun lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)
@kotlin.jvm.JvmVersion
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
重载了两个 lazy 方法,其中第二个方法提供了一个参数 mode:LazyThreadSafetyMode,也是是线程安全的模式,存在三种
-
LazyThreadSafetyMode.SYNCHRONIZED
线程安全,并且只有一个现场可以访问到 initializer 初始化代码块,其实现类是 SynchronizedLazyImpl。
@JvmVersion private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // final field is required to enable safe publication of constructed instance private val lock = lock ?: this override val value: T get() { val _v1 = _value //先看变量是否初始化过,如果初始化过,直接返回变量值 if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } //synchronized return synchronized(lock) { val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { val typedValue = initializer!!() _value = typedValue initializer = null typedValue } } } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }
这里的代码非常简单,在get() 方法里面,使用 synchronized 给对象加锁,确保只有一个线程能够访问得到该方法。
-
LazyThreadSafetyMode.PUBLICATION
线程安全,可以允许多个线程
-
LazyThreadSafetyMode.NONE
非线程安全
Kotlin DSL
DSL 总体来说是非常简洁的,在使用 Android 开发 java 的过程中,貌似么有使用过 DSL之类的写法,而在 kotlin 中,使用最多的 DSL 就是 anko
根据 anko 的官方文档说明,使用 DSL 有以下好处(也就是使用 xml 的坏处):
By default, UI in Android is written using XML. That is inconvenient in the following ways:
It is not typesafe 非类型安全的,也即是 findView 的时候,需要注意类型
It is not null-safe 可能为空的
It forces you to write almost the same code for every layout you make layout 布局里面存在大量的共同代码,复用率较低
XML is parsed on the device wasting CPU time and battery xml 解析消耗额外的 cpu 和时间
Most of all, it allows no code reuse 代码不存在复用关系
同样对比传统的 硬编码编写UI 来说,anko 也存在吸引点,就是代码简洁。
例如,传统的硬编码编写 UI 如下:
val act = this
val layout = LinearLayout(act)
layout.orientation = LinearLayout.VERTICAL
val name = EditText(act)
val button = Button(act)
button.text = "Say Hello"
button.setOnClickListener {
Toast.makeText(act, "Hello, ${name.text}!", Toast.LENGTH_SHORT).show()
}
layout.addView(name)
layout.addView(button)
但是如果使用 anko-layout:
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
anko-layout 原理
简单来说,anko-layout 的原理就是扩展函数以及DSL,例如在 AnkoLayoutActivity.kt 中有如下代码:
class AnkoLayoutActivity:Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun initLayout(){
verticalLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
}
}
实际上使用的是 Activity 的一个扩展函数,如下:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
后面的操作,实际上就是帮你创建一个 LinearLayout ,这个 LinearLayout 也是特殊的 LinearLayout,增加了对应的扩展方法,如下:
open class _LinearLayout(ctx: Context): LinearLayout(ctx) {
inline fun <T: View> T.lparams(
c: Context?,
attrs: AttributeSet?,
init: LinearLayout.LayoutParams.() -> Unit
): T {
val layoutParams = LinearLayout.LayoutParams(c!!, attrs!!)
layoutParams.init()
this@lparams.layoutParams = layoutParams
return this
}
......
}
整体流程图如下:
(图片来源网络)
View的创建一层一层的传递下去,其中共有三种情况,即创建View的三种上下文:
Activity,在Activity的onCreate中调用,判断到最后通过setContentView来填充整个布局
Context & Fragment,在其他场景下创建的View,可直接通过UI { } 域生成相应的View
AnkoContext,在自定义AnkoComponent中的createView方法中定义
也就意味着,你可以在 Activity,Fragment,或者自定义 ViewGroup 中使用 Anko-layout.
原生DSL
anko 支持 DSL 实现原理有两个要素:
我们以一个案例讲解这里的用法:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
首先,在Activity 的扩展函数 verticalLayout() 里面,传入一个 lambdas 作为参数,init: (@AnkoViewDslMarker _LinearLayout).() -> Unit),然后这个命名为 init 的函数,是一个带接受者的函数,也就是 _LinearLayout.() -> Unit,其接受者类型为 _LinearLayout,这个类继承自 LinearLayout。
上述代码可以简化成这样:
fun Context.createLinearLayout(init:LinearLayout.() ->Unit):LinearLayout{
val layout = LinearLayout(this)
layout.init()
return layout
}
所以你在某种 Context 内调用这个 createLinearLayout 的时候,就可以直接传入一个 Lambdas 表达式,其this 指向 LinearLayout,所以你调用该方法的时候,如下:
//anko 的写法
verticalLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
//我们自己的写法
createLinearLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
DslMarker
其次Anko 为了提高 DSL 的效率,还是使用了 @DslMarker 注解:
//Ui.kt 中
@DslMarker
@Target(AnnotationTarget.TYPE)
annotation class AnkoViewDslMarker
//Annotatios.kt 中
@Target(ANNOTATION_CLASS)
@Retention(BINARY)
@MustBeDocumented
@SinceKotlin("1.1")
public annotation class DslMarker
根据kotlin 官方文档的解释:
The general rule:
an implicit receiver may belong to a DSL @X if marked with a corresponding DSL marker annotation
two implicit receivers of the same DSL are not accessible in the same scope
the closest one wins
-
other available receivers are resolved as usual, but if the resulting resolved call binds to such a receiver, it's a compilation error
这里规定了DSL 的一些有意义的规范,例如DSL 块必须有接收者,其次不允许在一个代码块,存在两个相同的接收者,只要靠近代码块那个,才会生效。
简单来说,DslMarker 是为了控制作用范围,例如以下的情况:
html {
head {
head { } // 错误:外部接收者的成员
}
// ……
}
也就是说,第二个 head 代码块的接受者实际上应该是 html,但是在这种情况下,明显就不是了,所以使用 DslMarker,编译器会提示这种错误。
从 1.1 版本开始, kotlin 使用 @DslMaker 用来检查 dsl 块的作用范围,如果没有显示声明代码块的接收者,编译器会发出警告。
why anko
控件通用库,从业务场景来说,一个 TextView 的样式,可能是固定几种,例如聊天列表的样式,首页标题栏样式,复用效果可能比 style 要好,可读性也要好一些。
使用 anko 代替 xml 的好处(见上kotlin DSL 开头的介绍)
标准控件库(插件版本),可以将代码复用程度提到最高。
缺点:
java 调用,十分不方便。