Bir Android projesinde RecyclerView ile listeleme işlemini yapabiliriz. Küçük boyutlu listeler gösterebildiğimiz gibi liste elemanlarının fazla olduğu listeleri de RecyclerView üzerinde gösterebiliriz. Büyük boyutlu liste elemanlarının hepsini aynı anda göstermek efektif bir çözüm olmayacaktır. Listeyi parçalar halinde alıp göstermek daha uygun ve performanslı bir çözüm olacaktır. İşte burada paging devreye girer.
Paging, Android Jetpack içerisinde bulunan ve verileri istenen büyüklükteki parçalar halinde gösterebilmemizi sağlayan bir componenttir. Paging 3 önceki versiyonlarından farklı olarak tamamıyla Kotlin ile yazılmıştır ve Kotlin Coroutines kullanmaktadır. Coroutines Flow yapısını desteklediği gibi RxJava ve LiveData desteği de bulunmaktadır. Bunun yanında error handling ve loading, fail, success gibi state kontrolü, caching gibi birçok özelliği de bulunmaktadır.
Kurulum
Projenizde Paging 3 kullanmak için app/build.gradle
içerisine alltaki gibi dependencyleri ekliyoruz.
1 2 3 |
def paging_version = "3.0.0-alpha07" implementation "androidx.paging:paging-runtime:$paging_version" |
Eğer RxJava ile kullanmak istiyorsanız alttaki dependencyleri de eklemelisiniz.
1 |
implementation "androidx.paging:paging-rxjava2:$paging_version" |
DataStore
Öncelikle verilerin sayfalar halinde alınabilmesi için bir DataSource oluşturmamız gerekiyor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class PopularMoviesPagingSource @Inject constructor( val movieApiService: MovieApiService ) : PagingSource<Int, Movie>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Movie> { val position = params.key ?: STARTING_INDEX return try { val movies = movieApiService.getPopularMovies(position) LoadResult.Page( data = movies.results, prevKey = if (position == STARTING_INDEX) null else position - 1, nextKey = if (movies.results.isEmpty()) null else position + 1 ) } catch (e: Exception) { LoadResult.Error(e) } } companion object { const val STARTING_INDEX = 1 } |
Burada oluşturduğumuz DataSource classını PagingSource<Key, Value> classından türettik. PagingSource iki adet parametre almaktadır. Key sayfa numarasını temsil ederken, Value ise model classını temsil etmektedir.
Üstte gördüğünüz gibi load() metodu bir suspend metoddur. Bu metod içerisinde network istekleri ya da database çağırımları yapılabilir. Örnekte MovieApiService içerisindeki getPopularMovies(page: Int) metodunu load() metodu içerisinde çağırdık.
LoadParams objesi key ve sayfa numarası gibi değerler tutar. Bu sayede sayfa sayısına göre istekler gönderilmiş olur.
API tarafından dönen değerin success durumunda LoadResult.Page, fail olma durumunda ise gelen datalar LoadResult.Error objeleriyle wrap edilir.
Not: Eğer RxJava kullanıyorsanız oluşturduğumuz DataSource classını RxPagingSource classından extend etmelisiniz.
ViewModel
Oluşturulan PopularMoviesPagingSource classından gelen dataları ViewModel üzerinde almayı alttaki gibi yapabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class PopularMoviesViewModel @ViewModelInject constructor( private val repository: MoviesRepository ) : ViewModel() { fun getPopularMovies(): Flow<PagingData<Movie>> { return Pager( config = PagingConfig(pageSize = PAGE_SIZE, enablePlaceholders = false), pagingSourceFactory = { PopularMoviesPagingSource(repository) } ).flow .cachedIn(viewModelScope) } companion object { const val PAGE_SIZE = 20 } } |
Pager objesi PopularMoviesPagingSource içerisindeki load()
metodunu çağırır. Ayrıca PagingConfig objesinde verilen pageSize
değerine göre liste elemanları getirilir.
Biz bu örnekte Flow ile çalıştığımız için .flow
kullandık. Eğer LiveData olarak expose etmek istiyorsanız .liveData
, RxJava için Flowable kullanıyorsanız .flowable
, Observable kullanıyorsanız .observable
kullanabilirsiniz.
RecyclerView üzerinde gösterme
Şimdi de aldığımız dataları RecyclerView üzerinde nasıl gösteririz buna bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class MovieAdapter : PagingDataAdapter<Movie, MovieAdapter.MovieViewHolder>(MovieDiffCallback){ override fun onBindViewHolder(holder: MovieViewHolder, position: Int) { val movie = getItem(position) holder.itemView.movie_title.text = movie.title holder.itemView.movie_image.load(ImageUrlProvider.posterPathUrl(movie.posterPath)) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder { return MovieViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.movie_item, parent, false) ) } class MovieViewHolder(view: View) : RecyclerView.ViewHolder(view) object MovieDiffCallback: DiffUtil.ItemCallback<Movie>() { override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean { return oldItem == newItem } } } |
Normal RecyclerView adapterden farkı PagingDataAdapter classından extend edilmiş olmasıdır. PagingDataAdapter 2 parametre alır, birincisi model classı ikinci ise ViewHolder’dır.
Son olarak listeyi Activity/Fragment üzerinde göstereceğiz.
1 2 3 4 5 |
lifecycleScope.launch { viewModel.getPopularMovies().collectLatest { adapter.submitData(it) } } |
Artık veriler RecyclerView üzerinde parçalar halinde kullanıcı liste sonuna geldiğinde listelenecektir.
Ek olarak her parça liste çağrısı yapıldığında stateleri kontrol edebilmek için RecyclerView adapter içerisindeki addLoadStateListener
metodu kullanılabilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
adapter.addLoadStateListener { combinedLoadStates -> when (val loadState = combinedLoadStates.source.refresh) { is LoadState.NotLoading -> { popular_movies_refresh_layout.visible() popular_movies_progress.gone() } is LoadState.Loading -> { popular_movies_progress.visible() popular_movies_refresh_layout.gone() } is LoadState.Error -> { Timber.e(loadState.error.localizedMessage) } } } |
Referanslar
https://developer.android.com/topic/libraries/architecture/paging/v3-overview
https://proandroiddev.com/how-to-use-the-paging-3-library-in-android-5d128bb5b1d8