Android Paging 3: Как изменить параметры RemoteMediator

Я борюсь с библиотекой Paging 3 Jetpack.

Я настраиваю

  • Модернизация для вызовов сетевого API
  • Место для хранения полученных данных
  • Репозиторий, который предоставляет Pager.flow (см. Код ниже)
  • RemoteMediator для кэширования сетевых результатов в базе данных комнаты.

PagingSource создан Room.

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

Все идет нормально. Вот моя ViewModel для получения списка Sources:

    private lateinit var _sources: Flow<PagingData<Source>>
    val sources: Flow<PagingData<Source>>
        get() = _sources

    
    private fun fetchSources() = viewModelScope.launch {
        _sources = sourcesRepository.getSources(
            selectedRepositoryUuid,
            selectedRef,
            selectedPath
        )
    }

val sources собирается в Fragment.

fetchSources() вызывается при изменении одного из трех параметров (selectedRepositoryUuid, selectedRef или selectedPath)

Вот репозиторий для вызова пейджинга

    fun getSources(repositoryUuid: String, refHash: String, path: String): Flow<PagingData<Source>> {
        return Pager(
            config = PagingConfig(50),
            remoteMediator = SourcesRemoteMediator(repositoryUuid, refHash, path),
            pagingSourceFactory = { sourcesDao.get(repositoryUuid, refHash, path) }
        ).flow
    }

Теперь я вижу, что сначала вызывается Repository.getSources с правильными параметрами, создаются RemoteMediator и PagingSource, и все в порядке. Но как только один из трех параметров изменится (скажем, path), ни RemoteMediator, ни PagingSource не будут воссозданы. Все запросы по-прежнему пытаются получить исходные записи.

Мой вопрос: как я могу использовать здесь библиотеку Paging 3 в случаях, когда содержимое подкачки зависит от динамических переменных?

Если это помогает понять мой вариант использования: RecyclerView отображает список файлов и папок с разбивкой по страницам. Как только пользователь щелкает папку, содержимое RecyclerView должно измениться, чтобы отобразить файлы выбранной папки.


Обновлять:

Благодаря ответу dlam код теперь выглядит так. Код - это упрощение реального кода. Я в основном инкапсулирую всю необходимую информацию в классе SourceDescription.:

ViewModel:

    private val sourceDescription = MutableStateFlow(SourceDescription())

    fun getSources() = sourceDescription.flatMapConcat { sourceDescription ->

        // This is called only once. I expected this to be called whenever `sourceDescription` emits a new value...?

        val project = sourceDescription.project
        val path = sourceDescription.path

        Pager(
            config = PagingConfig(30),
            remoteMediator = SourcesRemoteMediator(project, path),
            pagingSourceFactory = { sourcesDao.get(project, path) }
        ).flow.cachedIn(viewModelScope)
    }

    fun setProject(project: String) {
        viewModelScope.launch {
            val defaultPath = Database.getDefaultPath(project)
            val newSourceDescription = SourceDescription(project, defaultPath)
            sourceDescription.emit(newSourceDescription)
        }
    }

В пользовательском интерфейсе пользователь сначала выбирает проект, который поступает из ProjectViewModel через LiveData. Как только у нас есть информация о проекте, мы устанавливаем ее в SourcesViewModel, используя метод setProject, указанный выше.

Фрагмент:

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

        // load the list of sources
        viewLifecycleOwner.lifecycleScope.launchWhenStarted {
            sourcesViewModel.getSources().collectLatest { list ->
                sourcesAdapter.submitData(list) // this is called only once in the beginning
            }
        }

        projectsViewModel.projects.observe(viewLifecycleOwner, Observer { project -> 
            sourcesViewModel.setProject(project)
        })
    }

person muetzenflo    schedule 08.03.2021    source источник


Ответы (1)


Общий вывод Paging - это Flow<PagingData>, поэтому обычно лучше всего подойдет смешивание вашего сигнала (пути к файлу) с потоком с помощью некоторой операции потока. Если вы можете смоделировать путь, по которому нажимает пользователь, как Flow<String>, может сработать что-то вроде этого:

ViewModel.kt

class MyViewModel extends .. {
  val pathFlow = MutableStateFlow<String>("/")
  val pagingDataFlow = pathFlow.flatMapLatest { path ->
    Pager(
      remoteMediator = MyRemoteMediator(path)
      ...
    ).flow.cachedIn(..)
  }
}

RemoteMediator.kt

class MyRemoteMediator extends RemoteMediator<..> {
  override suspend fun load(..): .. {
    // If path changed or simply on whenever loadType == REFRESH, clear db.
  }
}

Другая стратегия, если у вас все загружено, - передать путь прямо в PagingSource, но похоже, что ваши данные поступают из сети, поэтому подход RemoteMediator, вероятно, здесь лучше всего.

person dlam    schedule 13.03.2021
comment
Большое спасибо за этот вклад! Теперь я на шаг впереди. Но у меня есть два дополнительных вопроса, потому что это еще не работает: 1. Компилятор позволяет использовать только flatMapConcat вместо flatMap. Это имеет значение? 2. Если я вызываю в своей ViewModel pathFlow.emit("/new/path"), тогда pagingDataFlow не обновляется, хотя я собираю его в своем фрагменте. Вы знаете почему? Я обновлю свой вопрос, чтобы дать вам свой текущий код. - person muetzenflo; 14.03.2021
comment
Извините, я забыл, что Flow.flatMap устарел, flatMapConcat является правильным, поскольку flatMapMerge предназначен для ограничения числа одновременных пользователей ›1. - person dlam; 14.03.2021
comment
По второй проблеме - собираете ли вы данные из pagingDataFlow, используя collect или collectLatest? Обязательно используйте collectLatest, поскольку он отменит предыдущее поколение при появлении нового, поскольку .submitData() может работать в течение некоторого времени, прежде чем аннулирование его закроет. - person dlam; 14.03.2021
comment
Я пробовал и collect, и collectLatest. Ни один не работал. Я обновил код в своем вопросе. Высоко ценю ваши комментарии! - person muetzenflo; 14.03.2021
comment
Если это не ваша проблема, мне нужно немного больше информации ... может быть, какой-нибудь тестовый код для выбросов pagingDataFlow, который показывает, каким должно быть ваше ожидаемое поведение? - person dlam; 14.03.2021
comment
Убедитесь, что ваш код фрагмента также обновлен, поскольку он все еще показывает использование collect. - person dlam; 14.03.2021
comment
К сожалению, вы также должны использовать flatMapLatest вместо flatMap. Мне там плохо. - person dlam; 14.03.2021
comment
О_О что-то происходит !! По-прежнему не работает end2end, но теперь это скорее проблема API. Я не могу в это поверить, ха-ха. Бился головой об это уже несколько дней. Я надеюсь закончить это в ближайшие дни, и если Flow теперь работает, я с радостью потрачу здесь немного денег за ваши усилия и быструю реакцию! Большое спасибо, мне нравится это сообщество :) - person muetzenflo; 15.03.2021
comment
Хорошо, рад, что теперь это работает для вас! :) - person dlam; 15.03.2021
comment
У этого решения есть проблема: если действие воссоздается или если пользователь покидает приложение и возвращается к нему, сбор потока перезапускается. Затем MutableStateFlow повторно выдаст свое последнее значение, вызывая создание нового экземпляра Pager. Таким образом, кэширование данных подкачки бесполезно, потому что все будет загружаться с нуля в каждой новой коллекции Flow. - person BladeCoder; 16.05.2021