Omówienie biblioteki Stron 2 Część stanowiąca część Androida Jetpack.

Biblioteka stronicowania ułatwia ładowanie i wyświetlanie niewielkich fragmentów danych jednocześnie. Ładowanie częściowych danych na żądanie zmniejsza wykorzystanie przepustowości sieci i zasobów systemowych.

W tym przewodniku znajdziesz kilka koncepcyjnych przykładów biblioteki wraz z omówieniem jej działania. Aby zapoznać się z pełnymi przykładami działania tej biblioteki, zapoznaj się z ćwiczeniami w Codelabs i z przykładami dostępnymi w sekcji z zasobami dodatkowymi.

Skonfiguruj

Aby zaimportować komponenty stronicowania do aplikacji na Androida, dodaj do jej pliku build.gradle te zależności:

Odlotowy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

Architektura biblioteki

W tej sekcji opisujemy i pokazujemy główne komponenty biblioteki stronicowania.

Lista stron

Kluczowym komponentem biblioteki stronicowania jest klasa PagedList, która wczytuje fragmenty danych aplikacji, czyli strony. W miarę zapotrzebowania na dane są one przenoszone do istniejącego obiektu PagedList. Jeśli wczytane dane ulegną zmianie, obiekt oparty na LiveData lub RxJava2 spowoduje wysłanie do obserwowanego właściciela danych nowego wystąpienia PagedList. W miarę generowania obiektów PagedList interfejs aplikacji prezentuje ich zawartość z poszanowaniem cykli życia kontrolerów interfejsu.

Ten fragment kodu pokazuje, jak skonfigurować model widoku aplikacji w celu wczytywania i prezentowania danych za pomocą operatora LiveData obiektów PagedList:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

Dane

Każda instancja obiektu PagedList wczytuje aktualny zrzut danych aplikacji z odpowiadającego mu obiektu DataSource. Przepływy danych z backendu lub bazy danych aplikacji do obiektu PagedList.

W tym przykładzie do porządkowania danych aplikacji wykorzystano bibliotekę przechowywania danych. Jeśli jednak chcesz przechowywać dane w inny sposób, możesz też udostępnić własną fabrykę źródeł danych.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

Więcej informacji o wczytywaniu danych do obiektów PagedList znajdziesz w przewodniku Wczytywania danych stronicowanych.

Interfejs użytkownika

Klasa PagedList współpracuje z PagedListAdapter w celu wczytywania elementów do RecyclerView. Klasy te współpracują ze sobą przy pobieraniu i wyświetlaniu wczytywanych treści, pobieraniu z wyprzedzeniem treści poza widocznym obszarem i animowaniu zmian treści.

Więcej informacji znajdziesz w przewodniku Wyświetlanie list z podziałem na strony.

Obsługują różne architektury danych

Biblioteka stronicowania obsługuje te architektury danych:

  • Udostępniane tylko z serwera backendu.
  • Są przechowywane tylko w bazie danych na urządzeniu.
  • Połączenie innych źródeł z wykorzystaniem bazy danych na urządzeniu jako pamięci podręcznej.

Rysunek 1 pokazuje przepływ danych w każdym z tych scenariuszy architektury. W przypadku rozwiązania działającego tylko w sieci lub baz danych dane trafiają bezpośrednio do modelu interfejsu użytkownika aplikacji. Jeśli stosujesz połączone podejście, dane przepływają z serwera backendu do bazy danych na urządzeniu, a następnie do modelu interfejsu aplikacji. Co jakiś czas w punkcie końcowym każdego przepływu danych brakuje danych do wczytania. Następnie od komponentu, który je dostarczył, wysyła żądanie dodatkowych danych. Jeśli np. zabraknie danych w bazie danych na urządzeniu, do serwera wysyłaj żądanie ich większej ilości.

Diagramy przepływów danych
Rysunek 1. Jak dane przepływają przez każdą z architektur obsługiwanych przez bibliotekę stronicowania

W pozostałej części tej sekcji znajdziesz zalecenia dotyczące konfigurowania poszczególnych przypadków użycia przepływu danych.

Tylko sieć

Aby wyświetlić dane z serwera backendu, użyj synchronicznej wersji Retrofit API, aby wczytać informacje do własnego niestandardowego obiektu DataSource.

Tylko baza danych

Skonfiguruj RecyclerView, aby obserwować pamięć lokalną. Najlepiej jest używać biblioteki trwałości sal. Dzięki temu za każdym razem, gdy dane są wstawiane lub modyfikowane w bazie danych aplikacji, zmiany te są automatycznie odzwierciedlane w usłudze RecyclerView, która je wyświetla.

Sieć i baza danych

Po rozpoczęciu obserwacji bazy danych możesz użyć polecenia PagedList.BoundaryCallback, aby wykrywać, kiedy w bazie danych doszło do wyczerpania danych. Możesz wtedy pobrać więcej elementów z sieci i umieścić je w bazie danych. Jeśli Twój interfejs użytkownika obserwuje bazę danych, nie musisz nic więcej robić.

Obsługa błędów sieci

Jeśli korzystasz z sieci do pobierania lub strony na stronie danych, które wyświetlasz za pomocą biblioteki stron docelowych, pamiętaj, aby nie traktować tej sieci przez cały czas jako „dostępnej” lub „niedostępnej”, ponieważ wiele połączeń jest niestabilnych lub niestabilnych:

  • Określony serwer może nie odpowiedzieć na żądanie sieciowe.
  • Urządzenie może być połączone z siecią, która działa wolno lub jest słaba.

Zamiast tego aplikacja powinna sprawdzać każde żądanie pod kątem błędów i jak najefektywniej przywracać dane w przypadku, gdy sieć jest niedostępna. Możesz na przykład udostępnić przycisk „Ponów”, który użytkownicy będą mogli wybrać, jeśli odświeżenie danych nie zadziała. Jeśli podczas kroku stronicowania danych wystąpi błąd, najlepiej ponawiać żądania automatycznie.

Aktualizowanie istniejącej aplikacji

Jeśli aplikacja pobiera już dane z bazy danych lub źródła backendu, możesz przejść bezpośrednio na wersję udostępnianą przez bibliotekę stronicowania. W tej sekcji dowiesz się, jak uaktualnić aplikację, która ma wspólny wygląd.

Niestandardowe rozwiązania do stronicowania

Jeśli korzystasz z funkcji niestandardowych do wczytywania niewielkich podzbiorów danych ze źródła danych aplikacji, możesz zastąpić tę logikę tą z klasy PagedList. Instancje PagedList oferują wbudowane połączenia z typowymi źródłami danych. Instancje te udostępniają też adaptery obiektów RecyclerView, które możesz uwzględnić w interfejsie aplikacji.

Dane wczytywane przy użyciu list zamiast stron

Jeśli jako bazowej struktury danych dla adaptacji interfejsu używasz listy w pamięci, rozważ obserwację aktualizacji danych za pomocą klasy PagedList, jeśli liczba elementów na liście może być bardzo duża. Instancje PagedList mogą używać LiveData<PagedList> lub Observable<List> do przekazywania aktualizacji danych do interfejsu aplikacji, co minimalizuje czasy wczytywania i wykorzystanie pamięci. Co więcej, zastąpienie obiektu List obiektem PagedList w aplikacji nie wymaga zmian w strukturze interfejsu aplikacji ani logiki aktualizacji danych.

Powiązywanie kursora danych z widokiem listy za pomocą CursorAdapter

Aplikacja może używać elementu CursorAdapter do powiązania danych z Cursor z ListView. W takim przypadku zwykle trzeba przeprowadzić migrację z ListView do RecyclerView jako kontenera interfejsu listy aplikacji, a następnie zastąpić komponent Cursor elementem Sala lub PositionalDataSource w zależności od tego, czy instancje Cursor mają dostęp do bazy danych SQLite.

W niektórych sytuacjach, np. podczas pracy z instancjami Spinner, udostępniasz tylko sam adapter. Biblioteka pobiera z niego wczytane dane i wyświetla je za Ciebie. W takiej sytuacji zmień typ danych adaptera na LiveData<PagedList>, a potem umieść tę listę w obiekcie ArrayAdapter, zanim klasa biblioteki będzie uzupełniać te elementy w interfejsie użytkownika.

Asynchroniczne ładowanie treści za pomocą metody AsyncListUtil

Jeśli używasz obiektów AsyncListUtil do asynchronicznego wczytywania i wyświetlania grup informacji, biblioteka stronicowania ułatwia ładowanie danych:

  • Dane nie muszą być pozycjonowane. Biblioteka stronicowania umożliwia ładowanie danych bezpośrednio z backendu za pomocą kluczy dostarczanych przez sieć.
  • Ilość danych może być niewyobrażalnie duża. Za pomocą biblioteki stronicowania możesz wczytywać dane na stronach tak długo, aż znikną wszystkie dane.
  • Możesz łatwiej obserwować dane. Biblioteka stronicowania może prezentować dane przechowywane przez model ViewModel aplikacji w obserwowalnej strukturze danych.

Przykłady baz danych

Poniższe fragmenty kodu pokazują kilka możliwych sposobów współdziałania wszystkich elementów.

Obserwowanie danych podziału na strony przy użyciu LiveData

Poniższy fragment kodu przedstawia wszystkie połączone ze sobą elementy. Gdy wydarzenia z koncertów są dodawane, usuwane lub zmieniane w bazie danych, zawartość RecyclerView jest automatycznie i skutecznie aktualizowana:

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

Obserwowanie danych z podziałem na strony przy użyciu RxJava2

Jeśli wolisz użyć RxJava2 zamiast LiveData, możesz utworzyć obiekt Observable lub Flowable:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

Możesz uruchomić i zatrzymać obserwację danych za pomocą kodu w tym fragmencie:

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

Java

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

Kody ConcertDao i ConcertAdapter są takie same w przypadku rozwiązań opartych na RxJava2 i LiveData.

Prześlij opinię

Podziel się z nami swoją opinią i pomysłami, korzystając z tych zasobów:

Śledzenie problemów
Zgłoś problemy, żebyśmy mogli naprawić błędy.

Dodatkowe materiały

Więcej informacji o bibliotece stronicowania znajdziesz w tych materiałach:

Próbki

Ćwiczenia z programowania

Filmy