List view with Pagination using Jetpack Compose
Introduction: -
Jetpack Compose is Android’s modern toolkit for building native UI using composable functions. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs. Composable functions increase code reusability.
Benefits Of Jetpack Compose:-
- It’s fully written in Kotlin.
- User interfaces are broken up into small components called composable functions.
- Supports Material Design theming and components and animations from the start, allowing developers to focus on creating beautiful user interfaces that much quicker.
- You own the state. So just simply pass the state into the composable so that it can observe the state and update itself accordingly.
In this blog, we will learn:
- Building Lists with Jetpack compose
- API Call using Retrofit
- Jetpack compose Paging Library 3.0
It is very easy to create a list(with paging) in Jetpack compose, and even without handling any adapter for the item list.
Let’s begin!
- You need the latest android studio canary version. You can download it from here
- If you are new to Jetpack Compose I would recommend going through the official documentation first.
Dependency Setup:-
//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//okHttp
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
//Coil (Image Uploading)
implementation 'com.google.accompanist:accompanist-coil:0.12.0'
//Paging 3.0
implementation 'androidx.paging:paging-compose:1.0.0-alpha10'
Manifest:-
Add Internet permission in your manifest file.
<uses-permission android:name="android.permission.INTERNET"/>
Code Setup:-
Listview
The equivalent component to ListView
in Jetpack Compose is LazyColumn
for a vertical list and LazyRow
for a horizontal list. Compose provides a set of components which only compose and lay out items which are visible in the component’s viewport.
You can use it by formatting your data as a list and passing it with a @Composable
callback that emits the UI for a given item in the list.
@Composable
fun EmployeeItem(empData: User, onClick: () -> Unit) {
Card(
modifier = Modifier
.padding(bottom = 5.dp, top = 5.dp,
start = 5.dp, end = 5.dp)
.fillMaxWidth()
.clickable(onClick = onClick),
shape = RoundedCornerShape(15.dp),
elevation = 12.dp
) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
) {
Surface(
modifier = Modifier.size(130.dp),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colors.surface.copy(
alpha = 0.2f)
) {
val image = rememberCoilPainter(
request = empData.avatar,
fadeIn = true)
Image(
painter = image,
contentDescription = null,
modifier = Modifier
.height(100.dp)
.clip(shape = RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)
}
Column(
modifier = Modifier
.padding(start = 12.dp)
.align(Alignment.CenterVertically)
) {
Text(
text = empData.first_name,
fontWeight = FontWeight.Bold,
style = TextStyle(fontSize = 22.sp),
color = Color.Black
)
CompositionLocalProvider(
LocalContentAlpha provides ContentAlpha.medium
) {
Text(
text = empData.email,
style = typography.body2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 25.dp)
)
}
}
}
}
}
We need to pass two arguments in EmployeeItem
composable function: empData
: User data for list item.onClick
: Lambda function to execute when the list item is clicked.
API Call Using Retrofit
Now we have to create a data class to define how our data from the network
will look like and hold it temporarily.
data class UserResponse (
var page: Int,
var per_page: Int,
var total: Int,
var total_pages: Int,
var data: List<User>,
var support: Support
)
data class User(
var id: Int,
var email: String,
var first_name: String,
var last_name: String,
var avatar: String
)
data class Support(
var url: String,
var text: String
)
Now create an interface to define the endpoint of our api. The interface contains a method which when called will run the API using specified endpoints.
interface RetrofitService {
@GET("users")
suspend fun getUserList(@Query("page") page: Int) : UserResponse
}
This API call is of type GET
. This will return a call object containing data which is in the form of UserResponse
data class.
Next, we create a RetrofitClient class to call API using a retrofit.
object RetrofitClient {
private fun getClient(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://reqres.in/api/")
.client(getOkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: RetrofitService = getClient().create(RetrofitService::class.java)
private fun getLoggingIntercepter() =
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC) private fun getOkHttpClient() = OkHttpClient.Builder().addInterceptor(getLoggingIntercepter()).build()
}
We have used OKhttp
client for retrofit to make a call. Next, we create a retrofit builder object that will contain the base URL of the API, the Gson
Converter is used to convert our Json API response. Then we declared apiService
variable that will be used to connect the retrofit builder object with our interface and make one complete retrofit call.
Now we are going to use this RetrofitClient object to set and bind response data with our item list using pagination.
Pagination using Jetpack paging 3.0
When exposing large data sets through APIs, it needs to provide a mechanism to paginate the list of resources.
First of all, you need to implement the PagingSource
. To implement that you need to pass the type of the paging key and type of data to load.
class UserSource : PagingSource<Int, User>() {
override fun getRefreshKey(state: PagingState<Int, User>): Int?
{
return state.anchorPosition
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
return try {
val nextPage = params.key ?: 1
val userList = RetrofitClient.apiService.getUserList(page = nextPage)
LoadResult.Page(
data = userList.data,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (userList.data.isEmpty()) null else userList.page + 1
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
}
}
}
After implementing PagingSource we have to create Pager
in ViewModel and also have to specify the page size.
class EmployeeViewModel : ViewModel() {
val user: Flow<PagingData<User>> = Pager(PagingConfig(pageSize = 6)) {
UserSource()
}.flow.cachedIn(viewModelScope)
}
In Paging 3.0 we don’t need to handle or convert any data to survives the screen configuration changes. You can simply cache the API result. Using cachedIn(viewModelScope) we can cache the paging data into viewModel.
To notify change of paging data you can handle loading state as below using CombinedLoadState callback:
@Composable
fun UserList(modifier: Modifier = Modifier, viewModel: EmployeeViewModel, context: Context) {
UserInfoList(modifier, userList = viewModel.user, context)
}
@Composable
fun UserInfoList(modifier: Modifier, userList: Flow<PagingData<User>>, context: Context) {
val userListItems: LazyPagingItems<User> = userList.collectAsLazyPagingItems()
LazyColumn {
items(userListItems) { item ->
item?.let {
EmployeeItem(empData = it, onClick = {
Toast.makeText(context, item.id.toString(), Toast.LENGTH_SHORT).show()
})
}
}
userListItems.apply {
when {
loadState.refresh is LoadState.Loading -> {
//You can add modifier to manage load state when first time response page is loading
}
loadState.append is LoadState.Loading -> {
//You can add modifier to manage load state when next response page is loading
}
loadState.append is LoadState.Error -> {
//You can use modifier to show error message
}
}
}
}
}
Now to represent data to UI layer, you just have to call above UserList composable function from your Jetpack MainActivity’s setContent, pass viewModel and context in the parameter as below:
class MainActivity : ComponentActivity() {
private val employeeViewModel: EmployeeViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ListWithAPIAndPaginationTheme {
UserList(viewModel = employeeViewModel, context = this)
}
}
}
}
So yeah that’s it….you’re done with your listing with API call and pagination using Jetpack compose.
That’s it if you run this setup now it will produce the following output.
Conclusion:-
In this blog, we have learned list view in Jetpack Compose. To show a list of elements using the legacy way is by using Recyclerview. It’s a little complicated, tedious to implement and it’s the exact opposite of the Jetpack Compose and the declarative approach. After all, we only want to create a list with some elements inside.
With the current approach of pagination, you have to put a new element into your data set and force the RecyclerView to render it as a progress bar. With Compose, implementing pagination is really simple — it only takes a few lines of code!
Happy Learning :)
If you found this article helpful then please give multiple 👏 and also share with your friends. Thank you.