Android Kotlin Paging Library with Retrofit

20-05-2020

PostsDataSource

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()
    }
}

© 2019 All rights reserved. Codesenior.COM