Kotlin Android擴展和findViewById說再見

本文鏈接
本文結合自己的感受,做一下簡單的翻譯。原文作者也是《Kotlin for Android developer》的作者。此譯文供大家學習參考之用。

你們大概已經厭倦了日復一日使用findViewById來獲取Android的頁面元素,或者很可能你們已經放棄這樣,使用聞名的ButterKnife庫來實現。接下來你會喜歡Kotlin Android 擴展庫的。

Kotlin Android 擴展庫是什么?

Kotlin Android 擴展庫是另外一種Kotlin常規插件,它使用一種神奇的方式,讓你從Activity、Fragment和View這些元素集合中無縫獲取view元素。這個插件生成的代碼讓你訪問布局文件中的元素,就像訪問屬性一樣,可以直接使用布局文件中的ID名稱訪問。它也構建了一個view緩存,當你第一次使用這個屬性的時候,它會去做findViewById操作。但是第2次,這個就直接從緩存中獲取view,所以訪問起來就更快。

怎么使用他們

讓我們看一下使用起來多簡單。我先用一個Activity來做第一個例子:

在我們代碼中集成Kotlin Android擴展庫

雖然這個擴展插件將要集成到主庫中(你不必新安裝一個),但是目前,如果你要使用它,你不得不在Android 模塊配置中添加一個擴展配置。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

這些就是你全部要做的?,F在你就可以開始用它工作了。(不知道是不是因為版本的問題,配置貌似沒有這么簡單有一些小坑,項目和App下面需要重復配置kotlin的依賴)

從布局XML中獲取到頁面元素

此時,在你的Activity中獲取頁面元素就和直接在XML中使用元素ID一樣簡單。想象一下你有如下的布局XML文件:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
        android:id="@+id/welcomeMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello World!"/>
 
</FrameLayout>

你可以看到TextView的ID是welcomeMessage。然后到MainActivity中代碼如下:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
 
    welcomeMessage.text = "Hello Kotlin!"
}

這樣你就能使用它了,你需要一個特殊的import語句(如下面所寫),但是IDE能自動導入它,不能再簡單了哦。

import kotlinx.android.synthetic.main.activity_main.*

我上面提到代碼,其實生成的代碼包含頁面元素緩存,因此你再次獲取這個頁面元素的時候,就不需要再使用findViewById方法了。

讓我們看一下實際使用情況吧。

Kotlin Android 神奇的擴展支持

當你開始用Kotlin工作,在使用其中的某些特性的時候,你會有興趣去了解那些生成的字節碼。
這里有一個強大的操作,在AS的菜單中,Tools –> Kotlin –> 顯示Kotlin字節碼。如果你點擊它,你會看到你打開的已經編譯過的Kotlin文件生成的字節碼。這些字節碼對大部分人來說未必有用,但是有另外的選項就是反編譯。(在AS Kotlin Bytecode 窗口里面有反編譯按鈕)這樣你會看到一個由Kotlin生成的,用Java表示的字節碼。因此你能了解更多Kotlin和Java等價的寫法。我正想在我的Activity這樣做,然后看一下Kotlin擴展插件生成的kotlin。(比對Java和Kotlin等價的代碼可以讓從Java轉換到Kotlin的使用過程中,幫助大家更好的理解Kotlin)

下面就是有趣的部分之一:

private HashMap _$_findViewCache;
...
public View _$_findCachedViewById(int var1) {
   if(this._$_findViewCache == null) {
      this._$_findViewCache = new HashMap();
   }
 
   View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
   if(var2 == null) {
      var2 = this.findViewById(var1);
      this._$_findViewCache.put(Integer.valueOf(var1), var2);
   }
 
   return var2;
}
 
public void _$_clearFindViewByIdCache() {
   if(this._$_findViewCache != null) {
      this._$_findViewCache.clear();
   }
 
}

這些就是我們之前說的頁面元素緩存。

當我們想要獲取一個頁面元素的時候,首先會試圖在緩存中找到它。如果不在緩存中,它會直接取這個頁面元素,并且把這個頁面元素緩存起來。其實就這么簡單。同時,也添加了一個清空緩存的方法clearFindViewByIdCache。當你重新構建頁面元素,這些舊的頁面元素不再有效的時候,你可以用它。

然后下面這行:

welcomeMessage.text = "Hello Kotlin!"

被轉換成如下:

((TextView)this._$_findCachedViewById(id.welcomeMessage)).setText((CharSequence)"Hello Kotlin!");

因此這些屬性不是真實的,這個插件不是用來生成每個頁面元素屬性的。在編譯后代碼被替換成訪問頁面元素緩存,并且調用相應方法及轉成適合的類型。

在Fragment上使用Kotlin Android擴展庫

這個插件也能在Fragment上面使用。
Fragment的情況是這些頁面元素會被重新生成,但是Fragment實例會被保持。然后會發生什么呢?這個意味著緩存中的頁面元素不再長期有效。
讓我們看一下在Fragment中插件生成的代碼。我先創建一個簡單的Fragment,使用簡單的布局XML,就如下面寫的:

class Fragment : Fragment() {
 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment, container, false)
    }
 
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        welcomeMessage.text = "Hello Kotlin!"
    }
}

在onViewCreated方法中,我設置了TextView的文本。那這些生成的字節碼是什么樣的?基本上和Activity中的一樣,有些許不同如下:

// $FF: synthetic method
public void onDestroyView() {
   super.onDestroyView();
   this._$_clearFindViewByIdCache();
}

當這些Fragment開始銷毀,這個方法會調用clearFindViewByIdCache,因此我們這樣使用是安全的。

在自定義View上使用Kotlin Android擴展庫

在自定義視圖上也是類似的方式。我們有一個試圖如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
    
    <ImageView
        android:id="@+id/itemImage"
        android:layout_width="match_parent"
        android:layout_height="200dp"/>
    
    <TextView
        android:id="@+id/itemTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
 
</merge>

我創建一個非常簡單的自定義視圖,使用@JvmOverloads注解生成一個使用新intent的構造方法。

class CustomView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
 
    init {
        LayoutInflater.from(context).inflate(R.layout.view_custom, this, true)
        itemTitle.text = "Hello Kotlin!"
    }
}

在上面的例子中,我改變了itemTitle的文本。生成代碼試圖在緩存中獲取到頁面元素。不需要再次復制相同的代碼,但是你能看到文本的變化。

((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");

贊!在自定義視圖中我們也只是第一次使用findViewById方法。

從其他視圖獲取一些頁面元素

另外Kotlin Android 擴展庫提供了從其他視圖使用屬性直接訪問的能力。我使用和前面相似的布局,假設通過一個Adapter實例來渲染頁面。

你就能通過擴展庫直接訪問這個子視圖:

val itemView = ...
itemView.itemImage.setImageResource(R.mipmap.ic_launcher)
itemView.itemTitle.text = "My Text"

雖然插件能通過import幫你引入,還是有一些不同。

import kotlinx.android.synthetic.main.view_item.view.*

還有一些你需要了解的:
? 在編譯時,你能從其他視圖引用它的任意子元素。這意味著你能從這個視圖引用非直接子元素的。但是在執行時由于插件試圖獲取不存在的頁面元素時可能失敗。
? 這個例子,這個頁面元素沒有被緩存在Activity和fragment中。

為什么呢?和前面的例子截然不同,這里插件沒有使用緩存來替換生成代碼。如果你查看插件生成代碼,你可以看到當它從視圖中獲取屬性,看到是如下代碼:

((TextView)itemView.findViewById(id.itemTitle)).setText((CharSequence)"My Text");

如你看到的,這次不是從緩存中獲取的。要小心,如果你的視圖是復雜的并且使用的是Adapter。它可能對性能有影響。
或者可以使用Kotlin1.1.4。

在Kotlin1.1.4中使用Kotlin Android擴展庫

從新版Kotlin開始,Android擴展庫結合了一些新的有趣的特性:在任何類中緩存(包括ViewHolder),新的注解標記@Parcelize。有辦法自定義生成的緩存。馬上你就能看到,但是你必須知道,這些新的特性不是最終版本,因此你必須在build.gradle中增加如下配置:

androidExtensions {
    experimental = true
}

在ViewHolder或者其他自定義類中使用擴展庫

現在你能使用簡單的方法在任何類中構建緩存。只有一件必須做的事情,你的類必須實現接口LayoutContainer。這個接口提供了讓插件找到頁面子元素的方法。想象一下我有一個ViewHolder持有之前例子類似的布局。

你只需要這樣做:

class ViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), 
        LayoutContainer {
 
    fun bind(title: String) {
        itemTitle.text = "Hello Kotlin!"
    }
}

這給containerView是我們重寫自LayoutContainer接口的一部分。但是這是你所有你需要做的。至此,你能直接訪問這些頁面元素了,不必預先獲取itemView,來訪問其子元素了。

而且,你檢查一下生成的代碼,你會看到從緩存中獲取View。

((TextView)this._$_findCachedViewById(id.itemTitle)).setText((CharSequence)"Hello Kotlin!");

我在ViewHolder中使用它,但是你能看到它也能被用來在任何其他類上面使用。

用Kotlin Android擴展庫來實現Parcelable接口

使用新的 @Parcelize注解,你能用一種簡單的方式讓任何類都實現Parcelable接口。
你只需要加注解,插件會做所有的臟活累活:

@Parcelize
class Model(val title: String, val amount: Int) : Parcelable

然后,你知道你能加這個對象到任何intent中

val intent = Intent(this, DetailActivity::class.java)
intent.putExtra(DetailActivity.EXTRA, model)
startActivity(intent)~~~

并且在任何點從intent中接收這個對象(這個例子是在目標Activity內):

~~~ ruby
val model: Model = intent.getParcelableExtra(EXTRA)~~~

#### 自定義緩存構建方式

一個新的特性被包含在實驗性配置中,一個新的叫@ContainerOptions注解。這個注解允許你自定義緩存構建,甚至可以在創建的時候阻止一個類使用緩存。默認,它會使用Hashmap進行緩存,在我們看之前。但是在Android框架中會使用SparseArray來替換,在相同情況下更有效率。或者,有一些理由,你不想緩存一些類,你也可以選擇這樣做。

這是怎么使用的:

~~~ ruby
@ContainerOptions(CacheImplementation.SPARSE_ARRAY)
class MainActivity : AppCompatActivity() {
...
}

當然,緩存方式可選項如下:

public enum class CacheImplementation {
    SPARSE_ARRAY,
    HASH_MAP,
    NO_CACHE;
 
    ...
}

結論

用Kotlin你能看到獲取Android頁面元素是多么簡單。用簡單的插件,我們能忘掉所有那些從視圖中獲取元素的可怕代碼。插件會幫助我們創建必要的元素,并轉成對的類型,還沒有問題。

而且Kotlin1.1.4加了一些有趣的特性,對一些使用場景有幫助,是之前的插件版本沒有覆蓋到的。

最后Kotlin for Android develop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容