Nedir Bu Paging3 (MVVM, Flow, DataBinding, Hilt ile Kullanım)?

Oguz Şahin
Huawei Developers - Türkiye
8 min readJan 11, 2022
developer.android.com

Giriş

Bir android app geliştirirken en çok kullandığımız componentlerin başında şüphesiz Recyclerview geliyor. Recylerview’ı uygulamaların bir veya birçok sayfasında veri listelerini göstermek için kullanıyoruz. Bu veri kümelerinin boyutları arttığında, sistem kaynaklarını efektif şekilde kullanmaya ve ui performansının da akıcı bir şekilde ilerlemesine dikkat etmemiz gerekiyor. Bu yazıda büyük veri setlerini recylerviewda göstermenin efektif ve doğru yolunu öğreneceğiz. Yazının içeriğinde öğreneceğimiz başlıklar aşağıdaki gibi olacak;

  • Pagination nedir ve niçin kullanırız?
  • Paging3 nedir ve sunduğu avantajlar nelerdir?
  • Paging3'ü anlamak ve implementasyonu

Pagination nedir ve niçin kullanırız ?

Instagram, Twitter veya Facebook gibi uygulamaları göz önüne alalım. Ana akış sayfalarında çok büyük hatta sonsuz verileri liste üzerinde gösteriyorlar. Bu büyük verileri tek seferde yüklemek yerine, kullanıcı ekranda gösterilen içerikten daha fazlasını görmek için scroll yaptığında; yeni verileri listeye yükleyerek gerçekleştiriyorlar. İşte pagination nedirin cevabı aslında bu mantık. Büyük veri setlerini liste üzerinde gösterirken, tüm veri setini bir anda yüklemek yerine, bu veri setini parçalar halinde yükleme işlemi.

Peki neden böyle bir işleme ihtiyaç duyuyoruz ve yahut bu yöntem bize ne fayda sağlıyor ?

  • Uygulamanızın ağ bant genişliğini ve sistem kaynaklarını verimli bir şekilde kullanılması
  • Sayfadaki verileri daha hızlı alma
  • Daha az bellek kullanımı
  • Gereksiz veriler için kaynak tüketmeme

Paging3 nedir ve sunduğu avantajlar nelerdir?

Paging3 , data kaynağından(local,remote,file..vb)büyük veri setlerini kolaylıkla yüklememizi sağlayan bir jetpack kütüphanesidir. Ağ ve sistem kaynakları kullanımını azaltarak kademeli şekilde veriyi yükler. Kotlin ile yazılmıştır ve diğer Jetpack kütüphaneleriyle koordineli şekilde çalışır. Kotlin Coroutine ile birlikte Flow, LiveData ve RxJava’yı destekler. Ayrıca veri yüklemeniz gerektiğinde manuel olarak uygulamanız gereken birçok işlev için destek sağlar:

  • Sonraki ve önceki sayfayı almak için kullanılacak keyin kaydını tutar.
  • Kullanıcı listenin sonuna kaydırdığında otomatik olarak sayfa isteğini atar.
  • Birden çok isteğin aynı anda tetiklenmemesini sağlar.
  • Verileri cacheleminize izin verir: Kotlin kullanıyorsanız, bunu CoroutineScope’ta yapabilirsiniz; Java kullanıyorsanız, LiveData ile yapılabilir.
  • Yükleme durumunu izler ve bunu bir RecyclerView liste öğesinde veya kullanıcı arayüzünüzdeki başka bir yerde görüntülemenize olanak tanır.
  • Hata aldığınız yükleme durumunda kolayca yeniden çağırım yapmanıza yarar.
  • Görüntülenecek listede map,filtre ..vb gibi genel işlemleri gerçekleştirmenizi sağlar.
  • Liste ayırıcıları uygulamanın kolay bir yolunu sağlar.

Paging3'ü anlamak ve implementasyonu

Bu bölüm için bir örnek repo oluşturdum. Bu repo random kullanıcı oluşturan free bir API’den kullanıcı bilgilerini alıyor . Bu repo üzerinden hem Paging3'ün componentlerini tanıyacağız hem de kullanımını nasıl yapacağımızı öğreneceğiz.

Uygulama Ekran Kaydı

1.Bağımlılıkların Eklenmesi

Öncelikle gerekli bağımlılıklarımızı implemente ederek başlayalım. Paging 3'ün rxjava, guava ve jetpack compose için de desteği var. Dilerseniz linkten ihtiyacınıza göre bağımlılıklarınızı ekleyebilirsiniz.

2. Network İşlemleri

Uzak sunucudan user bilgilerini almak için retrofit işlemlerini yaparak başlayalım.

NetworkModule.kt

Retrofit ve Okhttp classlarını singleton bir şekilde oluşturalım. Hilt ile birlikte dependency injection yöntemini kullanarak, SOLID bir şekilde gerekli sınıf bağımlılıkları sağlamış olacağız.

Daha sonrasında service ve model classlarımızı tanımlayalım.

UserService.kt

Burada api tarafından istenilen iki parametreyi anlamakta fayda var. Her istekte istenilen sayfa numarasını ve tek seferde yüklenecek item sayısını belirtiyor. Mantık olarak kullanıcı her listenin sonuna ulaştığında yeni sayfayı yükleyeceğimiz için burada yeni datayı sayfa numarasına göre çekeceğiz.

Model Sınıfları

3.PagingSource

User bilgilerini alacağımız network tarafını hallettikten sonra datayı çekme işlemi için ilk olarak PagingSource classımızı hazırlayarak başlayalım.

USerPagingDataSource.kt

PagingSource sınıfı paging3 ile gelen bir component. Bu class sayfalanmış verinin kaynağını ve bu kaynaktan nasıl veri alınacağıyla sorumlu bir abstract generic class. Bu yazıda sadece tek bir kaynaktan(remote ,local, file ..vb) veriyi sağladığımız için PagingSource classını kullanıyoruz. Eğer ki hem remote hem de local bazlı(Buradaki kasıt remotetaki veriyi locale yazıp tek bir kaynak üzerinden gitme işlemi) bir yapıda çalışacaksak RemoteMediator kullanmamız gerecekti. Bu yazı içeriğinde RemoteMediator kavramına değinmeyeceğimiz için dilerseniz linkten gerekli bilgiye ulaşabilirsiniz.

Not: rxJava ile RxPagingSourceveya guava için ListenableFuturePagingSource kullanabilirsiniz.

PagingSource sınıfı generic olarak iki parametre tipi belirtmenizi ister.

public abstract class PagingSource<Key : Any, Value : Any>
  • key : Key olarak bizim api servisimiz index bazlı çalışıyor o yüzden Int olarak belirteceğiz.
  • value: Yükleyeceğiniz datanın tipi olarak vermeniz gerek. Bizim örneğimiz için UserModel olarak belirtebiliriz.

Load fonksiyonu ise bize datayı yüklemekle görevli asıl fonksiyon. Bu fonksiyon, kullanıcı listenin sonuna ulaştığı an bir sonraki key alarak yeni liste için asenkron bir şekilde isteği atar ve bu işlem Paging kütüphanesi tarafından otamatik olarak yapılır. Ayrıca suspend bir fonksiyon olup yapacağımız network isteklerini arka planda yapmamız için güzel de bir yapı sunuyor.

Parametre olarak params isimli LoadParams classı alır ki bu class yüklenecek sayfa numarasını ve yüklenecek item sayısının bilgisini tutar. Biz de buradan doğru sayfa ve yüklenecek item sayısıyla isteğimizi kolayca atabiliyor olacağız. İlk yükleme esnasında key null olacaktır(Eğer ki inital değer belirtmezseniz). Bu durumda için de STARTING_PAGE_INDEX ‘ i belirttik. Default olarak Paging3 ilk yükleme esnasında LOADSIZE*3 kadar item yükleyecektir. Bu şekilde liste ilk kez yüklendiğinde kullanıcının yeterli öğe görmesi sağlanır ve kullanıcı sayfayı scroll etmedikçe çok fazla ağ isteğini tetiklemez.

offficial guide

Dönüş tipi olarak da LoadResult belirtmemiz gerek. Bu sınıf ise attığımız isteğin durumunu tutan bir sealed class. Eğer ki data çekme işlemi başarılı bir şekilde gerçekleşmisse LoadResult.Page seklinde datayı, sonraki istekler içinse prevKey ve nextKeyi belirterek nesneyi dönebiliriz. Eğer ki bir sorun oluşması durumda ise LoadRestlt.Error ile error dönebiliriz.

Override etmemiz gereken getRefreshKey abstract methodu ise pagingSource nesnemizin geçersiz olduktan sonra tekrar yeni pagingSource sağlanırken initial key sağlamakta. Mevcut liste yerine yeni bir liste isteği için kullanılabilir. Örneğin swipe to refresh veya database updates, config changes gibi nedenlerle geçersiz kılınması.

4.Repository

UserRepositoryImpl.kt

Datayı elde edecek PagingSource classını yazdıktan sonra buradaki datayı akış şeklinde sağlayacak bir Pager’ a ihtiyacımız var. PagingSource’dan dönen veriler PagingData kapsayıcısıyla döner. Pager sınıfına datanın hangi kaynaktan ve nasıl çekileceğini belirtmemiz gerek. Bizden 3 parametre bekler:

public class Pager<Key : Any, Value : Any>
// Experimental usage is propagated to public API via constructor argument.
@ExperimentalPagingApi constructor(
config: PagingConfig,
initialKey: Key? = null,
remoteMediator: RemoteMediator<Key, Value>?,
pagingSourceFactory: () -> PagingSource<Key, Value>
)
  • config : Bir PagingConfig classı oluşturmayı bekler ve bu sınıf bir PagingSource’tan içeriğin nasıl yükleneceğine ilişkin bilgileri sağlar. Örneğin ne kadar ileri yükleneceği, ilk yükleme için boyut isteği..vb. Tanımlamanız gereken tek zorunlu parametre pageSize , her istekte kaç öğe yüklenmesi gerektiğini belirtmek için.
  • initalKey: PagingSource initial olduğunda atılacak ilk istek için initial key verebilirsiniz. Bizim örneğimizde inital değer belirtmediğimiz için null olarak tanımlanacak. Fakat PagingSource tarafında null durumunu handle edip initial değerini verdik.
  • pagingSourceFactory: Yükleyeceğiniz datanın kaynağını belirtmemiz gereken parametre. Bizim örneğimizde UserPagingDataSource olarak vereceğiz.

Son olarak Pager sınıfının PagedData akışını sağlamak için .flow diyebiliriz. Böylece kullanıcı listenin sonuna geldiği an PagingSource otamatik ve asenkron bir şekilde sıradaki sayfa için isteği atacak, daha sonrasında gelen paginatedData bize flow sayesinde akış olarak sunulacak her defasında.

Not: Burada akışı livedata veya rxjava kullanıyorsanız observable veya flowable olarak dönebilirsiniz.

5.ViewModel

UserViewModel.kt

Datayı elde edecek tarafı başarılı bir şekilde oluşturduktan sonra, şimdi de ui tarafına geçelim. İlk olarak viewModel tarafında repositoryden users datalarını isteyelim. Dönen yapı bir PagingData<UserModel> olacak ve biz de bunu recylerviewda kullanacağımız her itemda gerekli bir model classına mapleyelim. Flow döndüğümüz için Paging library bize burada da esneklik sağlıyor ve datayı filtreleme, mapleme ..vb gibi işlemler yapabiliriz.

cachedIn ile de viewmodelscopeunda datayı cacheliyoruz. Herhangi bir configuration değişikliğinde datayı sıfırdan almak yerine var olanı sağlayacak. Ayrıca memory leaki de önlemiş olacak.

6.Adapter

Paging3'ün bizlere sunduğu componentlerden biri de PagingDataAdapter. Bu Adapter esasında PagingData göstermek için RecyclerView.Adapter classınan kalıtılmış özel bir class. ListAdapter’a çok benzer bir yapı ve DiffUtil.ItemCallback ile background tarafında asenkron olarak yeni gelen listeyi en optimize şekilde hesaplayarak recylerviewa ekliyor.

UsersAdapter.kt
adapter_item_user.xml
UsersItemUiState.kt
executeWithAction fonksiyonu

Adapter kısmına geçersek, viewmodel tarafına gelen UserModel datamızı recylerviewdaki her item için UserItemUiState olarak maplemiştik. Dolayısıyla Adaptera gelecek PagingData’nın türünü UserItemUiState olarak belirtiyoruz. Değişiklikler için de Comprator objesi. ViewHolder tarafındaki executeWithAction fonksiyonu da her defasında executePendingBindings yazmamak için tanımlanan bir extension fonksiyon. Esasında dataBinding için değişken atamasını yapıyor.

7. Render Data

Adapterı da hazırladıktan sonra, sıra gelen datayı adaptera gönderip ui render işini tamamlamakta.

UserActivity.kt
LifecycleOwner.kt

Öncelikle viewmodel tarafından gelecek flow akışını collectlememiz lazım. lifecycle aware ve yeniden kullanılır olması için LifecycleOwner extensionlarını tanımladık. Buradaki kavramlardan flow için bu linkten , repeatOnLifecyle için ise linkten bilgi sahibi olabilirsiniz.

Ui tarafında datanın yüklenme durumunu handle etmek için de, Paging3 paginated datanın yükleme işlemini izler ve bize LoadState sealed classı üzerinden sunar. O anki durumdan PagingDataAdapter üzerinden bir listener aracılığıyla haberdar olabiliriz ve ui tarafını handle edebiliriz. Bu işlem adapter üzerinden yapılma sebebi ui ile yapılan işlemin senkron şekilde ilerlemesi. Bu da page load işlemi ui tarafında uygulandığında adapterdaki listener bu bilgiyi bize ulaştırması anlamına geliyor.

LoadState 3 durumda olabilir:

Yüklenme durumundan haberdar olmak için PagingDatadapter üzerinden sağlanan loadStateFlow veya addLoadStateListener() kullanılabilir. Bu mekanizma bize CombinedLoadStates objesini sağlar ki bu obje her load type için tüm bilgiye sahiptir.

activity_user.xml
UsersUiState.kt

Biz de ui kısmında bu değişiklikleri gözlemleyerek loading, success veya error durumlarını handle edelim. İlk olarak paging source durumunu dinlemek için CombinedState’in bize sunduğu source parametresi kullanabiliriz. Böylece ilk istek atıldığında yükleme durumunu handle edebiliriz. PagingDataAdapter üzerinden dinlediğimiz durumu UsersUiState model classı üzerinden databinding ile set edelim.

Paging kütüphanesinin bizlere sunduğu bir diğer adapter componenti de LoadStateAdapter . Bu adapter o anki listenin yüklenme durumuna erişim sağlar. Custom bir ViewHolder kullanarak kullanıcın liste sonuna geldiğinde yüklenme durumuna göre işlemler yapalım. Burada header veya footer şeklinde ekleyebiliriz. İkisini beraber şekilde de ekleyebiliriz. Detaylı bilgiye linkten ulaşabilirsiniz.

FooterAdapter.kt
FooterUiState.kt
item_paging_footer.kt

Bu yapımızı hazırladıktan sonra recylerview adapterımızı withLoadStateFooter() fonksiyonunu kullanarak set edebiliriz. Esasında bu yapı bize bir ConcatAdapter dönüyor. PagingDataAdapter üzerinden listeye ekleme durumunu LoadStateAdapter’a haber veriyor ve biz de oluşturduğumuz FooterAdapter üzerinden custom bir şekilde kullanıcının liste sonuna geldiğinde yeni eklenecek paginatedData’nın yüklenme durumunu gösterebiliyoruz. Ayrıca ConcatAdapter yapısıyla recylerview üzerinde multiviewtype göstermiş oluyoruz.

fun withLoadStateFooter(
footer: LoadStateAdapter<*>
): ConcatAdapter {
addLoadStateListener { loadStates ->
footer.loadState = loadStates.append
}
return ConcatAdapter(this, footer)
}

withLoadStateFooter bizden bir LoadStateAdapter türünden bir parametre bekliyor. Biz de burada kendi oluşturduğumuz FooterAdapterı veriyoruz. FooterAdapter da constructorda retry fonksyionu beklemekte. Bu yapı da pagingDataAdapter üzerinden sağlanıyor. Sayfa sonuna gelindiğinde yüklemede bir hata oluşursa error mesajıyla birlikte retry butonu gözükecek. Yüklenecek sayfa isteğini tekrar atmamız için oluşturduğumuz pagingDataAdapater üzerinden retry fonksiyonu çağırmak yeterli oluyor. Bu durum da yine paging3 kütüphanesi tarafından bize sağlanıyor.

Sonuç

Bu yazıda büyük veri setleri ile çalışırken kullanılan pagination yöntemini ve android geliştiricilerine bu yöntemi kolay şekilde uygulamak için sunulan Paging3'ü anlatmaya çalıştım. Paging3 sağladığı avantajlarla birlikte bizlere fazlasıyla kolaylık sağlayarak, büyük veri setlerini en optimize şekilde kullanıcıya göstermemizi sağlıyor. Ayrıca önerilen android app mimarisini düşünülerek tasarlandığından kolayca uygulamanıza implemente edebilirsiniz. Bunun yanında diğer jetpack öğeleriyle uyumu ve 1.sınıf kotlin desteği de kullanmak için heyecanlandıran detaylar. Bir sonraki yazıda görüşmek üzere 👋👋

Yazdığımız repoyu incelemek için:

Dip Not : Bu yazı ve repo hakkında eksik veya yanlış gördüğünüz şeyleri paylaşmaktan çekinmeyiniz.

--

--