Home > OS >  Showing dialog in MVVM Android throws exception only when certain variable changes in the ViewModel
Showing dialog in MVVM Android throws exception only when certain variable changes in the ViewModel

Time:10-01

I have a MainActivity using a DrawerLayout and tabs with 2 fragments.

My first fragment contains a list of elements in a RecyclerView, and I can click on each element to "select" it (which calls a SDK function to login to a hardware device). When selected, this triggers a change on the Fragment's ViewModel:

// Selected device changes when an item is clicked
private val _devices = MutableLiveData<List<DeviceListItemViewModel>>()
private val _selectedDevice = MutableLiveData<ConnectedDevice>()
val devices: LiveData<List<DeviceListItemViewModel>> by this::_devices
val selectedDevice: LiveData<ConnectedDevice> by this::_selectedDevice

Then I have a shared ViewModel between both fragments, which also has a currentDevice variable like this:

private val _currentDevice = MutableLiveData<ConnectedDevice>()
val currentDevice: LiveData<ConnectedDevice> by this::_currentDevice

So in the Fragment that contains the list, I have the following code to update the shared ViewModel variable:

private val mViewModel: DeviceManagementViewModel by viewModels()
private val mSharedViewModel: MainActivityViewModel by activityViewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        Log.d(classTag, "Fragment view created")
        val binding = ActivityMainDevicesManagementFragmentBinding.bind(view)
        binding.apply {
            viewModel = mViewModel
            lifecycleOwner = viewLifecycleOwner
        }

        // Observe fragment ViewModel
        // If any device is clicked on the list, do the login on the shared ViewModel
        mViewModel.selectedDevice.observe(this, {
            mSharedViewModel.viewModelScope.launch {
                if (it != null) {
                    mSharedViewModel.setCurrentDevice(videoDevice = it)
                } else mSharedViewModel.unsetCurrentDevice()
            }
        })
    }

The problem is that if the shared ViewModel's currentDevice variable is set, I get exceptions whenever I try to open a Dialog or start a new activity. If I modify the setCurrentDevice function in the shared ViewModel, then it works fine (or if I don't select any device).

The exceptions I see are this when starting a new Activity:

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.placeholder.easyview/com.example.myapp.activities.settings.SettingsActivity}: java.lang.IllegalArgumentException: display must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3430)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3594)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2067)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7698)
        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:952)
     Caused by: java.lang.IllegalArgumentException: display must not be null
        at android.app.ContextImpl.createDisplayContext(ContextImpl.java:2386)
        at android.content.ContextWrapper.createDisplayContext(ContextWrapper.java:977)
        at com.android.internal.policy.DecorContext.<init>(DecorContext.java:50)
        at com.android.internal.policy.PhoneWindow.generateDecor(PhoneWindow.java:2348)
        at com.android.internal.policy.PhoneWindow.installDecor(PhoneWindow.java:2683)
        at com.android.internal.policy.PhoneWindow.getDecorView(PhoneWindow.java:2116)
        at androidx.appcompat.app.AppCompatActivity.initViewTreeOwners(AppCompatActivity.java:219)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:194)
        at com.example.myapp.activities.settings.SettingsActivity.onCreate(SettingsActivity.kt:34)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1310)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3403)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3594) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2067) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:223) 
        at android.app.ActivityThread.main(ActivityThread.java:7698) 
        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:952) 

And this if I try to open a Dialog:

java.lang.ArrayIndexOutOfBoundsException: length=16; index=2448
        at android.view.InsetsState.peekSource(InsetsState.java:374)
        at android.view.InsetsSourceConsumer.updateSource(InsetsSourceConsumer.java:291)
        at android.view.InsetsController.updateState(InsetsController.java:654)
        at android.view.InsetsController.onStateChanged(InsetsController.java:621)
        at android.view.ViewRootImpl.setView(ViewRootImpl.java:1058)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:409)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:110)
        at android.app.Dialog.show(Dialog.java:340)
        at android.app.AlertDialog$Builder.show(AlertDialog.java:1131)
        at com.example.myapp.activities.main.fragments.DeviceManagementFragment.showAddDeviceMethodDialog(DeviceManagementFragment.kt:151)
        at com.example.myapp.activities.main.fragments.DeviceManagementFragment.access$showAddDeviceMethodDialog(DeviceManagementFragment.kt:33)
        at com.example.myapp.activities.main.fragments.DeviceManagementFragment$onViewCreated$$inlined$apply$lambda$1.onClick(DeviceManagementFragment.kt:52)
        at android.view.View.performClick(View.java:7448)
        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:28309)
        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:7698)
        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:952)

EDIT: Looks like the problem actually lies in the other fragment, where I have the following code (in onViewCreated method):

// If the shared view model device changes, this must change too
        mSharedViewModel.currentDevice.observe(this, {
            if (it != null) {
                mViewModel.setCurrentDevice(it)
            } else mViewModel.unsetCurrentDevice()
        })

        
        mViewModel.currentDevice.observe(this, {
            if (it != null) {
                mViewModel.fetchChannels()
            }
        })

If I comment out the second part (where the fetchChannels occurs), it works well. Even if I comment out the fetchChannels call only, it works.

This is the code of the fetchChannels function:

fun fetchChannels() = viewModelScope.launch {
        Log.d(classTag, "Getting channels for device ${currentDevice.value}")
        currentDevice.value?.let {
            val fetchedChannels = deviceLibManager.getChannels(it.videoDevice)
            _currentDevice.value?.channels?.clear()
            _currentDevice.value?.channels?.addAll(fetchedChannels)
            if (fetchedChannels.isNotEmpty()) {
                _currentDevice.value?.currentChannel = fetchedChannels[0]
            }
        }
    }

The following line is the one giving me trouble:

val fetchedChannels = deviceLibManager.getChannels(it.videoDevice)

That function is just this:

suspend fun getChannels(videoDevice: VideoDevice): List<VideoChannel> {
        try {
            Log.i(classTag, "Getting channels from device ${videoDevice}")
            val channels = videoDevice.getChannelsAsync()
            return channels
        } catch (exception: Exception) {
            when (exception) {
                is UnknownVendorException -> {
                    Log.w(classTag, "Device ${videoDevice} cannot get channels because the vendor is unknown")
                }
                is NetworkException -> {
                    Log.w(classTag, "Device ${videoDevice} cannot get channels because it is unreachable")
                }
                else -> {
                    Log.w(classTag, "Device ${videoDevice} cannot get channels, reason: ${exception.message}")
                }
            }
            return emptyList()
        }
    }

And the implementation in the SDK is this:

override suspend fun getChannelsAsync(): List<VideoChannel> = withContext(Dispatchers.IO) {
        Log.i(classTag, "Trying to get channels for device: $logName")
        val channels = ArrayList<VideoChannel>()
        getZeroChannel()?.let {
            channels.add(it)
        }
        channels.addAll(getAllChannels())
        if (channels.isNotEmpty()) {
            Log.i(classTag, "Successfully retrieved ${channels.size} channels for device: $logName")
            return@withContext channels
        } else {
            Log.w(classTag, "Error retrieving channels for device $logName or no channels exist")
            throw Exception()
        }
    }

The other functions just make a network call and retrieve some data, it should not be messing with the UI at all.

I am testing with a Xiaomi Mi A3 using Android 10.

Can someone help me? Thank you.

CodePudding user response:

So I don't really know why but I found the answer.

In the SDK, the functions getZeroChannel and getAllChannels were not suspending functions, although they make a network call. So what I did is:

  1. Move the withContext(Dispatchers.IO) part to those two functions (the ones who actually make the network call), and make them suspend functions.

  2. Remove the withContext(Dispatchers.IO) part from getChannelsAsync function. Keep it as a suspend function though.

After these changes everything works as expected. I still don't know why, so if someone could shed some light, that would be very much appreciated.

  • Related