Отображение диалогового окна в MVVM Android вызывает исключение только при изменении определенной переменной в модели представления

#android #android-fragments #mvvm #android-alertdialog #android-databinding

Вопрос:

У меня есть основная активность, использующая разметку и вкладки с 2 фрагментами.

Мой первый фрагмент содержит список элементов в окне просмотра вторсырья, и я могу щелкнуть по каждому элементу, чтобы «выбрать» его (что вызывает функцию SDK для входа на аппаратное устройство). При выборе этого параметра происходит изменение модели представления фрагмента:

 // 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
 

Затем у меня есть общая модель представления между обоими фрагментами, которая также имеет 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()
            }
        })
    }
 

Проблема в том, что если currentDevice переменная общей модели представления установлена, я получаю исключения всякий раз, когда пытаюсь открыть диалоговое окно или начать новое действие. Если я изменю функцию setCurrentDevice в общей модели просмотра, то она будет работать нормально (или если я не выберу какое-либо устройство).

Исключения, которые я вижу, это при запуске нового действия:

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

Следующая строка доставляет мне неприятности:

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

Другие функции просто выполняют сетевой вызов и извлекают некоторые данные, это вообще не должно мешать пользовательскому интерфейсу.

Я тестирую Xiaomi Mi A3 с помощью Android 10.

Кто-нибудь может мне помочь? Спасибо.

Ответ №1:

Так что я действительно не знаю, почему, но я нашел ответ.

В SDK функции getZeroChannel и getAllChannels не приостанавливали функции, хотя они выполняли сетевой вызов. Так что я сделал вот что:

  1. Переместите withContext(Dispatchers.IO) часть в эти две функции (те, которые фактически выполняют сетевой вызов) и заставьте их приостановить функции.
  2. Удалите withContext(Dispatchers.IO) деталь из getChannelsAsync функции. Однако сохраняйте его как функцию приостановки.

После этих изменений все работает так, как ожидалось. Я до сих пор не знаю, почему, так что, если бы кто-нибудь мог пролить немного света, это было бы очень ценно.

Комментарии:

1. Привет, это внутренний SDK, который я разрабатываю, поэтому я контролирую исходный код. Он использует SDK некоторых производителей внутри, но функции можно обобщить в разделе «сделайте какой-нибудь сетевой вызов, извлеките какой-нибудь объект». Это SDK для камер видеонаблюдения и регистраторов.