Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android UI Not Crashing When Modifying View off UI Thread

Tags:

Scenario:

I ran into a strange issue while testing out threads in my fragment.

I have a fragment written in Kotlin with the following snippet in onResume():

override fun onResume() {
    super.onResume()

    val handlerThread = HandlerThread("Stuff")
    handlerThread.start()
    val handler = Handler(handlerThread.looper)
    handler.post {
        Thread.sleep(2000)
        tv_name.setText("Something something : " + isMainThread())
    }
}

is MainThread() is a function that checks if the current thread is the main thread like so:

private fun isMainThread(): Boolean = Looper.myLooper() == Looper.getMainLooper()

I am seeing my TextView get updated after 2 seconds with the text "Something something : false"

Seeing false tells me that this thread is currently not the UI/Main thread.

I thought this was strange so I created the same fragment but written in Java instead with the following snippet from onResume():

@Override
public void onResume() {
    super.onResume();

    HandlerThread handlerThread = new HandlerThread("stuff");
    handlerThread.start();
    new Handler(handlerThread.getLooper()).post(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            textView.setText("Something something...");
        }
    });
}

The app crashes with the following exception as expected:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7313)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1161)

I did some research but I couldn't really find something that explains this. Also, please assume that my views are all inflated correctly.

Question:

Why does my app not crash when I modify my TextView in the runnable that's running off my UI thread in the Fragment written in Kotlin?

If there's something in some documentation somewhere that explains this, can someone please refer me to this?

I am not actually trying to modify my UI off the UI thread, I am just curious why this is happening.

Please let me know if you guys need any more information. Thanks a lot!

Update: As per what @Hong Duan mentioned, requestLayout() was not getting called. This has nothing to do with Kotlin/Java but with the TextView itself.

I goofed and didn't realize that the TextView in my Kotlin fragment has a layout_width of "match_parent." Whereas the TextView in my Java fragment has a layout_width of "wrap_content."

TLDR: User error + requestLayout(), where thread checking doesn't always occur.

like image 876
Dung Ta Avatar asked Jul 11 '18 00:07

Dung Ta


People also ask

How many threads can modify the UI components of Android?

Seven Threading Patterns in Android.

Can we update UI from thread in Android?

However, note that you cannot update the UI from any thread other than the UI thread or the "main" thread. To fix this problem, Android offers several ways to access the UI thread from other threads. Here is a list of methods that can help: Activity.

Why should you avoid to run non UI code on the main thread?

If you put long running work on the UI thread, you can get ANR errors. If you have multiple threads and put long running work on the non-UI threads, those non-UI threads can't inform the user of what is happening.

What is difference between main thread and UI in Android?

Main Thread: The default, primary thread created anytime an Android application is launched. Also known as a UI thread, it is in charge of handling all user interface and activities, unless otherwise specified. Runnable is an interface meant to handle sharing code between threads. It contains only one method: run() .


1 Answers

The CalledFromWrongThreadException only throws when necessary, but not always. In your cases, it throws when the ViewRootImpl.checkThread() is called during ViewRootImpl.requestLayout(), here is the code from ViewRootImpl.java:

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

And for TextView, it's not always necessary to relayout when we update it's text, we can see the logic in the source code:

/**
 * Check whether entirely new text requires a new view layout
 * or merely a new text layout.
 */
private void checkForRelayout() {
    // If we have a fixed width, we can just swap in a new text layout
    // if the text height stays the same or if the view height is fixed.

    if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
            || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
            && (mHint == null || mHintLayout != null)
            && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
        // Static width, so try making a new text layout.

        int oldht = mLayout.getHeight();
        int want = mLayout.getWidth();
        int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

        /*
         * No need to bring the text into view, since the size is not
         * changing (unless we do the requestLayout(), in which case it
         * will happen at measure).
         */
        makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                      mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                      false);

        if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
            // In a fixed-height view, so use our new text layout.
            if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                    && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                autoSizeText();
                invalidate();
                return; // return with out relayout
            }

            // Dynamic height, but height has stayed the same,
            // so use our new text layout.
            if (mLayout.getHeight() == oldht
                    && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                autoSizeText();
                invalidate();
                return; // return with out relayout
            }
        }

        // We lose: the height has changed and we have a dynamic height.
        // Request a new view layout using our new text layout.
        requestLayout();
        invalidate();
    } else {
        // Dynamic width, so we have no choice but to request a new
        // view layout with a new text layout.
        nullLayouts();
        requestLayout();
        invalidate();
    }
}

As you can see, in some cases, the requestLayout() is not called, so the main thread check is not introduced.

So I think the key point is not about Kotlin or Java, it's about the TextViews' layout params which determined whether requestLayout() is called or not.

like image 87
Hong Duan Avatar answered Oct 07 '22 16:10

Hong Duan