Android Architecture Components: Paging

Jest to cykl artykułów poświęcony komponentom architektury Androida. Wszystkie artykuły znajdziesz na tej stronie.

Ile razy widziałeś listę w aplikacji na system Android? A ile razy tworzyłeś listę w swoim projekcie z mnóstwem danych? Gdy mamy mało informacji nie ma większego problemu z wydajnością i optymalizacją. Problem pojawia się, gdy mamy bardzo dużo rekordów do wyświetlenia. Zastanawiamy się, czy ściągnąć wszystkie dane, czy po trochu. Gdy ściągamy wszystkie dane możemy “zablokować aplikacji na pewien czas”, aż wszystko się wczyta. Z drugiej strony, gdy pobieramy dane w kawałkach musimy odpowiednio zaprogramowac takie pobieranie i wyświetlanie informacji, co nie zawsze jest łatwe. Istnieją pewne biblioteki, które częściowo nam w tym pomagają. System Android dostał taką bibliotekę od Google, która została zaprezentowana na IO w 2018 roku. Zwie się ona Paging. Dziś własnie nią sie zajmiemy.

Android Architecture Components: Paging

Biblioteka Paging ułatwia stopniowe ładowanie danych do aplikacji. Biblioteka ta obsługuje zarówno listy ograniczone jak i  nieograniczone. Oprócz tego pomaga Twojej aplikacji obserwować i  wyświetlać rozsądny podzbiór danych. Ta funkcja ma kilka zalet. Jeżeli pobierasz informacje z internetu to dane będą pobierane częsciowo nie  wszystkie na raz. Dzięki temu aplikacja zużywa mniej zasobów systemowych, a także sieciowych. Kolejną zaletą jest to, że podczas aktualizacji danych i odświeżania, aplikacja nadal reaguje szybko na  nowe informacje. Nie tylko te, które otrzyma, ale także te, które zostaną wprowadzone przez użytkownika. Biblioteka Paging składa się z trzech głównych komponentów. Są to:

  • DataSource — przechowuje dane, na przykład z bazy danych lub zewnętrznych źródeł, takie jak sieciowe interfejsy API. Klasa ta służy jako lokalna pamięć dla PagedList.
  • PagedList — ten komponent umożliwia modułowi RecyclerView załadowanie fragmentu danych ze źródła danych. PagedList to kolekcja, która ładuje dane asynchronicznie. Najprościej mówiąc jest to lista.
  • PagedListAdapter — klasa ta prezentuje dane z PagedList w RecyclerView. PagedListAdapter nadsłuchuje wywołań zwrotnych z PagedList.

Biblioteki można używać tylko z RecyclerView. Jeśli używasz ListView do  wyświetlania list, powinieneś najpierw przeprowadzić migrację do wzorca ViewHolder. Oprócz tego działa bezproblemowo z istniejącymi komponentami architektury, takimi jak Room, ViewModel, LiveData. Za chwilę przekonasz się w jaki sposób. Uzupełnij wiedze o: Lista rozwijana i podzielona na kategorie w Androidzie

Prosty przykład

Poniżej przedstawiam prosty przykład, który wyświetla komentarze. Aplikacja składa się z przycisku, który generuje przykładowe dane oraz z listy RecyclerView. Dane będą pobierane z bazy danych — skorzystamy z biblioteki Room. Na początku jak zawsze musimy dodać zależności do pliku gradle w naszym projekcie. W tym celu udaj się na stronę i dodaj odpowiednią wpisy dla swojego projektu. Nie zapomnij również dodać RecyclerView, Room, ViewModle i LiveData, ponieważ są używane w przykładzie. Skorzystałem również z Java Faker w celu wygenerowania przykładowych komentarzy, imion i nazwisk.

Tworzymy źródło danych

W pierwszej kolejności zajmiemy się źródłem danych. W naszym przypadku będzie to baza danych. Spójrz na kod.

    @Entity(tableName = "Comments")
    public class CommentEntity {
        @PrimaryKey(autoGenerate = true)
        private int id;
        private String fullName;
        private String txtComment;
        public CommentEntity(String fullName, String txtComment) {
            this.fullName = fullName;
            this.txtComment = txtComment;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getFullName() {
            return fullName;
        }
        public void setFullName(String fullName) {
            this.fullName = fullName;
        }
        public String getTxtComment() {
            return txtComment;
        }
        public void setTxtComment(String txtComment) {
            this.txtComment = txtComment;
        }
    }
    @Dao
    public interface CommentsDAO {
        @Query("SELECT * FROM Comments")
        DataSource.Factory<Integer, CommentEntity> getallcomments();
        @Query("SELECT * FROM Comments WHERE id=:id")
        CommentEntity getComment(int id);
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        long insertComment(CommentEntity comment);
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insertComments(List<CommentEntity> comment);
        @Update
        void update(CommentEntity commen);
        @Query("DELETE FROM Comments")
        void deleteAll();
    }
    @Database(entities = {CommentEntity.class}, version = 1, exportSchema = false)
    public abstract class AppDatabase extends RoomDatabase {
        private static String DATA_BASE_NAME = "DataBase_Pagging";
        private static AppDatabase DATA_BASE_INSTANCE;
        public static AppDatabase getDatabase(final Context context) {
            if (DATA_BASE_INSTANCE == null) {
                synchronized (AppDatabase.class) {
                    if (DATA_BASE_INSTANCE == null) {
                        DATA_BASE_INSTANCE = Room.databaseBuilder(
                                context.getApplicationContext(),
                                AppDatabase.class, DATA_BASE_NAME)
                                .build();
                    }
                }
            }
            return DATA_BASE_INSTANCE;
        }
        public abstract CommentsDAO commentsDAO();
    }

Na początku tworzymy klasę, w której obiekty będziemy przechowywać w naszej bazie danych. Jest tworzona tabela bazy danych do przechowywania elementów. W naszym przypadku będą to komentarze, Musisz odwołać się do klasy w klasie AppDatabase. W klasie DAO mamy metodę, która pobiera nam komentarze. Ta funkcja zwraca DataSource.Factory (realizowane przez Room) i tworzy DataSource. Następnie LivePagedListBuilder buduje LiveData<PagedList>. 

ViewModel i Repository

    public class CommentsViewModel extends AndroidViewModel {
        private CommentsRepository repositoryComments;
        private LiveData<PagedList<CommentEntity>> commentsList;
        public CommentsViewModel(@NonNull Application application) {
            super(application);
            repositoryComments = new CommentsRepository(application);
        }
        public LiveData<PagedList<CommentEntity>> getComments() {
            if (commentsList == null) {
                commentsList = repositoryComments.getAllComments();
            }
            return commentsList;
        }
        public CommentEntity getComment(int id) throws ExecutionException, InterruptedException {
            return repositoryComments.getComment(id);
        }
        public void insertSampleData() {
            repositoryComments.insertSampleComments();
        }
    }
    public class CommentsRepository {
        private CommentsDAO mCommentsDAO;
        public CommentsRepository(Application application) {
            AppDatabase db = AppDatabase.getDatabase(application);
            mCommentsDAO = db.commentsDAO();
        }
        public LiveData<PagedList<CommentEntity>> getAllComments() {
            PagedList.Config config
                    = new PagedList.Config.Builder()
                    .setPageSize(20)
                    .setInitialLoadSizeHint(15)
                    .setPrefetchDistance(10)
                    .setEnablePlaceholders(true)
                    .build();
            DataSource.Factory<Integer, CommentEntity> factory = mCommentsDAO.getallcomments();
            return new LivePagedListBuilder<>(factory, config).build();
        }
        public CommentEntity getComment(int id) throws ExecutionException, InterruptedException {
            return mCommentsDAO.getComment(id);
        }
        public void insertSampleComments() {
            new insertCommentsAsync(mCommentsDAO).execute();
        }
        private static class insertCommentsAsync extends AsyncTask<Void, Void, Void> {
            private CommentsDAO mCommentsDAO;
            insertCommentsAsync(CommentsDAO commentsDAO) {
                mCommentsDAO = commentsDAO;
            }
            @Override
            protected Void doInBackground(Void... comments) {
                List<CommentEntity> commentsList = new ArrayList<>();
                Faker faker = new Faker();
                for (int x=0 ; x<=20 ; x++) {
                    String name = faker.name().fullName() ;
                    String text = faker.howIMetYourMother().quote();
                    commentsList.add(new CommentEntity(name,text));
                }
                mCommentsDAO.insertComments(commentsList);
                return null;
            }
        }
    }

W klasie CommentsRepository mamy metodę getAllComments(), w której LivePagedListBuilder generuje listę LiveData w porcjach (stronach), korzystając z przekazanego DataSource.Factory i  konfiguracji PagedList. Cały mechanizm działa w tle ponieważ nie chcemy blokować aplikacji. Po utworzeniu listy wysyła nową listę PagedList do ViewModel, który z  kolei przekazuje ją do interfejsu użytkownika. Robi się to w ten sposób, aby zapobiec przekazywaniu do wątku UI listy bez zawartości, która nie  powinna być wyświetlana. Aby zbudować i skonfigurować LiveData, użyj LivePagedListBuilder. Oprócz DataSource.Factory należy podać konfigurację PagedList, która może zawierać następujące opcje:

  • setPageSize — rozmiar listy ładowanej przez PagedList.
  • setPrefetchDistance — kiedy pobrać kolejne elementy z bazy. Czyli różnica między wyświetlanymi elementami a pobieranymi.
  • setInitialLoadSizeHint — początkowa liczba elementów do załadowania. Czyli ile elementów na początku ma posiadać lista po otwarciu aplikacji.
  • setEnablePlaceholders — określa, czy puste pozycje mają zostać dodane do PagedList, aby reprezentować dane, które nie zostały jeszcze załadowane.

Interfejs użytkownika

onCreate() tworzymy instancję ViewModel. Obserwujemy listę PagedList i przekazujemy ją do PagedListAdaptera do aktualizacji widoku RecyclerView.

    public class MainActivity extends AppCompatActivity {
        private RecyclerView mRecyclerView;
        private CommentsAdapter mAdapter;
        private CommentsViewModel mViewModel;
        private Button generateData;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            init();
            mViewModel = ViewModelProviders.of(this).get(CommentsViewModel.class);
            mViewModel.getComments().observe(this, comments -> {
                mAdapter.submitList(comments);
            });
            generateData.setOnClickListener(view -> { mViewModel.insertSampleData(); } );
        }
        private void init() {
            generateData = findViewById(R.id.generateData);
            mAdapter = new CommentsAdapter();
            mRecyclerView = findViewById(R.id.recycler_view);
            mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
            mRecyclerView.setHasFixedSize(true);
            mRecyclerView.setItemAnimator(new DefaultItemAnimator());
            mRecyclerView.setAdapter(mAdapter);
        }
    }
package net.myenv.architecturecomponents.paging.basic;


import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import net.myenv.architecturecomponents.R;
import net.myenv.architecturecomponents.paging.basic.db.CommentEntity;

import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;

public class CommentsAdapter extends PagedListAdapter<CommentEntity, CommentsAdapter.CustomViewHolder> {

    public CommentsAdapter() {
        super(DIFF_CALLBACK);
    }

    @NonNull
    @Override
    public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_comments_list, parent, false);
        return new CustomViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull CustomViewHolder holder, int position) {
        CommentEntity comment = getItem(position);
        if (comment != null) {
            holder.bindTo(comment);
        }
    }

    public class CustomViewHolder extends RecyclerView.ViewHolder {

        TextView id, fullName, txtComments;

        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);

            id=itemView.findViewById(R.id.id);
            fullName =  itemView.findViewById(R.id.fullName);
            txtComments =  itemView.findViewById(R.id.textComment);

        }

        public void bindTo(CommentEntity comment) {
            id.setText(String.valueOf(comment.getId()));
            fullName.setText(comment.getFullName());
            txtComments.setText(comment.getTxtComment());

        }
    }

    public static final DiffUtil.ItemCallback<CommentEntity> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<CommentEntity>() {
                @Override
                public boolean areItemsTheSame(@NonNull CommentEntity oldComment, @NonNull CommentEntity newComment) {
                    return oldComment.getId() == newComment.getId();
                }
                @Override
                public boolean areContentsTheSame(@NonNull CommentEntity oldComment, @NonNull CommentEntity newComment) {
                    return oldComment.equals(newComment);
                }
            };


}

Aby powiązać PagedList z RecyclerView, korzystamy z PagedListAdapter. PagedListAdapter jest implementacją RecyclerView.Adapter, Adapter zostaje powiadomiony o zmianach w liście, a następnie informuje RecyclerView, aby zaktualizował widok. Aby obliczyć różnicę między dwoma elementami (czy są takie same), musisz zaimplementować nową klasę DiffUtil. Zwróć uwagę, że nie zastępujemy metody getItemCount(), ponieważ jest ona dostarczana przez obiekt PageList. Jeśli potrzebujemy zastąpić tę metodę, musimy dodać super.getItemCount() do tej metody. I to wszystko! Mam nadzieję, że spodobał Ci się ten artykuł i okazało się, że jest przydatny, Myślę, że przykład jest prosty i zrozumiały. Jeżeli masz pytania śmiało zadaj je w komentarzu.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂