Kotlin Android扩展和findViewById说再见

96
逸群_9c5c
2017.11.21 16:04* 字数 2406

本文链接
本文结合自己的感受,做一下简单的翻译。原文作者也是《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'

这些就是你全部要做的。现在你就可以开始用它工作了。(不知道是不是因为版本的问题,配置貌似没有这么简单有一些小坑,项目和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

日记本
Web note ad 1