5.4 Kotlin 扩展函数详解与应用

一、什么是扩展函数?

扩展函数数是指在一个类上增加一种新的行为,甚至我们没有这个类代码的访问权限。这是一个在缺少有用函数的类上扩展的方法,Kotlin能够为我们做到那些令人关注的事情,而这些Java做不到。

在Java中,通常会实现很多带有static方法的工具类,而Kotlin中扩展函数的一个优势是我们不需要在调用方法的时候把整个对象当作参数传入,它表现得就像是属于这个类的一样,而且我们可以使用this关键字和调用所有public方法。

二、扩展函数的使用

(1)函数的扩展

简单来说,Kotlin扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。例如,我们能够为Activity中添加新的方法,让我们以更简单术语显示toast,并且这个函数不需要传入任何context,它可以被任何Context或者它的子类调用,比如Activity或者Service:

fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, message, duration).show()
}

当然你也可以这样写:

fun Activity.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT){
    Toast.makeText(this, message, duration).show()
}

对参数的解释:

  • Activity:表示函数的接收者,也就是函数扩展的对象
  • . :扩展函数修饰符
  • toast:扩展函数的名称

我们可以在任何地方(例如在一个工具类文件中)声明这个函数,然后在我们的Activity中将它作为普通方法来直接使用(这里的两种使用方式后文会做解释):

override fun onCreate(savedInstanceState: Bundle?) { 
    super<BaseActivity>.onCreate(savedInstanceState)
    toast("This is onCreate!!")
    toast("Hello world!", Toast.LENGTH_LONG)
}

当然了,Anko已经包括了自己的toast扩展函数,跟上面这个很相似(关于Anko是什么,你可以看我的这篇文章。。。)。Anko提供了一些针对CharSequence和resource的函数,还有两个不同的toast和longToast方法:

toast("Hello world!")
longToast(R.id.hello_world)

有一点值得注意:扩展函数并不是真正地修改了原来的类,它的这些作用效果是以静态导入的方式来实现的。扩展函数可以被声明在任何文件中,因此有个通用的方式是把一系列有关的函数放在一个新建的文件里,就像我们刚才所说的工具类当中。

我觉得还是再举几个几个例子来说明一下吧,因为它们完全显示扩展函数的力量。

● 在 Toast 中的高级用法

我们首先看看这个例子:

fun Context.niceToast(message: String,
                tag: String = javaClass<MainActivity>().getSimpleName(),
                length: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(this, "[$className] $message", length).show()
}

我们增加了一个默认值是类名的参数,如果这是在Java中的话,那么总数开销会以几何形式增长,而现在我们可以通过以下方式调用:

toast("Hello")
toast("Hello", "MyTag")
toast("Hello", "MyTag", Toast.LENGTH_SHORT)

我们甚至还有其它选择,因为我们可以使用参数名字来调用,也就是说我们可以通过在值前写明参数名来传入我们希望的参数:

toast(message = "Hello", length = Toast.LENGTH_SHORT)

这样我们就使用了第二个参数的默认形式,而使用了第一第三个参数的传入值形式。但是你可能觉得这样的函数调用比较难以理解“[$className] $message”,这个是 Kotlin 中的String模版,我们接下来讲解一下它。

String模版:
我们可以在String中直接使用模版表达式,它可以帮助我们很简单地在静态值和变量的基础上编写复杂的String。在上面的例子中,我们使用了"[$className] $message"。
这就意味着,在任何时候我们使用一个$符号就可以插入一个表达式。如果这个表达式有一点复杂,我们就需要使用一对大括号括起来,比如:"My name is ${user.name}"。字符串可以包含模板表达式,即可求值的代码片段,并将其结果连接到字符串中。下面我们举几个例子:

  • 一个模板表达式由一个 $ 和简单名称组成
val i = 10  
val s = "i = $i" // 结果为 "i = 10"  
  • 一个模板表达式由一个$ 和大括号括起来的表达式组成
val s = "abc"  
val str = "$s.length is ${s.length}" // 结果为 "abc.length is 3"
  • **如果想输出字符,比如“$” **
${'$'}
● 在 onCreateViewHolder 中的使用

第一个是我们在 RecyclerView 中的适配器中用到的例子,正常情况下我们这样使用:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val v = LayoutInflater.from(parent.context).inflate(R.layout.view_item, parent, false)
    return ViewHolder(v)
}

但是实际上加载布局的逻辑实在是太麻烦了,并且绝大多数情况下我们都在重复编写同样的适配器代码,那么我们为什么不给 ViewGroups 赋予 inflate 的能力呢:

fun ViewGroup.inflate(layoutRes: Int): View {
    return LayoutInflater.from(context).inflate(layoutRes, this, false)
}

然后我们可以想现在这样使用它:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val v = parent.inflate(R.layout.view_item)
    return ViewHolder(v)
}
● 在加载图片的时候使用

如果你使用过 Picasso 加载图片的话,那真是再好不过了,而且你会发现你也可以用相同的方法给 ImageView 增加一个扩展函数:

fun ImageView.loadUrl(url: String) {
    Picasso.with(context).load(url).into(this)
}

然后我们在使用的时候只需这样简单的一行:

imageView.loadUrl(url)

(2)属性的扩展

扩展函数也可以是一个属性,所以我们可以通过相似的方法来扩展属性。我们知道Kotlin由于互操作性的特性已经提供了getter、setter这个属性,但是我们任然通过下面的例子来展示一下使用自己的getter/setter生成一个属性的方式,因为这很有助于理解扩展属性背后的思想:

public var TextView.text: CharSequence
    get() = getText()
    set(v) = setText(v)

再比如,我们可以用此方法来设置View的padLeft属性:

// 使用扩展属性(extension property)
var View.padLeft: Int
    set(value) {
        setPadding(value, paddingTop, paddingRight, paddingBottom)
    }

    get() {
        return paddingLeft
    }

有一点值得注意:由于扩展属性实际上不会向类添加新的成员,因此无法让一个扩展属性拥有一个后端域变量,所以对于扩展属性不允许存在初始化器。扩展属性的行为只能通过明确给定的取值方法与设值方法来定义,也就意味着扩展属性只能被声明为val而不能被声明为var,如果强制声明为var,即使进行了初始化,在运行也会报异常错误,提示该属性没有后端域变量。

(3)扩展函数中的操作符

通常来讲,我们不需要去扩展我们自己的类,但是我需要去使用扩展函数扩展我们已经存在的类来让第三方的库能提供更多的操作,比如我们可以去像访问List的方式去访问 ViewGroup 的 view(关于 View 和 ViewGroup 请参看这篇。。。):

operator fun ViewGroup.get(position: Int): View = getChildAt(position)

于是我们就可以像这样非常简单地从一个 ViewGroup 中通过 position 得到一个 view:

val container: ViewGroup = find(R.id.container)
val view = container[2]

三、可选参数和默认值

聪明的你不知道有没有注意到刚才的那段代码中,Toast 的第二个形参为什么在一开始就赋予了一个默认值呢?其实这就涉及到了 Kotlin 中可选参数的概念。

什么是可选参数呢?简而言之,就是我们在调用该函数的时候对于该参数既可以传参也可以不传参,比如上文代码中的第二个参数。那么在不传参时,默认的参数自然就成了我们上文代码中指明的“Toast.LENGTH_SHORT”。

所以说这样做的好处也是显而易见的,那就是借助于参数和构造函数的默认值,我们将不再需要进行函数重载了。只需要我们做一个函数的声明就可以满足我们几乎所有的需求。还是拿刚才的 Toast 来说事:

fun Activity.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT){
    Toast.makeText(this, message, duration)
}

第二个参数表示 toast 的显示持续时间,这就是一个我们刚刚说的可选参数,但没有显式指定时,它将使用默认的Toast.LENGTH_SHORT这个值。因此,我们可以采用前面的两种方式来调用这个函数:

override fun onCreate(savedInstanceState: Bundle?) { 
    super<BaseActivity>.onCreate(savedInstanceState)
    toast("This is onCreate!!")
    toast("Hello world!", Toast.LENGTH_LONG)
}

再比如,我们可以采用这样的方式在 Activity 中支持 lollipop 动画(我们暂时不必给出具体实现):

inline public fun <reified T : Activity> Activity.navigate(
        id: String,
        sharedView: View? = null,
        transitionName: String? = null) {
    ...

所以我们就有了三种方式去调用这个函数,先看一下我们刚介绍过的前两种:

navigate<DetailActivity>("2")
navigate<DetailActivity>("2", sharedView, TRANSITION_NAME)

而对于第三种方式,虽说在这种情况下意义不大,但却让我们知道了我们也是可以通过使用参数名字来决定哪个参数会被调用的:

navigate<DetailActivity>(id = "2", transitionName = TRANSITION_NAME)

所以,参数的默认值可以让我们只声明一个构造函数,但却会得到很多重载。

除此之外,我们呢还可以扩展View的dp转换函数:

// 使用扩展函数
fun View.dp_f(dp: Float): Float {
    // 引用View的context
    return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)
}

// 转换Int
fun View.dp_i(dp: Float): Int {
    return dp_f(dp).toInt()
}

可以看到,扩展函数让我们在编写代码时省去了很多功夫。

我们总结下 Kotlin 的三个特点:
  • Kotlin的扩展函数功能使得我们可以为现有的类添加新的函数,实现某一具体功能 。
  • 扩展函数是静态解析的,并未对原类添加函数或属性,对类本身没有任何影响。
  • 扩展属性允许定义在类或者kotlin文件中,不允许定义在函数中。

四、Kotlin 标准库扩展函数集合

Kotlin 标准库提供了一些扩展 Javahttp库的函数,我们接下来一一介绍下。

  • apply
    apply 是 Any 的扩展函数,因而所有类型都能调用。
    apply 接受一个lambda表达式作为参数,并在apply调用时立即执行,apply返回原来的对象。
    apply 主要作用是将多个初始化代码链式操作,提高代码可读性。
    比如:
val task = Runnable { println("Running") }
val thread = Thread(task)
thread.setDaemon(true)
thread.start()

上面这段代码可以简化为:

val task = Runnable { println("Running") }
Thread(task).apply { setDaemon(true) }.start()
  • let
    let 和 apply 类似, 唯一的不同是返回值,let返回的不是原来的对象,而是闭包里面的值。
val outputPath = Paths.get("/user/home").let {
    val path = it.resolve("output")
    path.toFile().createNewFile()
    path
}

outputPath 结果是闭包里面的 path。

  • with
    with 是一个顶级函数, 当你想调用对象的多个方法但是不想重复对象引用,比如代码:
val g2: Graphics2D = ...
g2.stroke = BasicStroke(10F)
g2.setRenderingHint(...)
g2.background = Color.BLACK

可以用 with 这样写:

with(g2) {
    stroke = BasicStroke(10F)
    setRenderingHint(...)
    background = Color.BLACK
}
  • run
    run 是 with和let 的组合,例如:
val outputPath = Paths.get("/user/home").run {
    val path = resolve("output")
    path.toFile().createNewFile()
    path
}
  • lazy
    lazy延迟运算,当第一次访问时,调用相应的初始化函数,例如:
fun readFromDb(): String = ...
val lazyString = lazy { readFromDb() }
val string = lazyString.value

当第一次使用 lazyString时, lazy 闭包会调用,它一般用在单例模式当中。

  • use
    use 用在 Java 上的 try-with-resources 表达式上, 例如:
val input = Files.newInputStream(Paths.get("input.txt"))
val byte = input.use({ input.read() })
  • repeat
    顾名思义,repeat 就是重复的意思,它接受函数和整数作为参数,函数会被调用 n 次,这个函数避免写循环。
repeat(10, { println("Hello") })
  • require/assert/check
    require/assert/check 用来检测条件是否为true, 否则抛出异常。 其中 require 用在参数检查; 而 assert/check 用在内部状态检查,assert 抛出 AssertionException 、 check 抛出 IllegalStateException。
fun neverEmpty(str: String) {
    require(str.length > 0, { "String should not be empty" })
    println(str)
}

五、用 Kotlin 的扩展函数 findViewOften 丢掉 ViewHolder

(1)ViewHolder 介绍

作为一名 Android 开发者,对 ViewHolder 应该再熟悉不过了。ViewHolder 一开始并不是 Android 原生提供的(现在已经是 RecycleView 的默认实现了),而是 Google 为了提高 ListView 的使用性能,为开发者提供的一种最佳实践。

Google 提供的 ViewHolder 的标准实现如下,熟悉者可以直接跳到下个部分 ViewHolder的变种 继续阅读:

staticclassViewHolder{
    TextView text;
    TextView timestamp;
    ImageView icon;
    ProgressBar progress;
}

在 Item 第一次创建视图的时候,填充 ViewHolder 并且将其保存在视图中:

ViewHolder holder = newViewHolder();
holder.icon = (ImageView) convertView.findViewById(R.id.listitem_image);
holder.text = (TextView) convertView.findViewById(R.id.listitem_text);
holder.timestamp = (TextView) convertView.findViewById(R.id.listitem_timestamp);
holder.progress = (ProgressBar) convertView.findViewById(R.id.progress_spinner);
convertView.setTag(holder);

这样在填充 Item 数据的时候,直接使用 Viewholder 对象的属性,这样可以减少在滚动 ListView 频繁调用 findViewById() 而导致的性能问题。当然关于 ListView 性能优化的问题还有一些内容可以介绍,不过我们这里暂不作展开。

(2)ViewHolder 的变种

Google 提供的 ViewHolder 的确能够提升 ListView 的使用效率,但是 ViewHolder 的实现相对繁琐,需要为每一种 Item 定义一个 ViewHolder,对代码书写和维护都是额外的开销。于是有人针对 ViewHolder 的实现做了一些优化,让 ViewHolder 写起来更方便。网上有很多种写法,下面提供一种最为简单优雅又高效的方式:

public class ViewHolder { 
  @SuppressWarnings("unchecked") 
  public static <T extends View> T get(View view, int id) { 
    SparseArray<View> viewHolder = (SparseArray<View>) view.getTag(); 
    if (viewHolder == null) { 
      viewHolder = new SparseArray<View>(); 
      view.setTag(viewHolder); 
    } 
    View childView = viewHolder.get(id); 
    if (childView == null) { 
      childView = view.findViewById(id); 
      viewHolder.put(id, childView); 
    } 
    return (T) childView; 
  } 
}

这里我们使用 SparseArray 映射每个视图 id 和对应的视图,并将其保存在视图中,这样既保证在滚动过程中频繁获取视图的效率,使用起来也极其方便:

ImageView bananaView = ViewHolder.get(convertView, R.id.banana); 
TextView phoneView = ViewHolder.get(convertView, R.id.phone); 
BananaPhone bananaPhone = getItem(position); 
phoneView.setText(bananaPhone.getPhone());

(3)Kotlin 扩展函数 findViewOften()

这里Kotlin 实现 ViewHolder 的扩展函数和上面的变种使用的同一种思路,但得益于 Kotlin 语言提供的特性,实现和使用起来更加方便流畅,甚至都感觉不到 ViewHolder 这种特殊机制的存在:

fun<T : View> View.findViewOften(viewId:Int): T {
    varviewHolder: SparseArray<View> = tagas? SparseArray<View> ?: SparseArray()
    tag = viewHolder
    varchildView: View? = viewHolder.get(viewId)
    if(null== childView) {
        childView = findViewById(viewId)
        viewHolder.put(viewId, childView)
    }
    returnchildView as T
}

这里实现了一个 View 的扩展函数 findViewOften(viewId: Int) 意味着在需要频繁寻找一个视图的子视图的情况下使用,这样我们在 Item 中就可以这样写了:

val subTitle: TextView = convertView.findViewOften(R.id.list_item_subtitle)
subTitle.text = itemData.subTitle

由于 Kotlin 提供类型推断功能,所以 findViewOften 的返回值不用手动转换或者手动指定泛型类型。所以利用 Kotlin 的语言特性,为 View 扩展一个方法,从此再也不用繁琐的定义 Viewholder 了,使用的时候也是如此的顺畅。

(4)RecycleView 中的 ViewHolder

最后,不得不提一下在 RecycleView 应该怎么办,因为在 RecycleView 的机制里面,在创建 Item 的 View 的时候,必须创建一个 RecyclerView.ViewHolder 并且返回。对于我们上面那么完美的封装, Google 这明显是在帮倒忙,还好这忙虽然帮倒了,不过还不至于无法挽回。

如果大家在使用 RecycleView 还想使用本文提供的方法的话,可以参考下面的方式实现:提供一个 RecyclerView.ViewHolder 默认实现类,该类提供一个通过 id 获取视图的方法,在创建 Item 的 View 的时候默认都返回这个类的实例。

class MyViewHolder(val convertView: View) : RecyclerView.ViewHolder(convertView) {
        fun <T : View> findView(viewId: Int): T {
        return convertView.findViewOften(viewId)
    }
}

如果不想 MyViewHolder 的外部有不需要的依赖,可以将 findViewOften 直接实现在 MyViewHolder 里面。

感谢优秀的你跋山涉水看到了这里,欢迎关注下让我们永远在一起!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容