用Kotlin语言重新编写Plaid APP:经验教训(I)

原文标题:Converting Plaid to Kotlin: Lessons learned (Part 1)

原文链接:http://antonioleiva.com/plaid-kotlin-1/

原文作者:Antonio Leiva(http://antonioleiva.com/about/

原文发布:2015-11-03

经常有人问我用Kotlin语言编写Android APP有哪些优点。可问题是我从来没有直接将用Java语言开发的Android APP转到Kotlin语言,所以这是一个很难回答的问题。而且没有将特性置于其相关环境中,仅仅解释大量抽象概念,不是一个谈论编程语言优势的最佳方法。

所以,在测试Plaid APP之后,其开发者Nick Butcher,惊叹APP的精美外观和页面过渡,我想更多的了解它。比用Kotlin语言重新编写APP更好的方法是什么?

我只转换HomeActivity,就对比代码,有显著地提升啊。当然你可以阅读代码得出自己的结论。我首先声明,无论是否会发生,我的主要目标不是将整个APP转换到Kotlin语言。由于转换整个APP工作相当大的,所以不能确定我是否有时间(或需要)这样做。

视图绑定

Nick决定用Butterknife接收视图,它是Java语言的出色解决方案。但是,Kotlin语言提供Kotlin Android Extensions,它可自动地将视图绑定到Activity。这样,我们就节省所有@Bind代码。

然而,我们还需要做一些Butterknife提供的其它事情,如:onClick和资源绑定。对于前者,在Kotlin语言中十分简单,并没有真正地添加太多的公式化代码。在onoCrate中,我们这样做:

 fab.onClick { fabClick() }

这里我用Anko函数,但是用setOnclickListener会更简单一些。

至于恢复columns的值,仅在onCreate中是必须的,所以我将它移到声明那里。但是,类似的事情可以通过属性委托(property delegation)来实现:

 private val columns by lazy { resources.getInteger(R.integer.num_columns) }

在Activity已经实例化和我们可以访问resource后,在调用属性时,lazy委托才赋值。

属性声明

在Java语言中,必须在Activity已经准备好后才能对field进行赋值。但是,如果我们不想处理不必要的null和不确定变量,那么在Kotlin语言中,在创建属性时就需要有值。所以在声明时就直接赋值是非常通用的做法。

再就是,许多这些属性有需要上下文的问题。所以在此,lazy是十分有用的:

 private val dribbblePrefs by lazy { DribbblePrefs.get(ctx) }
private val designerNewsPrefs by lazy { DesignerNewsPrefs.get(ctx) }

当然,这些声明可以与我们需要的一样复杂。如:DataManager需要扩展类和重载方法:

 private val dataManager by lazy {
object : DataManager(this, filtersAdapter) {
override fun onDataLoaded(data: MutableList<out PlaidItem>?) {
feedAdapter.addAndResort(data)
checkEmptyState()
}
}
}

这样一来,我们就可以只在声明部分见到对属性的声明,而不是在onCreate中间进行声明。加之,我们可以确保在使用它们时,它们不为null,所以就可以省去不必要的NullPointerException。

标准函数的使用

Kotlin语言标准库提供了一套很好、十分有用的函数。关于标准库,你可以阅读Cedric的文章第一部分第二部分

例如,我们有apply()函数,它是所为调用它的对象扩展函数运行的,其返回同一个对象。这方面的一个完整例子是展显(inflate)no_filters ViewStub。首先,说明为lazy,所以直到调用stub时才展显(inflate),其次,对这个展显(inflation)结果进行初始化:

 private val noFilterEmptyText by lazy {
// create the no filters empty text
(stub_no_filters.inflate() as TextView).apply {
...
onClick { drawer.openDrawer(GravityCompat.END) }
}
}

如你所见,这个函数应用在展显(inflation)结果,apply()赋值给noFilterEmptyText,返回相同的对象。另一个很好的例子是在代码内部。SpannableStringBuilder就是如此:

 text = SpannableStringBuilder(emptyText).apply {
// show an image of the filter icon
setSpan(ImageSpan(ctx, R.drawable.ic_filter_small, ImageSpan.ALIGN_BASELINE),
filterPlaceholderStart,
filterPlaceholderStart + 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// make the alt method (swipe from right) less prominent and italic
setSpan(ForegroundColorSpan(
ContextCompat.getColor(ctx, R.color.text_secondary_light)),
altMethodStart,
emptyText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
setSpan(StyleSpan(Typeface.ITALIC),
altMethodStart,
emptyText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

apply()函数对我们的视图初始化也很有用。首先,明显地将代码拆分为代码块,更易阅读。其次,在类内部执行代码,所以可以使用对象的所有public方法,而无需在前面添加对象名称。

 stories_grid.apply {
adapter = feedAdapter
val columns = resources.getInteger(R.integer.num_columns)
val gridManager = GridLayoutManager(ctx, columns).apply {
setSpanSizeLookup { pos -> if (pos == feedAdapter.dataItemCount) columns else 1 }
}
layoutManager = gridManager
addOnScrollListener { recycler, dx, dy ->
gridScrollY += dy
if (gridScrollY > 0 && toolbar.translationZ != -1f) {
toolbar.translationZ = -1f
} else if (gridScrollY == 0 && toolbar.translationZ != 0f) {
toolbar.translationZ = 0f
}
}
addOnScrollListener(object : InfiniteScrollListener(gridManager, dataManager) {
override fun onLoadMore() = dataManager.loadAllDataSources()
})
setHasFixedSize(true)
}

这里将adapter(适配器)、layout manager(布局管理器)和listener(侦听器)加到RecyclerView中。大家见过由Java语言的getter和setter方法所产生的综合属性的用法。而我们只layoutManager = gridManager,替代了setLayoutManager(gridManager)。

Lambda表达式

虽然无处不在使用函数,但是,还是有一些可以进行简化地方。可以通过用lambda表达式来代替在Java语言中需要创建对象才能进行的调用。非常好的例子是closeDrawerRunnable。用Java语言需要这样编写:

 final Runnable closeDrawerRunnable = new Runnable() {
@Override
public void run() {
drawer.closeDrawer(GravityCompat.END);
}
};
...
drawer.postDelayed(closeDrawerRunnable, 2000);
};

而用Kotlin语言是这样:

 val closeDrawerRunnable = { drawer.closeDrawer(GravityCompat.END) }
...
drawer.postDelayed(closeDrawerRunnable, 2000)

之前,我们见过onClick的例子,同样也可以帮助setOnApplyWindowInsetsListener:

 drawer.setOnApplyWindowInsetsListener { v, insets ->
...
}

4 无效处理

Kotlin语言的另一个出色的特性是提升处理无效的方法。原本为此需要大量的代码。例如animateToolbar方法,为了确保TextView处理的是non-null,在Java语言中,需要这样做:

 View t = toolbar.getChildAt(0);
if (t != null && t instanceof TextView) {
TextView title = (TextView) t;
...
}

而用Kotlin语言,可以这样:

 val title = toolbar.getChildAt(0) as? TextView
title?.apply {
...
}

现在的优点是,apply内代码是一个扩展函数,所以不再需要编写title了。第一行试图转换任何返回值到TextView中。如果子类是空或者不是TextView,title就是null。第二行与if (title != null) title.apply{}相同。只有title不为null,apply()函数才执行。

在整个Activity代码中,可以找到很多地提升之处。尽管,由于这Activity要处理的事情太多了(甚至包括Retrofit客户端的实例化),这些改进并不是非常出色。但是,这是理解用Kotlin语言开发应用的良好开端。

Kotlin语言与Java语言对比数字

最后,我想要分享一些数字,当然由于有一些外部因素的影响,可能不太准确。但是还是可以帮助我们获得一点概念。Kotlin语言并非完美,编译时间就是需要改进的例子。

  Kotlin Java Comparison(对比)
Line count(行数) 576 702 -22%
Character count(字符数) 24001 30589 -27%
Clean compilation(完全编译) 1m 40s 1m 5s +67%
Compilation after 1 line change(修改1行后编译) 29s 10s +190%
APK sizeAPK大小) 4.7MB 4.1MB +14%
Method count(方法数) 41615 30129 +38%

Kotlin语言编译器现有的主要问题是,不能局部编译,即使修改一行代码,它也要对所有类进行重新编译。在将来,这些都会改进,但是这就是现状。

如你所见,Kotlin语言 + Anko库增加约11000个方法。Anko库十分庞大(有大于3000个方法),如果不使用它的核心部分,可以想象要自己创建多少函数啊。对比如下:

用Kotlin语言重新编写Plaid APP:经验教训(I)

总结

采用Kotlin语言编程是非常愉快的。可通过较少的代码干更多的工作。如果整个APP都采用Kotlin语言开发,就可以摆脱更多的公式化代码,这样本例将会获得更多的提升。但这已经是帮助理解Kotlin语言在那些方面可以提升代码的可读性和减少代码的好方法。

在我持续转换该APP到Kotlin语言,我会发现更多的有趣事情可告诉大家。敬请关注新文章!同时,大家还可以持续通过我的书其它文章学习Kotlin语言。当然,大家也可以阅读完整的HomeActivity代码。

上一篇:C语言变参函数的实现原理


下一篇:听说awk语言也可以编写脚本