Giới thiệu custom library giúp sử dụng Paging Android nhanh chóng và hỗ trợ update dữ liệu trên page
Bài đăng này đã không được cập nhật trong 5 năm
Giới thiệu
Chào các bạn, như các bạn đã biết Google cho ra mắt thư viện Paging nằm trong bộ Android Jetpack từ khá lâu. Tuy nhiên, thực tế là không nhiều dự án áp dụng thư viện này bởi 3 điểm hạn chế chí mạng của nó so với các phương pháp tạo danh sách infinite scroll khác:
- Nếu chỉ lấy dữ liệu từ network để phân trang, list dữ liệu hiển thị lên UI chỉ có thể xem chứ không thể tương tác để cập nhật dữ liệu cho các item riêng biệt được (ví dụ bạn sẽ không thể update thuộc tính Like/Dislike của một item trong list lên network)
- Nếu chỉ lấy dữ liệu từ local database để phân trang (ví dụ sử dụng Room), bạn có thể cập nhật các item khi tương tác với chúng, tuy nhiên hầu như các trường hợp dữ liệu không bao giờ chỉ có ở local database thôi.
- Nếu lấy dữ liệu từ network lưu về local database, rồi sử dụng dữ liệu local database để phân trang và cập nhật ngược trở lại network khi có thay đổi, bạn sẽ giải quyết được cả 2 vấn đề còn tồn tại phía trên. Tuy nhiên triển khai phương pháp này tốn rất nhiều công sức, có thể khiến chúng ta nản lòng và quay trở lại với những cách phân trang cũ.
Mặc dù vậy, Paging có nhiều ưu điểm về hiệu suất hơn so với những người anh em khác, chúng ta không thể bỏ qua và không sử dụng nó chỉ vì những vấn đề chưa được giải quyết này.
Vì vậy mình đã làm sẵn một thư viện nhỏ giúp triển khai Paging với combo Room database + network trở nên đơn giản hơn rất nhiều. Trong bài viết này mình sẽ giới thiệu cách sử dụng thư viện này cho các bạn.
Mục tiêu
Chúng ta sẽ làm 1 ứng dụng hiển thị danh sách phim chứa từ khoá avenger, có tính năng cuộn vô tận và có thể kéo thả thay đổi vị trí của item
Link ứng dụng sample: https://github.com/trunghq3101/LoadMoreDbNetworkPagination
Cài đặt
Chúng ta bắt đầu bằng việc thêm thư viện vào danh sách dependencies
Hãy chắc chắn trong file build.gradle (module: project) đã có đoạn code này
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Sau đó hãy thêm các thư viện sau vào file build.gradle (module: app)
dependencies {
...
// Thư viện custom Paging
implementation 'com.github.trunghq3101:responsivepaging:1.0.1'
// ViewModel & LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
// Paging
implementation 'androidx.paging:paging-runtime:2.1.0'
implementation 'androidx.paging:paging-runtime-ktx:2.1.0'
// Room
implementation 'androidx.room:room-runtime:2.1.0'
implementation 'androidx.room:room-ktx:2.1.0'
kapt 'androidx.room:room-compiler:2.1.0'
// Rx
implementation("io.reactivex.rxjava2:rxjava:2.2.2")
implementation("io.reactivex.rxjava2:rxandroid:2.1.0")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.5.0")
implementation("com.squareup.retrofit2:converter-gson:2.5.0")
implementation("com.squareup.okhttp3:logging-interceptor:3.11.0")
implementation("com.squareup.retrofit2:adapter-rxjava2:2.5.0")
// Gson
implementation 'com.google.code.gson:gson:2.8.5'
// Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
kapt 'com.github.bumptech.glide:compiler:4.9.0'
// Koin
implementation 'org.koin:koin-core:2.0.1'
implementation 'org.koin:koin-android:2.0.1'
implementation 'org.koin:koin-androidx-viewmodel:2.0.1'
implementation 'org.koin:koin-java:2.0.0-beta-1'
Chọn API
Mình sẽ sử dụng API miễn phí từ trang OMDb để lấy dữ liệu đầu vào là danh sách các phim có chứa keyword avenger
Link API: https://www.omdbapi.com/?apikey=2ed35bde&s=avenger
Cấu trúc project
Sơ đồ package trong ứng dụng này sẽ đơn giản như sau:
Tạo model
Bên trong package data > model mình sẽ tạo 2 model phục vụ cho Paging
- Movie
Chú ý:
- Phải implement interface BaseLoadMoreEntity
- Phải đặt tableName là page_data
@Entity(tableName = "page_data")
data class Movie(
@PrimaryKey(autoGenerate = true)
val id: Long? = null,
@SerializedName("Title")
val title: String? = null,
@SerializedName("Year")
val year: String? = null,
@SerializedName("Poster")
val poster: String? = null
): BaseLoadMoreEntity {
override var indexInResponse: Int = -1
}
- MovieListResponse
Chú ý:
- Phải implement interface BaseLoadMoreResponse
data class MovieListResponse (
@SerializedName("Search")
val data: List<Movie>
) : BaseLoadMoreResponse<Movie> {
override fun getListData(): List<Movie> {
return data
}
}
Tạo lớp kế thừa Room Database và DAO của thư viện
Bên trong package data > local chúng ta sẽ tạo 2 class cho Room Database và DAO
- MovieRoomDao
Bởi vì các câu query đã viết sẵn trong class LoadMoreDao nên chúng ta chỉ cần khai báo đơn giản như sau:
@Dao
abstract class MovieRoomDao : LoadMoreDao<Movie>
- MovieRoomDb
Chúng ta khai báo như bình thường, tuy nhiên cần chú ý:
- Phải implement LoadMoreDb
- Phải override LoadMoreDao() và trả về MovieRoomDao
@Database(
entities = [Movie::class],
version = 1,
exportSchema = false
)
abstract class MovieRoomDb : LoadMoreDb<Movie>() {
companion object {
private var INSTANCE: MovieRoomDb? = null
fun create(context: Context): MovieRoomDb? {
if (INSTANCE == null) {
synchronized(MovieRoomDb::class.java) {
INSTANCE =
Room.databaseBuilder(
context.applicationContext,
MovieRoomDb::class.java,
"MovieRoomDb"
).fallbackToDestructiveMigration()
.build()
}
}
return INSTANCE
}
}
abstract override fun LoadMoreDao(): MovieRoomDao
}
Tạo ApiService để call API
Trong package data > remote chúng ta tạo class ApiService
Chú ý
- Kiểu trả về là Call<...> chứ không phải Observable của Rx như bình thường
interface ApiService {
@GET("/")
fun searchMovies(
@Query("apikey") apiKey: String? = "2ed35bde",
@Query("s") keyword: String? = null,
@Query("page") page: Int? =null
): Call<MovieListResponse>
}
Tạo Repository
Trong package data > repository chúng ta tạo 1 interface và 1 class thực thi
- MovieRepository
Đơn giản chỉ giúp việc mở rộng code sau này dễ dàng hơn
interface MovieRepository: ILoadMoreWithDbRepository<Movie, Int, MovieListResponse>
- MovieRepositoryImpl
Chú ý
- Phải extend BaseLoadMoreWithDbRepository
class MovieRepositoryImpl(
ioExecutor: Executor,
db: LoadMoreDb<Movie>,
private val apiService: ApiService
) : BaseLoadMoreWithDbRepository<Movie, Int, MovieListResponse>(
db = db,
ioExecutor = ioExecutor,
networkPageSize = 10
), MovieRepository {
private var key = 1
override fun getKey(): Int? {
return key
}
override fun nextKey(response: MovieListResponse) {
key ++
}
override fun fetchDataFromNetwork(
key: Int?,
loadSize: Int?
): Call<MovieListResponse> {
return apiService.searchMovies(keyword = "Avenger", page = key)
}
}
Xử lí UI
Trong package ui > home chúng ta sẽ tạo lần lượt ViewModel, Fragment và Adapter cho màn hiển thị danh sách phim
class MovieViewModel(
private val repository: MovieRepository
) : BaseLoadMoreWithDbViewModel<Movie>() {
override fun getInitData(): Listing<Movie> {
return repository.refreshData()
}
fun swapItems(from: Movie, to: Movie) {
repository.swapItem(from, to)
}
override fun syncDataToNetwork(item: Movie) {
}
override fun syncDataToNetwork(items: List<Movie>) {
Log.d("------>", " : Sync data")
}
}
class MovieAdapter : BaseLoadMoreAdapter<Movie>(homeSpotCallback) {
override val itemBindingVariable: Int = BR.item
override fun getItemLayoutRes(): Int {
return R.layout.item_movie
}
class SwipeCallback(
private val adapter: MovieAdapter,
private val onItemMove: (from: Movie, to: Movie) -> Unit
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT) {
private var from: Int = 0
private var to: Int = 0
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
from = viewHolder.adapterPosition
to = target.adapterPosition
val item1 = adapter.getItem(from)
val item2 = adapter.getItem(to)
if (item1 != null && item2 != null) {
adapter.notifyItemMoved(from, to)
return true
}
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun isLongPressDragEnabled(): Boolean {
return true
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
override fun clearView(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
val item1 = adapter.getItem(from)
val item2 = adapter.getItem(to)
if (item1 != null && item2 != null) {
onItemMove(item1, item2)
}
}
}
}
val homeSpotCallback = object : 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
}
}
class HomeMovieFragment : Fragment() {
private val viewModel: MovieViewModel by viewModel()
private val adapter: MovieAdapter = MovieAdapter()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initData()
observeField()
}
fun initData() {
recyclerHomeSpot.adapter = adapter
recyclerHomeSpot.addItemDecoration(SpacesItemDecoration(resources.getDimensionPixelSize(R.dimen.dp_8)))
ItemTouchHelper(MovieAdapter.SwipeCallback(adapter) { from, to ->
viewModel.swapItems(from, to)
}).apply {
attachToRecyclerView(recyclerHomeSpot)
}
viewModel.loadData()
}
fun observeField() {
viewModel.data.observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
viewModel.syncDataToNetwork(it.snapshot())
})
viewModel.networkState.observe(viewLifecycleOwner, Observer {
adapter.networkState = it
})
}
companion object {
fun newInstance() = HomeMovieFragment()
}
}
Inject dependency cần thiết
Mình sử dụng Koin để inject các thành phần phụ thuộc, chi tiết các bạn có thể xem trong package di
Hoàn thành
Chỉ sau 1 vài bước set up không quá phức tạp, chúng ta sẽ thu được kết quả là 1 danh sách phim có thể cuộn vô tận và kéo thả để thay đổi vị trí của 1 phim trong danh sách
Link ứng dụng sample: https://github.com/trunghq3101/LoadMoreDbNetworkPagination
All rights reserved