Kotlin — Part 3:扩展函数、Android 扩展、委托属性

在这个部分,我们将会看到不同的 Kotlin 概念,这将帮助我们创建我们的 NewsFragment .在这个部分的结束,你将学到:

  • 扩展函数
  • 参数默认值
  • Android 扩展(视图绑定)
  • 属性委托

全部章节:

Kotlin — Part 0:关于这个系列
Kotlin — Part 1:配置 Android Studio
Kotlin — Part 2:语法,空安全,静态类型
Kotlin — Part 3:扩展函数、Android 扩展、委托属性
Kotlin — Part 4:RecyclerView— Kotlin 适配器委托&数据类
Kotlin — Part 5:Kotlin,RxJava&RxAndroid
Kotlin — Part 6:API-Retrofit&Kotlin)
Kotlin — Part 7:无限滑动:高阶函数& Lambdas
Kotlin — Part 8:方向改变(序列化&数据类)
Kotlin — Part 9:单元测试与 Kotlin(Mockito,RxJava)

Github 仓库:https://github.com/imuhao/KedditBySteps

创建我们的 NewsFragment.kt

让我们创建我们的 NewFragment.kt 文件,它将负责显示Reddit Api 最新的新闻,我们将使用 RecyclerView 来显示它们.

在 Java 中我们通常需要创建一个私有的字段来存储 RecyclerView,当我们inflating 视图的时候访问它.我们尝试在 Kotlin 中这样做.

1
2
3
4
5
6
7
8
9
10
private var newsList: RecyclerView? = null

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
var view = inflater?.inflate(R.layout.fragment_news, container, false)
newsList = view?.findViewById(R.id.news_list)
newsList?.setHasFixedSize(true)
newsList?.layoutManager = LinearLayoutManager(context)

return view
}

这是一个有效的代码,但是它在 Kotlin 中不是最好的实现,所以我们来使它更好.我们来用这个语言来改善这一小块代码.

扩展函数

扩展函数允许我们为一个类添加函数来扩展这个类.这个类可以不属于我们,也不用使用继承.

这是一个非常强大的功能!我们将添加一个新的方法来扩展ViewGroup类.
你应该知道 ViewGroup 是一个 Android SDK 的类,为了inflate 视图我们必须这样做:

1
inflater.inflate(R.layout.news_fragment, container, false)

但是这不是一个直观的方法.这应该是 ViewGroup 可以做的事,像这样:

1
val view = container?.inflate(R.layout.news_fragment)

这像 ViewGroup 本身可以 inflate一个视图.很棒的技巧!但是如何做?让我们创建第一个扩展函数:

在 commons 包下创建一个文件” Extensions.kt“,添加下面的代码:

1
2
3
fun ViewGroup.inflate(layoutId: Int): View {
return LayoutInflater.from(context).inflate(layoutId, this, false)
}

我们所做的事情是在 ViewGroup 中添加一个新方法,但是我们没有修改ViewGroup 类.

这个函数将成为 ViewGroup 的静态方法,你可以从类的任何实例中调用它.像这种:container.inflate(…) 不是 ViewGroup.inflate().这是因为编辑器将为我们创建 *Util class.* 如果你需要在 Java 中使用这个函数需要这样做:

1
2
3
4
// Java
ExtensionsKt.inflate(container, R.layout.news_fragment);
// Kotlin
container?.inflate(R.layout.news_fragment)

在 Kotlin 世界它是一个很方便的方法.我们使用”?”符号是因为我们的container 可以为空,防止我们得到一个 NullPointerException.

这个实用类将会与它的文件名相同,你可以使用注解来修改它的名字:

1
2
3
4
5
6
7
8
9
10
11
@file:JvmName("ExtensionsUtils")

package com.caimuhao.kedditbysteps.commons

import ...

fun ViewGroup.inflate(layoutId: Int): View {
...
}
// Use it in this way in Java:
ExtensionsUtils.inflate(container, R.layout.news_fragment);

让我们返回我们的函数:

1
2
3
fun ViewGroup.inflate(layoutId: Int): View {
return LayoutInflater.from(context).inflate(layoutId, this, false)
}

这个代码块像是你在类中写的新方法,这就是为什么你可以使用” this“关键字访问这个类的实例,还可以访问本地变量” context

这是我们第一个扩展函数,我们知道了更多的概念.我们使用下面的方法更新我们的代码:

1
2
3
4
// 旧代码:
val view = inflater.inflate(R.layout.news_fragment, container, false)
// 使用下面替换:
val view = container?.inflate(R.layout.news_fragment)

但是我们也没有办法使用 attachToRoot 参数来 inflate 布局.让我们来添加到我们的代码.

参数默认值

在 Kotlin 中你可以为函数中的参数添加默认值.让我们来为attachToRoot参数添加默认值:

1
2
3
fun ViewGroup.inflate(layoutId: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layoutId, this, attachToRoot)
}

在这种情况下你如果不指定 attachToRoot 参数它将使用默认值,所以你可以使用不同的方法调用这个函数:

1
2
container?.inflate(R.layout.news_fragment) // 默认: false
container?.inflate(R.layout.news_fragment, true)

Commit

在这里你可以查看这次所有的改变

https://github.com/imuhao/KedditBySteps/commit/e2dfe36311e189980f81d07225a6b1a711354725

Android Extensions

这是 Kotlin 用来替换” findViewById()“方法.Android Extensions 添加了一些方便的扩展属性,允许我们在 Activity 或 Fragment 内部直接访问 view元素.

在开始之前我们来配置我们的项目支持 Android Extensions. 修改我们 app 模块下的 build.gradle 文件,应用 kotlin-android-extensions 插件,重新构建 gradle:

1
2
3
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

这是所有的!让我们来替换 findViewById

NewsFragment Layout:

1
2
3
4
5
6
7
8
9
10
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<android.support.v7.widget.RecyclerView
android:id="@+id/news_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

NewsFragment class:

我们将在我们的 NewsFragment 中 使用 news_list 访问 RecyclerView,在这之前,我们需要导入一个属性,使插件生效.

1
import kotlinx.android.synthetic.main.fragment_news.*

使用这种方法你可以访问你导入的所有元素,为了确认布局已经被加载,我们移动我们的 newsList 赋值时间从 onViewCreate() 方法到 onActivityCreate(),在这里确保视图已经加载.

现在我们可以直接访问 news_list:

1
2
3
4
5
6
7
// 旧代码:
newsList = view?.findViewById(R.id.news_list) as RecyclerView?
newsList?.setHasFixedSize(true)
newsList?.layoutManager = LinearLayoutManager(context)
// 新代码:
news_list.setHasFixedSize(true)
news_list.layoutManager = LinearLayoutManager(context)

注意我们使用的 news_list 是一个不为空的对象,所以我们可以不用使用?符号.

Commit

https://github.com/imuhao/KedditBySteps/commit/9a5fc711181ce32a11c8a510d63012bfb3777783

委托属性

多次使用一个属性时,委托属性是一个很好的方法重用多次循环的行为.在 Kotlin 语言中已经定义了一些常用的委托属性(也可以自己创建).这里我们将使用现有的委托属性:

  • Lazy 属性:它的值只在第一次访问时计算

Lazy

这是一个伟大的属性帮助我们避免初始化我们的 newsList 为空对象.使用 Lazy 我们可以创建一个非空的属性,只在我们第一次使用时初始化

1
2
3
private val newsList by lazy {
news_list
}

这个 lazy 代码快只在我们使用它时执行,在 onActivityCreate() 方法中

1
2
3
4
5
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
newsList.setHasFixedSize(true)
newsList.layoutManager = LinearLayoutManager(context)
}

我们也可以移动初始化代码从 onActivityCreate 到 lazy 代码块:

1
2
3
4
5
private val newsList by lazy {
news_list.setHasFixedSize(true)
news_list.layoutManager = LinearLayoutManager(context)
news_list
}

Commit

https://github.com/imuhao/KedditBySteps/commit/22bd5a789d192275dae300a5d3c1cd3ee484d325