Сопоставьте несколько функций приостановки с одним LiveData

#android #kotlin #android-livedata #kotlin-coroutines

#Android #kotlin #android-livedata #kotlin-сопрограммы

Вопрос:

Компания, в которой я только начал работать, использует так называемый Navigator, который я пока интерпретирую как модель просмотра без состояния. Мой навигатор получает несколько вариантов использования, каждый из которых содержит 1 функцию приостановки. Результат любого из этих вариантов использования может оказаться в одном LiveData. У навигатора нет области действия сопрограммы, поэтому я передаю ответственность за приостановку использования фрагмента fetchValue() .

Самый текущий код в project имеет LiveData на уровне данных, чего я старался не делать. Из-за этого их livedata связана из view с dao.

Мои упрощенные классы:

 class MyFeatureNavigator(
    getUrl1: getUrl1UseCase,
    getUrl1: getUrl1UseCase
) {
    val url = MediatorLiveData<String>()

    fun goToUrl1() {
        url.fetchValue { getUrl1() }
    }

    fun goToUrl2() {
        url.fetchValue { getUrl2() }
    }

    fun <T> MediatorLiveData<T>.fetchValue(provideValue: suspend () -> T) {
        val liveData = liveData { emit(provideValue()) }
        addSource(liveData) {
            removeSource(liveData)
            value = it
        }
    }
}
 
 class MyFeatureFragment : Fragment {
    val viewModel: MyFeatureViewModel by viewModel()
    val navigator: MyFeatureNavigator by inject()

    fun onViewCreated() {
         button.setOnClickListener { navigator.goToUrl1() }

         navigator.url.observe(viewLifecycleOwner, Observer { url ->
             openUrl(url)
         })
    }
}
 

Мои два вопроса:

  • fetchValue() Хороший ли способ связать функцию приостановки с LiveData? Может ли это произойти утечка? Есть другие проблемы?
  • Моя основная причина использовать только сопрограммы (и поток) на уровне данных — «потому что так сказал Google». Какая лучшая причина для этого? И: каков наилучший компромисс в соответствии с проектом и текущими передовыми методами кодирования?

Ответ №1:

  • Является ли fetchValue() хорошим способом связать функцию приостановки с LiveData? Может ли это произойти утечка? Есть другие проблемы?

Как правило, это должно работать. Вероятно, вам следует удалить предыдущий источник MediatorLiveData перед добавлением нового, в противном случае, если вы получите два вызова fetchValue подряд, первый URL-адрес может быть извлечен медленнее, поэтому он придет позже и выиграет. Я не вижу никаких других проблем с корректностью, но этот код довольно сложный, создает пару промежуточных объектов и, как правило, его трудно читать.

  • Моя основная причина использовать только сопрограммы (и поток) на уровне данных — «потому что так сказал Google». Какая лучшая причина для этого?

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

Вероятно, вы имеете в виду рекомендацию использовать LiveData вместо уровня Flow пользовательского интерфейса. Это не строгое правило, и у него есть одна причина: LiveData является владельцем значения, он сохраняет свою ценность и немедленно предоставляет ее новым подписчикам, не выполняя никакой работы. Это особенно полезно на уровне пользовательского интерфейса / ViewModel — когда происходит изменение конфигурации и действие / фрагмент воссоздается, вновь созданный activity / fragment использует ту же модель представления, подписывается на LiveData нее и получает значение бесплатно.

В то же время Flow «холодно», и если вы предоставляете поток из своей модели представления, каждая реконфигурация будет запускать новую коллекцию потоков, и поток будет выполняться с нуля.

Так, например, если вы извлекаете данные из базы данных или сети, LiveData просто предоставите последнее значение новому подписчику и Flow снова выполните дорогостоящую операцию db / network.

Итак, как я уже сказал, строгого правила не существует, это зависит от конкретного варианта использования. Также я нахожу его очень полезным для использования Flow в моделях просмотра — он предоставляет множество операторов и делает код чистым и кратким. Но затем я преобразовываю его в a LiveData с помощью расширений, подобных asLiveData() и предоставляю это LiveData пользовательскому интерфейсу. Таким образом, я получаю лучшее от обоих слов — LiveData улавливает значение между реконфигурациями и Flow делает код моделей представления красивым и чистым. Также вы можете использовать последние StateFlow версии, и SharedFlow часто они также могут помочь преодолеть упомянутую Flow проблему на уровне пользовательского интерфейса.

Возвращаясь к вашему коду, я бы реализовал его следующим образом:

 class MyFeatureNavigator(
    getUrl1: getUrl1UseCase,
    getUrl1: getUrl1UseCase
) {
    private val currentUseCase = MutableStateFlow<UseCase?>(null)

    val url = currentUseCase.filterNotNull().mapLatest { source -> source.getData()}.asLiveData()


    fun goToUrl1() {
        currentUseCase.value = getUrl1
    }

    fun goToUrl2() {
        currentUseCase.value = getUrl2
    }
}
 

Таким образом, нет никаких условий гонки, о которых нужно заботиться, и код чистый.

  • И: каков наилучший компромисс в соответствии с проектом и текущими передовыми методами кодирования?

Это спорный вопрос, и это должно быть в первую очередь командное решение. В большинстве проектов, в которых я участвовал, мы приняли это правило: при исправлении ошибок, выполнении обслуживания существующего кода следует придерживаться того же стиля. При выполнении большого рефакторинга / внедрении новых функций следует использовать новейшие методы, принятые командой.