你的位置:首页 > 操作系统

[操作系统]用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中,我们这样做:

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 text3   (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 += dy10     if (gridScrollY > 0 && toolbar.translationZ != -1f) {11       toolbar.translationZ = -1f12     } else if (gridScrollY == 0 && toolbar.translationZ != 0f) {13       toolbar.translationZ = 0f14     }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   @Override3   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? TextView2 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语言并非完美,编译时间就是需要改进的例子。

 KotlinJavaComparison(对比)
Line count(行数)576702-22%
Character count(字符数)2400130589-27%
Clean compilation(完全编译)1m 40s1m 5s+67%
Compilation after 1 line change(修改1行后编译)29s10s+190%
APK sizeAPK大小)4.7MB4.1MB+14%
Method count(方法数)4161530129+38%

 

 

 

 

 

 

 

 

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

 

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

 

总结

 

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

 

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