2

There is a large data-bound view, which may take several seconds to inflate. I would like to display the user a splash screen and inflate the main view a delayed action. Android studio throws an exception "Failed to call observer method".

MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.screen_splash)
    Handler(Looper.getMainLooper()).postDelayed({
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
            this,
            R.layout.activity_main
        )
        binding.lifecycleOwner = this // this line throws exception
    }, 1000)
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">

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

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
            android:id="@+id/map_list"
            android:name="com.google.android.gms.maps.SupportMapFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity" />
</RelativeLayout>

Exception:

2021-12-05 13:42:56.638 23701-23701/com.example E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example, PID: 23701
java.lang.RuntimeException: Failed to call observer method
    at androidx.lifecycle.ClassesInfoCache$MethodReference.invokeCallback(ClassesInfoCache.java:226)
    at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeMethodsForEvent(ClassesInfoCache.java:194)
    at androidx.lifecycle.ClassesInfoCache$CallbackInfo.invokeCallbacks(ClassesInfoCache.java:185)
    at androidx.lifecycle.ReflectiveGenericLifecycleObserver.onStateChanged(ReflectiveGenericLifecycleObserver.java:37)
    at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
    at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:196)
    at androidx.databinding.ViewDataBinding.setLifecycleOwner(ViewDataBinding.java:434)
    at com.example.databinding.ActivityMainBindingImpl.setLifecycleOwner(ActivityMainBindingImpl.java:166)
    at com.example.MainActivity.onCreate$lambda-3(MainActivity.kt:106)
    at com.example.MainActivity.$r8$lambda$lffeScwTEbHi2B1isKEoQYU2po4(Unknown Source:0)
    at com.example.MainActivity$$ExternalSyntheticLambda5.run(Unknown Source:2)
    at android.os.Handler.handleCallback(Handler.java:888)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:213)
    at android.app.ActivityThread.main(ActivityThread.java:8178)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
 Caused by: java.lang.NumberFormatException: s == null
    at java.lang.Integer.parseInt(Integer.java:577)
    at java.lang.Integer.valueOf(Integer.java:801)
    at com.example.databinding.ControlPanelBindingImpl.executeBindings(ControlPanelBindingImpl.java:800)...
Zain
  • 37,492
  • 7
  • 60
  • 84
Gonki
  • 567
  • 1
  • 8
  • 17
  • What`s the reason for using "Databinding" with post delay? – Roll no1 Dec 05 '21 at 07:42
  • The main view takes 2+ seconds to inflate while app users are seeing a white screen. I'd like to show a splash message and then load the view. – Gonki Dec 05 '21 at 08:40
  • You can handle this the other way show the progress bar which covers for 2 seconds and after 2 seconds you can hide progress bar and show actual screen. – Roll no1 Dec 05 '21 at 09:43
  • How would I inflate progress before the main view? It's preferable to show the splash screen. – Gonki Dec 05 '21 at 09:59
  • so you will be having two relative view in your XML . One view contains progress bar and the second contains original layout which is taking 2 seconds to render. – Roll no1 Dec 05 '21 at 14:32
  • It would still take 2+ seconds to inflate while the user is seeing a blank screen – Gonki Dec 06 '21 at 00:09
  • 1
    Have you checked how use databinding in your XML? You're getting `java.lang.NumberFormatException: s == null` – Alvin Dizon Dec 08 '21 at 03:07
  • It's working when inflated synchronously – Gonki Dec 08 '21 at 04:45
  • have you tried `AsyncLayoutInflater` if not give it try . this way you won't be depending on fixed delay because you will get the callback of inflation. [Give it a try](https://stackoverflow.com/questions/49516363/use-asynclayoutinflater-with-databinding) . – ADM Dec 08 '21 at 09:56
  • You should post more of the layout and what variables are being set on the binding. – dominicoder Dec 09 '21 at 06:29
  • The problem may be related to FragmentContainerView with "context" attribute inside the view. The variables set in bindings are observables. – Gonki Dec 09 '21 at 06:50

4 Answers4

0

I am not sure about the structure of your application. In our case we had a similar requirement where we wanted to show a loader until the initial fragment is bound. So we created a viewStub in the activity. Then when the fragment is attached we set a liveData in the shared view model to SHOW which notifies the activity to inflate the viewStub. This way we inflate the view stub which hides the full screen displaying a splash image. Then once the view in the fragment is created and in the onViewCreated we again set the liveData in the shared view model to HIDE which hides the viewStub and the fragment is displayed.

Sagar
  • 405
  • 1
  • 4
  • 16
  • The command "viewStub.inflate()" is throwing "java.lang.NumberFormatException: s == null". I'd be really happy to see a working example of lazy loading a data-bound view. – Gonki Dec 08 '21 at 03:03
0

Use fragmentContainerView inside your main activity. Show the view that you want to show in this container. Create a view in front of the container. Show splash message in this view. Make the visibility of the splash view gone when the main view is loaded. So the splash screen will use the activity life cycle and the main view will use the fragment lifecycle. This may be a solution for you.

alpertign
  • 296
  • 1
  • 4
  • 13
  • Thanks for the suggestion. I tried the FragmentContainerView approach like in this example repo https://github.com/codehustler53/FragmentContainerViewVsFrameLayout and unluckily Android studio throws the same exception when lifecycle is applied to the fragment. – Gonki Dec 08 '21 at 02:47
0

Disclaimer: turns out that the issue is resolved in UPDATE 2 section in the answer; the other sections are left if they could help future visitors in other potential issues

At first look, thought that Caused by: java.lang.NumberFormatException: s == null is related to the issue; although you told in comments that it's working synchronously.

And the exception java.lang.RuntimeException: Failed to call observer method won't help to know the error by tracing it in the code.

But your code successfully worked with me in simple layouts; probably the issue is related to the heavy layout that you try to load synchronously along while accessing the binding.lifecycleOwner; I guess the latter snippet requires a while before accessing the lifecycleOwner. So, you could post some delay in advance.

For that, I am going to use coroutines instead of posting a delay; as the code would be more linear and readable:

CoroutineScope(Main).launch {

    delay(1000) // Original delay of yours
    val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
        this@MainActivity,
        R.layout.activity_main
    )

    delay(1000) // try and error to manipulate this delay 
    binding.lifecycleOwner = this@MainActivity 

}

If not already used, the coroutine dependency is

def coroutine_version = "1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

UPDATE

The posted delay in your code doesn't help in showing the splash/launch screen during that delay while the main activity is loading;

Handler(Looper.getMainLooper()).postDelayed({
    val binding = DataBindingUtil.setContentView<ActivityMainBinding>(
        this,
        R.layout.activity_main
    ) // This won't be called unless the 1000 sec is over
    binding.lifecycleOwner = this 
}, 1000)

What your code does:

  1. A splash screen is shown
  2. A delay is posted (still the main layout is not loading in here)
  3. main layout is shown when the delay is over

So, the posted delay is just accumulating to the time of loading the main layout; this even make it more lagged. Furthermore this is not the recommended way of using splash screen (This medium post would help in that)

Instead, I think what you intend to do:

  1. Show a splash screen
  2. Load main layout
  3. Post a delay so that the main layout takes time to load during the delay
  4. Show the main layout when the delay is over

But, the problem is that the thing need to be loaded is UI which requires to do that in the main thread, not in a background thread. So, we instead of using two different layout and call setContentView() twice; you could instead create a single layout for your main layout, and add some view that represents the splash screen which will obscure the main layout entirely (be in front of it) until the layout is loaded (i.e. the delay is over); then remove this splash view then:

Demo:

splash_screen.xml (Any layout you want that must match parent to obscure it):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/splash_screen"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:gravity="center">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />
</LinearLayout>

Main activity:

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "LOG_TAG"
    }

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Log.d(TAG, "Start Inflating layout")
        binding = DataBindingUtil.setContentView(
            this@MainActivity,
            R.layout.activity_main
        )

        // Show only the first time app launches, not in configuration changes
        if (savedInstanceState == null) {
            CoroutineScope(IO).launch {
                Log.d(TAG, "Start of delay")
                delay(1000)
                Log.d(TAG, "End of delay")
                withContext(Main) {
                    hideSplash()
                }
            }
            showSplash()
        }

        binding.lifecycleOwner = this@MainActivity
        Log.d(TAG, "End Inflating layout")

    }

    private fun showSplash() {
        supportActionBar?.hide()

        // Inflate splash screen layout
        val splashLayout =
            layoutInflater.inflate(
                R.layout.splash_screen,
                binding.rootLayout,
                false
            ) as LinearLayout

        binding.rootLayout.addView(
            splashLayout
        )

    }

    private fun hideSplash() {
        supportActionBar?.show()
        binding.rootLayout.removeView(
            findViewById(R.id.splash_screen)
        )
    }

}

Logs

2021-12-11 21:59:18.349 20681-20681/  D/LOG_TAG: Start Inflating layout
2021-12-11 21:59:18.452 20681-20707/  D/LOG_TAG: Start of delay
2021-12-11 21:59:18.476 20681-20681/  D/LOG_TAG: End Inflating layout
2021-12-11 21:59:20.457 20681-20707/  D/LOG_TAG: End of delay

Now the delay is running along with inflating the layout; the splash screen shown while it loads; and ends when the delay is over.

UPDATE 2

It's definitely not going to work: databinding = ... line takes 2.5 seconds to complete, can't add a view to "databinding.root" before it's ready. It works in the presented code because your main view is tiny.

Now try to separate inflating the layout from setContentView() in dataBinding; still both requires to be in the main thread

setContentView(R.layout.screen_splash)

CoroutineScope(Main).launch {
    // Inflate main screen layout asynchronously 
    binding = ActivityMainBinding.inflate(layoutInflater) 

    delay(2500) // 2.5 sec delay of loading the mainLayout before setContentView

    setContentView(binding.root)
    binding.lifecycleOwner = this@MainActivity
}
Zain
  • 37,492
  • 7
  • 60
  • 84
  • If that didn't make a clue; please post something that could allow reproduce the issue – Zain Dec 09 '21 at 22:43
  • I tried your code and it produces the same error as soon as the first delay is finished. The layout is indeed heavy, the idea is to load it in its entirety at the start for quicker navigation across screens. – Gonki Dec 10 '21 at 04:40
  • According to logcat, binding.lifecycleOwner assignment takes a few milliseconds while secContentView 2500. – Gonki Dec 10 '21 at 04:49
  • Can you check this out with a simple layout.. This already worked with me with either case... I think the problem in the heavy layout load – Zain Dec 10 '21 at 11:02
  • It's definitely not going to work: databinding = ... line takes 2.5 seconds to complete, can't add a view to "databinding.root" before it's ready. It works in the presented code because your main view is tiny. – Gonki Dec 12 '21 at 03:29
  • Wonder why you posted only 1 sec for the handler then? This will definitely accumulate to 3.5 sec. Still the issue not producible to us to test it.. You have to tell us how to do that.. Unless it will be a kind of a guessing game... What you need is achievable without data binding – Zain Dec 12 '21 at 05:04
  • @Gonki Check UPDATE 2 in answer .. hope it works – Zain Dec 12 '21 at 05:16
  • Indeed it worked, the problem might be in the view or it just being too large. I'll need to investigate it later. – Gonki Dec 13 '21 at 04:20
0

Finally found the problem and, in retrospect, it was too elementary for the question:

Must assign ViewModel before lifecycleOwner

binding.viewModel = myViewModer
binding.livecycleOwner = this@MainActivity

Just changing order of these lines fixed it.

Gonki
  • 567
  • 1
  • 8
  • 17