原文标题: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中,我们这样做:
1 fab.onClick { fabClick() }
这里我用Anko函数,但是用setOnclickListener会更简单一些。
至于恢复columns的值,仅在onCreate中是必须的,所以我将它移到声明那里。但是,类似的事情可以通过属性委托(property delegation)来实现:
1 private val columns by lazy { resources.getInteger(R.integer.num_columns) }
在Activity已经实例化和我们可以访问resource后,在调用属性时,lazy委托才赋值。
属性声明
在Java语言中,必须在Activity已经准备好后才能对field进行赋值。但是,如果我们不想处理不必要的null和不确定变量,那么在Kotlin语言中,在创建属性时就需要有值。所以在声明时就直接赋值是非常通用的做法。
再就是,许多这些属性有需要上下文的问题。所以在此,lazy是十分有用的:
1 private val dribbblePrefs by lazy { DribbblePrefs.get(ctx) } 2 private val designerNewsPrefs by lazy { DesignerNewsPrefs.get(ctx) }
当然,这些声明可以与我们需要的一样复杂。如:DataManager需要扩展类和重载方法:
1 private val dataManager by lazy { 2 object : DataManager(this, filtersAdapter) { 3 override fun onDataLoaded(data: MutableList<out PlaidItem>?) { 4 feedAdapter.addAndResort(data) 5 checkEmptyState() 6 } 7 } 8 }
这样一来,我们就可以只在声明部分见到对属性的声明,而不是在onCreate中间进行声明。加之,我们可以确保在使用它们时,它们不为null,所以就可以省去不必要的NullPointerException。
标准函数的使用
Kotlin语言标准库提供了一套很好、十分有用的函数。关于标准库,你可以阅读Cedric的文章第一部分和第二部分。
例如,我们有apply()函数,它是所为调用它的对象扩展函数运行的,其返回同一个对象。这方面的一个完整例子是展显(inflate)no_filters ViewStub。首先,说明为lazy,所以直到调用stub时才展显(inflate),其次,对这个展显(inflation)结果进行初始化:
1 private val noFilterEmptyText by lazy { 2 // create the no filters empty text 3 (stub_no_filters.inflate() as TextView).apply { 4 ... 5 onClick { drawer.openDrawer(GravityCompat.END) } 6 } 7 }
如你所见,这个函数应用在展显(inflation)结果,apply()赋值给noFilterEmptyText,返回相同的对象。另一个很好的例子是在代码内部。SpannableStringBuilder就是如此:
1 text = SpannableStringBuilder(emptyText).apply { 2 // show an image of the filter icon 3 setSpan(ImageSpan(ctx, R.drawable.ic_filter_small, ImageSpan.ALIGN_BASELINE), 4 filterPlaceholderStart, 5 filterPlaceholderStart + 1, 6 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 7 // make the alt method (swipe from right) less prominent and italic 8 setSpan(ForegroundColorSpan( 9 ContextCompat.getColor(ctx, R.color.text_secondary_light)), 10 altMethodStart, 11 emptyText.length, 12 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) 13 setSpan(StyleSpan(Typeface.ITALIC), 14 altMethodStart, 15 emptyText.length, 16 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 17 }
apply()函数对我们的视图初始化也很有用。首先,明显地将代码拆分为代码块,更易阅读。其次,在类内部执行代码,所以可以使用对象的所有public方法,而无需在前面添加对象名称。
1 stories_grid.apply { 2 adapter = feedAdapter 3 val columns = resources.getInteger(R.integer.num_columns) 4 val gridManager = GridLayoutManager(ctx, columns).apply { 5 setSpanSizeLookup { pos -> if (pos == feedAdapter.dataItemCount) columns else 1 } 6 } 7 layoutManager = gridManager 8 addOnScrollListener { recycler, dx, dy -> 9 gridScrollY += dy 10 if (gridScrollY > 0 && toolbar.translationZ != -1f) { 11 toolbar.translationZ = -1f 12 } else if (gridScrollY == 0 && toolbar.translationZ != 0f) { 13 toolbar.translationZ = 0f 14 } 15 } 16 addOnScrollListener(object : InfiniteScrollListener(gridManager, dataManager) { 17 override fun onLoadMore() = dataManager.loadAllDataSources() 18 }) 19 setHasFixedSize(true) 20 }
这里将adapter(适配器)、layout manager(布局管理器)和listener(侦听器)加到RecyclerView中。大家见过由Java语言的getter和setter方法所产生的综合属性的用法。而我们只layoutManager = gridManager,替代了setLayoutManager(gridManager)。
Lambda表达式
虽然无处不在使用函数,但是,还是有一些可以进行简化地方。可以通过用lambda表达式来代替在Java语言中需要创建对象才能进行的调用。非常好的例子是closeDrawerRunnable。用Java语言需要这样编写:
1 final Runnable closeDrawerRunnable = new Runnable() { 2 @Override 3 public void run() { 4 drawer.closeDrawer(GravityCompat.END); 5 } 6 }; 7 ... 8 drawer.postDelayed(closeDrawerRunnable, 2000); 9 };
而用Kotlin语言是这样:
1 val closeDrawerRunnable = { drawer.closeDrawer(GravityCompat.END) } 2 ... 3 drawer.postDelayed(closeDrawerRunnable, 2000)
之前,我们见过onClick的例子,同样也可以帮助setOnApplyWindowInsetsListener:
1 drawer.setOnApplyWindowInsetsListener { v, insets -> 2 ... 3 }
4 无效处理
Kotlin语言的另一个出色的特性是提升处理无效的方法。原本为此需要大量的代码。例如animateToolbar方法,为了确保TextView处理的是non-null,在Java语言中,需要这样做:
1 View t = toolbar.getChildAt(0); 2 if (t != null && t instanceof TextView) { 3 TextView title = (TextView) t; 4 ... 5 }
而用Kotlin语言,可以这样:
1 val title = toolbar.getChildAt(0) as? TextView 2 title?.apply { 3 ... 4 }
现在的优点是,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 size(APK大小) | 4.7MB | 4.1MB | +14% |
Method count(方法数) | 41615 | 30129 | +38% |
Kotlin语言编译器现有的主要问题是,不能局部编译,即使修改一行代码,它也要对所有类进行重新编译。在将来,这些都会改进,但是这就是现状。
如你所见,Kotlin语言 + Anko库增加约11000个方法。Anko库十分庞大(有大于3000个方法),如果不使用它的核心部分,可以想象要自己创建多少函数啊。对比如下:
总结
采用Kotlin语言编程是非常愉快的。可通过较少的代码干更多的工作。如果整个APP都采用Kotlin语言开发,就可以摆脱更多的公式化代码,这样本例将会获得更多的提升。但这已经是帮助理解Kotlin语言在那些方面可以提升代码的可读性和减少代码的好方法。
在我持续转换该APP到Kotlin语言,我会发现更多的有趣事情可告诉大家。敬请关注新文章!同时,大家还可以持续通过我的书和其它文章学习Kotlin语言。当然,大家也可以阅读完整的HomeActivity代码。