原文標題: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代碼。