Splash और Onboarding Screens
पहली बार vs बाद में खोलना
Step 1: SplashScreen API
core-splashscreen library Android 6 से हर version पर Android 12 वाली splash देती है। Launcher activity एक special splash theme से शुरू होती है; splash के बाद normal app theme अपने आप लग जाती है।
<!-- 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: कहाँ जाना है तय करें
setKeepOnScreenCondition { !ready } splash को तब तक दिखाता है जब तक coroutine DataStore से token, onboarding flag और theme पढ़ती है। फिर हम या तो दूसरी activity खोलते हैं (और इसे finish() करते हैं, ताकि Back यहाँ न लौटे) या 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 और splash icon
App icon एक adaptive icon है: background और foreground layer जिन्हें हर launcher circle, squircle या square में काट सकता है। दोनों layers vectors हैं, इसलिए कोई PNG 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>Splash icon 288dp canvas पर है; Android बीच का 192dp circle दिखाता है, इसलिए हम white circle के अंदर brand-colour cart बनाते हैं:
<?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
ViewPager2 pages रखता है, TabLayout सिर्फ dots के लिए है, और एक button आख़िरी page पर Next से Get started बन जाता है।
<?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>Dot एक selector है: selected होने पर लंबी brand-colour pill, वरना छोटा grey dot।
<?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
Pages data की एक छोटी list हैं। एक छोटा inner adapter हर page bind करता है; TabLayoutMediator dots को pager से जोड़ता है। User finish (या skip) करे तो हम flag save करके 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
तीनों illustrations vector drawables हैं: एक soft circle, छोटे decoration dots और 5× बड़ा Material icon। हर एक लगभग 1 KB का है और हर screen पर sharp दिखता है।
<?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 करें
- 1Fresh install → splash → onboarding page 1
- 2Swipe करें और Next दबाएँ → page 3 पर "Get started" और Skip गायब
- 3Get started → Login screen
- 4बंद करके फिर खोलें → onboarding वापस नहीं आता
- 5Login करें, बंद करें, फिर खोलें → सीधे Home (login screen नहीं)
