在 MVVM Android 中显示对话框仅当 ViewModel 中的某些变量发生变化时才会抛出异常

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

我有一个 MainActivity 使用 DrawerLayout 和带有 2 个片段的选项卡。

我的第一个片段包含 RecyclerView 中的元素列表,我可以单击每个元素以“select”它(调用 SDK 函数来登录硬件设备)。当 selected 时,这会触发 Fragment 的 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

然后我在两个片段之间有一个共享的 ViewModel,它还有一个 currentDevice 变量,如下所示:

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

所以在包含列表的片段中,我有以下代码来更新共享的 ViewModel 变量:

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()
            }
        })
    }

问题是,如果设置了共享 ViewModel 的 currentDevice 变量,每当我尝试打开对话框或启动新的 activity 时,我都会遇到异常。如果我修改共享 ViewModel 中的 setCurrentDevice 函数,那么它工作正常(或者如果我不 select 任何设备)。

我在开始新的 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) 

如果我尝试打开一个对话框,这就是:

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.onClick(DeviceManagementFragment.kt:52)
        at android.view.View.performClick(View.java:7448)
        at android.view.View.performClickInternal(View.java:7425)
        at android.view.View.access00(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)

编辑:看起来问题实际上出在另一个片段上,我有以下代码(在 onViewCreated 方法中):

// 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()
            }
        })

如果我注释掉第二部分(出现 fetchChannels 的地方),效果很好。即使我只注释掉 fetchChannels 调用,它仍然有效。

这是fetchChannels函数的代码:

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]
            }
        }
    }

下面一行给我带来了麻烦:

val fetchedChannels = deviceLibManager.getChannels(it.videoDevice)

那个函数就是这个:

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()
        }
    }

SDK中的实现是这样的:

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()
        }
    }

其他函数只是进行网络调用并检索一些数据,根本不应该与 UI 混淆。

我正在用 Android 10.

的小米 Mi A3 进行测试

有人可以帮助我吗?谢谢。

所以我真的不知道为什么,但我找到了答案。

在SDK中,函数getZeroChannelgetAllChannels虽然进行了网络调用,但并没有挂起函数。所以我所做的是:

  1. withContext(Dispatchers.IO)部分移动到那两个函数(实际进行网络调用的函数),并使它们挂起函数。

  2. getChannelsAsync 函数中删除 withContext(Dispatchers.IO) 部分。不过,请将其保留为暂停功能。

进行这些更改后,一切正常。我仍然不知道为什么,所以如果有人能透露一些信息,那将不胜感激。