I'm new to Android. I've learned that it is not allowed to access UI components in backgroud threads, but what I've encountered made me confused. I did the following.
(1) I created a new Android Studio project with package name com.example.myapplication
and minimum SDK version 29, using "Empty Activity" template.
(2) I changed activity_main.xml
, adding a button and a TextView, where the android:layout_width
of the TextView was set to wrap_content
.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:id="@ id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
<Button
android:id="@ id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
(3) I changed MainActivity.kt
, adding a click listener to the button, which starts a thread and changes the text of the TextView in the thread.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.textView)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
thread {
textView.text = "Accessing textView from ${Thread.currentThread().name}"
}
}
}
}
(4) I launched the app in an API 31 emulator, and clicked the button. The app crashed with the following error:
2022-03-29 23:48:42.364 24327-24360/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.myapplication, PID: 24327
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
This is the expected behavior. But when I changed the android:layout_width
property of the TextView to match_parent
(or a fixed width, like 100dp
), rebuilt and started the app, and clicked the button again, the text of the TextView was successfully changed.
The thread name indicated that I was definitely accessing the TextView from a background thread.
Why does this happen? Why a TextView's layout_width
property can determine whether accessing UI components from background threads will succeed?
CodePudding user response:
You can definitely access the UI Thread from a background thread.
You should note that the ViewGroup
(LinearLayout
) that holds the TextView is responsible for it's width and height if you set it as match_parent
.
So that means that every dynamic changes to the width or height of the TextView
at runtime MUST run on the UI Thread not from a background thread. That explains this exception
2022-03-29 23:48:42.364 24327-24360/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: Thread-2
Process: com.example.myapplication, PID: 24327
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
The keynote in the above Exception is:
Only the original thread that created a view hierarchy can touch its views
Since it was the UI Thread that created the ViewGroup
(LinearLayout
), only the UI Thread is permitted to run layout changes on the ViewGroup
and it's children Views.
Now the reason the Exception was resolved when you gave the android:layout_width
a finite value such as 100dp
is because the finite value means there won't be any dynamic layout changes for the UI Thread (which created the ViewGroup
) to be worried about.
The only change you are going to make at runtime is the value shown in the TextView
and this would not change it's layout structure (since you've set the android:layout_width
with a finite 100dp
and the android:layout_height
is at wrap_content
).
The only time you should be worried about the android:layout_height
is when you change the font size or TextView
's height dynamically in another thread which is not the UI Thread.
Now to access the UI Thread in another thread. Do this:
button.setOnClickListener {
thread {
runOnUiThread {
textView.text = "Accessing textView from ${Thread.currentThread().name}"
}
}
}
The above snippet accesses the UI Thread from the background thread.
I hope this helps.