Kotlin — Part 4:RecyclerView— Kotlin 委托适配器&数据类

在这第四个部分,我们将介绍的 Kotlin 主题:

  • 初始化函数
  • 对象表达式
  • 单个表达式
  • 数据类
  • 分类
  • List & Lambdas(介绍)

全部章节:

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

创建 NewsAdapter

我们将为 RecyclerView创建一个新的适配器,在这种情况下我们将使用一个模式叫做” Delegate Adapter”,它是我看的这篇文章受的启发.

我们的 Adapter 将是一个委托列表适配器,它负责知道 RecyclerView 如何加载和返回一个指定的 View.一般的做法是接受一个 ViewType 列表,然后委托适配器根据 ViewType 加载和填充视图到 item 中.我们将通过参数将 item 传递给委托适配器,所以 item 可以根据指定的是数据到这个 View.

背后的思想是匹配委托适配器可以匹配指定的 item, 像下面的图片:


感谢 ViewType ,我们的委托适配器知道需要为这个 item 创建那个 View.在这种情况下我们将会有一个列表的 items ,在列表的底部将添加一个加载的 item 来显示正在加载更多的状态.所以我们需要两个委托适配器,一个是新闻,另一个是加载状态.

这个方法给予你许多的灵活性来添加新类型的 View 到你的 RecyclerView. 只需要添加一个新的委托适配器来对应一个新类型的 ViewType.例如, 我们可以为促销活动添加一个新的 ViewType,对应的委托适配器加载一个促销活动视图.

ViewType

是一个用来显示 RecyclerView 上 item 的接口.每一个 item 必须实现这个接口,所以我们可以得到每一个 item 的 ViewType.然后根据这个类型搜索对性的委托适配器.

1
2
3
interface ViewType {
fun getViewType(): Int
}

在我们的 Adapter 将有一个集合存储 ViewType 集合.

1
private var items: ArrayList<ViewType>

在这里我们来存储新闻和加载的 item. 我们新模型将继承 ViewType. 我们将为加载 item 创建一个新的本地 item.

让我们添加Loading视图到我们的 RecyclerView

我们需要做3件事:

  • 这个 ViewType 的类型:这是一个 integer 类型的 id, 来匹配我们的 ViewType 与委托适配器.
  • 这个 ViewType 的 Item:这是一个实现 ViewType接口 的对象,返回它的 ViewType 类型.这将允许我们插入这个 item 到我们的 items 集合,告诉适配器如何渲染这个 View.
  • 加载中的委托适配器:将负责加载我们的视图,返回给我们的 NewsAdapter.

Loading 委托适配器实现

1
2
3
4
5
6
interface ViewTypeDelegateAdapter {

fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder

fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: ViewType)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
class LoadingDelegateAdapter : ViewTypeDelegateAdapter {

override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder = TurnsViewHolder(parent)


override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: ViewType) {

}

class TurnsViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(parent.inflate(R.layout.news_item_loading)) {

}
}

你可以看到我们通过实现” ViewTypeDelegateAdapter”接口,创建了我们自己的委托适配器.这个接口允许我们有一个通用的列表委托适配器,调用这个方法将不需要 NewsAdapter 知道委托适配器具体的实现.一个方法来创建 ViewHolder 另一个绑定数据.

用委托适配器绑定 ViewType 类型

我们将为 ViewType 与委托适配器绑定一个映射.

1
2
3
4
private var delegateAdapters = SparseArrayCompat<ViewTypeDelegateAdapter>()
init {
delegateAdapters.put(AdapterConstants.LOADING, LoadingDelegateAdapter())
}

Init 构造器

init 是Kotlin 中的一个保留字,它代表一个类的构造函数.在这里我们初始化 map ,添加每一个 ViewType 类型和相应的委托适配器.在这种情况下:

AdapterConstants.LOADING > LoadingDelegateAdapter()

在 Kotlin 中创建一个对象不需要使用 new 关键字

加载 ViewType 类型

让我们来创建Loading Item, 我们将插入到 items 集合中,这个 item 将根据位置加载视图.

1
2
3
private val loadingItem = object : ViewType {
override fun getViewType(): Int = AdapterConstants.LOADING
}

将这个 item 添加到items 集合中的第一个位置来渲染它.

1
2
3
4
5
init {
delegateAdapters.put(...)
items = ArrayList()
items.add(loadingItem)
}

对象表达式

在 Kotlin 中有一个叫”Object expressions”的东西,它类似 Java 中的匿名内部类,允许你显示地创建一个新的子类,在这种情况下我们没有创建一个新类文件来创建了一个加载 item.这个语法是非常直观的,你可以看到我们从 ViewType接口派生了一个类并且实现了需要的方法.

单个表达式

这个 getViewType() 函数在内部只有一个表达式.在 Kotlin 中我们可以利用 Kotlin 的优势,来转换这个方法:

1
2
3
override fun getViewType() : Int {
return AdapterConstants.LOADING
}

转变为:

1
override fun getViewType() = AdapterConstants.LOADING

它像我们将 AdapterConstants.LOADING 分配给一个函数.这是一个短的方法做相同实现,更加简洁.你也不需要指定返回类型,它可以从上下文中推断出来.所以它现在看起来这样:

1
2
3
private val loadingItem = object : ViewType {
override fun getViewType() = AdapterConstants.LOADING
}

Commit

所有的代码可以在这里查看:

https://github.com/imuhao/KedditBySteps/commit/74e770e2bd18340e23d42520bdf23fb03b98d05b

新闻 Item 与数据类

在创建我们的新闻委托适配器之前,配置 NewsAdapter 来接受新闻集合,我们需要 UI 对象代表每一个新闻.在 Java 中正常情况下需要这样一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class RedditNewsItem {

private String author;
private String title;

public MyNews(String author, String title) {
this.author = author;
this.title = title;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}
}

Kotlin 再一次为我们带来帮助的是一个叫做” data class”的数据类型,它带来的许多的方便,下面是这个相同的实现:

1
data class RedditNewsItem(var author: String, var title: String) {}

使用者简单的一行就完成了之前 Java 代码相同的功能,它意味着 author 和 title 有自己的 getter 和 setter 方法,它有一个构造器需要两个参数,这是非常惊人的!但是等等!使用这个数据类你会有更多的好处:

  • equals/hashCode方法
  • toString()包含所有的参数
  • copy()方法简单的复制这个对象
  • 还有其它有用的方法在这我们没有说,你可以在这个页面查看更多

Commit:创建数据类

https://github.com/imuhao/KedditBySteps/commit/8367173a606e325bf8366a59041ae8e76f958765

另外我们需要我们的类继承 ViewType,这样它就可以包含在 NewsAdapter 的 items 中作为一个 item.在这种情况下作为一个新的 Item:

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

创建 NewsDelegateAdapter

现在我们已经创建了新闻 Bean 对象,我们需要委托适配器负责创建 View.这有一个我们需要做的预览图:


所以我们需要:

  • 显示一些文本像 title, 头像,评论
  • 一个图片,我们将使用 Picasso
  • 一个显示时间的扩展方法

这里有所有我们创建 NewsDelegateAdapter 的方法.我们将不再说明每一个细节:

让我们重新查看一些 Kotlin 的东西

Picasso 扩展函数

运行我们使用 ImageView 加载一个网络图片

1
2
3
4
5
6
7
fun ImageView.loadImg(imageUrl: String) {
if (TextUtils.isEmpty(imageUrl)) {
Picasso.with(context).load(R.mipmap.ic_launcher).into(this)
} else {
Picasso.with(context).load(imageUrl).into(this)
}
}

时间扩展函数

我们将从 Reddit 接受一个时间,这个时间是一个 long 格式,我们将转换 Long 类型到一个字符串类型,像”3 day and 1 minute age”

1
2
3
fun Long.getFriendlyTime(): String {
// logic here...
}

打开文件” TimeExt.kt”来查看代码,

NewsView Android 扩展

打开文件” NewsDelelgateAdapter.kt”,你将查看我们也使用了 Android Extension, 但是在这种情况下我们添加了 synthetic 包,在结尾添加了一个”view”额外值.

1
import kotlinx.android.synthetic.main.news_item.view.*

这是一个在没有 Activity 或Fragment 上下文时绑定 View 的一个办法.

更新 NewsAdapter 查看新闻

让我们修改 NewsAdapter 接受一个新闻集合,来显示我们的新闻列表和正在加载状态.

Range

Kotlin 允许你通过”1..10”这种表达式简单的创建一个范围的数字(Int,Long 和Char),IntRange 继承 IntProgression实现了 Iterable 接口.感谢这个特征我们可以迭代一个范围的数字,下面是从1到10的代码

1
2
3
for(i in 1..10){
...
}

你可以控制它间隔的步数,或使他递减像从10到1.更多关于这个你可以在这里找到

mutableListOf

这是一个 Kotlin 函数,返回一个 MutableList, 一个可以被修改的集合,在这种情况下我们用来存储适配器中的 news 集合.

1
val news = mutableListOf<RedditNewsItem>()

Lists 函数 & Lambdas

我们将创建一个方法,在晚些时候使用,我们使用 filter 和 map 事件将集合中的 item 转换到另一种类型.

在我们的代码中有一个” getNews”方法,返回一个 RedditNewsItems 集合.为了 过滤和转换我们的列表,我们需要这样做

1
2
3
4
5
6
7
fun getNews(): List<RedditNewsItem> {
return items.filter {
it.getViewType() == AdapterConstants.NEWS
}.map {
it as RedditNewsItem
}
}

Filter

每一个 list 都有一些有用的函数 像” filter”,允许我们使用一些条件过滤集合中的数据.在我们的 items 集合中保存的 ViewType 类型,里面的类型有 News item 或Loading item, 使用 filter 函数,我们确保只返回 news item .

Map

另一个伟大的函数式” map”,他对集合中的每一项进行转换,在这种情况下我们将 ViewType 类型转换成一个 RedditnNewsItem. 另外,我们也返回一个新创建的对象.

Lambdas

1
.map{it as RedditNewsItem}

Map 不是一个新的事物,但是 Kotlin 使它更伟大,因为它允许你在函数名后定义代码块,省略了括号.这个代码块就是 Lambda 表达式,一个不需要定义的函数.

在这里我们没有花更多的时间讲解关于 List 和 Lambdas,但是我想这是一个好的起点来了解如何使用这个伟大的特性,在之后的章节我们将花费更多的时间来谈论关于这个.

结论

我知道我花费了更多的时间来解释委托适配器模式,我考虑到这个模式是一个很优秀的模式,我希望通过这个代码你学习到了 Kotlin 新的特征.

GitHub 仓库地址:https://github.com/imuhao/KedditBySteps