Сеть Android PagingLibrary + база данных

Я работаю над приложением обмена сообщениями, и я реализую базу данных + сеть, чтобы сохранять сообщения чата из api и показывать их из базы данных. Я использую BoundaryCallback для получения сообщения, когда в базе данных больше нет данных. Мой api работает так:

getlist(       @Query("msgid") long msgid,
               @Query("loadolder") boolean olderOrNewer,
               @Query("showcurrentMessage") boolean showcurrentMessage,
               @Query("MsgCountToLoad") int MsgCountToLoad);
  • msgid: идентификатор последнего сообщения этого чата. если база данных пуста, я запрашиваю chat.getlastmessageid, если в базе данных есть данные, но данных больше нет, я отправлю идентификатор последнего сообщения в базе данных, чтобы загрузить больше, и если в первый раз открывается чат, идентификатор последнего сообщения в базе данных не был равен чату. lastmessageid означает, что нужно загрузить новое сообщение.
  • loadolder: этот флаг false, чтобы сообщить api о загрузке новых сообщений с этого идентификатора сообщения, который я отправил вам, и т.д.
  • showcurrentMessage: если true, то мне также будет выдано текущее сообщение (msgid)
  • MsgCountToLoad: сколько сообщений принимать из api

вопрос в том, как с этим справиться в Pagginglibrary? Как указать ему загружать более старое или новое сообщение в зависимости от положения прокрутки. В первый раз загрузить данные легко, он вернет нулевой объект, поэтому я буду использовать chat.lastmessageid в следующий раз, открывая чат, где я могу проверить, равно ли chat.lastmessageid db.lastmessageid, и сказать ему, чтобы он загружал больше новых сообщений.


person lvl4fi4    schedule 29.06.2020    source источник
comment
Вот пример codelab, который может помочь: codelabs.developers.google.com/codelabs/android- пейджинг   -  person Sumit Sahoo    schedule 29.06.2020


Ответы (2)


PagedList.BoundaryCallback имеет два отдельных API для добавления и добавления.

Вам следует попробовать реализовать эти методы:

onItemAtEndLoaded
onItemAtFrontLoaded

Предполагая, что при начальной загрузке загружаются самые последние сообщения, а при прокрутке вверх загружаются более старые сообщения, вы можете просто передать true вместо loadolder в onItemAtFrontLoaded и false в onItemAtEndLoaded.

person dlam    schedule 29.06.2020

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

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

Мое решение состоит из двух основных частей!

  1. Наблюдение за базой данных с помощью библиотеки подкачки.
  2. Наблюдение за RecyclerView, чтобы понять, когда запрашивать у сервера страницы данных.

Для демонстрации мы собираемся использовать класс сущности, представляющий Person:

@Entity(tableName = "persons")
data class Person(
@ColumnInfo(name = "id") @PrimaryKey val id: Long,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "update_time") val updateTime: Long

)

1. Соблюдайте базу данных

Начнем с первого, более простого: для наблюдения за базой данных мы собираемся определить метод в нашем dao, который возвращает DataSource.Factory ‹Int, Person›

@Dao
interface PersonDao {
@Query("SELECT * FROM persons ORDER BY update_time DESC")
fun selectPaged(): DataSource.Factory<Int, Person>
}

И теперь в нашей ViewModel мы собираемся создать PagedList из нашей фабрики.

class PersonsViewModel(private val dao: PersonDao) : ViewModel() {
val pagedListLiveData : LiveData<PagedList<Person>> by lazy {
    val dataSourceFactory = personDao.selectPaged()
    val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .build()
    LivePagedListBuilder(dataSourceFactory, config).build()
}
}

И, с нашей точки зрения, мы можем наблюдать список с разбивкой по страницам.

class PersonsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_persons)

    viewModel.pagedListLiveData.observe(this, Observer{
        pagedListAdapter.submitList(it)
    })
  }
}

Хорошо, теперь это то, чем мы должны заниматься в первой части. Обратите внимание, что мы используем PagedListAdapter. Также мы можем сделать некоторые дополнительные настройки в нашем объекте PagedList.Config, но для простоты мы его опускаем. Снова обратите внимание, что мы не использовали BoundaryCallback в нашем LivePagedListBuilder.

2. Обратите внимание на RecyclerView

В основном то, что мы должны сделать здесь, - это наблюдать за списком и в зависимости от того, где в списке мы находимся прямо сейчас, попросить сервер предоставить нам соответствующую страницу данных. Для наблюдения за положением RecyclerView мы будем использовать простую библиотеку под названием Paginate.

class PersonsActivity : AppCompatActivity(), Paginate.Callbacks {
private var page = 0
private var isLoading = false
private var hasLoadedAllItems = false
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_persons)

    viewModel.pagedListLiveData.observe(this, Observer{
        pagedListAdapter.submitList(it)
    })
    Paginate.with(recyclerView, this).build()
 }
override fun onLoadMore() {
    // send the request to get corresponding page
 }
override fun isLoading(): Boolean = isLoading
override fun hasLoadedAllItems(): Boolean = hasLoadedAllItems
}

Как видите, мы связали Paginate с представлением recycler, и теперь у нас есть три обратных вызова. isLoading () должен возвращать состояние сети. hasLoadedAllItems () показывает, достигли ли мы последней страницы и больше нет данных для загрузки с сервера. Большая часть того, что мы делаем, реализуем последний метод onLoadMore ().

На этом этапе мы должны сделать три вещи:

  1. Основываясь на позиции recyclerView, мы просим сервер предоставить нам правильную страницу данных.
  2. Используя свежие данные с сервера, мы обновляем базу данных, что приводит к обновлению PagedList и отображению свежих данных. Не забывайте, что мы наблюдаем за базой данных!
  3. Если запрос не выполняется, мы показываем ошибку.

С помощью этих простых шагов мы решаем две проблемы. Прежде всего, несмотря на то, что BoundaryCallbak не имеет обратного вызова для получения уже полученных данных, мы запрашиваем каждую страницу по запросу, чтобы мы могли заметить обновленные объекты, а также обновить нашу собственную локальную базу данных. Во-вторых, мы можем легко показать состояние сети, а также показать возможные сбои в сети. Звучит нормально, правда? Что ж, мы еще не решили ни одной конкретной проблемы. И вот что, если один объект будет удален с удаленного сервера. Как мы это заметим! Что ж, здесь на помощь приходит упорядочение данных. С помощью действительно старого трюка сортировки данных мы можем заметить промежутки между нашими людьми. Например, теперь мы можем отсортировать наших людей по их update_time, если возвращенная страница JSON с сервера выглядит так:

{
 "persons": [{
 "id": 1,
 "name": "Reza",
 "update_time": 1535533985000
}, {
 "id": 2,
 "name": "Nick",
 "update_time": 1535533985111
}, {
 "id": 3,
 "name": "Bob",
 "update_time": 1535533985222
}, {
 "id": 4,
 "name": "Jafar",
 "update_time": 1535533985333
}, {
 "id": 5,
 "name": "Feryal",
 "update_time": 1535533985444
}],
 "page": 0,
 "limit": 5,
 "hasLoadedAllItems": false
}

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

override fun onLoadMore() {
if (!isLoading) {
    isLoading = true
    viewModel.loadPersons(page++).observe(this, Observer { response ->
        isLoading = false
        if (response.isSuccessful()) {
            hasLoadedAllItems = response.data.hasLoadedAllItems
        } else {
            showError(response.errorBody())
        }
    })
  }
}

Но волшебство происходит во ViewModel

class PersonsViewModel(
    private val dao: PersonDao,
    private val networkHelper: NetworkHelper
 ) : ViewModel() {
 fun loadPersons(page: Int): LiveData<Response<Pagination<Person>>> {
    val response = 
            MutableLiveData<Response<Pagination<Person>>>()
    networkHelper.loadPersons(page) {
        dao.updatePersons(
                it.data.persons,
                page == 0,
                it.hasLoadedAllItems)
        response.postValue(it)
    }
    return response
   }
 }

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

@Dao
interface PersonDao {
@Transaction
fun updatePersons(
        persons: List<Person>,
        isFirstPage: Boolean,
        hasLoadedAllItems: Boolean) {
    val minUpdateTime = if (hasLoadedAllItems) {
        0
    } else {
        persons.last().updateTime
    }

    val maxUpdateTime = if (isFirstPage) {
        Long.MAX_VALUE
    } else {
        persons.first().updateTime
    }

    deleteRange(minUpdateTime, maxUpdateTime)
    insert(persons)
   }

   @Query("DELETE FROM persons WHERE
        update_time BETWEEN
        :minUpdateTime AND :maxUpdateTime")
   fun deleteRange(minUpdateTime: Long, maxUpdateTime: Long)
   @Insert(onConflict = REPLACE)
   fun insert(persons: List<Person>)
  }

Здесь, в нашем dao, сначала мы удаляем всех людей, время обновления которых находится между первым и последним человеком в списке, возвращаемом с сервера, а затем вставляем список в базу данных. При этом мы позаботились о том, чтобы любой человек, который был удален на сервере, также был удален из нашей локальной базы данных. Также обратите внимание, что мы обрабатываем эти два вызова методов внутри базы данных @Transaction для лучшей оптимизации. Изменения в базе данных будут отправляться через наш PagedList, таким образом обновляя пользовательский интерфейс, и на этом мы закончили.

person vinod yadav    schedule 29.06.2020
comment
спасибо за подробное объяснение, которое я разработал на основе `BoundaryCallback`, но я почти уверен, что ваш способ работы будет работать отлично. это поможет кому-то - person lvl4fi4; 29.06.2020
comment
представьте, что вы добавляете переменную времени обновления в свои бэкэнд-модели, чтобы заставить пользовательский интерфейс работать на внешнем интерфейсе - person DennisVA; 12.01.2021