January 1, 2022 5:31 pm

valokafor

In this post, we will implement onboarding screens with Jetpack Compose. Onboarding screens can also be called intro screens or walkthrough screens, they all mean the same thing. These are the first screens that are presented to first-time users of your app. It’s possible that they have forgotten why they downloaded your app and the onboarding screens are a good way to remind the user of the core features of your app. Onboarding is a virtual unboxing experience that helps users get started with an app according to the Material Design guide and onboarding screens are mostly limited to 3 screens.

Here are the onboarding screens and flow we will be creating in this post.

Sample Onboarding Screen Created with Jetpack Compose

Here are the acceptance criteria for this onboarding screen

  • There shall be three screens in the onboarding flow
  • Each screen shall have
    • Title
    • Description
    • Image 
    • Mode indicator indicating current position of the onboarding screen
  • First and second screen shall have a skip and next button
  • Second and third screen shall have a previous button
  • Last screen shall have a Get Started button
  • The screens shall be swipable
  • The screens shall be navigable using the next and previous buttons
  • Clicking the skip button or the Get Started button should indicate the end of the flow 
    • Note that Jetpack Compose Navigation is required to navigate out of the Onboarding flow and I will cover that in the next post.

Follow the steps below to create an Onboarding screen using Jetpack Compose.

Step 1 – Setup

Download the Pronto Login app and switch to the onboarding_start branch. If you are not able to download the branch and/or you are starting from a different project, ensure that in addition to the compose dependencies being added, that you also add the following Google Accomplist dependencies.

implementation("com.google.accompanist:accompanist-pager:0.14.0") // Pager
implementation("com.google.accompanist:accompanist-pager-indicators:0.14.0") // Pager Indicators
implementation("com.google.accompanist:accompanist-insets:0.21.2-beta")

The Accompanist libraries are a collection of libraries that aim to supplement Jetpack Compose with features that are commonly required by developers but not yet available.

Step 2 – Add Data Class

This step is not required but it makes the code more readable. Add a data class that represents the data in each Onboarding screen. Add a package named onboarding to the root of the project and then add a data class named OnboardingData and below is the content of this class:

data class OnboardingData(
    val name: String,
    val title: String,
    val description: String,
    val imageId: Int,
)

Step 3 – Add Onboarding Data Provider

Next, create a data provider class that provides the data for the Onboarding screens. Add a Kotlin object file named OnboardingDataProvider to the onboarding package and in this class, you will return the OnboardingData that corresponds to the number of onboarding screens. Here is an example

object OnboardingDataProvider {
    fun getOnboardingScreensData(context: Context): List<OnboardingData> {
        val screens = mutableListOf<OnboardingData>()
        screens.add( OnboardingData(
            name = "Onboarding Screen 1",
            title = context.getString(R.string.on_boarding_value_header_1),
            description = context.getString(R.string.on_boarding_value_1),
            imageId = R.drawable.onboarding_illustration_1,
        ))
        screens.add( OnboardingData(
            name = "Onboarding Screen 2",
            title = context.getString(R.string.on_boarding_value_header_2),
            description = context.getString(R.string.on_boarding_value_2),
            imageId = R.drawable.onboarding_illustration_2,
        ))
        screens.add( OnboardingData(
            name = "Onboarding Screen 3",
            title = context.getString(R.string.on_boarding_value_header_3),
            description = context.getString(R.string.on_boarding_value_3),
            imageId = R.drawable.onboarding_illustration_3,
        ))
        return screens
    }
}

Step 4 – Compose Individual Onboarding Screen Page

The acceptance criteria state that each onboarding screen should have an image, a title, and a description.  We can implement this requirement with Image composable and two Text composable inside a Column that is housed in a Box. You can think of these as a vertical Linear Layout contained in a FrameLayout. Here is a screenshot highlighting the composable on each screen.

Onboarding screen with the Composables Highlighted.

Add a file named OnboardingPage to the onboarding package and inside add a function named OnboardingPage that accepts an onboarding data instance. It should look like this.

@Composable
fun OnboardingPage(data: OnboardingData) {
    Box(contentAlignment = Alignment.TopEnd) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Spacer(modifier = Modifier.height(50.dp))
            Image(
                painterResource(id = data.imageId),
                contentDescription = data.name,
                contentScale = ContentScale.Inside,
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight(0.6f)
            )

            Text(
                text = data.title,
                color = Color.Black,
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h5
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = data.description,
                style = MaterialTheme.typography.body1,
                lineHeight = 22.sp,
            )
        }
    }
}

Step 5 – Start Composing the Onboarding Screens

Now we want to bring everything together to create a scrollable onboarding screen. Add a class fule called OnboardingScreen.kt to the onboarding package. Inside this class add a function called ShowOnBoardingScreens() that accepts a callback called onboarding complete. It should look like this.

@Composable
fun ShowOnBoardingScreens(onboardingComplete: () -> Unit) {
}

Next, we need to obtain the list of the onboarding data and hold it in a variable called onBoardingScreens as shown below.

@Composable
fun ShowOnBoardingScreens(onboardingComplete: () -> Unit) {
   val context = LocalContext.current
   val onBoardingScreens: List<OnboardingData> =
       OnboardingDataProvider.getOnboardingScreens(context)
}

Next, we need to add a PagerState to control the horizontal scrolling on the onboarding screens as well as a coroutine scope as shown in the next code snippet. Here is the updated function. The PagerState is an experimental replacement for ViewPager so it requires an ExperimentalPagerApi annotation.

@ExperimentalPagerApi
@Composable
fun ShowOnBoardingScreens(onboardingComplete: () -> Unit) {
   val context = LocalContext.current
   val onBoardingScreens: List<OnboardingData> =
       OnboardingDataProvider.getOnboardingScreens(context)

   val pagerState: PagerState = run {
       rememberPagerState(pageCount = onBoardingScreens.size)
   }
   val scope = rememberCoroutineScope()
}

Step 6 – Add a Scaffold

We will be using a Scaffold to sort of “layout” the screen. A Scaffold has different slots for things like AppBar which we do not need, we will only be using the content slot which will contain a single Box composable. The Scaffold should be modified to fill max-width and the Box should be modified to fill max-size and should look like this:

Scaffold(modifier = Modifier.fillMaxWidth()) { innerPadding ->
   Box(
       modifier = Modifier
           .fillMaxSize()
           .padding(all = 16.dp),
   ) {
      
   }
}

Step 7 – Add the Horizontal Pager

Add this code snippet to the Box-scope

HorizontalPager(state = pagerState) { page ->
   OnboardingPage(onBoardingScreens[page])
}

The HorizontalPager allows for left and right scrolling of items, it accepts the PagerSate we added in Step 5 and the list of the OnboardingData we created. It iterates through the list on Onboarding Data and calls the OnboardingPage which composes the screen. If you update the MainActivity at this time to call the onboarding screen as shown in the next code snippet.

class MainActivity : ComponentActivity() {
    @ExperimentalPagerApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ShowOnBoardingScreens {
                //Handle onboarding complete
                Log.i("MainActivity", "Onboarding flow complete")
            }
        }
    }
}

Then the onboarding screen should appear as shown in the next screenshot.

Unfinished Onboarding Screen Created with Jetpack Compose

At this point, your ShowOnboarding() function should look like this:

@ExperimentalPagerApi
@Composable
fun ShowOnBoardingScreens(onboardingComplete: () -> Unit) {
   val context = LocalContext.current
   val onBoardingScreens: List<OnboardingData> =
       OnboardingDataProvider.getOnboardingScreens(context)

   val pagerState: PagerState = run {
       rememberPagerState(pageCount = onBoardingScreens.size)
   }
   val scope = rememberCoroutineScope()

   Scaffold(modifier = Modifier.fillMaxWidth()) { innerPadding ->
       Box(
           modifier = Modifier
               .fillMaxSize()
               .padding(all = 16.dp),
       ) {
           HorizontalPager(state = pagerState) { page ->
               OnboardingPage(onBoardingScreens[page])
           }
       }
   }
}

Step 8 – Add the Skip Button

The design of the onboarding screen includes a Skip button. We will implement this Skip button with a clickable Text composable, the Skip button is sitting at the top of the screen so we will implement it with a Row that is anchored to the top of the screen as shown in the next code snippet. Add this snippet below the HorizontalPager.

Row(
   modifier = Modifier.align(Alignment.TopStart),
   verticalAlignment = Alignment.CenterVertically
) {
   Spacer(modifier = Modifier.weight(1f))
   CapsText(
       text = stringResource(id = R.string.skip),
       style = MaterialTheme.typography.h6,
       modifier = Modifier
           .padding(vertical = 16.dp)
           .clickable(
               enabled = true,
               onClick = onboardingComplete)
   )
}

Step 9 – Add Next, Prev and Getting Started Buttons

Here are the acceptance criteria for this onboarding screen and the completed tasks are crossed out

  1. There shall be three screens in the onboarding flow
  2. Each screen shall have
    1. Title
    2. Description
    3. Image 
    4. Mode indicator indicating current position of the onboarding screen
  3. First and second screen shall have a skip and next button
  4. Second and third screen shall have a previous button
  5. Last screen shall have a Get Started button
  6. The screens shall be swipable
  7. The screens shall be navigable using the next and previous buttons
  8. Clicking the skip button or the Get Started button should indicate the end of the flow 
    1. Note that Jetpack Compose Navigation is required to navigate out of the Onboarding flow and I will cover that in the next post.

The remaining tasks can be accomplished by adding a single Row and inside this Row, we use some if statements to decide what gets displayed. Here is the redacted version of this Row.

Row(
   horizontalArrangement = Arrangement.SpaceBetween,
   verticalAlignment = Alignment.CenterVertically,
   modifier = Modifier
       .fillMaxWidth()
       .align(Alignment.BottomCenter)
       .padding(bottom = 16.dp),
   content = {
       if (pagerState.currentPage != 0) {
           //Show Prev Arrow
       }
       //Always show position indicator dots
       Row(content = {
           onBoardingScreens.forEachIndexed { index, _ ->
               OnboardPagerSlide(
                   selected = index == pagerState.currentPage,
                   MaterialTheme.colors.primary,
               )
           }
       })

       if (pagerState.currentPage != onBoardingScreens.size - 1) {
           //Show Next Arrow
       } else {
           //Show Get Started Button.
       }
   }
)

You will need to add another Composable function to handle the highlighting of the position indicator dot colors. Add the following function to your OnBoardingScreen.kt file.

@Composable
fun OnboardPagerSlide(selected: Boolean, color: Color) {
   Box(
       modifier = Modifier
           .padding(end = 8.dp)
           .size(10.dp)
           .clip(CircleShape)
           .background(if (selected) color else Color.Gray)
   )
}

Conclusion

In this post, I have shown you how to create a standard onboarding screen with Jetpack compose. We utilized the HorizontalPager and PagerState libraries from Google’s Accompanist libraries. The click handler passed to the ShowOnBoardingScreens()simply prints a log statement. In the next post, I will demo how to transition from the onboarding screens to another screen when we look at the Navigation component.

Here is the full code and the complete source code can be found in the onboarding_complete branch of the ProntoLogin source code.

@ExperimentalPagerApi
@Composable
fun ShowOnBoardingScreens(onboardingComplete: () -> Unit) {
    val context = LocalContext.current
    val onBoardingScreens: List<OnboardingData> =
        OnboardingDataProvider.getOnboardingScreens(context)
    val pagerState: PagerState = run {
        rememberPagerState(pageCount = onBoardingScreens.size)
    }
    val scope = rememberCoroutineScope()
    ProntoLoginTheme() {
        Scaffold(modifier = Modifier.fillMaxWidth()) { innerPadding ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(all = 16.dp),
            ) {
                Row(
                    modifier = Modifier.align(Alignment.TopStart),
                    verticalAlignment = Alignment.CenterVertically
                ) {

                    Spacer(modifier = Modifier
                        .weight(1f)
                        .width(4.dp))

                    if (pagerState.currentPage != onBoardingScreens.size - 1) {
                        CapsText(
                            text = stringResource(id = R.string.skip),
                            style = MaterialTheme.typography.h6,
                            modifier = Modifier
                                .padding(vertical = 16.dp)
                                .clickable(
                                    enabled = true,
                                    onClick = onboardingComplete)
                        )
                    }
                }

                HorizontalPager(state = pagerState) { page ->
                    OnboardingPage(onBoardingScreens[page])
                }

                Row(
                    horizontalArrangement = Arrangement.SpaceBetween,
                    verticalAlignment = Alignment.CenterVertically,
                    modifier = Modifier
                        .fillMaxWidth()
                        .align(Alignment.BottomCenter)
                        .padding(bottom = 16.dp),
                    content = {
                        if (pagerState.currentPage != 0)
                            Row(verticalAlignment = Alignment.CenterVertically) {
                                Icon(
                                    Icons.Filled.ArrowBack,
                                    stringResource(id = R.string.content_description_left_arrow)
                                )
                                Spacer(modifier = Modifier.width(4.dp))
                                CapsText(
                                    modifier = Modifier.clickable {
                                        if (pagerState.currentPage != 4) {
                                            scope.launch {
                                                pagerState.scrollToPage(pagerState.currentPage - 1)
                                            }
                                        }
                                    },
                                    text = stringResource(id = R.string.prev),
                                    style = MaterialTheme.typography.subtitle1,
                                )
                            }

                        Row(content = {
                            onBoardingScreens.forEachIndexed { index, _ ->
                                OnboardPagerSlide(
                                    selected = index == pagerState.currentPage,
                                    MaterialTheme.colors.primary,
                                )
                            }
                        }
                        )

                        if (pagerState.currentPage != onBoardingScreens.size - 1)
                            Row(verticalAlignment = Alignment.CenterVertically) {
                                CapsText(
                                    modifier = Modifier.clickable {
                                        if (pagerState.currentPage != onBoardingScreens.size - 1) {
                                            scope.launch {
                                                pagerState.scrollToPage(pagerState.currentPage + 1)
                                            }
                                        }
                                    },
                                    text = stringResource(id = R.string.next),
                                    color = MaterialTheme.colors.primary,
                                    style = MaterialTheme.typography.subtitle1
                                )
                                Spacer(modifier = Modifier.width(4.dp))
                                Icon(
                                    Icons.Filled.ArrowForward,
                                    stringResource(id = R.string.content_description_right_arrow)
                                )
                            } else {
                            Button(
                                modifier = Modifier
                                    .padding(horizontal = 16.dp),
                                onClick = onboardingComplete,
                                shape = RoundedCornerShape(8.dp),
                                elevation = ButtonDefaults.elevation(0.dp),
                                colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary),
                                enabled = true,
                            ) {
                                Text(
                                    modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
                                    text = "Get Started",
                                    color = Color.White,
                                    style = MaterialTheme.typography.subtitle1
                                )
                            }
                        }
                    }
                )
            }
        }
    }
}
About the Author

I am a Software Engineer & Entrepreneur. I create remarkable apps for my portfolio, employer and clients.

Leave a Reply

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}