ArtsAutosBooksBusinessEducationEntertainmentFamilyFashionFoodGamesGenderHealthHolidaysHomeHubPagesPersonal FinancePetsPoliticsReligionSportsTechnologyTravel

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

Updated on March 27, 2020
Dav Vendator profile image

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 | Source

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 | Source

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 | Source

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 | Source

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 | Source

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 | Source
<?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

    0 of 8192 characters used
    Post Comment
    • Dav Vendator profile imageAUTHOR

      Dav Vendator 

      2 months ago from Amritsar

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

    • hmkrishna profile image

      Halemane Muralikrishna 

      2 months ago from South India

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

    • lifehackpundit profile image

      Sumit Goswami 

      2 months ago from Kolkata

      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 profile imageAUTHOR

      Dav Vendator 

      2 months ago from Amritsar

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

    • bhattuc profile image

      Umesh Chandra Bhatt 

      2 months ago from Kharghar, Navi Mumbai, India

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

    working

    This website uses cookies

    As a user in the EEA, your approval is needed on a few things. To provide a better website experience, hubpages.com uses cookies (and other similar technologies) and may collect, process, and share personal data. Please choose which areas of our service you consent to our doing so.

    For more information on managing or withdrawing consents and how we handle data, visit our Privacy Policy at: https://maven.io/company/pages/privacy

    Show Details
    Necessary
    HubPages Device IDThis is used to identify particular browsers or devices when the access the service, and is used for security reasons.
    LoginThis is necessary to sign in to the HubPages Service.
    Google RecaptchaThis is used to prevent bots and spam. (Privacy Policy)
    AkismetThis is used to detect comment spam. (Privacy Policy)
    HubPages Google AnalyticsThis is used to provide data on traffic to our website, all personally identifyable data is anonymized. (Privacy Policy)
    HubPages Traffic PixelThis is used to collect data on traffic to articles and other pages on our site. Unless you are signed in to a HubPages account, all personally identifiable information is anonymized.
    Amazon Web ServicesThis is a cloud services platform that we used to host our service. (Privacy Policy)
    CloudflareThis is a cloud CDN service that we use to efficiently deliver files required for our service to operate such as javascript, cascading style sheets, images, and videos. (Privacy Policy)
    Google Hosted LibrariesJavascript software libraries such as jQuery are loaded at endpoints on the googleapis.com or gstatic.com domains, for performance and efficiency reasons. (Privacy Policy)
    Features
    Google Custom SearchThis is feature allows you to search the site. (Privacy Policy)
    Google MapsSome articles have Google Maps embedded in them. (Privacy Policy)
    Google ChartsThis is used to display charts and graphs on articles and the author center. (Privacy Policy)
    Google AdSense Host APIThis service allows you to sign up for or associate a Google AdSense account with HubPages, so that you can earn money from ads on your articles. No data is shared unless you engage with this feature. (Privacy Policy)
    Google YouTubeSome articles have YouTube videos embedded in them. (Privacy Policy)
    VimeoSome articles have Vimeo videos embedded in them. (Privacy Policy)
    PaypalThis is used for a registered author who enrolls in the HubPages Earnings program and requests to be paid via PayPal. No data is shared with Paypal unless you engage with this feature. (Privacy Policy)
    Facebook LoginYou can use this to streamline signing up for, or signing in to your Hubpages account. No data is shared with Facebook unless you engage with this feature. (Privacy Policy)
    MavenThis supports the Maven widget and search functionality. (Privacy Policy)
    Marketing
    Google AdSenseThis is an ad network. (Privacy Policy)
    Google DoubleClickGoogle provides ad serving technology and runs an ad network. (Privacy Policy)
    Index ExchangeThis is an ad network. (Privacy Policy)
    SovrnThis is an ad network. (Privacy Policy)
    Facebook AdsThis is an ad network. (Privacy Policy)
    Amazon Unified Ad MarketplaceThis is an ad network. (Privacy Policy)
    AppNexusThis is an ad network. (Privacy Policy)
    OpenxThis is an ad network. (Privacy Policy)
    Rubicon ProjectThis is an ad network. (Privacy Policy)
    TripleLiftThis is an ad network. (Privacy Policy)
    Say MediaWe partner with Say Media to deliver ad campaigns on our sites. (Privacy Policy)
    Remarketing PixelsWe may use remarketing pixels from advertising networks such as Google AdWords, Bing Ads, and Facebook in order to advertise the HubPages Service to people that have visited our sites.
    Conversion Tracking PixelsWe may use conversion tracking pixels from advertising networks such as Google AdWords, Bing Ads, and Facebook in order to identify when an advertisement has successfully resulted in the desired action, such as signing up for the HubPages Service or publishing an article on the HubPages Service.
    Statistics
    Author Google AnalyticsThis is used to provide traffic data and reports to the authors of articles on the HubPages Service. (Privacy Policy)
    ComscoreComScore is a media measurement and analytics company providing marketing data and analytics to enterprises, media and advertising agencies, and publishers. Non-consent will result in ComScore only processing obfuscated personal data. (Privacy Policy)
    Amazon Tracking PixelSome articles display amazon products as part of the Amazon Affiliate program, this pixel provides traffic statistics for those products (Privacy Policy)
    ClickscoThis is a data management platform studying reader behavior (Privacy Policy)