本文链接
本文结合自己的感受,做一下简单的翻译。原文作者也是《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加了一些有趣的特性,对一些使用场景有帮助,是之前的插件版本没有覆盖到的。