Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
📱 Android App (Kotlin + XML)  ·  Lesson 15

Splash & Onboarding Screens

First launch vs next launches

Splashread DataStoreOnboarding?only the first timeLogged in?token saved?Homeor Login screen
MainActivity is the launcher; it decides and forwards
9:41▮▮ 85%🛒KartEasy9:41▮▮ 85%Skip🖼️shopping bag illustrationEverything you needMobiles, fashion, electronics,books and more in one simple app.━ • •Next9:41▮▮ 85%🖼️delivery truck illustrationFast doorstep deliveryPay cash on delivery and trackevery order.• • ━Get started
Splash → onboarding page 1 → page 3 ("Skip" hides on the last page)

Step 1: SplashScreen API

The core-splashscreen library gives the Android 12 splash on every version from Android 6. The launcher activity starts with a special splash theme; after the splash the normal app theme is applied automatically.

XML – splash theme + manifest
<!-- res/values/themes.xml (splash part) -->
<style name="Theme.KartEasy.Splash" parent="Theme.SplashScreen">
    <item name="windowSplashScreenBackground">@color/brand</item>
    <item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash</item>
    <item name="postSplashScreenTheme">@style/Theme.KartEasy</item>
</style>

<!-- AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:theme="@style/Theme.KartEasy.Splash"
    android:windowSoftInputMode="adjustResize">
    <!-- intent-filter MAIN / LAUNCHER … -->
</activity>

Step 2: Decide where to go

setKeepOnScreenCondition { !ready } keeps the splash visible while the coroutine reads the token, onboarding flag and theme from DataStore. Then we either open another activity (and finish() this one, so Back does not return to it) or build the main UI.

Kotlin – MainActivity.kt (start)
// MainActivity.kt – the start of onCreate (full file in the Drawer & Bottom Nav lesson)
override fun onCreate(savedInstanceState: Bundle?) {
    // 1) Splash screen stays until we know where to go (onboarding / login / home).
    val splash = installSplashScreen()
    splash.setKeepOnScreenCondition { !ready }
    super.onCreate(savedInstanceState)

    lifecycleScope.launch {
        val app = kartApp
        app.session.load()
        applyDarkMode(app.session.darkMode())
        when {
            !app.session.isOnboardingDone() -> openAndFinish(OnboardingActivity::class.java)
            !app.session.isLoggedIn -> openAndFinish(AuthActivity::class.java)
            else -> setupUi()
        }
        ready = true
    }
}

private fun openAndFinish(target: Class<*>) {
    startActivity(Intent(this, target))
    finish()
}
Kotlin – SessionManager.kt (onboarding part)
// SessionManager.kt (onboarding part)
private object Keys {
    val ONBOARDING_DONE = booleanPreferencesKey("onboarding_done")
    // …
}

suspend fun isOnboardingDone(): Boolean = context.dataStore.data.first()[Keys.ONBOARDING_DONE] == true

suspend fun setOnboardingDone() {
    context.dataStore.edit { it[Keys.ONBOARDING_DONE] = true }
}

Step 3: App icon and splash icon

The app icon is an adaptive icon: a background and a foreground layer that each launcher can cut into a circle, squircle or square. Both layers are vectors, so there are no PNG files to export.

XML – mipmap-anydpi-v26/ic_launcher.xml
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
XML – drawable/ic_launcher_foreground.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <group android:scaleX="2" android:scaleY="2" android:translateX="30" android:translateY="30">
        <path android:fillColor="#FFFFFFFF" android:pathData="M7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2 -0.9,-2 -2,-2zM1,2v2h2l3.6,7.59 -1.35,2.45c-0.16,0.28 -0.25,0.61 -0.25,0.96 0,1.1 0.9,2 2,2h12v-2H7.42c-0.14,0 -0.25,-0.11 -0.25,-0.25l0.03,-0.12 0.9,-1.63h7.45c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49C21.25,4.16 20.78,3.5 20.1,3.5H5.21l-0.94,-2H1zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
    </group>
    <path android:fillColor="#F97316" android:pathData="M72,34m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0" />
</vector>

The splash icon sits on a 288dp canvas; Android shows the middle 192dp circle, so we draw a white circle with the brand-colour cart inside it:

XML – drawable/ic_splash.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Splash icon: the cart in the middle of a 288dp canvas (the system shows a 192dp circle) -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="288dp"
    android:height="288dp"
    android:viewportWidth="288"
    android:viewportHeight="288">
    <path android:fillColor="#FFFFFFFF" android:pathData="M144,144m-64,0a64,64 0,1 1,128 0a64,64 0,1 1,-128 0" />
    <group android:scaleX="3.4" android:scaleY="3.4" android:translateX="103.2" android:translateY="103.2">
        <path android:fillColor="#4F46E5" android:pathData="M7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2 -0.9,-2 -2,-2zM1,2v2h2l3.6,7.59 -1.35,2.45c-0.16,0.28 -0.25,0.61 -0.25,0.96 0,1.1 0.9,2 2,2h12v-2H7.42c-0.14,0 -0.25,-0.11 -0.25,-0.25l0.03,-0.12 0.9,-1.63h7.45c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49C21.25,4.16 20.78,3.5 20.1,3.5H5.21l-0.94,-2H1zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
    </group>
</vector>

Step 4: Onboarding layouts

A ViewPager2 holds the pages, a TabLayout is used only as the dots, and one button changes from Next to Get started on the last page.

XML – layout/activity_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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/surface"
    android:padding="24dp">

    <com.google.android.material.button.MaterialButton
        android:id="@+id/skip"
        style="@style/Widget.Material3.Button.TextButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/skip"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/pager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/dots"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/skip" />

    <!-- TabLayout used only as page dots -->
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/dots"
        android:layout_width="wrap_content"
        android:layout_height="24dp"
        android:layout_marginBottom="24dp"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toTopOf="@id/next"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:tabBackground="@drawable/tab_dot"
        app:tabGravity="center"
        app:tabIndicatorHeight="0dp"
        app:tabMaxWidth="18dp"
        app:tabMinWidth="18dp"
        app:tabPaddingEnd="0dp"
        app:tabPaddingStart="0dp" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/next"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@string/next"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
XML – layout/item_onboarding.xml (one page)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="8dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="260dp"
        android:layout_height="240dp"
        android:importantForAccessibility="no" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:gravity="center"
        android:textAppearance="?attr/textAppearanceHeadlineSmall"
        android:textColor="@color/text_primary"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp"
        android:gravity="center"
        android:lineSpacingExtra="4dp"
        android:textAppearance="?attr/textAppearanceBodyLarge"
        android:textColor="@color/text_secondary" />
</LinearLayout>

The dot is a selector: a long brand-colour pill when selected, a small grey dot otherwise.

XML – drawable/tab_dot.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Page dot: long brand pill when selected, small grey dot otherwise -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <layer-list>
            <item android:gravity="center" android:width="16dp" android:height="6dp">
                <shape android:shape="rectangle">
                    <solid android:color="@color/brand" />
                    <corners android:radius="3dp" />
                </shape>
            </item>
        </layer-list>
    </item>
    <item>
        <layer-list>
            <item android:gravity="center" android:width="6dp" android:height="6dp">
                <shape android:shape="oval">
                    <solid android:color="@color/divider" />
                </shape>
            </item>
        </layer-list>
    </item>
</selector>

Step 5: OnboardingActivity

The pages are a small list of data. A tiny inner adapter binds each page; TabLayoutMediator connects the dots to the pager. When the user finishes (or skips), we save the flag and open the login screen.

Kotlin – ui/onboarding/OnboardingActivity.kt
package com.codingeasily.karteasy.ui.onboarding

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.ActivityOnboardingBinding
import com.codingeasily.karteasy.databinding.ItemOnboardingBinding
import com.codingeasily.karteasy.ui.auth.AuthActivity
import com.codingeasily.karteasy.util.kartApp
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch

/** Three intro slides shown only on the first launch. */
class OnboardingActivity : AppCompatActivity() {

    private data class Page(val image: Int, val title: Int, val text: Int)

    private val pages = listOf(
        Page(R.drawable.onboard_shop, R.string.onboard_title_1, R.string.onboard_text_1),
        Page(R.drawable.onboard_deals, R.string.onboard_title_2, R.string.onboard_text_2),
        Page(R.drawable.onboard_delivery, R.string.onboard_title_3, R.string.onboard_text_3)
    )

    private lateinit var binding: ActivityOnboardingBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityOnboardingBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.pager.adapter = PageAdapter()
        TabLayoutMediator(binding.dots, binding.pager) { _, _ -> }.attach()

        binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageSelected(position: Int) {
                val last = position == pages.lastIndex
                binding.next.setText(if (last) R.string.get_started else R.string.next)
                binding.skip.visibility = if (last) android.view.View.INVISIBLE else android.view.View.VISIBLE
            }
        })

        binding.next.setOnClickListener {
            val current = binding.pager.currentItem
            if (current < pages.lastIndex) binding.pager.currentItem = current + 1 else finishOnboarding()
        }
        binding.skip.setOnClickListener { finishOnboarding() }
    }

    private fun finishOnboarding() {
        lifecycleScope.launch {
            kartApp.session.setOnboardingDone()
            startActivity(Intent(this@OnboardingActivity, AuthActivity::class.java))
            finish()
        }
    }

    private inner class PageAdapter : RecyclerView.Adapter<PageAdapter.Holder>() {
        inner class Holder(val b: ItemOnboardingBinding) : RecyclerView.ViewHolder(b.root)

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
            Holder(ItemOnboardingBinding.inflate(LayoutInflater.from(parent.context), parent, false))

        override fun getItemCount() = pages.size

        override fun onBindViewHolder(holder: Holder, position: Int) {
            val p = pages[position]
            holder.b.image.setImageResource(p.image)
            holder.b.title.setText(p.title)
            holder.b.text.setText(p.text)
        }
    }
}

Step 6: Vector illustrations

The three illustrations are vector drawables built from a soft circle, small decoration dots and a big Material icon scaled 5×. They weigh about 1 KB each and look sharp on every screen.

XML – drawable/onboard_deals.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="260dp"
    android:height="240dp"
    android:viewportWidth="260"
    android:viewportHeight="240">
    <path android:fillColor="@color/brand_light" android:pathData="M130,120m-100,0a100,100 0,1 1,200 0a100,100 0,1 1,-200 0" />
    <path android:fillColor="#4F46E5" android:pathData="M40,40m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0" />
    <path android:fillColor="#F97316" android:pathData="M222,62m-7,0a7,7 0,1 1,14 0a7,7 0,1 1,-14 0" />
    <path android:fillColor="#4F46E5" android:pathData="M214,196m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0" />
    <group android:scaleX="5" android:scaleY="5" android:translateX="70" android:translateY="60">
        <path android:fillColor="#F97316" android:pathData="M21.41,11.58l-9,-9C12.05,2.22 11.55,2 11,2H4c-1.1,0 -2,0.9 -2,2v7c0,0.55 0.22,1.05 0.59,1.42l9,9c0.36,0.36 0.86,0.58 1.41,0.58 0.55,0 1.05,-0.22 1.41,-0.59l7,-7c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-0.55 -0.23,-1.06 -0.59,-1.42zM5.5,7C4.67,7 4,6.33 4,5.5S4.67,4 5.5,4 7,4.67 7,5.5 6.33,7 5.5,7z" />
    </group>
</vector>

Test it

  1. 1Install fresh → splash → onboarding page 1
  2. 2Swipe and tap Next → page 3 shows "Get started" and Skip disappears
  3. 3Get started → Login screen
  4. 4Close and reopen → onboarding does not come back
  5. 5Log in, close, reopen → straight to Home (no login screen)
First-launch checklist

Frequently Asked Questions

Why not a SplashActivity with a 3-second delay?
Android 12+ always shows a system splash, so a second splash screen means users see two. A fixed delay also wastes time. The SplashScreen API shows one splash for exactly as long as we need to read DataStore — usually a few milliseconds.
How do I show onboarding again for testing?
Long-press the app icon → App info → Storage → Clear storage. DataStore is wiped, so the app behaves like a fresh install.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicLogin & Register Screens Your progress: 15 / 28 (54%) · Lesson 16 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free