Android Animation in Kotlin: Onboarding screen with Lottie Animation, Navigation Component and ViewPager2
Tags: android, kotlin, lottie, navigation
Published: 2020-11-07 20:17:44

Animation in android is a way to make your app lively while passing information to the user. There are different APIs available in android you can use to create animations read more about it [here](https://developer.android.com/training/animation/overview). For this post we will be using a library called [Lottie](https://airbnb.io/lottie/#/) which makes it easier to add animations to your application. What you will learn

* Creating an onboarding screen 
* Using Navigation Component
* Animations with Lottie

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/orinbfmutryuouad2f5e.gif)

Onboarding screen is mainly used to explain the features of an app using images, text or animations. To begin add the following dependencies to your app module build.gradle file 
```
//navigation component
Implementation "androidx.navigation:navigation-fragment-ktx:2.3.1"
implementation "androidx.navigation:navigation-ui-ktx:2.3.1"
//viewpager2
implementation "androidx.viewpager2:viewpager2:1.0.0"
//indicator
implementation 'me.relex:circleindicator:2.1.4'
//lottie
implementation "com.airbnb.android:lottie:3.4.1"
//datastore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha02"
```
We need to enable safe args to ensure type-safety by adding the plugin to gradle. Add the following to the top level build.gradle file in the dependencies block
```
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1"
```
then apply the plugin in the app module build.gradle file 
```
apply plugin: 'androidx.navigation.safeargs.kotlin'
```

We will be navigating into the onboarding screen from the splash screen. 
The flow of navigation is Splash screen -> Onboarding Screen -> Main Page. Following the single activity principle the project will have only one activity(Main Activity) and three(3) fragments.

* Create a new project with an empty template
* Right click on res folder and select New -> Android Resource File
* Enter a name for your navigation graph file, select Navigation as Resource type and click ok

nav_graph.xml

```
<navigation
    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:id="@+id/nav_graph"
    app:startDestination="@id/splashFragment">

    <fragment
        android:id="@+id/splashFragment"
        android:name="com.wellnesscity.health.ui.intro.SplashFragment"
        android:label="fragment_splash"
        tools:layout="@layout/fragment_splash" >
        <action
            android:id="@+id/action_splashFragment_to_welcomeFragment"
            app:destination="@id/welcomeFragment"
            app:popUpTo="@id/splashFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_splashFragment_to_onboardingFragment"
            app:destination="@id/onboardingFragment"
            app:popUpTo="@id/splashFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/onboardingFragment"
        android:name="com.wellnesscity.health.ui.intro.OnboardingFragment"
        android:label="fragment_onboarding"
        tools:layout="@layout/fragment_onboarding" >
        <action
            android:id="@+id/action_onboardingFragment_to_welcomeFragment"
            app:destination="@id/welcomeFragment"
            app:popUpTo="@id/onboardingFragment"
            app:popUpToInclusive="true" />
    </fragment>
    <fragment
        android:id="@+id/welcomeFragment"
        android:name="com.wellnesscity.health.ui.welcome.WelcomeFragment"
        android:label="fragment_welcome"
        tools:layout="@layout/fragment_welcome" >
    </fragment>

</navigation>
```
The nav_graph contains splash fragment which is the start destination, onboarding fragment and finally the welcome fragment. The first fragment added is set as the start destination which you can change later if need be by changing the id in the `app:startDestination` which is an attribute in the `<navigation>` tag to a different fragment id that you choose. 

* To add the fragments; in the design view at the top left corner click on the add button to see a list of fragments to choose from. 
* To add an action that connects the SplashFragment to the OnboardingFragment; hover on the SplashFragment and drag the circular connection point that shows up to the OnboardingFragment
* Repeat same for OnboardingFragment to WelcomeFragment
* The action tag represents possible path to a destination
* The `app:destination` contains the id to the destination fragment
* The `app:popUpTo` contains the id to the fragment you want directly in your back stack . 
* The `app:popUpToInclusive="true"` indicates that the popUpTo fragment should also be removed from the back stack

Connecting the nav_graph to the MainActivity

activity_main.xml
```
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".ui.activity.MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:navGraph="@navigation/nav_graph"
        app:defaultNavHost="true" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
* The `android:name` attribute indicates the type of navigation been used
* The `app:navGraph` attribute connects the layout to a navigation graph
* The `app:defaultNavHost="true"` intercepts the system back button

The nav-graph is all setup, attached to the MainActivity and ready for use

fragment_splash.xml
```
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context=".ui.intro.SplashFragment">

    <!-- TODO: Update blank fragment layout -->
    <ImageView
        android:id="@+id/splash_iv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srcCompat="@drawable/splash_screen" />

</FrameLayout>
```

## N:B
Dagger Hilt is used as a dependency injection library in the full project and you will be seeing some of its annotations.

SplashFragment.kt
```
@AndroidEntryPoint
class SplashFragment : Fragment() {
    private var binding: FragmentSplashBinding? = null
    @Inject lateinit var prefs:DataStore<Preferences>
    private val handler = Handler()
    private val runnable = Runnable {

//saves the onboarding screen to datastore the first its viewed and ensures it only shows up ones after installation
        lifecycleScope.launch {
        prefs.data.collectLatest {
            if (it[preferencesKey<Boolean>("onBoard")] == true)
                requireView().findNavController()
                    .navigate(SplashFragmentDirections.actionSplashFragmentToWelcomeFragment())
            else
                requireView().findNavController()
                    .navigate(SplashFragmentDirections.actionSplashFragmentToOnboardingFragment())
        }
    }}

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentSplashBinding.inflate(layoutInflater)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        requireActivity().window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN)
    }

    override fun onResume() {
        super.onResume()
        handler.postDelayed(runnable,3000)
    }

    override fun onPause() {
        super.onPause()
        handler.removeCallbacks(runnable)
    }
}
```
In the SplashFragment we check if the onboarding has been displayed before, if yes then we navigate to the welcome screen if not we navigate into the  OnboardingFragment because we want the onboarding to be displayed only when the app is first installed. To achieve this we used the preference datastore to save a boolean value to true on first installation.

The OnboardingFragment contains three item describing the content of the app, these items will be displayed with ViewPager2 and a RecyclerAdapter. You can either create your own animations with [after effect]( https://airbnb.io/lottie/#/after-effects) and export as a json file or choose from [lottie website](https://lottiefiles.com/popular)

To add the json files to the asset folder;
* Right click on your app folder
* select New->folder->AssetsFolder
* copy and paste them in the asset folder

slide_item_layout.xml
```
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="15dp">

    <com.airbnb.lottie.LottieAnimationView
        android:id="@+id/imageSlideIcon"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        app:layout_constraintTop_toTopOf="parent"
        app:lottie_autoPlay="true"
        app:lottie_fileName="diet.json"
        app:lottie_loop="true" />

    <TextView
        android:id="@+id/textTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Title Text"
        android:textColor="@color/colorPrimary"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@id/imageSlideIcon" />

    <TextView
        android:id="@+id/textDescription"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginEnd="10dp"
        android:gravity="center"
        android:text="Description Text"
        android:textColor="@color/colorTextSecondary"
        android:textSize="15sp"
        app:layout_constraintTop_toBottomOf="@id/textTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
Lottie files can be referenced from two folders: raw and asset folder and for this post the json files are in the later. Notice the Lottie view widget in the item layout above;

* The `app:lottie_autoPlay="true"` starts the animation as soon the screen is in view
* The `app:lottie_fileName="diet.json"` attribute is used to add file name when the json file is in asset folder
* The `app:lottie_loop="true"` attribute keeps the animation active until user exits the screen

IntroSlide
```
data class IntroSlide (
    val title: String,
    val description: String,
    val icon: String
)
```
The above model class is used to represent each item present in the onboarding page 

IntroSliderAdapter.kt
```
class IntroSliderAdapter(private val introSlides: List<IntroSlide>)
    : RecyclerView.Adapter<IntroSliderAdapter.IntroSlideViewHolder>(){
//for adding text to speech listener in the onboarding fragment
    var onTextPassed: ((textView:TextView) -> Unit)? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntroSlideViewHolder {
        return IntroSlideViewHolder(
            SlideItemContainerBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        )
    }

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

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

    inner class IntroSlideViewHolder(private val binding: SlideItemContainerBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(introSlide: IntroSlide) {
            binding.textTitle.text = introSlide.title
            binding.textDescription.text = introSlide.description
            binding.imageSlideIcon.imageAssetsFolder = "images";
            binding.imageSlideIcon.setAnimation(introSlide.icon)
            onTextPassed?.invoke(binding.textTitle)
        }
    }
}
```
The above adapter is a normal recyclerView adapter containing a viewHolder with an item layout. In the viewHolder we direct Lottie to where the images are stored by calling `.imageAssetsFolder` on the Lottie view and setting it to `"images"` and then call `setAnimation` on the view as well.

fragment_onboarding.xml
```
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".ui.intro.OnboardingFragment">

    <!-- TODO: Update blank fragment layout -->
    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@id/indicator"/>

    <me.relex.circleindicator.CircleIndicator3
        android:id="@+id/indicator"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/indicator_height"
        app:ci_drawable="@drawable/indicator_active"
        app:ci_drawable_unselected="@drawable/indicator_inactive"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toTopOf="@id/buttonNext" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/buttonNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/standard_padding"
        android:text="@string/next"
        android:textColor="@android:color/white"
        android:textStyle="bold"
        app:backgroundTint="@color/colorPrimaryDark"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
The onboarding fragment layout contains the [relex indicator](https://github.com/ongakuer/CircleIndicator) which is a library that automatically controls indicators with  view pagers. 

OnboardingFragment.kt
```
@AndroidEntryPoint
class OnboardingFragment : Fragment() {
    private var binding: FragmentOnboardingBinding? = null
    @Inject
    lateinit var prefs: DataStore<Preferences>

//the items are added to the adapter 
    private val introSliderAdapter = IntroSliderAdapter(
        listOf(
            IntroSlide(
                "Health Tips / Advice",
                "Discover tips and advice to help you to help maintain transform and main your health",
                "exercise.json"
            ),
            IntroSlide(
                "Diet Tips / Advice",
                "Find out basics of health diet and good nutrition, Start eating well and keep a balanced diet",
                "diet.json"
            ),
            IntroSlide(
                "Covid 19 Symptoms/Prevention tips",
                "Get regular Reminders of Covid-19 prevention tips ensuring you stay safe",
                "covid19.json"
            )
        )
    )

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        binding = FragmentOnboardingBinding.inflate(layoutInflater)
        return binding?.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
//set the adapter to the viewpager2
        binding?.viewPager?.adapter = introSliderAdapter
//sets the viewpager2 to the indicator
        binding?.indicator?.setViewPager(binding?.viewPager)
        
binding?.viewPager?.registerOnPageChangeCallback(
            object : ViewPager2.OnPageChangeCallback() {

                override fun onPageSelected(position: Int) {
                    super.onPageSelected(position)

/*
*check if its the last page, change text on the button
*from next to finish and set the click listener to
*to navigate to welcome screen else set the text to next
* and click listener to move to next page
*/
   if (position == introSliderAdapter.itemCount - 1) {
//this animation is added to the finish button
                        val animation = AnimationUtils.loadAnimation(
                            requireActivity(),
                            R.anim.app_name_animation
                        )
                        binding?.buttonNext?.animation = animation
                        binding?.buttonNext?.text = "Finish"
                        binding?.buttonNext?.setOnClickListener {
                            lifecycleScope.launch {
                                saveOnboarding()
                            }
                            requireView().findNavController()
                                .navigate(OnboardingFragmentDirections.actionOnboardingFragmentToWelcomeFragment())
                        }
                    } else {
                        binding?.buttonNext?.text = "Next"
                        binding?.buttonNext?.setOnClickListener {
                            binding?.viewPager?.currentItem?.let {
                                binding?.viewPager?.setCurrentItem(it + 1, false)
                            }
                        }
                    }
                }
            })
    }

//suspend function to save the onboarding to datastore
    suspend fun saveOnboarding() {
        prefs.edit {
            val oneTime = true
            it[preferencesKey<Boolean>("onBoard")] = oneTime
        }
    }

}
``` 
In the onboardingFragment the IntroSLideAdapter is setup with the list of Items for the Onboarding page, connects the adapter to ViewPager2 and setup the indicator with viewPager2. In the `onPageChangeCallback` we check if the currentPage is the last one and change the text on the button to finish and navigate into the WelcomeFragment if not the text remains Next and when clicked moves to the next page, finally we have 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/orinbfmutryuouad2f5e.gif)

This sample code is part of a community project [WellnessCity](https://github.com/kulloveth/WellnessCity), you can check out just the onboarding branch [here](https://github.com/kulloveth/WellnessCity/tree/onboardingSample)

If you have any suggestions or question kindly drop them in the comment section. Thank you