使用Kotlin Android Extensions时碰到的一个问题

本文记录了使用kotlin android extions时遇到的小问题以及对应的解决方法。

Kotlin Android Extensions

Kotlin Android Extensions是用于Kotlin android开发的插件。

1
apply plugin: 'kotlin-android-extensions'

借助该插件我们在Kotlin代码中不必再使用findViewById(),直接通过view id访问相应View即可。例如:

1
2
3
4
5
6
7
8
9
10
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main_clip.*

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btn_clip_demo.setOnClickListener {}
iv_demo.setImageDrawable(ContextCompat.getDrawable(this, R.color.colorAccent))
}

对Java代码中繁琐的findViewById()方便许多,对吧。

更多用法可以参考这篇文章

遇到的问题

很不幸,使用Kotlin Android Extensions时遇到一个奇怪的问题。布局和代码如下:

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.cm.drawabledemo.MainActivity">

<FrameLayout
android:id="@+id/fl_container"
android:layout_width="0dp"
android:layout_height="300dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/btn_clip_demo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="clip demo"
app:layout_constraintStart_toEndOf="@+id/btn_gravity"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

content_main_clip.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:padding="2dp"
android:id="@+id/iv_demo"
android:layout_width="92dp"
android:layout_height="78dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="104dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btn_clip_demo.setOnClickListener {
fl_container.removeAllViews()
layoutInflater.inflate(R.layout.content_main_clip, fl_container)
iv_demo.setImageDrawable(ContextCompat.getDrawable(this, R.color.colorAccent))
}
}
}

代码功能:点击btn_clip_demo后,将content_main_clip中的内容(其中有一个id为iv_demo的ImageView)重新添加到fl_container,并且将iv_demo设置为红色。

问题描述:第一次点击btn_clip_demoiv_demo被设置成红色。但之后再点击btn_clip_demo,iv_demo没有被设置成红色

问题分析

Android Studio查看Kotlin对应Java代码的方式: Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile

上述kotlin代码对应的Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131296283);
((Button)this._$_findCachedViewById(id.btn_clip_demo)).setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
((FrameLayout)M.this._$_findCachedViewById(id.fl_container)).removeAllViews();
M.this.getLayoutInflater().inflate(2131296285, (FrameLayout)M.this._$_findCachedViewById(id.fl_container));
((ImageView)M.this._$_findCachedViewById(id.iv_demo)).setImageDrawable(ContextCompat.getDrawable((Context)M.this, 2130968614));
}
}));
}

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();
}

}
}

高大上的Kotlin Android Extensions生成的代码其实非常简单,要点如下:

  • View cache策略
  • 自动生成一个HashMap类型的findViewCache,用于缓存访问过的View
  • 自动生成findCachedViewById()和clearFindViewByIdCache()方法
  • Kotlin代码中对view id的直接访问被转换成相应的findCachedViewById()调用
1
2
3
4
5
btn_clip_demo.setOnClickListener {
fl_container.removeAllViews()
layoutInflater.inflate(R.layout.content_main_clip, fl_container)
iv_demo.setImageDrawable(ContextCompat.getDrawable(this, R.color.colorAccent))
}

对照代码不难理解问题的原因在于:

第一次点击btn_clip_demo时,view cache策略并不生效,所以一切正常。第二次点击btn_clip_demo时,view cache策略生效。但view cache策略对我们的场景是错误的(从另一角度讲,可能是我的用法有误?)。从view cache中拿到的iv_demo对象是旧的。关键是,它已经从屏幕上移除,是不可见的。我们给它设置红色当然不起作用!而inflate()操作新添加到布局中的、在屏幕上可见的那个iv_demo,实际上被我们晾在一边。

所以这里我们不得不使用findViewById来获取那个被晾在一边的新的iv_demo。以下是修改后的代码:

1
2
3
4
5
6
btn_clip_demo.setOnClickListener {
fl_container.removeAllViews()
layoutInflater.inflate(R.layout.content_main_clip, fl_container)
val ivDemoNew = findViewById<ImageView>(R.id.iv_demo)
ivDemoNew.setImageDrawable(ContextCompat.getDrawable(this, R.color.colorAccent))
}

这回,无论我们如何点击按钮,iv_demo永远会被正确地设置为红色。