在 Kotlin1.1.4版本 發佈後,原作者依據 Kotlin 新版本的一系列新特性和讀者的提問對幾個月前寫文章進行更新。這篇是重寫的KAD04。在這篇重寫的文章中,他涵蓋了所有KAE(1.1.4版本前後)可以完成的事情。現在你會喜歡在任何類(不只是activity, fragment 或 vie... ...
時間:Aug 16, 2017
原文鏈接:https://antonioleiva.com/kotlin-android-extensions/
在 Kotlin1.1.4版本 發佈後,原作者依據 Kotlin 新版本的一系列新特性,以及有讀者關於如何在 Fragment 和 custom view 中使用Kotlin 等等向他提問,原作者決定針對這些內容進行更新、重寫幾個月的文章。
在這篇重寫的文章中,他涵蓋了所有KAE(1.1.4版本前後)可以完成的事情。現在你會喜歡在任何類(不只是activity, fragment 或 view)使用它們,包括一個新的註釋來實現Parcelable。
你可能會厭倦日復一日地使用findViewById來恢復Androidview。或許你已放棄了,並開始使用著名的Butterknife庫。那麼你會喜歡上Kotlin Android Extensions。
Kotlin Android Extensions:這是什麼?
Kotlin Android Extensions是Kotlin的一個插件,它包含在普通的那個插件中,這就允許以驚人的無縫方式從Activitie,Fragment和View中恢復View。
該插件將生成一些額外的代碼,允許你訪問佈局XML的View,就像它們是在佈局中定義的屬性一樣,你可以使用 id 的名稱。
它還構建本地視圖緩存。所以首次使用一個屬性時,它會做一個普通的findViewById。而接下來,View則是從緩存中恢復,因此訪問速度更快。
怎樣使用它們
讓我們看看它的使用是多麼容易。我會以一個Activity做第一個例子:
將Kotlin Android Extensions集成到我們的代碼中
雖然這個插件被集成到普通的插件(你不需要安裝新的插件)中,但是,你要使用它,還必須在Android模塊中添加額外的應用:
1 apply plugin: 'com.android.application' 2 apply plugin: 'kotlin-android' 3 apply plugin: 'kotlin-android-extensions'
只需要做這些,可以開始使用它了。
從XML中恢復 View
從這一刻起,恢復View就像將你在XML中定義的View ID直接用於你的Activity一樣簡單。
假設你有一個這樣的XML:
1 <?xml version="1.0" encoding="utf-8"?> 2 <FrameLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent"> 6 7 <TextView 8 android:id="@+id/welcomeMessage" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:layout_gravity="center" 12 android:text="Hello World!"/> 13 14 </FrameLayout>
如你所見,TextView
有welcomeMessage ID。
在你的 MainActivity 中,僅僅需要這樣編寫:
1 override fun onCreate(savedInstanceState: Bundle?) { 2 super.onCreate(savedInstanceState) 3 setContentView(R.layout.activity_main) 4 5 welcomeMessage.text = "Hello Kotlin!" 6 }
為了能夠使用它,你需要如下的import,而IDE能夠自動加入它。不是很容易嗎?
1 import kotlinx.android.synthetic.main.activity_main.*
如上所述,生成的代碼將包括View緩存,因此再次詢問視圖,就不需要另一個findViewById。
讓我們看看其背後都是什麼。
Kotlin Android Extensions背後的魔力
在你開始使用Kotlin時,理解所使用特性時生成的位元組碼是非常有趣。這也有助於你瞭解在你的決定背後隱藏的成本。
在Tools-->Kotlin下,有一個強大的功能,稱為顯示Kotlin位元組碼(Show Kotlin Bytecode)。如果你點擊它,你將看到當你打開的類文件編譯後生成的位元組碼。
對大多數人來說,位元組碼並不是很有用,但是還有另一個選擇:Decompile(反編譯)。
這將顯示由Kotlin生成的位元組碼的Java表示。所以你可以或多或少地瞭解你寫的Kotlin代碼對應Java的代碼。
在我生成的Activity中使用它,並查看由Kotlin Android Extensions生成的代碼。
有趣的是這一個:
1 private HashMap _$_findViewCache; 2 ... 3 public View _$_findCachedViewById(int var1) { 4 if(this._$_findViewCache == null) { 5 this._$_findViewCache = new HashMap(); 6 } 7 8 View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1)); 9 if(var2 == null) { 10 var2 = this.findViewById(var1); 11 this._$_findViewCache.put(Integer.valueOf(var1), var2); 12 } 13 14 return var2; 15 } 16 17 public void _$_clearFindViewByIdCache() { 18 if(this._$_findViewCache != null) { 19 this._$_findViewCache.clear(); 20 } 21 22 }
這就是我們正在談論的視圖緩存(View Cache)。
在要求查看時,首先會嘗試在緩存中找。如果它不存在,它會查找它,並將其添加到緩存。非常簡單。
此外,它還添加了一個清除緩存的功能:clearFindViewByIdCache
。如果你必須重建視圖,因為舊視圖將不再有效,就可以使用它。
那麼這一行:
1 welcomeMessage.text = "Hello Kotlin!"
則轉換為:
1 ((TextView)this._$_findCachedViewById(id.welcomeMessage)).setText((CharSequence)"Hello Kotlin!");
因此,屬性不是真實的,插件不會為每個視圖生成屬性。在編譯期間,只需要替換代碼即可訪問視圖緩存,將其轉換為正確的類型並調用該方法。
Fragment的 Kotlin Android Extensions
這個插件也能夠用於Fragment。
Fragment的問題是View可以重新創建,而Fragment實例卻保持有效。這會怎麼樣?這就意味著緩存中的視圖將不再有效。
如果我們把View移動到Fragment中,我們來看看生成的代碼。這是我創建這個簡單的Fragment,它使用與上面寫的相同的XML:
1 class Fragment : Fragment() { 2 3 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 4 return inflater.inflate(R.layout.fragment, container, false) 5 } 6 7 override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 8 super.onViewCreated(view, savedInstanceState) 9 welcomeMessage.text = "Hello Kotlin!" 10 } 11 }
在onViewCreated
中,我再次更改TextView
的文本。生成怎樣的位元組碼呢?
一切都與Activity中的一樣,僅有一個微小的區別:
1 // $FF: synthetic method 2 public void onDestroyView() { 3 super.onDestroyView(); 4 this._$_clearFindViewByIdCache(); 5 }
當View被銷毀時,此方法將調用clearFindViewByIdCache
,所以我們是安全的!
自定義View的Kotlin Android extensions
在自定義視圖下,Kotlin Android extensions非常類似。假設我們有這樣的View:
1 <merge xmlns:android="http://schemas.android.com/apk/res/android" 2 android:orientation="vertical" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent"> 5 6 <ImageView 7 android:id="@+id/itemImage" 8 android:layout_width="match_parent" 9 android:layout_height="200dp"/> 10 11 <TextView 12 android:id="@+id/itemTitle" 13 android:layout_width="match_parent" 14 android:layout_height="wrap_content"/> 15 16 </merge>
我創建一個非常簡單的自定義視圖,並使用@JvmOverloads註釋的新Intent生成構造函數:
1 class CustomView @JvmOverloads constructor( 2 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 3 ) : LinearLayout(context, attrs, defStyleAttr) { 4 5 init { 6 LayoutInflater.from(context).inflate(R.layout.view_custom, this, true) 7 itemTitle.text = "Hello Kotlin!" 8 } 9 }
在上面的示例中,我將文本更改為itemTitle
。生成的代碼應該嘗試從緩存中查找View。再次複製所有相同的代碼已無意義了,但是你可以在更改文本的行中看到這一點:
1 ((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
太棒了!在自定義View中,我們只是首次調用findViewById
。
從另一個View中恢復View
Kotlin Android Extensions提供的最後一個選擇是直接從另一個視圖使用屬性。
我用與上節非常相似的佈局。假設一下這是在適配器(Adapter)中對實例進行inflate。
使用此插件,你還可以直接訪問子視圖(subview):
1 val itemView = ... 2 itemView.itemImage.setImageResource(R.mipmap.ic_launcher) 3 itemView.itemTitle.text = "My Text"
雖然這個插件會幫你填寫import
,但是在這方面還是有點不同:
1 import kotlinx.android.synthetic.main.view_item.view.*
這裡有幾件事情,你需要瞭解:
- 在編譯時,你可以從任何其他視圖(View)引用任何視圖。 這意味著你可以引用一個視圖,該視圖不是其的直接子節點。 但是,當試圖嘗試恢復不存在的視圖時,執行會失敗。
- 在這種情況下,視圖不像Activity和Fragment那樣被緩存。
為什麼會這樣?與之前的情況相反,這裡的插件沒有地方可以為緩存生成所需的代碼。
如果你再次查看代碼,它是當從視圖調用屬性時由插件生成的,你會看到:
1 ((TextView)itemView.findViewById(id.itemTitle)).setText((CharSequence)"My Text");
如你所見,沒有調用緩存。如果你的視圖很複雜,而且你是在適配器中使用,請註意,它可能會影響性能。
或者你可以選擇:Kotlin 1.1.4
版本1.1.4中的Kotlin Android Extensions
Kotlin新版本中,Android Extensions已經引入了一些新的有趣的功能:任何類中的緩存(有趣的包括ViewHolder
)和一個新的@Parcelize
註釋。還有一種方法可以自定義生成的緩存。
稍後,我們會看到它們,但你需要知道這些功能並不是最終的版本,所以你需要將它們添加到build.gradle中來啟動它們:
1 androidExtensions { 2 experimental = true 3 }
在ViewHolder(或任何自定義類)中使用它
現在,你可以通過簡單的方式在任何類中構建緩存。唯一要求是你的類要實現LayoutContainer
介面。該介面將提供一個視圖,它是由插件查找的子視圖。假設,我們有一個ViewHolder
,它保持前面示例中講述的佈局視圖。你只需要做:
1 class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), 2 LayoutContainer { 3 4 fun bind(title: String) { 5 itemTitle.text = "Hello Kotlin!" 6 } 7 }
containerView
是我們從LayoutContainer
介面覆蓋的。但這就是你需要的。
之後,你就可以直接訪問視圖,無需使用前置itemView
來訪問子視圖。
另外,如果你檢查生成代碼,你會看到它從緩存中獲取視圖:
1 ((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");
這裡,我已經在ViewHolder上使用了它,但是你可以看到這是通用的,完全可以在任何類中使用。
Kotlin Android Extension實現Parcelable
用新的@Parcelize
註釋,你可以以非常簡單的方式使任何類都能實現Parcelable
。
你只需要添加註釋,插件就會完成所有複雜的工作:
1 @Parcelize 2 class Model(val title: String, val amount: Int) : Parcelable
然後,如你所知,你可以將對象添加到任何intent中:
1 val intent = Intent(this, DetailActivity::class.java) 2 intent.putExtra(DetailActivity.EXTRA, model) 3 startActivity(intent)
並且在任何位置(在這種情況下是在在目標Activity)上,從intent恢復對象:
1 val model: Model = intent.getParcelableExtra(EXTRA)
自定義構建緩存
在這組實驗中包含的新功能是一個新註釋@ContainerOptions。這允許你以自定義方式構建緩存,甚至阻止類創建它。
預設情況下,它將用Hashmap,如我們之前看到的那樣。但是,這可以用Android框架的SparseArray來改變,這在某些情況下可能會更有效率。如果由於某種原因,你不需要一個類的緩存,你也可以使用該選項。
這是它的用法:
1 @ContainerOptions(CacheImplementation.SPARSE_ARRAY) 2 class MainActivity : AppCompatActivity() { 3 ... 4 }
目前,已有的選擇是:
1 public enum class CacheImplementation { 2 SPARSE_ARRAY, 3 HASH_MAP, 4 NO_CACHE; 5 6 ... 7 }
結論
你已經看到Kotlin處理Android視圖的如此簡易。通過一個簡單的插件,我們可以拋棄那些在inflation後視圖恢復的所有可怕的代碼。此插件將為我們創建所需的屬性,而不會出現任何問題。
此外,Kotlin 1.1.4增加了一些有趣的特性,這些特性在以前的插件沒有覆蓋的情況下,是非常有用。