Updated date:

How to Create a Simple Gif Browser With Giphy® Api and Android Pagination Library

Davinder Singh is a Polymath. He loves reading, learning and writing technical cookbooks about new technologies.

What You Will Learn

  • You will learn about fetching and parsing REST responses.
  • How to load data from APIs that supports paging.
  • Knowledge about different types of data sources that are provided in the Android Paging Library.
  • How to use Giphy® API to query and download graphics integrated images.
  • How to implement an infinite recyclerview with beautiful native animations.

Prerequisites

Following is the list of things, I am assuming you already know irrespective of your level of expertise.

  • How to create and setup new project in Android Studio a.k.a IntelliJ Idea.
  • Basic familiarity with Kotlin Programming Language.
  • Creating new packages, files and layouts in the Android Studio.
  • Already know, how to implement recycler list view (Adapter, Item layout etc.).
  • Have stable Internet connection.

What We Will Make

We will make a simple application implementing Android Paging Library component. Application will have an infinite recycler list view displaying search results from Giphy® through developer API.

What is Pagination

Pagination is an important concept in REST APIs. It is a technique by which search results are divided into smaller chunks called pages. Every page is thus a partition of larger result.

Advantage of this technique is that it divides the amount of data that client needs to process in a single go. And because of display area only certain number of results can be displayed on-screen; We can substantially reduce the memory footprint and network requirement of an application.

However, because of pagination instead of sending whole data at once, server sends data in chunks which create issue of synchronization between client and server. To tackle this, first result of call to API contains extra information for synchronization purposes. Every pagination API contains following mandatory fields:

  • Total number of search results
  • Number of search in current page
  • Total number of pages
  • Current page number

URLs can be provided with an [optional] argument page-number which gives client access to random page from set of pages (just like array indices).

Paging imparts additional overhead on servers. Because servers are powerful and implement technologies like caching and parallel processing; It becomes a win-win situation for both server and client.

Step 1: Setting Up Developers Account on Giphy®

First thing first let set up a developer's account at Giphy®. Giphy is a search engine platform for searching and sharing graphics integrated images(GIF) or moving picture. Go to Giphy.com and create a new login account if you don't already have one.

Developer's account is something that gives you access to all the application program interfaces that certain site provides to access their services. Some are paid while some are freely available with or without constraints. We will now create a developer's account on Giphy® to get access to services. Visit giphy® developer site and click button that says Get Started.

Create a new application

Create a new application

Click on Create an App. I am assuming that you already have made an account on Giphy®.

Create a new application modal

Create a new application modal

Give a name to your application. A description and finally check I only want to use Giphy® API. And then click Create New App. And we are done!

Next step copy the API key and save it for later.

Copy API Key

Copy API Key

Working With REST APIs

Before moving forward, let's have taste of Giphy's API. In this little demonstration, we will query giphy for all GIFs containing or tagged with word hello world.

Whole process consists of forming correct URL and then sending a get request to it. All APIs have following basic structure [BASE URL]\[END POINT]. Base URL of giphy API is https://api.giphy.com/v1/. If you follow the link then you will get a text response from Giphy similar to one given below

{
  "data": [],
  "meta": {
    "status": 404,
    "msg": "Not Found!",
    "response_id": "XXXXXXX"
  }
}

The response indicates 404 HTTP error also known as Not Found. Notation used to write response is called JSON which is acronym for JavaScirpt Object Notation. This is what all those applications that use REST API behind the curtain see. They parse specific fields out of these responses to respond to user. Let's do another one; this time with all the required arguments before moving forward to coding android application. Click on following link with all the X's replaced by API Key https://api.giphy.com/v1/gifs/search?q=hello&limit=5&api_key=XXXXXXXXXXXXX

Response from Giphy® Search API

//comments are not part of original respone!
//Original: https://api.giphy.com/v1/gifs/search?q=hello&limit=5&api_key=XXXXXX

///////////Break Down////////////
//1) Base URL = https://api.giphy.com/v1/
//2) We want to search in gifs hence add=> gifs/search?
//3) Query is hello therefore => q=hello&
//4) Maximum results{5} => limit=5&
//5) API Key{replace X's with your own} api_key=XXXXXX
///////////////Response////////////

{
  "data": [
	{..},
	{..},
	{..},
	{..},
	{..}
  ],
//Pagination Meta-data
  "pagination": {
    "total_count": 18446,
    "count": 5,
    "offset": 0
  },
  "meta": {
    "status": 200,
    "msg": "OK",
    "response_id": "XXXXXXXXXXX"
  }
}

   

You can know about this and many other end points from Giphy®'s documentation by visiting here.

Step 2: Android Application

Launch Android Studio and create a new android project with AndroidX articles. Also set Kotlin as main language. Also, add network access permissions in manifest of application.

Creating A New Project

Creating A New Project

Step 3: Dependencies

Our application can be divided into three separate parts which are

  • Network Handling: This portion handles network aspect of application. Objectives of this portion are to manage network resources, caching results and then providing responses to data processing section.
  • Data Processing: This is where raw responses are kept. Here we need to parse JSON responses from Giphy into kotlin objects. These objects store data corresponding to fields in JSON response.
  • UI Handling: This is final portion. Here we load image data into ImageView and finally display them on screen in recycler list view.

Luckily, android has libraries already developed by someone which can handle everything for us all we need to do is to include them. To include them, add following dependencies in your application's gradle app module.

//Material Library for theming {this can be skipped}
implementation 'com.google.android.material:material:1.2.0-alpha05'
//Facebook Fresco API with GIF extension
implementation 'com.facebook.fresco:fresco:1.11.0'
implementation 'com.facebook.fresco:animated-gif:1.11.0'
//GSON for JSON Parsing
implementation 'com.google.code.gson:gson:2.8.5'
//Volley for making calls to the REST API
implementation 'com.android.volley:volley:1.1.1'
//JetPack
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'android.arch.paging:runtime:1.0.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'

Step 4: Setup Project Tree

Before moving forward to coding, create following packages in the main package of application.

  • For activities: activity
  • For network handling code: network
  • To store paging and view model related code: viewmodel


Finally, for user interface, create ui package.

Project Tree

Project Tree

Step 4: UI Layout

First we will create layout for application. Application consists of a recycler list view, edit text view for query input, a text view to display total results and finally a button to search for query.

Layout

Layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressBar" />

    <EditText
        android:id="@+id/searchEditText"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:background="@android:color/transparent"
        android:hint="@string/hint_text"
        android:importantForAutofill="no"
        android:inputType="text"
        android:textColor="#000000"
        android:textSize="18sp"
        android:imeOptions="actionGo"
        app:layout_constraintBottom_toBottomOf="@+id/btSearch"
        app:layout_constraintEnd_toStartOf="@+id/btSearch"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/btSearch" />

    <TextView
        android:id="@+id/btSearch"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:text="@string/search"
        android:textColor="#000000"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/searchEditText"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/infoText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000000"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/searchEditText" />

    <ProgressBar
        android:id="@+id/progressBar"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:indeterminateTint="@color/colorPrimary"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/infoText" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now, add two new layout resource files named small_item_layout and large_item_layout. And add a CardView in both of them with having width 150dp and height 250dp in large_item_layout while 210dp in small_item_layout. Lastly add SimpleDraweeView in both with match_parent in both width and height.

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="150dp"
  android:layout_height="250dp" <!--large_item_layout-->
  xmlns:app="http://schemas.android.com/apk/res-auto">
    <com.facebook.drawee.view.SimpleDraweeView
            android:id="@+id/image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:actualImageScaleType="centerCrop"
            />
</androidx.cardview.widget.CardView>

Step 5: Network Handler

Network handling module contains code that talk directly with Giphy® API; It parses the response and gives back kotlin class object containing fields from response. Network handler also handle errors that occur during network I/O. Add following files in network package:

  • ApiConstants.kt: Contains all constants related to Giphy® API, for example API Key, base URL and search URL.
  • EndPointGenerator.kt: This file consists of methods that return formatted string URLs based on request that we want to send. This saves us time and also reduces URL errors that occur during writing large URLs.
  • NetworkHandler.kt: This file contains a static class which consists of methods providing abstraction over volley's network layer.

ApiConstants.kt

//network package
const val GIPHY_API_SECRET = "XXXXXXX" //API Key here
const val GIPHY_BASE_URL = "https://api.giphy.com"

//Endpoints
const val SEARCH_GIPHY = "$GIPHY_BASE_URL/v1/gifs/search"

EndpointGenerator.kt

//network package
object EndpointGenerator{

    fun searchLiveImages(
        pageNumber: Int,
        perPage: Int = 10,
        searchTerm: String
    ): String {
        return "$SEARCH_GIPHY?limit=$perPage&q=$searchTerm&offset=$pageNumber&api_key=$GIPHY_API_SECRET&lang=${
        Locale.getDefault().language}"
    }

}

As said earlier, We are using Volley. Before moving forward let's first understand basics of Volley network library. Volley consists of two main components

  1. Request Queue: Global request queue is a queue of request that Volley executes in first-in-first-out fashion. Every request has associated listeners: A failure listener and A response listener. Type of response depends upon the type of request.
  2. Request: Volley supports different types of requests suitable for different scenarios. String request (which we will use) is for getting raw string response, JsonArrayRequest is for requesting array of objects in JSON and so on.

Response provided by volley in success callback is then given to GSon library which parses JSON response(basically a readable string) into Koltin object.

That was very brief explanation of Volley. You can learn more about it in here.

NetworkHandler.kt

//network package
object NetworkHandler {
    //Volley Request Queue
    private lateinit var mRequestQueue: RequestQueue
    //JSON Parser
    private lateinit var mJsonParser: Gson

    private var isInitialized: Boolean = false

    fun initialize(context: Context) {
        if (!isInitialized) {
            mRequestQueue = Volley.newRequestQueue(context)
            mJsonParser = GsonBuilder().create()
            isInitialized = true
        }
    }

    fun searchGiphy(query: String,
                    successCallback: (GiphySearchImagePoko) -> Unit,
                    failureCallback: (VolleyError) -> Unit){
        mRequestQueue.add(StringRequest(
            query,
            {
                val result = mJsonParser.fromJson(it, GiphySearchImagePoko::class.java)
                successCallback(result)
            },
            failureCallback
        ))
    }

    //Object class for evernote event
   data class QueryResultEventObject(val query: String,
                                  val isSuccess: Boolean,
                                  val maxPages: Int,
                                  val currentPage: Int,
                                  val length: Int)
}

One final step in network left to do is to add JSON parsing information for GSON. For this, we need to add schema(structural information) of JSON response that Giphy® sends. This can be done in two ways, either with the help of automated tools such as this or by writing it from the scratch. For the sake of this tutorial we will write it from the scratch.

GiphySearchImagePoko.kt

//network package
data class GiphySearchImagePoko(
    @Expose
    @SerializedName("data")
    var data: List<GiphySearchDatum>,
    @Expose
    @SerializedName("pagination")
    var pageInformation: PageInformation

) {
    data class PageInformation(
        @Expose
        @SerializedName("total_count")
        var totalCount : Int = 0,
        @Expose
        @SerializedName("count")
        var count: Int = 0,
        @Expose
        @SerializedName("offset")
        var offset: Int = 0
    )

    data class GiphySearchDatum(
        @Expose
        @SerializedName("type")
        var type: String = "",
        @Expose
        @SerializedName("id")
        var id: String = "",
        @Expose
        @SerializedName("url")
        var url: String = "",
        @Expose
        @SerializedName("title")
        var title: String = "",
        @Expose
        @SerializedName("images")
        var images: Images = Images()
    ) {
        data class Images(
            @Expose
            @SerializedName("original")
            var original: Urls = Urls(),
            @Expose
            @SerializedName("fixed_width")
            var fixedWidth: Urls = Urls(),
            @Expose
            @SerializedName("fixed_width_downsampled")
            var downSampledFixedWidth: Urls = Urls(),
            @Expose
            @SerializedName("downsized")
            var downsized: Urls = Urls()
        )

        data class Urls(
            @Expose
            @SerializedName("width")
            var width: String = "",
            @Expose
            @SerializedName("height")
            var height: String = "",
            @Expose
            @SerializedName("url")
            var url: String = "",
            @Expose
            @SerializedName("size")
            var size: String = "",
            @Expose
            @SerializedName("mp4")
            var mp4Url: String = "",
            @Expose
            @SerializedName("mp4_size")
            var mp4Size: String = "",
            @Expose
            @SerializedName("webp")
            var webpUrl: String = "",
            @Expose
            @SerializedName("webp_size")
            var webpSize: String = ""
        )

        data class Url(@Expose @SerializedName("url") var Url: String = "")

    }
}

Step 6: ViewModel and Paging

Pagination consists of two main components: A View-Model and Data Source. View-Model provides state and lifecycle support while Data source acts as provider of data. All GUIs (recycler list view in our case) observe view-model for data changes or availability of new data and then update themselves accordingly.

Datasource is middle man between view-model and actual data source(either SQLite database or network feed). Pagination provides three types of data sources depending on useful in different scenarios:

  • PagedKeyedDataSource: This data source is best suited for situations where response from API contains links for loading adjacent pages
  • ItemKeyedDataSource: This data source comes in handy when data is already sorted and each item in data has unique id. To load items from 0 to N-1 you have to give id of 0th item and size up to which you want to load the list. And then to load next set of items i.e. from (N-1)+1 till N+K (say), id of Nth item and size K.
  • PositionalDataSource: Positional data source is probably the most used and simplest of all data sources. It is used when result of API consist of number of results per page and number of pages such that you can randomly ask for any portion of result just by providing the right index.

Add following new files in viewmodel package: SearchViewModel.kt, GiphyDataSource.kt and DataSourceFactory.kt

SearchViewModel.kt

//viewmodel
class SearchViewModel : ViewModel() {
    lateinit var imageList: LiveData<PagedList<GiphySearchImagePoko.GiphySearchDatum>>
    var mLatestQuery: String = "hello"
    private var dataSourceFactory = DataSourceFactory("")

    init {
        loadImageData(mLatestQuery)
    }

    fun loadImageData(query: String) {
        mLatestQuery = query
        dataSourceFactory = DataSourceFactory(query)
        imageList = LivePagedListBuilder<Int, GiphySearchImagePoko.GiphySearchDatum>(
            dataSourceFactory,
            PagedList.Config.Builder()
                .setPageSize(10)
                .setPrefetchDistance(10)
                .setEnablePlaceholders(false)
                .build()
        ).build()
    }
}

GiphyDataSource.kt

//viewmodel package
class GiphyDataSource(private val query: String): PositionalDataSource<GiphySearchImagePoko.GiphySearchDatum>(){

    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<GiphySearchImagePoko.GiphySearchDatum>) {

        NetworkHandler.searchGiphy(
            EndpointGenerator.searchLiveImages(
            searchTerm = query,
            pageNumber = params.startPosition,
            perPage = params.loadSize
        ), {
            callback.onResult(it.data)
        },{
            EventBus.getDefault()
                .post(it)
        })
    }

    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<GiphySearchImagePoko.GiphySearchDatum>) {

         NetworkHandler.searchGiphy(EndpointGenerator.searchLiveImages(
            searchTerm = query,
            pageNumber = params.requestedStartPosition,
            perPage = params.pageSize
        ), {
             Log.e("LOAD", "${it.pageInformation.count}")
            EventBus.getDefault()
                .post(NetworkHandler.QueryResultEventObject(query = query,
                    isSuccess = true,
                    maxPages = it.pageInformation.totalCount /
                            if (it.pageInformation.count == 0) 1 else it.pageInformation.count,
                    currentPage = it.pageInformation.offset,
                    length = it.pageInformation.count))

            callback.onResult(it.data, params.requestedStartPosition)
        },{
             Log.e("LOAD", "Error!!")
            EventBus.getDefault()
                .post(it)
        })
    }

}

DataSourceFactory.kt

//viewmodel package
class DataSourceFactory(query: String): DataSource.Factory<Int, GiphySearchImagePoko.GiphySearchDatum>(){
    val data = MutableLiveData<GiphyDataSource>()
    var dataSource = GiphyDataSource(query)

    override fun create(): DataSource<Int, GiphySearchImagePoko.GiphySearchDatum> {
       data.postValue(dataSource)
        return dataSource
    }
}

Step 7: Finalising

Finally, let's complete application by adding last few snippets of code. Add two new files in ui package: InfiniteListAdapter.kt and RecyclerViewDecoration.kt for recycler list view and following code in MainActivity.kt to bind them all.


InfiniteListAdapter.kt

//ui package
/*
InfiniteList adapter
Two items
type 0 -> Normal Item
type 1 -> Enlarged Item

Layout type: Grid System
*/

class InfiniteListAdapter(
    private val context: Context,
    val clickListener: (ImagePoko) -> Unit
) : PagedListAdapter<ImagePoko, RecyclerView.ViewHolder>(DIFF) {

    private var mWidth: Int = 0

    companion object {
        val DIFF = object : DiffUtil.ItemCallback<ImagePoko>() {
            override fun areItemsTheSame(oldItem: ImagePoko, newItem: ImagePoko): Boolean {
                Log.e("ITEM", oldItem.url)
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: ImagePoko, newItem: ImagePoko): Boolean {
                return oldItem.url.contentEquals(newItem.url)
            }
        }

        const val SMALL_ITEM_HEIGHT = 180
        const val LARGE_ITEM_HEIGHT = 250
    }


    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        val context = recyclerView.context as Activity
        val windowDimensions = Point()
        context.windowManager.defaultDisplay.getSize(windowDimensions)
        mWidth = (windowDimensions.x * 0.5f).roundToInt()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = when (viewType) {
            0 -> {
                (LayoutInflater.from(parent.context)
                    .inflate(R.layout.cover_list_item_small, parent, false))
            }
            else -> {
                (LayoutInflater.from(parent.context)
                    .inflate(R.layout.cover_list_item_large, parent, false))
            }
        }

        val params: ViewGroup.LayoutParams = ViewGroup.LayoutParams(
            mWidth,
            dpToPixels((if (viewType == 0) SMALL_ITEM_HEIGHT else LARGE_ITEM_HEIGHT).toFloat(), context).toInt()
        )
        view.layoutParams = params
        return GenericViewHolder(view)

    }

    override fun getItemViewType(position: Int): Int {
        return if (position % 4 == 0) 0 else 1
    }


    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        holder as GenericViewHolder
        holder.view.image.setOnClickListener{
            clickListener(getItem(position)!!)
        }
        if (getItem(position) == null) {
            val imageRequest = ImageRequestBuilder.newBuilderWithResourceId(R.drawable.bg_white_drawable).build();
            holder.view.image.setImageRequest(imageRequest)
        } else {
            //items.keys.elementAt(position)
            val controller = Fresco.newDraweeControllerBuilder()
                .setUri(getItem(position)?.images?.downSampledFixedWidth?.url?:"")
                .setAutoPlayAnimations(true)
                .build()
            holder.view.image.controller = controller
        }
    }

    private fun dpToPixels(value: Float, context: Context) = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP, value,
        context.resources.displayMetrics
    )

    data class GenericViewHolder(val view: View) : RecyclerView.ViewHolder(view)
}

RecyclerViewDecoration.kt

//ui package
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import androidx.recyclerview.widget.RecyclerView
import android.view.View

class RecyclerViewDecoration(
    private val sideOffset: Int = 30,
    private val crossSectionOffset: Int = 12,
    private val paintDivider: Paint = getPainter(3F),
    private val paintCorners: Paint = getPainter(1F),
    private val shouldDecorate: (Int) -> Boolean = fun(_: Int) = true
) : RecyclerView.ItemDecoration() {

    companion object {
        fun getPainter(width: Float): Paint {
            val defaultPainter = Paint(Paint.ANTI_ALIAS_FLAG)
            defaultPainter.color = Color.TRANSPARENT
            defaultPainter.style = Paint.Style.STROKE
            defaultPainter.strokeWidth = width
            return defaultPainter
        }
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state:  RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        if (parent.getChildAdapterPosition(view) < 0){
            return
        }
        if (shouldDecorate(parent.getChildAdapterPosition(view))) {
            outRect.set(sideOffset, crossSectionOffset, sideOffset, crossSectionOffset)
        }else{
            outRect.set(0, 0, 0, 0)
        }
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
        val layoutManager = parent.layoutManager
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            if (layoutManager != null) {
                c.drawRect(
                    (layoutManager.getDecoratedLeft(child) + 16).toFloat(),
                    layoutManager.getDecoratedTop(child).toFloat(),
                    (layoutManager.getDecoratedRight(child) - sideOffset).toFloat(),
                    layoutManager.getDecoratedBottom(child).toFloat(),
                    paintDivider
                )

                c.drawRect(
                    (layoutManager.getDecoratedLeft(child) + sideOffset).toFloat(),
                    (layoutManager.getDecoratedTop(child) + crossSectionOffset).toFloat(),
                    (layoutManager.getDecoratedRight(child) + sideOffset).toFloat(),
                    (layoutManager.getDecoratedBottom(child) - crossSectionOffset).toFloat(),
                    paintCorners
                )
            }
        }
    }
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val TAG = "MAIN-ACTIVITY"
    //Main Layout
    private lateinit var mLayout: View
    //Components
    private lateinit var mRecyclerView: RecyclerView
    private lateinit var mAdapter: InfiniteListAdapter
    private lateinit var mSearchBtn: TextView

    //ViewModel
    private lateinit var mViewModel: SearchViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Fresco.initialize(this)
        mLayout = findViewById(android.R.id.content)

        setupViewModel()
        setupRecyclerView()
        setupCallbacks()
        //initial search
        search(mViewModel.mLatestQuery)
    }

    private fun setupViewModel() {
        //Initializing Volley
        NetworkHandler.initialize(this)
        mViewModel = ViewModelProviders.of(this).get(SearchViewModel::class.java)
    }

    private fun setupRecyclerView() {
        mAdapter = InfiniteListAdapter(this) {
            Log.e(TAG, "Clicked ${it.id}")
        }

        val manager = StaggeredGridLayoutManager(2, RecyclerView.VERTICAL)
        val decoration = RecyclerViewDecoration(6, 6)
        mRecyclerView = findViewById(R.id.list)
        mRecyclerView.adapter = mAdapter
        mRecyclerView.layoutManager = manager
        mRecyclerView.addItemDecoration(decoration)
    }

    private fun setupCallbacks() {
        mSearchBtn = findViewById(R.id.btSearch)
        mSearchBtn.setOnClickListener {
            hideKeyboard(this)
            search(mLayout.searchEditText.text.toString())
        }

        mLayout.searchEditText.setOnKeyListener { _, keyCode, _ ->
            if (keyCode == KeyEvent.KEYCODE_ENTER) {
                hideKeyboard(this)
                search(mLayout.searchEditText.text.toString())
                true
            } else
                false
        }
    }

    //--Search
    private fun search(query: String) {
        this.mLayout.progressBar.visibility = View.VISIBLE
        Handler().postDelayed({
            mViewModel.loadImageData(query)
            mViewModel.imageList.observe(this, Observer {
                Log.e(TAG, "${it.size}")
                mAdapter.submitList(it)
                Handler().postDelayed({
                    this.mLayout.progressBar.visibility = View.GONE
                }, 2000)
            })
        }, 2000)
    }

    //--Lifecycle callbacks
    override fun onStart() {
        super.onStart()
        EventBus.getDefault().register(this)
    }

    override fun onStop() {
        super.onStop()
        EventBus.getDefault().unregister(this)
    }

    //--Utility functions
    @Subscribe
    fun banner(event: NetworkHandler.QueryResultEventObject) {
        //Show the banner
        this.mLayout.infoText.text = getString(
            R.string.loaded, if (event.isSuccess) "${event.maxPages *
                    event.length}" else "0"
        )
        this.mLayout.infoText.visibility = View.VISIBLE
        Handler().postDelayed({
            //Hide the banner
            this.mLayout.infoText.visibility = View.GONE
        }, 2000)
    }

    @Subscribe
    fun networkError(error: VolleyError) {
        Toast.makeText(
            this, "An error occurred while processing your request!" +
                    " Code: ${error.message}",
            Toast.LENGTH_LONG
        ).show()
    }

    private fun hideKeyboard(activity: Activity) {
        val imm: InputMethodManager =
            activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
        //Find the currently focused view, so we can grab the correct window token from it.
        var view = activity.currentFocus
        //If no view currently has focus, create a new one, just so we can grab a window token from it
        if (view == null) {
            view = View(activity)
        }
        imm.hideSoftInputFromWindow(view.windowToken, 0)
    }

}

Conclusion

Well, that was all folks. Of course this was just a brief tutorial of what's possible with android pagination library. It supports much more and can be used in variety of ways and in different scenarios: Network, SQLite etc. You can learn more about it here.

Finally, I hope that I helped you in learning something new. Do feel free to ask question I sure will help you out or to give suggestions. Have a nice day!

This content is accurate and true to the best of the author’s knowledge and is not meant to substitute for formal and individualized advice from a qualified professional.

© 2020 Dav Vendator

Comments

Dav Vendator (author) from Amritsar on March 29, 2020:

Thank you for your kind appreciations sir. I aim only to do that!

Halemane Muralikrishna from South India on March 29, 2020:

Thank you, Mr Davinder Singh, for sharing valuable android software coding. It was new learning experience for me here.

Sumit Goswami from Kolkata on March 29, 2020:

This is an incredible article with lot of information about Android Jetpack Paging Library. Also your photographs contain useful information about this technology.

Dav Vendator (author) from Amritsar on March 27, 2020:

Thankyou very much kind sir for the complement. I hope that article is easy to follow as much as it is elaborated!

Umesh Chandra Bhatt from Kharghar, Navi Mumbai, India on March 27, 2020:

Very elaborate and highly technical write up. Good article. Useful for Android professionals.