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'

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容