How to Make a Whatsapp-Like Gifs Picker using kotlin

Image for post
Image for post
Gifs Picker

The story begins, when I wanted to spice up my chat app making it appealing by adding some features to it, that’s when I challenged myself to do Gifs Picker like the one on whatsapp. From an indie developer point of view, my main concern was whether there are gif APIs out there, so I like-anyone-else googled it and It turned out that there is a lot of APIs not only for gifs but also for stickers and emojis. Here in this blog, I will show how I implemented one of these gif feeds into my app, but before that let’s explore what platforms offering such services. For those interested in the technical part you can skip it to First step.

By far, most of the social media apps are relying heavily on one or two of the gifs platforms, mainly, Tenor and GIPHY. Whatsapp for example is utilizing both of which in it. GIPHY as one of Facebook acquisitions, is the mainstream in the market. The platform offers also stickers and emojis to most of chatting apps. Tenor as GIPHY’s main competitor is also providing a third party keyboard app for searching and sharing gifs. Both services are providing their APIs and SDKs to integrate into any app, they are simple and well-documented, so I’ll choose either which for the rest of this article to integrate it into my app.

Other alternatives, Gyfcat and Imgur which are directly integrated into famous platforms such as Reddit, Skype, Microsoft Outlook and also Slack. What can I say about them other than they are good and simple with well-documented APIs which is what a developer need. Other platforms offering Gifs RESTful API are Tumblr and Imgflip, they deserve to be mentioned, though I did not explore them enough, but they offer the same as the others above. I think that is all you need to know about these Serives, now, let’s gets our hands dirty and do some coding implementing GIPHY API.

First, we need to signup to GIPHY and create an app, when prompt to choose either the SDK or API , choose API then hit next, give a name and a description for your app then hit done. An app will be created with its API key showing on your apps dashboard.

GIPHY Apps Dashboard

Now, that we have an API key for the app created, take note of it we will need it later, we shall move to the next step which is designing our app to add gifs picker functionality to it.

Second, we will make our gif picker in a way that mimics Whatsapp but a little bit different and that is by giving it a bottom sheet view behavior as the following

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:background="@color/white"
tools:context=".MainActivity">

<LinearLayout
android:id="@+id/user_input_layout_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/grey"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">


<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/input_tools_linear_lo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">

<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:background="@drawable/rounded_white"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">

<ImageView
android:id="@+id/emoji_iv"
android:layout_width="48dp"
android:layout_height="48dp"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_baseline_emoji_emotions_24" />

<EditText
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:hint="Type a message" />


<ImageView
android:id="@+id/gif_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:clickable="true"
android:focusable="true"
android:padding="10dp"
android:src="@drawable/ic_baseline_attach_file_24" />

</androidx.appcompat.widget.LinearLayoutCompat>

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="4dp"
android:background="@drawable/circle_bg"
android:padding="10dp"
android:src="@drawable/ic_baseline_mic_24" />

</androidx.appcompat.widget.LinearLayoutCompat>

<LinearLayout
android:id="@+id/lli"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tools_linear_lo">

<EditText
android:id="@+id/search_gif_edit_text"
android:layout_width="0dp"
android:layout_height="?actionBarSize"
android:layout_weight="1"
android:hint="search"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/white" />

<ImageView
android:id="@+id/remove_text_button"
android:layout_width="56dp"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:padding="12dp"
android:src="@drawable/ic_baseline_delete_forever_24" />
</LinearLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/gif_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"/>


</LinearLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

we have added an EditText view to take inserted search words from user and that is by setting a TextWatcher listener to it, as follows

val watcherCallback = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
Log.d(TAG, "beforeTextChanged: ")
}

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
Log.d(TAG, "onTextChanged: ")
}

override fun afterTextChanged(s: Editable?) {

if (s?.trim()?.isEmpty() == true) {

val call = getGifService.getTrendingGifs()
call?.enqueue(callback)

} else if (s.toString().trim() != "") {

val call = getGifService.getGifs("search", s.toString())
call?.enqueue(callback)
}

}

}

Here, I am directly calling GIPHY API whenever user insert a character ,it’s fine when there is no delay in network requests but it may show lagging behavior when internet connection is slow, we can solve this problem by using scheduled Runnables that fires after a specified time of the inserted character, say one second, if it is not followed by other character, you can read about it by googling “scheduling tasks with a delay”. As you may have notice that, we are using Retrofit to handle network request ,we’ll see how later.

Next, we also shall create an adapter for the gifs Recyclerview, here is a GifsAdapter in its simplest form

class GifsAdapter : RecyclerView.Adapter<GifsAdapter.VH>() {

private val TAG = "GifsAdapter"
val gifList:MutableList<String> = mutableListOf()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = DataBindingUtil.inflate<GifLiBinding>(LayoutInflater.from(parent.context),R.layout.gif_li,parent,false)
return VH(view)
}

override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(gifList[position])
}

override fun getItemCount(): Int {
return gifList.size
}

fun updateGifs(images: MutableList<String>) {
Log.d(TAG, "updateGifs: ${images.size}")
gifList.clear()
gifList.addAll(images)
notifyDataSetChanged()
}

inner class VH(binding: GifLiBinding): RecyclerView.ViewHolder(binding.root) {

val bindin = binding

fun bind(gifUrl: String) {

Log.d(TAG, "bind: show image$gifUrl")
Glide.with(bindin.gifIv.context)
.load(gifUrl)
.override(250,200)
.into(bindin.gifIv )
}

}

}

for the Recyclerview’s list item layout ,it’s just an imageView in linearlayout.

So far, we have set up chat app UI and it’s ready for data, to fetch data we will utilize Retrofit library to handle the networking for us, so add its dependency to your app Gradle file. For the converter, we shall use scalars library so add it too.

From GIPHY API documentation, we can see that the form of gifs url is as follows

“api.giphy.com/v1/gifs/trending”

And for stickers it is

“api.giphy.com/v1/stickers/trending”

both urls are followed by a required query parameter api_key , and here we shall insert the app’s API key that we got it from the First step.

api.giphy.com/v1/gifs/trending?api_key=D3x5C2pNDFzlzklDB4B3rMFY8IG10AeW

to search for a gif we use search Endpoint API which has the same structure but with trending path replaced by “search” word, followed by search query parameter “q” , see the following example as we searching for cat gifs

api.giphy.com/v1/gifs/search?api_key=D3x5C2pNDFzlzklDB4B3rMFY8IG10AeW&q=cat

Now that we know how the Endpoint APIs looks like, let’s create a RetrofitClientInstance class implementing GIPHY BASE URL “api.giphy.com”

public class RetrofitClientInstance {

private static Retrofit retrofit;
private static final String BASE_URL = "https://api.giphy.com";

public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(
ScalarsConverterFactory.create())
.build();
}
return retrofit;
}
}

We will need two GET methods to fetch the gifs urls, one for trending gifs which are shown when user first open the gifs picker and the other is for searched gifs through the EditText mentioned above.

interface GetService {
@GET("/v1/gifs/{search}? api_key=D3x5C2pNDFzlzklDB4B3rMFY8IG10AeW")
fun getGifs(
@Path("search") path: String,
@Query("q") q: String?
): Call<String>?
@GET("/v1/gifs/trending?api_key=D3x5C2pNDFzlzklDB4B3rMFY8IG10AeW")
fun getTrendingGifs():Call<String>?

}

To handle the resposne which is in json format we create the following basic callback

val callback = object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "onResponse: ${response.body()}")
try {
extractGifs(response.body())
} catch (e: Exception) {
e.printStackTrace()
}
}

override fun onFailure(call: Call<String>, t: Throwable) {
Log.d(TAG, "onFailure: ${t.message}")
}

}

the response body is a json object of three properties data, pagination, meta , the documentation is very detailed about this and providing everything about it, so I’ll leave it to you to explore, here I’m extracting preview url from the response which show a 2-seconds preview of that gif as in this extractGifs method

private fun extractGifs(body: String?) {
val jsonObject = JSONObject(body)
val array = jsonObject.getJSONArray("data")
val images = mutableListOf<String>()
for (i in 0 until array.length()) {

images.add(
array
.getJSONObject(i)
.getJSONObject("images")
.getJSONObject("preview_gif")
.getString("url")
)
}

gifAdapter.updateGifs(images)
}

That’s all we need to do to implement GIPHY API into our Chat app “GifsPicker” , as you may have noticed that we’re using Glide to show Gifs which is possible with this amazing library and I can not think we could achieve this goal without it, so here’s my appreciation for it.

Image for post
Image for post
Gifs Picker

Of course there is a lot to be done to make it better regarding networking, performance and memory usage reduction. The main goal of this tutorial was to show the basics of how to implement such Services to our apps to make them more attractive and enjoyable.

Developer for Android

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store