Splash & Onboarding Screens
First launch vs next launches
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.
<!-- 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.
// 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()
}// 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 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 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 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 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 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 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.
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 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
- 1Install fresh → splash → onboarding page 1
- 2Swipe and tap Next → page 3 shows "Get started" and Skip disappears
- 3Get started → Login screen
- 4Close and reopen → onboarding does not come back
- 5Log in, close, reopen → straight to Home (no login screen)
