Recently I was trying to fix an odd error in screen rotation. There were two input fields and one was preserved when the screen was rotated and the other was not.

The databinding is setup at the top of the layout XML file like this

1
2
3
4
5
6
7
8
9
<layout 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">

    <data>
        <variable
            name="viewModel"
            type="com.example.namespace.ViewModel" />
    </data>

The field that works correctly looks like this

1
2
3
4
5
6
7
8
9
<EditText
    android:id="@+id/txt1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:enabled="@{viewModel.txt1Editable}"
    android:text="@={viewModel.txt1String}"
    android:theme="@style/EditTextTheme"
    android:visibility="@{!viewModel.txt1Visible}"
    />

The field that dies not work properly looks like this

1
2
3
4
5
6
7
8
<EditText
    android:id="@+id/txt2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.txt2String}"
    android:theme="@style/EditTextTheme"
    android:visibility="@{!viewModel.txt2Visible}"
    />

In reality the declarations were a lot more verbose than this, I’ve simplified them to make it easier to see.

In the ViewModel class the bound variables look like this

val txt1Visible = ObservableBoolean()
val txt1Editable = ObservableBoolean()
val txt1String = ObservableField<String>()
val txt2Visible = ObservableBoolean()
val txt2String = ObservableField<SpannableString>()

When the screen was rotated then the txt1 field correctly kept its value whereas txt2 was cleared. The clue was in the obvious difference that txt2 was bound to a SpannableString. I stared at this code for a while before I spotted the problem and I do wonder why we do this to ourselves.

Why do we do this to ourselves?

I think everyone who started writing code in C can remember the pain that was caused by a missing = in this line of code

if (index = 0) {
    doStuff();
}

If its been a while since you saw this then the effect in C is that index is assigned the value of zero and, as zero is false, then doStuff() is never called. Almost certainly not what you intended. This became so much of an issue that C derivatives such as Java and C# mark the statement as an error or warning, by stopping the automatic cast of an int to a boolean.

One gap plugged another opens.

In the databinding example here, the reason that I was seeing the odd behaviour was because the binding was one-way. Like the name suggests one-way binding only allows data to be pushed from the variable txt2String into the UI control, to get the data back we need to access the Text property of the EditText. So when we rotate the screen for all two-way data bound controls the OS appears to persist the current values, for anything not bound or one-way bound it just silently discards the value. I guess I don’t have a problem with that behaviour but I do have a problem with the syntax. Like the old C = problem we seem to have invented a syntax that does its best to create problems.

One-way binding looks like this

    android:text="@{viewModel.txt2String}"

Two-way binding like this

    android:text="@={viewModel.txt1String}"

In a wall of text the missing = is hard to spot.

Fixing the issue

I did think all I had to do was make the binding two-way but that didn’t work, I get this error

Cannot find a getter for <EditText android:text> that accepts parameter type 'androidx.databinding.ObservableField<android.text.SpannableString>'

That does make sense, it does not know how to produce a SpannableString from text.

In the end I had to save the current text by getting the current value from the control and saving the state like this

1
2
3
4
5
override fun onSaveInstanceState(outState: Bundle) {
    // the message binding is one way so we need to save the state ourselves
    outState.putString(KEY_MESSAGE_TEXT, binding.txt2.text.toString())
    super.onSaveInstanceState(outState)
}

And then set it when the UI is initialised when the screen is recreated.

1
2
3
savedInstanceState?.getString(KEY_MESSAGE_TEXT)?.let {
    txt2String.set(spannableProvider.createSpannableString(it))
}