androidbroadcast / ViewBindingPropertyDelegate

Make work with Android View Binding simpler
https://proandroiddev.com/make-android-view-binding-great-with-kotlin-b71dd9c87719
Apache License 2.0
1.41k stars 102 forks source link

Crash on button press -> java.lang.RuntimeException: View must have a tag #65

Closed grad1e closed 2 years ago

grad1e commented 3 years ago

Hi, I'm facing an issue after upgrading to 1.4.7 while clicking a button. Downgrading to 1.4.6 fixed for me. Here's the stack trace and code

Stacktrace

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.atees.ayurdoc.debug, PID: 25736
    java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at by.kirich1409.viewbindingdelegate.internal.BindViewBinding.bind(ViewBindingCache.kt:101)
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:64)
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:63)
        at by.kirich1409.viewbindingdelegate.LifecycleViewBindingProperty.getValue(ViewBindingProperty.kt:77)
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:42)
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:34)
        at com.atees.ayurdoc.ui.login.LoginFragment.getBinding(LoginFragment.kt:33)
        at com.atees.ayurdoc.ui.login.LoginFragment.setupClickListeners$lambda-4(LoginFragment.kt:124)
        at com.atees.ayurdoc.ui.login.LoginFragment.lambda$snF-fOR_eVlj8Zahf8h-q80glhw(Unknown Source:0)
        at com.atees.ayurdoc.ui.login.-$$Lambda$LoginFragment$snF-fOR_eVlj8Zahf8h-q80glhw.onClick(Unknown Source:2)
        at android.view.View.performClick(View.java:7448)
        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119)
        at android.view.View.performClickInternal(View.java:7425)
        at android.view.View.access$3600(View.java:810)
        at android.view.View$PerformClick.run(View.java:28305)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7664)
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 
     Caused by: java.lang.RuntimeException: view must have a tag
        at com.atees.ayurdoc.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:75)
        at androidx.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
        at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
        at androidx.databinding.ViewDataBinding.bind(ViewDataBinding.java:695)
        at com.atees.ayurdoc.databinding.FragmentLoginBinding.bind(FragmentLoginBinding.java:136)
        at com.atees.ayurdoc.databinding.FragmentLoginBinding.bind(FragmentLoginBinding.java:124)
        at java.lang.reflect.Method.invoke(Native Method) 
        at by.kirich1409.viewbindingdelegate.internal.BindViewBinding.bind(ViewBindingCache.kt:101) 
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:64) 
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:63) 
        at by.kirich1409.viewbindingdelegate.LifecycleViewBindingProperty.getValue(ViewBindingProperty.kt:77) 
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:42) 
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:34) 
        at com.atees.ayurdoc.ui.login.LoginFragment.getBinding(LoginFragment.kt:33) 
        at com.atees.ayurdoc.ui.login.LoginFragment.setupClickListeners$lambda-4(LoginFragment.kt:124) 
        at com.atees.ayurdoc.ui.login.LoginFragment.lambda$snF-fOR_eVlj8Zahf8h-q80glhw(Unknown Source:0) 
        at com.atees.ayurdoc.ui.login.-$$Lambda$LoginFragment$snF-fOR_eVlj8Zahf8h-q80glhw.onClick(Unknown Source:2) 
        at android.view.View.performClick(View.java:7448) 
        at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119) 
        at android.view.View.performClickInternal(View.java:7425) 
        at android.view.View.access$3600(View.java:810) 
        at android.view.View$PerformClick.run(View.java:28305) 
        at android.os.Handler.handleCallback(Handler.java:938) 
        at android.os.Handler.dispatchMessage(Handler.java:99) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7664) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 

Fragment code

@AndroidEntryPoint
class LoginFragment : Fragment(R.layout.fragment_login) {

    private val binding: FragmentLoginBinding by viewBinding()
    private val viewModel: LoginViewModel by viewModels()
    private val args: LoginFragmentArgs by navArgs()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = viewModel
        setupClickListeners()
    }

    private fun setupClickListeners() {
        binding.btnLogin.setOnClickListener {
            binding.root.showSnackBar("Pressed")
        }
    }
}

Extension function

fun View.showSnackBar(message: String) {
        Snackbar.make(this, message, Snackbar.LENGTH_LONG).show()
}
kirich1409 commented 3 years ago

Please, create repo with sample where the issue is reproducible

1240 commented 3 years ago

same

kirich1409 commented 3 years ago

same

Please, create repo with sample where the issue is reproducible

kirich1409 commented 2 years ago

Quick search give me that issues connected with DataBinding https://stackoverflow.com/questions/53184433/what-the-actual-meaning-caused-by-java-lang-runtimeexception-view-must-have-a

ViewBindingPropertyDelegate doesn't support it and I didn't test with DataBinding, but will do that

kirich1409 commented 2 years ago

See #69 Add support of DataBinding

kirich1409 commented 2 years ago

@daryljodanny show code of the layout resource file that crash when use it with VBPD

grad1e commented 2 years ago

I was so busy that I literally forgot everything. I’ll help you with this by today night for sure

Berki2021 commented 2 years ago

Got the same error today, also using databinding. It is unnecessary, how the layout file looks like or if you use viewbindingdelegate with or without reflection. Always gives me this error. Here is my layout for research purpose:

Fragment Layout with viewpager2

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <include
            android:id="@+id/headline"
            layout="@layout/registration_toolbar"
            app:isBold="@{false}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:outsideToolbarTitle="@{@string/fragment_user_data_toolbar_title}" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tl_user_data"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/headline" />

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vp_user_data"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_marginTop="8dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tl_user_data" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Headline layout file

<layout>

    <data>

        <variable
            name="outsideToolbarTitle"
            type="String" />

        <variable
            name="isBold"
            type="boolean" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/user_standard_toolbar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/color_shop_item_background"
            android:theme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">

            <TextView
                android:id="@+id/toolbar_title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:text="@{outsideToolbarTitle}"
                android:textColor="@android:color/black"
                android:textSize="@dimen/textHeadlineNormal1"
                app:setStyle="@{isBold ? 1 : 0}" />
        </com.google.android.material.appbar.MaterialToolbar>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

EDIT: LOOKS LIKE I FOUND THE ERROR SOURCE AT LEAST FOR ME

After looking at the crash file, I found the following source of error: When you access the binding variable in your fragments onDestroyView() the app will crash with the error: View must have a tag. Here is the error log from me:

java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:502)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at by.kirich1409.viewbindingdelegate.internal.BindViewBinding.bind(ViewBindingCache.kt:101)
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:64)
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(Unknown Source:2)
        at by.kirich1409.viewbindingdelegate.LifecycleViewBindingProperty.getValue(ViewBindingProperty.kt:77)
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:42)
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:34)
        at com.example.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataHolderFragment.getBinding(UserDataHolderFragment.kt:29)
        at com.example.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataHolderFragment.onDestroyView(UserDataHolderFragment.kt:72)
        at androidx.fragment.app.Fragment.performDestroyView(Fragment.java:3200)
        at androidx.fragment.app.FragmentStateManager.destroyFragmentView(FragmentStateManager.java:742)
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:346)
        at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.java:742)
        at androidx.fragment.app.SpecialEffectsController$Operation.completeSpecialEffect(SpecialEffectsController.java:669)
        at androidx.fragment.app.DefaultSpecialEffectsController$SpecialEffectsInfo.completeSpecialEffect(DefaultSpecialEffectsController.java:774)
        at androidx.fragment.app.DefaultSpecialEffectsController.startAnimations(DefaultSpecialEffectsController.java:147)
        at androidx.fragment.app.DefaultSpecialEffectsController.executeOperations(DefaultSpecialEffectsController.java:120)
        at androidx.fragment.app.SpecialEffectsController.executePendingOperations(SpecialEffectsController.java:294)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2202)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106)
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
        at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 
     Caused by: java.lang.RuntimeException: view must have a tag
        at com.example.app.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:991)
        at androidx.databinding.MergedDataBinderMapper.getDataBinder(MergedDataBinderMapper.java:74)
        at androidx.databinding.DataBindingUtil.bind(DataBindingUtil.java:199)
        at androidx.databinding.ViewDataBinding.bind(ViewDataBinding.java:695)
        at com.example.app.databinding.FragmentUserDataHolderBinding.bind(FragmentUserDataHolderBinding.java:88)
        at com.example.app.databinding.FragmentUserDataHolderBinding.bind(FragmentUserDataHolderBinding.java:76)
        at java.lang.reflect.Method.invoke(Native Method) 
        at by.kirich1409.viewbindingdelegate.internal.BindViewBinding.bind(ViewBindingCache.kt:101) 
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(FragmentViewBindings.kt:64) 
        at by.kirich1409.viewbindingdelegate.ReflectionFragmentViewBindings$viewBinding$3.invoke(Unknown Source:2) 
        at by.kirich1409.viewbindingdelegate.LifecycleViewBindingProperty.getValue(ViewBindingProperty.kt:77) 
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:42) 
        at by.kirich1409.viewbindingdelegate.FragmentViewBindingProperty.getValue(FragmentViewBindings.kt:34) 
        at com.example.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataHolderFragment.getBinding(UserDataHolderFragment.kt:29) 
        at com.example.app.framework.ui.view.fragment.user.loggedIn.userdata.UserDataHolderFragment.onDestroyView(UserDataHolderFragment.kt:72) 
        at androidx.fragment.app.Fragment.performDestroyView(Fragment.java:3200) 
        at androidx.fragment.app.FragmentStateManager.destroyFragmentView(FragmentStateManager.java:742) 
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:346) 
        at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.java:742) 
        at androidx.fragment.app.SpecialEffectsController$Operation.completeSpecialEffect(SpecialEffectsController.java:669) 
        at androidx.fragment.app.DefaultSpecialEffectsController$SpecialEffectsInfo.completeSpecialEffect(DefaultSpecialEffectsController.java:774) 
        at androidx.fragment.app.DefaultSpecialEffectsController.startAnimations(DefaultSpecialEffectsController.java:147) 
        at androidx.fragment.app.DefaultSpecialEffectsController.executeOperations(DefaultSpecialEffectsController.java:120) 
        at androidx.fragment.app.SpecialEffectsController.executePendingOperations(SpecialEffectsController.java:294) 
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2202) 
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106) 
        at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002) 
        at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524) 
        at android.os.Handler.handleCallback(Handler.java:883) 
        at android.os.Handler.dispatchMessage(Handler.java:100) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 
D/TransportRuntime.SQLiteEventStore: Storing event with priority=HIGHEST, name=FIREBASE_CRASHLYTICS_REPORT for destination cct
V/FA: Connecting to remote service
D/TransportRuntime.JobInfoScheduler: Scheduling upload for context TransportContext(cct, HIGHEST, MSRodHRwczovL2NyYXNobHl0aWNzcmVwb3J0cy1wYS5nb29nbGVhcGlzLmNvbS92MS9maXJlbG9nL2xlZ2FjeS9iYXRjaGxvZ1xBSXphU3lCcnBTWVQ0RkZMMDlyZUhKaTZIOUZZZGVpU25VVE92Mk0=) with jobId=1585000176 in 1000ms(Backend next call timestamp 1627917474726). Attempt 1
V/FA: Recording user engagement, ms: 17787
I/Process: Sending signal. PID: 3697 SIG: 9

As you can see, the stacktrace points to the fragments#onDestroyView(), in my case it was:

    override fun onDestroyView() {
        super.onDestroyView()
        tabLayoutHelper.onDestroyView()
        binding.vpUserData.adapter = null <-- THIS CAUSED THE ERROR
    }

After deleting the access to the binding variable in onDestroyView(), the eror was gone. Unfortunately with this case, I am not able to use the viewbindingdelegate anymore, because I have to null my viewPager2.adapter in onDestroyView()

kirich1409 commented 2 years ago

I planed to add callback to the delegate, when view will be destroyed

wbervoets commented 2 years ago

Hello, is databinding support still being worked on? I tried with 1.5.2 but the crash is still happening. For me it is not related to onDestroyView like @Berki2021 but as the original poster: in onViewCreated when the viewBinding is accessed from a click listener on a button.

1.4.6 works fine, so something changed in 1.4.7 and later.

If you cannot reproduce, please let me know

kirich1409 commented 2 years ago

Hello, is databinding support still being worked on? I tried with 1.5.2 but the crash is still happening. For me it is not related to onDestroyView like @Berki2021 but as the original poster: in onViewCreated when the viewBinding is accessed from a click listener on a button.

1.4.6 works fine, so something changed in 1.4.7 and later.

If you cannot reproduce, please let me know

It will be great if you provide sample where the issue is reproducible

wbervoets commented 2 years ago

I will try to create a minimal sample.

What I have observed so far is that

private val viewBinding: FragmentLoginScreenBinding by viewBinding( FragmentLoginScreenBinding::bind )

works fine when viewBinding is accessed in onViewCreated() of the fragment.

It crashes when observing a livedata which is called in onViewCreated(). We pass in the viewLifecycleowner, not the fragment lifecycle owner.

viewModel.login().observe( viewLifecycleOwner, { when (it) { is Result.Loading -> { viewBinding.loginButton.showProgress() } is Result.Success -> { }

When viewBinding.loginButton.showProgress() is executed it will crash. This is due to the root ConstraintLayout of the login_fragment not having a tag anymore. (while it does have the correct tag in the xml in the apk)

I noticed the viewBinding became null again in your LifecycleViewBindingProperty, which seems strange:

@MainThread public override fun getValue(thisRef: R, property: KProperty<*>): T { viewBinding?.let { return it }

It seems to be cleared by ClearOnDestroyLifecycleObserver which is triggered when the view lifecycleowner is destroyed. So why is it destroyed?

First I see it being cleared on a previous fragment onDestroyView() is called (which contains a viewBinding access). But then the new login fragment which afaik is stil visible also gets a callback in ClearOnDestroyLifecycleObserver.

Using onViewDestroyed when creating the viewBinding does not make a difference. I wonder why it is needed?

wbervoets commented 2 years ago

Some more information:

I have two fragments: Home and Login. Home is the first fragment that is shown, it automatically redirects to Login when the user is not logged in.

So the following happens:

A bit later the following happens:

Later when an async operation is finished, I'm accessing the viewBinding again, but since it has become null, the library tries to recreate it, but fails with "view must have a tag" in the google databinding code (AGP 7.0.3)

`private inner class ClearOnDestroy : FragmentManager.FragmentLifecycleCallbacks() {

    override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
        // Fix for destroying view for case with issue of navigation
        postClear()
    }
}`

I don't understand why the postClear() is called for the Login Fragment (onDestroyView() is not called by Android on that Login fragment). If it would not call postClear() I believe this issue would be fixed.

Now that I have a clearer understanding I can probably make a small reproducable testcase tomorrow.

kirich1409 commented 2 years ago

I made fix for case when Lifecycle.onDestroy() wasn't called properly and I think this is the reason

kirich1409 commented 2 years ago

@wbervoets , I've just published 1.5.3 and it must be available in few hours. Please check it and write is the fix working or not

vcimka commented 2 years ago

same issue for my app fixed by update to 1.5.3 thx

wbervoets commented 2 years ago

yes, 1.5.3 works fine. Thanks!