Jetpack

📈【Jetpack】Android Paging3 分頁加載 範例🔃

📈【Jetpack】Android Paging3 分頁加載 範例🔃

Android Paging3 是一個新一代的 Android 內存管理庫,它的目的是提供一個安全可靠的方法,為 Android 數據庫和網絡數據提供分頁技術支持。

它提供了彈性的方法讓 UI 可以加載應用程序內容,無論是從數據庫或者網絡數據源加載,內存佔用也可以得到控制,來幫助需要處理大量數據的手機應用開發者提高產品的性能與響應。

Paging3 的最初的目標是處理昂貴的 SQL 查詢,處理數據庫內容,以及加載過多會導致 OutOfMemory 保護的網絡數據。它可以支持同步和異步的數據加載,並且同時兼容大部分的 Android 庫(比如 Room 數據庫)。


文章目錄

  1. Paging3 & Retrofit & Fragment KTX 導入
  2. Paging3 Response Model
  3. Paging3 Retrofit & Service
  4. Paging3 PagingSource
  5. Paging3 Repository
  6. Paging3 ViewModel
  7. Paging3 Adapter
  8. Paging3 FooterAdapter
  9. Paging3
  10. Developer Documents Paging3

1.Paging3 & Retrofit & Fragment KTX 導入

build.gradle
android {

    buildFeatures {
        dataBinding true
    }
}

dependencies {
    def fragment_version = "1.5.5"
    implementation "androidx.fragment:fragment-ktx:fragment_version"

    implementation 'androidx.paging:paging-runtime:3.2.0-alpha03'
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    def paging_version = "3.1.1"
    implementation "androidx.paging:paging-runtime:paging_version"
}

2.Paging3 Response Model

RepoRec.kt
data class RepoRec(
    @SerializedName("items") val items: List<Repo>
)

data class Repo(
    @SerializedName("id") val id: Int,
    @SerializedName("name") val name: String,
    @SerializedName("description") val description: String,
    @SerializedName("stargazers_count") val starCount: Int
)

3.Paging3 Retrofit & Service

RetrofitUtils.kt
class RetrofitUtils {

    companion object {
        val instance: RetrofitUtils by lazy { RetrofitUtils() }
    }

    fun <T> getService(clazz: Class<T>): T {
        return retrofit.create(clazz)
    }

    private val retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("https://api.github.com/")
        .build()
}
GitHubService.kt
interface GitHubService {

    @GET("search/repositories?sort=stars&q=Android")
    suspend fun searchRepositories(@Query("page") page: Int, @Query("per_page") perPage: Int) : RepoRec

}

4.Paging3 PagingSource

RepoPagingSource.kt
class RepoPagingSource(private val gitHubService: GitHubService): PagingSource<Int, Repo>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
        return try {
            val page = params.key ?: 1
            val pageSize = params.loadSize
            val repoRec = gitHubService.searchRepositories(page, pageSize)
            val repoItems = repoRec.items
            val prevKey = if (page > 1) page - 1 else null
            val nextKey = if (repoItems.isNotEmpty()) page + 1 else null
            LoadResult.Page(repoItems, prevKey, nextKey)
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Repo>): Int? = null
}

5.Paging3 Repository

Repository.kt
object Repository {

    private const val PAGE_SIZE = 50

    private val gitHubService = RetrofitUtils.instance.getService(GitHubService::class.java)

    fun getPagingData(): Flow<PagingData<Repo>> = Pager(
        config = PagingConfig(PAGE_SIZE),
        pagingSourceFactory = { RepoPagingSource(gitHubService) }
    ).flow

}

6.Paging3 ViewModel

MainViewModel.kt
class MainViewModel : ViewModel() {

    fun getPagingData(): Flow<PagingData<Repo>> {
        return Repository.getPagingData().cachedIn(viewModelScope)
    }

}

7.Paging3 Adapter

repo_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="repo"
            type="com.example.jetpackdemo.Repo" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:orientation="vertical">

        <TextView
            android:text="@{repo.name}"
            android:id="@+id/name_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:maxLines="1"
            android:ellipsize="end"
            android:textColor="#5194fd"
            android:textSize="20sp"
            android:textStyle="bold" />

        <TextView
            android:text="@{repo.description}"
            android:id="@+id/description_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:maxLines="10"
            android:ellipsize="end" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:gravity="end"
            tools:ignore="UseCompoundDrawables">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginEnd="5dp"
                android:src="@drawable/ic_star"
                tools:ignore="ContentDescription" />

            <TextView
                android:text="@{Integer.toString(repo.starCount)}"
                android:id="@+id/star_count_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical" />

        </LinearLayout>

    </LinearLayout>
</layout>
RepoAdapter.kt
class RepoAdapter : PagingDataAdapter<Repo, RepoAdapter.RepoViewHolder>(COMPARATOR) {

    companion object {
        private val COMPARATOR = object : DiffUtil.ItemCallback<Repo>() {
            override fun areItemsTheSame(oldItem: Repo, newItem: Repo): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Repo, newItem: Repo): Boolean {
                return oldItem == newItem
            }
        }
    }

    override fun onBindViewHolder(holder: RepoViewHolder, position: Int) {
        val repo = getItem(position)
        holder.bind(repo)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        RepoViewHolder(RepoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    class RepoViewHolder(private val binding: RepoItemBinding) : ViewHolder(binding.root) {
        fun bind(repo: Repo?) {
            binding.repo = repo
            binding.executePendingBindings()
        }
    }
}

8.Paging3 FooterAdapter

footer_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp">

        <ProgressBar
            android:id="@+id/progress_bar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

        <Button
            android:id="@+id/retry_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Retry" />

    </FrameLayout>
</layout>
FooterAdapter.kt
class FooterAdapter(private val retry: () -> Unit) : LoadStateAdapter<FooterAdapter.FooterViewHolder>() {

    override fun onBindViewHolder(holder: FooterViewHolder, loadState: LoadState) {
        holder.bind(loadState, retry)
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): FooterViewHolder {
        return FooterViewHolder(FooterItemBinding.inflate(
            LayoutInflater.from(parent.context), parent, false)
        )
    }

    class FooterViewHolder(private val binding: FooterItemBinding) : ViewHolder(binding.root) {
        fun bind(loadState: LoadState, retry: () -> Unit) {
            binding.retryButton.setOnClickListener {
                retry()
            }
            binding.progressBar.isVisible = loadState is LoadState.Loading
            binding.retryButton.isVisible = loadState is LoadState.Error
        }
    }
}

9.Paging3

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <ProgressBar
            android:id="@+id/progress_bar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

    </FrameLayout>
</layout>
MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: MainViewModel by viewModels()
    private val repoAdapter = RepoAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            adapter = repoAdapter.withLoadStateFooter(FooterAdapter { repoAdapter.retry() })
        }

        lifecycleScope.launch {
            viewModel.getPagingData().collect { pagingData ->
                repoAdapter.submitData(pagingData)
            }
        }

        repoAdapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.NotLoading -> {
                    binding.progressBar.visibility = View.INVISIBLE
                    binding.recyclerView.visibility = View.VISIBLE
                }
                is LoadState.Loading -> {
                    binding.progressBar.visibility = View.VISIBLE
                    binding.recyclerView.visibility = View.INVISIBLE
                }
                is LoadState.Error -> {
                    val state = it.refresh as LoadState.Error
                    binding.progressBar.visibility = View.INVISIBLE
                    Toast.makeText(this, "Load Error: ${state.error.message}", Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }
    }
}

10.Developer Documents Paging3

Open in Documents Paging3

發表迴響