Essential Libraries: Retrofit, Moshi, and Glide
In this chapter, we'll explore how to fetch dynamic content from a remote server. You'll learn about the libraries needed to retrieve and process this data.
By the end of this chapter, you'll be able to use Retrofit to fetch data from a network endpoint, parse JSON payloads into Kotlin data objects with Moshi, and load images into ImageView components using Glide.
Introduction
In the previous chapter, we learned about implementing navigation in our app. Now, we'll learn how to present dynamic content to users as they navigate.
Data can come from various sources. It can be hardcoded, but this has limitations. To change hardcoded data, we must release an app update. Some data, like currency rates or real-time weather, can't be hardcoded. Other data might become outdated, like terms of service.
In such cases, you typically fetch data from a server. A common architecture for this is the REST (Representational State Transfer) architecture. REST APIs rely on standard HTTP methods—GET, POST, PUT, DELETE, and PATCH—to retrieve and manipulate data.
To execute these HTTP methods, we can use Java's built-in HttpURLConnection class or libraries like OkHttp, which offers features like gzip compression and asynchronous calls. Retrofit, which we'll use, provides type safety for handling REST calls.
The most common data format is JSON (JavaScript Object Notation). JSON is a text-based data interchange format. When fetching JSON payloads, we receive a string. To convert this string into data objects, we can use libraries like Moshi.
Finally, we'll explore how to load images from the network. This allows us to provide up-to-date images and load the correct image for the user's device, keeping the APK size smaller.
Fetching Data from a Network Endpoint
For this section, we'll use TheCatAPI. To start, create a new project and grant internet permission in your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Next, add Retrofit to your project. Retrofit helps generate URLs and simplifies JSON decoding. Add the following to your app's build.gradle file:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
Now, define the contract for your API endpoint. For TheCatAPI's image search endpoint:
interface CatApiService {
@GET("images/search")
fun searchImages(
@Query("limit") limit: Int,
@Query("size") format: String
): Call<String>
}
Here, we define the endpoint using the @GET annotation. The return type is Call<String> for simplicity. We'll update this later to handle JSON parsing.
Now, create a Retrofit instance:
val retrofit = Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/v1/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
val catApiService = retrofit.create(CatApiService::class.java)
To execute a request, we use enqueue with a Callback:
val call = catApiService.searchImages(1, "full")
call.enqueue(object : Callback<String> {
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e("MainActivity", "Failed to get search results", t)
}
override fun onResponse(call: Call<String>, response: Response<String>) {
if (response.isSuccessful) {
// Handle successful response
} else {
// Handle error
}
}
})
Parsing JSON Responses
Now, let's parse the JSON response. We'll use Moshi for this. Add the Moshi converter to your build.gradle:
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
Define data classes to map the JSON response:
data class CatImageData(
@field:Json(name = "url") val imageUrl: String,
val breeds: List<CatBreedData>
)
data class CatBreedData(
val name: String,
val temperament: String
)
Update the Retrofit service to return a list of CatImageData:
@GET("images/search")
fun searchImages(
@Query("limit") limit: Int,
@Query("size") format: String
) : Call<List<CatImageData>>
Update the Retrofit instance to use Moshi:
val retrofit = Retrofit.Builder()
.baseUrl("https://api.thecatapi.com/v1/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
Update the callback to handle the parsed data:
override fun onResponse(
call: Call<List<CatImageData>>,
response: Response<List<CatImageData>>
) {
if (response.isSuccessful) {
val imageResults = response.body()
val firstImageUrl = imageResults?.firstOrNull()?.imageUrl ?: ""
// Use the URL
} else {
// Handle error
}
}
Loading Images from Remote URLs
To load images from URLs, we'll use Glide. Add Glide to your build.gradle:
implementation 'com.github.bumptech.glide:glide:4.11.0'
Define an ImageLoader interface:
interface ImageLoader {
fun loadImage(imageUrl: String, imageView: ImageView)
}
Implement it with Glide:
class GlideImageLoader(private val context: Context) : ImageLoader {
override fun loadImage(imageUrl: String, imageView: ImageView) {
Glide.with(context)
.load(imageUrl)
.centerCrop()
.into(imageView)
}
}
Add an ImageView to your layout and load the image:
if (!firstImageUrl.isBlank()) {
imageLoader.loadImage(firstImageUrl, profileImageView)
}
RecyclerView
In this chapter, you'll learn how to add lists and grids to your app and leverage the recycling capabilities of RecyclerView. You'll also learn how to handle user interactions with items and support different item view types.
By the end of this chapter, you'll have the skills to present interactive, rich lists of items.
Introduction
In the previous chapter, we learned how to fetch data from APIs, including lists of items and image URLs, and how to load images from URLs. Combining this knowledge with the ability to display lists of items is the goal of this chapter.
Historically, this was done using ListView or GridView. While these are still viable, they lack the robustness and flexibility of RecyclerView. RecyclerView coordinates creating, populating, and reusing views that represent items in a list.
To use RecyclerView, you need to be familiar with its two dependencies: the adapter (and through it, the view holder) and the layout manager. The adapter provides the content to display, and the layout manager tells RecyclerView how to arrange the items.
Adding RecyclerView to Your Layout
To add RecyclerView to your layout, add the following tag:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_sample" />
Populating RecyclerView
First, design your UI model:
data class CatUiModel(
val gender: Gender,
val breed: CatBreed,
val name: String,
val biography: String,
val imageUrl: String
)
Next, design the layout for each item (e.g., item\_cat.xml). Then, create a view holder:
class CatViewHolder(
containerView: View,
private val imageLoader: ImageLoader
) : RecyclerView.ViewHolder(containerView) {
fun bindData(catData: CatUiModel) {
imageLoader.loadImage(catData.imageUrl, catPhotoView)
// Bind other data to views
}
}
Now, implement the adapter:
class CatsAdapter(
private val layoutInflater: LayoutInflater,
private val imageLoader: ImageLoader
) : RecyclerView.Adapter<CatViewHolder>() {
private val catsData = mutableListOf<CatUiModel>()
fun setData(catsData: List<CatUiModel>) {
this.catsData.clear()
this.catsData.addAll(catsData)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatViewHolder {
val view = layoutInflater.inflate(R.layout.item_cat, parent, false)
return CatViewHolder(view, imageLoader)
}
override fun getItemCount() = catsData.size
override fun onBindViewHolder(holder: CatViewHolder, position: Int) {
holder.bindData(catsData[position])
}
}
Finally, set the adapter and layout manager in your activity:
recyclerView.adapter = catsAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
Responding to Clicks in RecyclerView
To handle item clicks, define an OnClickListener in your view holder:
interface OnClickListener {
fun onClick(catData: CatUiModel)
}
Pass this listener to the view holder and set a click listener on the item view:
containerView.setOnClickListener { onClickListener.onClick(catData) }
Define a similar listener in the adapter and pass it from the activity:
class CatsAdapter(
...
private val onClickListener: OnClickListener
) : RecyclerView.Adapter<CatViewHolder>() {
...
}
// In Activity
catsAdapter = CatsAdapter(layoutInflater, imageLoader, object : CatsAdapter.OnClickListener {
override fun onClick(catData: CatUiModel) {
// Handle click
}
})
Supporting Different Item Types
To support different item types (e.g., titles and cats), use a sealed class for your data model:
sealed class ListItemUiModel {
data class Title(val title: String) : ListItemUiModel()
data class Cat(val data: CatUiModel) : ListItemUiModel()
}
Override getItemViewType in the adapter:
override fun getItemViewType(position: Int) = when (listData[position]) {
is ListItemUiModel.Title -> VIEW_TYPE_TITLE
is ListItemUiModel.Cat -> VIEW_TYPE_CAT
}
Create different view holders for each type and inflate the appropriate layout in onCreateViewHolder.
Swiping to Delete Items
To enable swipe-to-delete, use ItemTouchHelper:
inner class SwipeToDeleteCallback :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
listItemsAdapter.removeItem(position)
}
}
Attach the ItemTouchHelper to your RecyclerView:
val itemTouchHelper = ItemTouchHelper(listItemsAdapter.swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
Interactively Adding Items
To add items, add a function to your adapter:
fun addItem(position: Int, item: ListItemUiModel) {
listData.add(position, item)
notifyItemInserted(position)
}
Add a button to your layout and set a click listener to add a new item:
addItemButton.setOnClickListener {
listItemsAdapter.addItem(1, ListItemUiModel.Cat(newCatData))
}