Android Kotlin Paging Library with Retrofit
20-05-2020PostsDataSource
class PostsDataSource(private val scope: CoroutineScope) :
PageKeyedDataSource<Int, Question>() {
private val repository = QuestionRepository()
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, Question>
) {
scope.launch {
try {
val response = repository.getQuestionsAsync(1)
when {
response.success -> {
val listing = response.data
val nextPage = 2
callback.onResult(listing?.data ?: listOf(), null, nextPage)
}
}
} catch (exception: Exception) {
Log.e("PostsDataSource", "Failed to fetch data!")
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) {
scope.launch {
try {
val response = repository.getQuestionsAsync(params.key)
when {
response.success -> {
val listing = response.data
val items = listing?.data
callback.onResult(items ?: listOf(), params.key + 1)
}
}
} catch (exception: Exception) {
Log.e("PostsDataSource", "Failed to fetch data!")
}
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Question>) {
}
override fun invalidate() {
super.invalidate()
scope.cancel()
}
}
QuestionAdapter Class
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.codesenior.period.tracker.R
import com.codesenior.period.tracker.databinding.RecyclerviewQuestionsBinding
import com.codesenior.period.tracker.models.Question
import com.codesenior.period.tracker.ui.fragments.QuestionFragment
class QuestionAdapter(private val listener: QuestionFragment.OnQuestionFragmentListener?) :
PagedListAdapter<Question, QuestionAdapter.QuestionViewHolder>(DiffUtilCallBack()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionViewHolder {
return QuestionViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.recyclerview_questions,
parent,
false
)
)
}
override fun onBindViewHolder(holder: QuestionAdapter.QuestionViewHolder, position: Int) {
holder.recyclerViewItem.question = getItem(position)
holder.recyclerViewItem.textViewTitle.setOnClickListener {view->
getItem(position)?.let { listener?.onQuestionItemClicked(it) }
}
}
inner class QuestionViewHolder( val recyclerViewItem: RecyclerviewQuestionsBinding) :
RecyclerView.ViewHolder(recyclerViewItem.root)
}
DiffUtilCallBack
import androidx.recyclerview.widget.DiffUtil
import com.codesenior.period.tracker.models.Question
class DiffUtilCallBack : DiffUtil.ItemCallback<Question>() {
override fun areItemsTheSame(oldItem: Question, newItem: Question): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Question, newItem: Question): Boolean {
return oldItem.title == newItem.title
&& oldItem.content == newItem.content
}
}
recyclerview_questions.xml File
<?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:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="question"
type="com.codesenior.period.tracker.models.Question" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="@dimen/dp_20"
android:padding="@dimen/dp_20"
android:background="@drawable/shape">
<TextView
android:id="@+id/text_view_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
android:text="@{question.title}"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
tools:text="Adet Gecikmesi" />
</LinearLayout>
</layout>
QuestionListViewModel
class QuestionListViewModel : ViewModel() {
var postsLiveData: LiveData<PagedList<Question>>
init {
val config = PagedList.Config.Builder()
.setPageSize(10)
.setEnablePlaceholders(false)
.build()
postsLiveData = initializedPagedListBuilder(config).build()
}
fun getPosts(): LiveData<PagedList<Question>> = postsLiveData
private fun initializedPagedListBuilder(config: PagedList.Config):
LivePagedListBuilder<Int, Question> {
val dataSourceFactory = object : DataSource.Factory<Int, Question>() {
override fun create(): DataSource<Int, Question> {
return PostsDataSource(viewModelScope)
}
}
return LivePagedListBuilder<Int, Question>(dataSourceFactory, config)
}
}
QuestionFragment
class QuestionFragment : Fragment() {
private lateinit var viewModel: QuestionListViewModel
var listener: OnQuestionFragmentListener? = null
private val adapter = QuestionAdapter(listener)
companion object {
fun newInstance() = QuestionFragment()
}
interface OnQuestionFragmentListener {
fun onQuestionItemClicked(question: Question)
}
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? OnQuestionFragmentListener
if (listener == null) {
throw ClassCastException("$context must implement OnArticleSelectedListener")
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.question_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(QuestionListViewModel::class.java)
viewModel.getPosts().observe(viewLifecycleOwner, Observer {
adapter.submitList(it)
recycler_view_questions.layoutManager = LinearLayoutManager(requireContext())
recycler_view_questions.adapter = adapter
})
fab_create_question.setOnClickListener { view ->
Snackbar.make(view, "Here's a Snackbar", Snackbar.LENGTH_LONG)
.setAction("Action", null)
.show()
}
}
}
Repository, Provider, Retrofit
class QuestionRepository {
private var questionProvider = QuestionProvider()
suspend fun getQuestionsAsync(page: Int) = questionProvider.getQuestions(page)
}
class QuestionProvider {
suspend fun getQuestions(page: Int): PaginationResponse<Question> {
val apiClient = ApiClient.getClient(Config.REST_API)
val service = apiClient.create(QuestionService::class.java)
return service.getQuestions(page)
}
}
interface QuestionService {
@GET("questions")
suspend fun getQuestions(@Query("page") page: Int): PaginationResponse<Question>
}
object ApiClient {
fun getClient(baseUrl: String?): Retrofit {
return getClient(baseUrl, null)
}
fun getClient(baseUrl: String?, accessToken: String?): Retrofit {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addInterceptor { chain ->
val ongoing = chain.request().newBuilder()
ongoing.addHeader("Accept", "application/json;versions=1")
if (accessToken != null) {
ongoing.addHeader("Authorization", "Bearer $accessToken")
}
chain.proceed(ongoing.build())
}.build()
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create()
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(client)
.build()
}
}