Image Slider with the indicator Using Jetpack compose

In this tutorial, we are going to learn about how to create an image slider with a dot indicator in jetpack compose. When we are working with XML to create an image slider we will use Viewpager.

But in jetpack compose we don’t have a viewpager for now. But, we have an Accompanist-Pager layout to do the same on jetpack compose.

let’s get detail in the Accompanist- Pager layouts.

Accompanist – Pager layouts

A library that provides paging layouts for Jetpack Compose. If you’ve used Android’s ViewPager before, it has similar properties.

Official Documentation – Guide – Accompanist (google.github.io)

The pager layouts are currently experimental and the APIs could change at any time. All of the APIs are marked with the @ExperimentalPagerApi annotation.

In our case, we are going to use HorizontalPager from the Accompanist pager library for the image slider effect.

HorizontalPager is a layout that lays out items in a horizontal row and allows the user to horizontally swipe between pages.

let’s get into the image slider example,

Image Slider With Dot Indicator Example

First, we need to add the Accompanist – Pager dependency in your build.gradle file.

implementation "com.google.accompanist:accompanist-pager:0.22.0-rc"

Also, in this project, we are going to use retrofit and coil to fetch the list of movies using API and display it.

implementation "io.coil-kt:coil-compose:1.4.0"
    // retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.3")

Setup Image Data

Already, I have explained the retrofit and coil in another tutorial. Please check the below link for the details.

Jetpack compose – Retrofit with Recyclerview – Howtodoandroid

For this example, I am going to use https://www.howtodoandroid.com/apis/movielist.json API to fetch the list of movies. The API response will be,

[{
            "category": "Latest",
            "imageUrl": "https://www.howtodoandroid.com/images/coco.jpg",
            "name": "Coco",
            "desc": "Coco is a 2017 American 3D computer-animated musical fantasy adventure film produced by Pixar"
        },
        {
            "category": "Latest",
            "imageUrl": "https://www.howtodoandroid.com/images/terminator_2.jpg",
            "name": "Terminator 2: Judgment Day 3D",
            "desc": "Similar to Cameron's Titanic 3D, Lightstorm Entertainment oversaw the work on the 3D version of Terminator 2, which took nearly a year to finish."
        },
        {
            "category": "Latest",
            "imageUrl": "https://www.howtodoandroid.com/images/dunkirk.jpg",
            "name": "Dunkirk",
            "desc": "Dunkirk is a 2017 war film written, directed, and co-produced by Christopher Nolan that depicts the Dunkirk evacuation of World War II. "
        }
    ]

Create an API service and Model class to get all the movies.

Data.kt

data class Movies(val name: String, val category: String, val imageUrl: String, val desc: String)

ApiService.kt

interface ApiService {

        @GET("movielist.json")
        suspend fun getAllMovies(): List<Movies>

        companion object {
            var apiService: ApiService? = null
            fun getInstance(): ApiService {
                if (apiService == null) {
                    val logging = HttpLoggingInterceptor()
                    logging.level = HttpLoggingInterceptor.Level.BASIC
                    apiService = Retrofit.Builder()
                        .baseUrl("https://www.howtodoandroid.com/apis/")
                        .addConverterFactory(GsonConverterFactory.create())
                        .client(OkHttpClient.Builder().addInterceptor(logging).build())
                        .build().create(ApiService::class.java)
                }
                return apiService!!
            }
        }

    }

Also, create a ViewModel class to call the ApiService.

class MainViewModel: ViewModel() {

        var movieListResponse: List<Movies> by mutableStateOf(listOf())

        fun getAllMovies() {
            viewModelScope.launch {

                try {
                    movieListResponse = ApiService.getInstance().getAllMovies()

                } catch (e: Exception) {
                    Log.e("Error",e.toString())
                }
            }

        }

    }

Create Image Slider

we write HorizontalPager logic for its page changes and content inside each page. In our case, we are showing one image only.

First, Creates a PagerState that is remembered across compositions.

val state = rememberPagerState()

Next, create HorizontalPager(state, count) that gets the pager state and pager size.

HorizontalPager(
            state = state,
            count = 3
        ) { page ->

    }

Finally, design the page inside the HorizontalPager.

Check the below link to learn more about creating views and layouts in jetpack compose.

Getting started with jetpack compose – Basic components (howtodoandroid.com)

Getting started with jetpack compose – Layouts – Howtodoandroid

val state = rememberPagerState()
    val imageUrl =
            remember { mutableStateOf("") }
        HorizontalPager(
            state = state,
            count = viewModel.movieListResponse.size, modifier = Modifier
                .height(200.dp)
                .fillMaxWidth()
        ) { page ->
            imageUrl.value = viewModel.movieListResponse[page].imageUrl

            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Top,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Box(contentAlignment = Alignment.BottomCenter) {

                    val painter = rememberImagePainter(data = imageUrl.value, builder = {
                        placeholder(R.drawable.placeholder)
                        scale(Scale.FILL)
                    })
                    Image(
                        painter = painter, contentDescription = "", Modifier
                            .padding(8.dp).clip(RoundedCornerShape(10.dp))
                            .fillMaxSize(), contentScale = ContentScale.Crop
                    )

                    Text(
                        text = viewModel.movieListResponse[page].name,
                        Modifier
                            .fillMaxWidth()
                            .height(60.dp)
                            .padding(8.dp)
                            .background(Color.LightGray.copy(alpha = 0.60F))
                            .padding(8.dp),
                        textAlign = TextAlign.Start,
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Medium
                    )
                }
            }
        }

The preview on the image slider will be like the below,

Sliding Image

Create Dot Indicator

For the dots indicator, we can create one composable function and call it below the HorizontalPager as we want to show the dots below sliding pages.

  1. We created a lazy row and passed the total number of dots we need to show.
  2. We checked each index if it was the selected page index and changed the color of the dot accordingly.
@Composable
   fun DotsIndicator(
   totalDots: Int,
   selectedIndex: Int
   ) {

        LazyRow(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight(), horizontalArrangement = Arrangement.Center
        ) {

            items(totalDots) { index ->
                if (index == selectedIndex) {
                    Box(
                        modifier = Modifier
                            .size(10.dp)
                            .clip(CircleShape)
                            .background(color = Color.DarkGray)
                    )
                } else {
                    Box(
                        modifier = Modifier
                            .size(10.dp)
                            .clip(CircleShape)
                            .background(color = Color.LightGray)
                    )
                }

                if (index != totalDots - 1) {
                    Spacer(modifier = Modifier.padding(horizontal = 2.dp))
                }
            }
        }
   }

The preview of the dot indicator will be like this,

Dot Indicator

Autoplay Images in Image Slider

Now we have created an Image slider with a dot indicator, the next step is to auto-play the images at a particular time interval. to do this we need to use LaunchEffect(key1= pagerState) to animate the images every 3 seconds.

If you want to jump to a specific page, you either call call pagerState.scrollToPage(index) or pagerState.animateScrollToPage(index) method in a CoroutineScope.

 LaunchedEffect(key1 = state.currentPage) {
        delay(3000)
        var newPosition = state.currentPage + 1
        if (newPosition > mainViewModel.movieListResponse.size - 1) newPosition = 0
        // scrolling to the new position.
        state.animateScrollToPage(newPosition)
    }

final MainActivity.kt

package com.velmurugan.imagesliderwithindicator

    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.viewModels
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.background
    import androidx.compose.foundation.layout.*
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.LazyRow
    import androidx.compose.foundation.lazy.items
    import androidx.compose.foundation.shape.CircleShape
    import androidx.compose.foundation.shape.RoundedCornerShape
    import androidx.compose.material.Card
    import androidx.compose.material.MaterialTheme
    import androidx.compose.material.Surface
    import androidx.compose.material.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.draw.clip
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.layout.ContentScale
    import androidx.compose.ui.text.font.FontWeight
    import androidx.compose.ui.text.style.TextAlign
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.compose.ui.unit.dp
    import androidx.compose.ui.unit.sp
    import androidx.lifecycle.ViewModel
    import coil.compose.rememberImagePainter
    import coil.size.Scale
    import com.google.accompanist.pager.ExperimentalPagerApi
    import com.google.accompanist.pager.HorizontalPager
    import com.google.accompanist.pager.PagerState
    import com.google.accompanist.pager.rememberPagerState
    import com.velmurugan.imagesliderwithindicator.ui.theme.ImageSliderWithIndicatorTheme
    import kotlinx.coroutines.delay

    class MainActivity : ComponentActivity() {
        @OptIn(ExperimentalPagerApi::class)
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ImageSliderWithIndicatorTheme {
                    // A surface container using the 'background' color from the theme
                    val mainViewModel by viewModels<MainViewModel>()

                    mainViewModel.getAllMovies()

                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colors.background
                    ) {

                        val state = rememberPagerState()
                        Column {
                            SliderView(state, mainViewModel)
                            Spacer(modifier = Modifier.padding(4.dp))
                            DotsIndicator(
                                totalDots = mainViewModel.movieListResponse.size,
                                selectedIndex = state.currentPage
                            )

                            LazyColumn() {
                                items(mainViewModel.movieListResponse) { item ->
                                    MovieCard(movie = item)
                                }
                            }
                        }
                        LaunchedEffect(key1 = state.currentPage) {
                            delay(3000)
                            var newPosition = state.currentPage + 1
                            if (newPosition > mainViewModel.movieListResponse.size - 1) newPosition = 0
                            // scrolling to the new position.
                            state.animateScrollToPage(newPosition)
                        }
                    }
                }
            }
        }
    }

    @OptIn(ExperimentalPagerApi::class)
    @Composable
    fun SliderView(state: PagerState, viewModel: MainViewModel) {

        val imageUrl =
            remember { mutableStateOf("") }
        HorizontalPager(
            state = state,
            count = viewModel.movieListResponse.size, modifier = Modifier
                .height(200.dp)
                .fillMaxWidth()
        ) { page ->
            imageUrl.value = viewModel.movieListResponse[page].imageUrl

            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Top,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Box(contentAlignment = Alignment.BottomCenter) {

                    val painter = rememberImagePainter(data = imageUrl.value, builder = {
                        placeholder(R.drawable.placeholder)
                        scale(Scale.FILL)
                    })
                    Image(
                        painter = painter, contentDescription = "", Modifier
                            .padding(8.dp).clip(RoundedCornerShape(10.dp))
                            .fillMaxSize(), contentScale = ContentScale.Crop
                    )

                    Text(
                        text = viewModel.movieListResponse[page].name,
                        Modifier
                            .fillMaxWidth()
                            .height(60.dp)
                            .padding(8.dp)
                            .background(Color.LightGray.copy(alpha = 0.60F))
                            .padding(8.dp),
                        textAlign = TextAlign.Start,
                        fontSize = 18.sp,
                        fontWeight = FontWeight.Medium
                    )
                }

            }

        }
    }

    @Composable
    fun MovieCard(movie: Movies) {

        Card(modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)) {
            Text(text = movie.name, Modifier.fillMaxWidth())

        }

    }

    @Composable
    fun DotsIndicator(
        totalDots: Int,
        selectedIndex: Int
    ) {

        LazyRow(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight(), horizontalArrangement = Arrangement.Center
        ) {

            items(totalDots) { index ->
                if (index == selectedIndex) {
                    Box(
                        modifier = Modifier
                            .size(10.dp)
                            .clip(CircleShape)
                            .background(color = Color.DarkGray)
                    )
                } else {
                    Box(
                        modifier = Modifier
                            .size(10.dp)
                            .clip(CircleShape)
                            .background(color = Color.LightGray)
                    )
                }

                if (index != totalDots - 1) {
                    Spacer(modifier = Modifier.padding(horizontal = 2.dp))
                }
            }
        }
    }

    }

Conclusion

Thanks for reading. Creating an image slider is very easy using jetpack compose compare with viewpager and XML. Please try it yourself and let me know your feedback in the comments.

you can download this example on Github.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *


Latest Posts