Фрагмент не получает значение из ViewModel - PullRequest
2 голосов
/ 25 марта 2020

У меня есть одно приложение активности с навигацией Jetpack, мне нужна переменная объекта для всего моего приложения во многих фрагментах. Поэтому я использую ViewModel и создал класс Parent Fragment, который предоставляет ViewModel:

class MyViewModel : ViewModel() {
    var myData : CustomClass? = null
    ...
}

open class ParentFragment : Fragment {
    val model : MyViewModel by activityViewModels()
    lateinit var myData : CustomClass

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        model.myData?.let {
            myData = it
        }
    }
}

myData не должно быть нулевым, когда я использую ParentFragment, но иногда, случайно, я получаю kotlin.UninitializedPropertyAccessException: lateinit property myData has not been initialized когда я использую myData

Возможно ли, что моя ViewModel не сохраняет myData? Как я могу быть уверен, что мое свойство было инициализировано?

ОБНОВЛЕНИЕ: Попробуйте 1

Я пробовал этот код в моем ParentFragment:

open class ParentFragment : Fragment {
    val model : MyViewModel by activityViewModels()
    lateinit var backingData : CustomClass
    val myData : CustomClass
        get() {
            if (!::backingData.isInitialized)
                model.getData()?.let {
                    backingData = it
                }
            return backingData
        }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        model.getData?.let {
            backingData = it
        }
    }
}

Но проблема не исчезает, когда я звоню myData, кажется, что ViewModel теряет мои данные

ОБНОВЛЕНИЕ 2: Больше деталей кода

Прежде чем go внутри fragment, который расширяется ParentFragment, я устанавливаю свои данные в ViewModel, а затем перехожу к следующему fragment, как показано ниже:

// Inside FirstFragment
if (myData != null) {
    model.setData(myData)
    findNavController().navigate(FirstFragmentDirections.actionFirstToNextFragment())
}

Это возможно, что мой NavController выполняет навигацию до того, как были установлены данные?

РЕДАКТИРОВАТЬ 3: Попробуйте использовать пользовательский класс приложения

Согласно приведенному ниже ответу, я имею реализовал custom Application class, и я попытался передать свой объект через этот класс:

class MyApplication: Application() {

    companion object {
        var myObject: CustomClass? = null
    }
}

Но, к сожалению, для меня нет изменений. Может быть, мой объект слишком велик для правильного размещения?

Ответы [ 5 ]

2 голосов
/ 04 апреля 2020

Попробуйте это:

class MyViewModel : ViewModel() {
  var myObject : CustomClass? = null
  ...
}

open class ParentFragment : Fragment {
  lateinit var model : MyViewModel by activityViewModels()


  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    model = ViewModelProvider(this).get(MyViewModel::class.java)
    if(model.myObject == null) {
        // initialize myObject, will be persisted by ViewModel
    }
  }
}

Обратите внимание, что MyViewModel и его объекты-члены не должны содержать какие-либо ссылки на Activity, Fragment или Context, что включите любые косвенные ссылки на Context, такие как представления пользовательского интерфейса.

Я не буду рекомендовать LiveData (или MutableLiveData) в вашем случае, потому что "особенность" LiveData заключается в том, что их значения отправляется и обновляется асинхронно, поэтому вызов observe может быть слишком поздним.

1 голос
/ 10 апреля 2020

Ваша формулировка намекает на некоторый дизайн fl aws, а именно: Вы ссылаетесь на свои данные как на переменную объекта и делаете их доступными в любое время, когда вы решили использовать ViewModel. Для меня это звучит так, что вы переосмыслили свои варианты.

Предложение

Жизненный цикл вашего объекта, кажется, управляется вручную самостоятельно. Поэтому вы должны просто использовать переменную stati c. Это означает Kotlin как свойство внутри (сопутствующего) объекта. Я предлагаю вам объявить пользовательский класс Application в вашем манифесте и в * onCreate -методе вы выделяете свой объект и помещаете его в объект-компаньон этого класса. Конечно, вы можете выделить его в любое время позже. Это приведет к следующему:

  • Доступ всегда возможен через YourApplication.mData в вашем коде.
  • Объектами, которые зависят от реализаций вне JVM, можно правильно управлять. Например: если вы уже привязаны к порту, вы не сможете сделать это при последовательном вызове - например, когда viewModel восстанавливает свое состояние. Возможно, базовая реализация не сообщила об ошибке обратно Java, но выделение не удалось. Чтобы продемонстрировать это предположение, вам необходимо предоставить описание вашей объектной переменной. Но в качестве известного примера в мире Android для такого поведения попробуйте создать soundPool через SystemServices. Вы получите сообщения о правильном использовании этого объекта.
  • Снятие выделения может быть выполнено с помощью метода onTerminate() вашего Application.class // edit_4: Do c из super. onTerminate () говорит, что система просто убьет ваше приложение. Поэтому нужно освободить место в вашей деятельности. Смотрите фрагменты кода ниже.

Пояснение

Модель представления компонентов JetPack в основном отвечает за сохранение и восстановление состояния представления и привязку к его модели. Смысл в том, что он обрабатывает жизненный цикл между действиями, фрагментами и, возможно, представлениями. Вот почему вы должны использовать действие в качестве владельца жизненного цикла на тот случай, если вы захотите поделиться моделью представления между несколькими фрагментами. Но я все же полагаю, что ваш объект сложнее, чем просто POJO, и мое приведенное выше предложение приводит к вашему ожидаемому поведению. Также обратите внимание, что при многопоточности вы не должны полагаться на правильный порядок методов жизненного цикла. Существуют только ограниченные обратные вызовы жизненного цикла, которые гарантированно будут вызываться в определенном порядке c системой android, но, к сожалению, часто используемые здесь не включены. В этом случае вы должны начать обработку в более подходящее время.

Несмотря на то, что данные должны быть аналогичны предыдущему состоянию, точная ссылка зависит от реализации hashCode, но это спецификация JVM c.

/ / edit:

ParentFragment также является некорректным наименованием, поскольку вы создали класс, который другие наследуют, а не ссылаются на него. Если вы хотите получить доступ к определенной переменной c во всех ваших фрагментах, это необходимо реализовать как объект (Singleton), поскольку компонент Navigation не позволит вам напрямую обращаться к фрагменту менеджера. Проще говоря, android, один фрагмент всегда может ссылаться на его parentFragment, если , этот parentFragment использовал свой собственный childFragmentManager для фиксации фрагментаTransaction. Это означает также, что фрагменты, добавленные вашим Activity -gmentManager, никогда не имеют parentFragment.

// edit_2 + 3:

ViewModelProvider(activity!!, ViewModelFactory())[clazz]

- это правильный вызов для создания и доступа к sharedViewModel: владельцем жизненного цикла должна быть операция, иначе после каждой выполненной фрагмента транзакции произойдет обратный вызов метода onCleared () и viewModel освобождают все ссылки, чтобы избежать утечек памяти.

// edit_4: то, что ваш объект не был правильно инициализирован, было лишь предположением, которое может возникнуть, если вы попытаетесь инициализировать его снова. Например, если вы используете метод get () для val, где это не подходит. Тем не менее, обработка вашего объекта таким образом гарантирует, что его жизненный цикл находится за пределами ваших фрагментов. Вот пример кода, чтобы прояснить мою формулировку:

// edit_5: чтобы утверждать, что ссылка на объект не повреждена, включите проверку нуля (только если конструкция CustomClass не тривиальна)

Объявите свое пользовательское приложение

class CustomApplication : Application() {

    companion object SharedInstances {

        /**
         *   Reference to an object accessed in various places in your application.
         *
         *   This property is initialized at a later point in time. In your case, once
         *   the user completed a required workflow in some fragment.
         *
         *   @Transient shall indicate that the state could also be not Serializable/Parcelable
         *              This _could_ require manually releasing the object.
         *              Also prohibits passing via safeArgs
         */
        @Transient var complex: CustomClass? = null
    }

}

Инициализация и использование в ваших классах:

class InitializeComplexStateFragment: Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        if (complex != null) return@onViewCreated // prohibit successive initialization.
        if (savedInstanceState != null) { /* The fragment was recreated but the object appears to be lost. */ }
        // do your heavy lifting and initialize your data at any point.
        CustomApplication.SharedInstances.complex = object : CustomClass() {
            val data = "forExampleAnSessionToken"
            /* other objects could need manual release / deallocation, like closing a fileDescriptor */
            val cObject = File("someFileDescriptorToBindTo")
        }
    }

}

class SomeOtherFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        CustomApplication.SharedInstances.complex?.let {
                // do processing
            }
            ?: propagateErrorStateInFragment()
    }

    private fun propagateErrorStateInFragment() { throw NotImplementedError("stub") }
}

Отключение при необходимости

class SomeActivity: Activity() {

    override fun onStop() {
        super.onStop()
        /* with multiple activities the effort increases */
        CustomApplication.complex?.close()
    }
}
1 голос
/ 25 марта 2020

Вы можете использовать так:

class MyViewModel : ViewModel() {
   var mData: MutableLiveData<CustomClass>? = null
   init {
     mData = MutableLiveData<CustomClass>()
     mData!!.value =  CustomClass()
   }
   fun getData(): LiveData<CustomClass>? {
     return mData
   }
}

И ваш фрагмент:

open class ParentFragment : Fragment {
    lateinit var model  : MyViewModel
    lateinit var myObject : CustomClass

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        model =  ViewModelProvider(this).get(MyViewModel::class.java)
        model.getData()?.observe(viewLifecycleOwner, Observer {
            myObject = it
        })
    }
}
1 голос
/ 03 апреля 2020

В идеале вы должны ie задействовать жизненный цикл sharedVM и затем использовать один и тот же экземпляр sharedVM во всех фрагментах. Также инициализируйте myObject в классе parentFragment / activity с помощью setter (). Затем получите объект, используя getter (). Пример кода:

// SharedViewModel
var myObject : CustomClass? = null

fun setMyObject(obj : CustomClass?){
   myObject = obj
}

fun getMyObject():CustomClass?{
   return myObject
}

// Activity 
val model: SharedViewModel by viewModels()
model.setMyObject(objectValue)

// ParentFragment
private val model: SharedViewModel by activityViewModels()
val obj = model.getMyObject()

Надеюсь, это вам поможет. Счастливый код:)

1 голос
/ 25 марта 2020

Вы можете проверить, используя isInitialized в вашей собственности. Как сказано в документации:

Возвращает true, если этому свойству lateinit было присвоено значение, и false в противном случае.

Вы можете инициализировать ваше свойство как null и сделать null -проверьте с let, как вы уже это делаете, не нужно использовать lateinit и будьте осторожны с ним, это не заменит использование обнуляемой переменной

...