Android Architecture Components: Paging — DataSource

W poprzednim wpisie przedstawiłem podstawy biblioteki Paging. Dziś rozszerzymy temat i skupimy się na źródle danych.

DataSource

Źródło danych — najprościej mówiąc, jest to klasa, która zwraca dane do PageList. DataSource przechowuje zawartość z różnych źródeł (baza danych, API itp.) i służy jako lokalna pamięć dla PagedList. Z kolei PagedList jest generowany przez PagedListBuilder.

Istnieją różne typy źródła danych:

  • PageKeyedDataSource — używamy, kiedy żądanie wymaga następnych / poprzednich kluczy indeksu. Na przykład przekazujemy numer strony jako parametr zapytania w żądaniu. Numer strony będzie wzrastał sekwencyjnie do momentu, aż wszystkie strony zostaną pobrane i wyświetlone.
  • ItemKeyedDataSource — gdy żądanie wymaga elementu jako klucza. Poniżej znajdziesz przykład.
  • PositionalDataSource — kiedy żądanie wymaga indeksu do pobrania następnej porcji danych. Na przykład żądanie może zwrócić 10 elementów danych zaczynających się od pozycji 100. Ta klasa jest przydatna, gdy źródło dostarcza listę o ustalonym rozmiarze.

Uzupełnij wiedzę o: Boilerplate w Androidzie

Własne źródło danych

Spróbujemy teraz stworzyć własne źródło danych. Skorzystamy z serwisu Unsplash, z którego będziemy pobierać obrazy. Obrazy będą ładowane sekwencyjnie, zaczynając od pozycji 0. Na początku załadujemy 5 zdjęć, a dalej będą ładowane po 10.

Android Architecture Components Paging - DataSource example
Kliknij aby zobaczyć animację

DataSource

Na początku przygotujemy sobie źródło danych. Korzystamy z implementacji ItemKeyedDataSource, a naszym kluczem będzie numer zdjęcia. Poniższy kod pokazuje, jak możemy utworzyć
ItemKeyedDataSource. Kiedy rozszerzymy naszą klasę, mamy trzy metody do nadpisania tj. LoadInitial, loadBefore loadAfter.

public class UnsplashPhotos {
    int id;
    String url;
    public UnsplashPhotos(int id, String url) {
        this.id = id;
        this.url = url;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
}
public class UnsplashPhotosDataSource extends ItemKeyedDataSource<Integer,  UnsplashPhotos> {
    private UnsplashPhotosRepository unsplashPhotosRepository;
    public UnsplashPhotosDataSource() {
        unsplashPhotosRepository = new UnsplashPhotosRepository();
    }
    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<UnsplashPhotos> callback) {
        List<UnsplashPhotos> photos = unsplashPhotosRepository.getCouponsBySize(0, params.requestedLoadSize);
        callback.onResult(photos,0,photos.size());
    }
    @Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<UnsplashPhotos> callback) {
        List<UnsplashPhotos> photos = unsplashPhotosRepository.getCouponsBySize(params.key+1, params.requestedLoadSize);
        callback.onResult(photos);
    }
    @Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<UnsplashPhotos> callback) {
    }
    @NonNull
    @Override
    public Integer getKey(@NonNull UnsplashPhotos item) {
        return item.getId();
    }
}
public class UnsplashPhotosDataSourceFactory extends DataSource.Factory<Integer, UnsplashPhotos> {
    private DataSource<Integer, UnsplashPhotos> dataSource;
    @NonNull
    @Override
    public DataSource<Integer, UnsplashPhotos> create() {
        dataSource = new UnsplashPhotosDataSource();
        return dataSource;
    }
}

Na początku wykonuje się metoda loadBefore(). Właśnie w niej tworzymy pierwsze elementy w liście. Ma ona wywołanie zwrotne.onResult(), która przekazuje dane do PagedList. Połączenie zwrotne można wywołać tylko raz, w przeciwnym wypadku dostaniesz błąd.

W metodzie loadAfter() tworzymy ciało funkcji, które będzie pobierać kolejne rekordy w przyszłości (podczas przewijania listy). Możesz zauważyć, że do klucza dodajemy +1 przy wywoływaniu metody getCouponsBySize(). Jest to celowy zabieg. Gdybyśmy tego nie dali, będziemy mieć jedno zdjęcie dwa razy wyświetlone. Dostaniemy taką listę:

generate link: https://source.unsplash.com/collection/0
...
generate link: https://source.unsplash.com/collection/5
method: loadAfter
generate link: https://source.unsplash.com/collection/5
generate link: https://source.unsplash.com/collection/6
generate link: https://source.unsplash.com/collection/7
...

Czyli dostaniemy dwa razy obraz nr 5 i tak za każdym razem, gdy będzie wywołamy funkcję loadAfter().

Z metody loadBefore() nie korzystamy, ale może się przydać. Jeśli umieścimy kod z loadafter()loadBefore(), wtedy listę będziesz przewijał z dołu do góry, a nie z góry do dołu. Może się przydać w aplikacjach, które wyświetlają wiadomości. Chyba nie chcesz czytać za każdym razem czytać wiadomości od początku?

Zauważ, że obie metody nic nie zwracają. Mają tylko wywołanie zwrotne. Jest to ważne, ponieważ metoda, która pobiera wywołanie zwrotne, może wykonać kod w późniejszym terminie. Dzięki temu DataSources może być w pełni asynchroniczne i obsługiwać tymczasowe stany błędów takie jak błąd sieci.

Oprócz tego dostarczamy klasę DataSource.Factory, która bezpośrednio oddziałuje z ViewModel i dostarcza listę adresów ze zdjęciami. W poprzednim wpisie klasę Factory dostarczała nam biblioteką Room. W tym przypadku wystarczy nadpisać metodę create(). DataSourceFactory jest odpowiedzialny za pobieranie danych przy użyciu konfiguracji DataSource i PagedList, którą utworzymy w dalszej części tego artykułu, a dokładniej w klasie ViewModel.

ViewModel i Repository

W klasie UnsplashPhotosRepository po prostu piszemy metodę generowania linków, a następnie zwraca nam listę z poprawnymi adresami obrazków.

class UnsplashPhotosViewModel extends ViewModel {
    private UnsplashPhotosDataSourceFactory factory;
    private LiveData<PagedList<UnsplashPhotos>> photos;
    private DataSource<Integer, UnsplashPhotos> dataSource;
    public UnsplashPhotosViewModel() {
        factory = new UnsplashPhotosDataSourceFactory();
        UnsplashPhotosDataSourceFactory dataSourceFactory = new UnsplashPhotosDataSourceFactory();
        dataSource = dataSourceFactory.create();
        PagedList.Config config = (new PagedList.Config.Builder())
                .setEnablePlaceholders(true)
                .setInitialLoadSizeHint(5)
                .setPageSize(10)
                .build();
        photos = new LivePagedListBuilder<>(dataSourceFactory, config).build();
    }
    public LiveData<PagedList<UnsplashPhotos>> getPhotos() {
        return photos;
    }
}
class UnsplashPhotosRepository {
    public List<UnsplashPhotos> getCouponsBySize(int key, int requestedLoadSize) {
        List<UnsplashPhotos> list = new ArrayList<>();
        for (int x=0; x<=requestedLoadSize ;x++ ) {
            int id = x+key;
            String url = "https://source.unsplash.com/collection/"+String.valueOf(id);
            list.add(new UnsplashPhotos(id,url));
            Log.d("TAGER generate link ", url);
        }
        return list;
    }
}


W klasie ViewModel musimy zbudować listę za pomocą LivePagedListBuilder. Metoda build() skonstruuje nam LiveData<PagedList>. Właśnie ją będziemy obserwować w aktywności.

Interfejs użytkownika

W klasie z aktywnością obserwujemy LiveData i przesyłamy do adaptera za pomocą metody submitList(), która jest częścią PagedListAdapter.

W klasie adaptera ustawiamy tylko obraz za pomocą biblioteki Glide. PagedListAdapter jest implementacją RecyclerView.Adapter, który prezentuje dane z PagedList. Podobnie jak w poprzednim artykule używamy DiffUtil jako parametru do obliczenia różnic danych i wykonania wszystkich aktualizacji.

public class MainActivity extends AppCompatActivity {
    private RecyclerView mRecyclerView;
    private UnsplashPhotosAdapter mAdapter;
    private UnsplashPhotosViewModel mViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_advanced_paging);
        init();
        mViewModel = ViewModelProviders.of(this).get(UnsplashPhotosViewModel.class);
        mViewModel.getPhotos().observe(this, photos -> {
            mAdapter.submitList(photos);
        });
    }
    private void init() {
        mAdapter = new UnsplashPhotosAdapter(this);
        mRecyclerView = findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setHasFixedSize(true);
        mRecyclerView.setItemAnimator(new DefaultItemAnimator());
        mRecyclerView.setAdapter(mAdapter);
    }
}
public class UnsplashPhotosAdapter extends PagedListAdapter<UnsplashPhotos, UnsplashPhotosAdapter.CustomViewHolder> {
    private Context context;
    public UnsplashPhotosAdapter(Context context) {
        super(DIFF_CALLBACK);
        this.context = context;
    }
    @NonNull
    @Override
    public CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater li = LayoutInflater.from(parent.getContext());
        View view = li.inflate(R.layout.item_photos_list, parent, false);
        return new CustomViewHolder(view);
    }
    @Override
    public void onBindViewHolder(@NonNull CustomViewHolder holder, int position) {
        UnsplashPhotos photos = getItem(position);
        if (photos != null) {
            holder.bindTo(photos);
        }
    }
    public class CustomViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;
        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.photo);
        }
        public void bindTo(UnsplashPhotos photo) {
            Glide.with(context).load(photo.getUrl()).into(imageView);
        }
    }
    public static final DiffUtil.ItemCallback<UnsplashPhotos> DIFF_CALLBACK = new DiffUtil.ItemCallback<UnsplashPhotos>() {
        @Override
        public boolean areItemsTheSame(@NonNull UnsplashPhotos oldPhoto,
                                       @NonNull UnsplashPhotos newPhoto) {
            return oldPhoto.getId() == newPhoto.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull UnsplashPhotos oldPhoto,
                                          @NonNull UnsplashPhotos newPhoto) {
            return oldPhoto.equals(newPhoto);
        }
    };
}

To tyle! Mam nadzieje, że przykład jest zrozumiały dla Ciebie 🙂 Jeżeli masz pytania odnośnie tego wpisu, zadaj je w komentarzu lub skontaktuj się ze mną 🙂

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 🙂