Android Core Interview Prep (Kotlin + Jetpack Compose) – 50 Essential Questions & Answers
Android Interview Core Guide
Q1. Explain Activity & Fragment lifecycle. How do you handle process death & configuration changes?
Activity Lifecycle:
onCreate()
→ Initialize UI, bind ViewModel, restore saved state.onStart()
→ Activity becomes visible but not interactive.onResume()
→ Foreground, interactive.onPause()
→ Partially visible (release sensors/camera).onStop()
→ Not visible (release heavy resources).onDestroy()
→ Cleanup.
Fragment Lifecycle (extra states):
onAttach()
→ Fragment attached to Activity.onCreateView()
→ Inflate layout.onViewCreated()
→ Bind views.onDestroyView()
→ Cleanup ViewBinding (to avoid memory leaks).
Handling Config Changes:
- Use ViewModel for data across rotations.
- Use SavedStateHandle or
onSaveInstanceState
for transient data. - Persist critical data in Room DB/SharedPreferences.
- Avoid
android:configChanges
unless absolutely required.
Handling Process Death:
- Use
savedInstanceState
+SavedStateHandle
. - Restore data in
onCreate
. - In Jetpack Compose, use
rememberSaveable
.
Code Example:
class MyViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val counter = savedStateHandle.getLiveData("counter", 0)
fun increment() {
counter.value = (counter.value ?: 0) + 1
}
}
Follow-up:
- Difference between
ViewModel
vsonSaveInstanceState
?
→ ViewModel survives config changes, not process death.
→ onSaveInstanceState survives both.
Q2. Difference between Service, Foreground Service, IntentService, and WorkManager.
- Service: Background task, no UI, runs on main thread unless managed.
- Foreground Service: Ongoing task with persistent notification (music, GPS).
- IntentService (deprecated): Background thread, auto-stops when task finishes.
- WorkManager: Deferrable, guaranteed execution, supports constraints (Wi-Fi, charging).
Best practice:
- Use ForegroundService for ongoing tasks.
- Use WorkManager for guaranteed background jobs.
- Do not use IntentService in new projects.
Q3. How does Handler, Looper, and MessageQueue work?
- Looper: Each thread can have a Looper processing messages.
- MessageQueue: FIFO queue of tasks.
- Handler: Posts tasks/messages to Looper.
Code Example:
val handler = Handler(Looper.getMainLooper())
handler.post {
textView.text = "Updated on main thread"
}
Follow-up:
- Why avoid
Thread.sleep()
in main thread?
→ It blocks Looper → causes ANR.
Q4. How do you prevent memory leaks?
Causes:
- Static references to Activity/Fragment.
- Long-lived coroutines not cancelled.
- Listeners not unregistered.
- ViewBinding not cleared in Fragments.
Prevention:
- Use
applicationContext
for long-lived objects. - Cancel coroutines in
onDestroy()
oronCleared()
. - Clear ViewBinding in
onDestroyView()
. - Use
WeakReference
when required. - Detect leaks with LeakCanary.
Follow-up:
- Common RecyclerView leak?
→ Adapter holding Activity reference.
Q5. LiveData vs StateFlow vs SharedFlow.
- LiveData: Lifecycle-aware, Android-only, simple.
- StateFlow: Hot flow, always has latest value, great for UI state.
- SharedFlow: Hot flow for one-time events (snackbar, navigation), supports replay.
Best practice:
- Use StateFlow for state.
- Use SharedFlow for events.
- Convert to LiveData only for legacy code.
Q6. How do you debug and fix ANR?
Causes:
- Blocking DB or network call on main thread.
- Infinite loops.
- Too much work in BroadcastReceiver or onCreate.
Fix:
- Move I/O to Dispatchers.IO.
- Optimize queries with indexes.
- Use WorkManager for background jobs.
Debug tools:
adb shell dumpsys activity ANR
- Android Studio Profiler.
Follow-up:
- Simulate ANR →
Thread.sleep(10000)
in main thread.
Q7. How do you reduce app startup time?
Optimizations:
- Delay heavy initializations.
- Use App Startup library.
- Avoid blocking in
Application.onCreate
. - Use SplashScreen API for smooth UX.
Debug tools:
- Startup Profiler.
- Traceview.
Follow-up:
- Should Firebase init be in Application.onCreate?
→ No, unless critical. Lazy init recommended.
Q8. Difference between suspend function, launch, async.
- suspend: Pausable, non-blocking function.
- launch: Fire-and-forget coroutine, returns
Job
. - async: Concurrent execution, returns
Deferred<T>
.
Code Example:
viewModelScope.launch {
val user = async { api.getUser() }
val posts = async { api.getPosts() }
println("User: ${user.await()}, Posts: ${posts.await()}")
}
Follow-up:
- Use async when results are needed.
- Use launch for side-effects.
Q9. What is structured concurrency?
- Coroutines are bound to a scope.
- If parent is cancelled, children are cancelled.
- Prevents leaks & orphan jobs.
Code Example:
suspend fun fetchData() = coroutineScope {
val user = async { api.getUser() }
val posts = async { api.getPosts() }
user.await() to posts.await()
}
Follow-up:
- Difference:
coroutineScope
→ failure cancels siblings.supervisorScope
→ siblings continue.
Q15. MVVM vs MVI vs MVP. Why Clean Architecture?
- MVP: Presenter → tight coupling, harder to test.
- MVVM: ViewModel holds state, UI observes (recommended by Google).
- MVI: Unidirectional, predictable state, verbose.
Clean Architecture:
- UI → ViewModel → UseCase → Repository → DataSource.
- Improves testability & scalability.
Q16. How do you design an offline-first app?
- Room DB = single source of truth.
- Repository exposes Flow from DB.
- Sync with server using WorkManager.
- UI always reads from DB.
Best practice:
- Use NetworkBoundResource or Mediator pattern.
Q17. How do you structure a modular Android project?
Modules:
app
: entry, DI setup.core
: networking, utils.feature-*
: independent features.shared
: design system, common components.
Benefits:
- Faster builds.
- Independent feature dev/testing.
- Supports dynamic feature delivery.
Q21. How do you detect and fix memory leaks?
Tools:
- LeakCanary.
- Android Profiler.
Fixes:
- Clear ViewBinding in
onDestroyView()
. - Cancel coroutines in
onCleared()
. - Avoid static references to Context.
Q22. How do you optimize RecyclerView performance?
- Use DiffUtil/ListAdapter.
- Call
setHasFixedSize(true)
. - Use RecycledViewPool for multiple lists.
- Use Paging 3 for large lists.
- Avoid nested RecyclerViews.
Follow-up:
- Debug jank with Profile GPU Rendering.
Q23. What is overdraw?
- Drawing the same pixel multiple times.
- Debug: GPU Overdraw tool.
Fix:
- Remove redundant backgrounds.
- Flatten layouts.
- Avoid nested transparency.
Q24. How do you reduce battery usage in background tasks?
- Use WorkManager with constraints.
- Batch network calls.
- Prefer push notifications over polling.
- Optimize location updates with FusedLocationProvider.
Best practice:
- Avoid foreground services unless absolutely needed.
📘 Android Kotlin & Flow Interview Guide
Q10. What are Coroutines Dispatchers? How do they differ?
Dispatchers:
Dispatchers.Main
→ Main thread (UI updates).Dispatchers.IO
→ I/O operations (network, DB).Dispatchers.Default
→ CPU-heavy work (sorting, parsing).Dispatchers.Unconfined
→ Starts on current thread, resumes where suspended (rare).
Best practice:
- Always use
withContext(Dispatchers.IO)
for blocking I/O. - Never block
Dispatchers.Main
.
Code Example:
viewModelScope.launch(Dispatchers.IO) {
val data = repository.getData()
withContext(Dispatchers.Main) {
textView.text = data
}
}
Follow-up:
- What happens if you do
Thread.sleep()
inDispatchers.Main
?
→ Causes ANR because it blocks UI thread.
Q11. Explain Flow basics and common operators.
Flow basics:
- Flow = cold asynchronous data stream.
- Emits values sequentially.
- Collected with
collect {}
.
Common operators:
map
→ Transform values.filter
→ Filter emissions.take(n)
→ Limit emissions.debounce()
→ Emit after quiet period.flatMapConcat
,flatMapMerge
→ Flatten flows.
Code Example:
val numbers = flowOf(1, 2, 3, 4, 5)
numbers
.filter { it % 2 == 0 }
.map { it * it }
.collect { println(it) } // Prints 4, 16
Follow-up:
- Why is Flow cold?
→ Collection triggers execution. Until collected, nothing runs.
Q12. Difference between Cold Flow and Hot Flow (StateFlow/SharedFlow)?
Cold Flow:
- Execution starts on collection.
- Each collector gets independent stream.
- Example:
flow { emit(x) }
.
Hot Flow:
- Always active, even without collectors.
- Multiple collectors share emissions.
- Examples:
StateFlow
,SharedFlow
.
Code Example:
val state = MutableStateFlow(0)
state.value = 5 // Always holds latest value
val shared = MutableSharedFlow<Int>()
viewModelScope.launch { shared.emit(10) }
Follow-up:
- When to use StateFlow? → For UI state.
- When to use SharedFlow? → For one-time events (navigation, toast).
Q13. Difference between flowOn and withContext.
withContext
: Changes context once for entire block (suspends).flowOn
: Changes context of upstream flow (non-blocking).
Code Example:
flow {
emit(loadData()) // Heavy I/O
}.flowOn(Dispatchers.IO) // Switches only upstream
.collect { println(it) } // Collected on original dispatcher
Follow-up:
- What if we use
withContext
inside flow builder?
→ Flow won’t be cancellable properly → avoid.
Q14. How do you test Flow?
- Use Turbine library (
app.cash.turbine
). - Collect emissions in test coroutine.
- Use
runTest {}
from kotlinx-coroutines-test.
Code Example:
@Test
fun testFlow() = runTest {
val flow = flowOf(1, 2, 3)
flow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
Follow-up:
- Why not use
collect
directly?
→ Harder to assert emissions and completion.
Q29. How do you handle backpressure in Kotlin Flow?
Definition:
- Backpressure = producer emits faster than consumer can process.
Handling techniques:
buffer()
→ Adds channel buffer.conflate()
→ Drops intermediate values, keeps latest.debounce()
→ Ignores rapid emissions.sample()
→ Emits periodically.
Code Example:
flow {
repeat(1000) {
emit(it)
}
}
.buffer() // Allow producer to keep emitting
.conflate() // Only keep latest
.collect { delay(100); println(it) }
Follow-up:
- Difference between
buffer()
vsconflate()
?
→ buffer stores all values, conflate skips intermediate values.
📘 Android System Design Interview Guide
Q18. Design WhatsApp chat architecture (1:1 chat).
Requirements:
- Real-time delivery.
- Offline storage.
- Read receipts.
- End-to-end encryption.
Architecture:
- Client: Stores chats in Room DB.
- Server: Message broker (Kafka, RabbitMQ).
- Transport: WebSocket (persistent).
- Push notifications for offline delivery.
- Encryption: AES (content), RSA (key exchange).
Flow:
- User A sends → stored in local DB → sent via WebSocket.
- Server stores + forwards to User B.
- If offline → push notification.
- User B receives → stored locally → sends ACK back.
Follow-up:
- How to sync across devices?
→ Use message IDs + server as source of truth.
Q19. How do you implement Repository pattern?
Definition:
- Repository mediates between data sources and domain.
Layers:
UI → ViewModel → Repository → DataSource (Network/DB)
.
Code Example:
class UserRepository(
private val api: ApiService,
private val dao: UserDao
) {
fun getUser(id: Int): Flow<User> = flow {
val cached = dao.getUser(id)
if (cached != null) emit(cached)
val remote = api.getUser(id)
dao.insert(remote)
emit(remote)
}
}
Follow-up:
- Why not call DAO & API directly in ViewModel?
→ Breaks separation of concerns, harder to test.
Q20. How would you implement push notifications system?
Architecture:
- Server uses FCM (Firebase Cloud Messaging).
- Client registers device token.
- Server pushes to FCM → FCM delivers to device.
Steps:
- Client app registers with FCM → gets token.
- Token sent to backend.
- Server sends notification payload to FCM API.
- FCM routes to correct device.
Code Example (Client):
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
sendTokenToServer(token)
}
}
Follow-up:
- How to handle notification when app in background?
→ FCM delivers automatically to system tray.
Q25. How do you scale an Android app for millions of users?
Scalability concerns:
- Networking: Use Retrofit + OkHttp + caching.
- Data: Room + offline-first sync.
- Architecture: Modularization for faster builds.
- Monitoring: Firebase Crashlytics, Performance.
- Testing: Automated CI/CD pipelines.
Follow-up:
- What about feature flags?
→ Use Remote Config or LaunchDarkly.
Q26. How to design video streaming (YouTube-like)?
Requirements:
- Adaptive bitrate.
- Resume playback.
- Offline download.
Client:
- ExoPlayer with DASH/HLS.
- Cache media chunks with OkHttp cache.
Server:
- Store video in multiple bitrates.
- CDN for distribution.
- Metadata API for titles, comments, etc.
Flow:
- App requests manifest (MPD/HLS).
- ExoPlayer selects bitrate based on bandwidth.
- Segments fetched via CDN.
Follow-up:
- How to handle slow networks?
→ Adaptive bitrate + prefetch.
Q27. How do you secure APIs in Android?
- Use HTTPS (TLS).
- Token-based auth (JWT, OAuth2).
- Short-lived tokens, refresh tokens.
- Certificate pinning (OkHttp).
- Obfuscation with ProGuard/R8.
- Avoid secrets in APK → use remote config or keystore.
Code Example:
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("yourdomain.com", "sha256/abc123...")
.build()
)
.build()
Follow-up:
- Can we fully hide API keys in Android?
→ No, only obscure them. Use backend proxy if possible.
Q28. How would you implement offline sync?
Pattern:
- Local DB = source of truth.
- Pending operations stored in queue.
- WorkManager retries sync with server.
Flow:
- User edits entity → saved in DB with
pending=true
. - Sync worker pushes pending changes.
- On success, mark as
synced
.
Code Example:
suspend fun sync() {
val pending = dao.getPending()
for (item in pending) {
val response = api.update(item)
if (response.isSuccessful) {
dao.markSynced(item.id)
}
}
}
Follow-up:
- Conflict resolution?
→ Last-write-wins or server-driven merge.
📘 Android Leadership & Behavioral Interview Guide
Q30. How do you mentor junior developers in your team?
Approach:
- Onboarding: Pair programming, walkthrough of architecture, coding standards.
- Knowledge sharing: Tech talks, code reviews, documentation.
- Guided autonomy: Let them solve problems, provide feedback instead of solutions.
- Feedback loop: Regular 1:1s, retrospective reviews.
Follow-up:
- What if a junior keeps repeating mistakes?
→ Identify gaps (knowledge vs carelessness). Create a growth plan with examples.
Q31. How do you handle conflicts in a team?
Steps:
- Listen actively to both sides.
- Focus on problem, not people.
- Encourage constructive discussion.
- Align with project goals.
- If unresolved → escalate respectfully.
Example:
- Two devs disagree on architecture → organize design review → pros/cons of each → consensus.
Q32. How do you make architecture decisions?
- Gather requirements (scalability, deadlines).
- Evaluate trade-offs (MVVM vs MVI, modularization vs monolith).
- Propose POC (proof of concept).
- Document decision (ADR – Architecture Decision Record).
- Revisit decisions when context changes.
Follow-up:
- Example: Why MVVM over MVP?
→ MVVM better supports reactive UIs with Flow/LiveData.
Q33. How do you communicate with non-technical stakeholders?
- Avoid jargon → explain in business terms.
- Use analogies → “Caching is like saving a shortcut instead of recalculating.”
- Provide timelines with risks & trade-offs.
- Share visual diagrams instead of long emails.
Follow-up:
- Example: Explaining app crash issue.
→ “The app stops working due to memory shortage, we need to optimize resources.”
Q34. How do you ensure quality in your team’s code?
- Define coding guidelines.
- Enforce PR reviews.
- Automate checks (ktlint, detekt, unit tests).
- Add CI/CD pipelines.
- Encourage TDD or at least unit tests for critical logic.
Follow-up:
- How to handle deadline pressure?
→ Deliver MVP with quality, avoid hacks that increase tech debt.
Q35. How do you balance coding and leadership responsibilities?
- Prioritize → delegate tasks where possible.
- Spend ~50% on coding, ~50% on mentoring, reviews, planning.
- Block calendar for deep work.
- Keep communication async when possible (Slack/Email).
Follow-up:
- What if project is delayed?
→ Step in hands-on, support critical tasks, shield team from stress.
📘 Android Jetpack Compose Interview Guide
Q36. Difference between remember
and rememberSaveable
.
remember
: Stores state in memory only → lost on config changes or process death.rememberSaveable
: Persists state usingSavedStateHandle
(Bundle) → survives config changes & process death.
Code Example:
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // Lost on rotation
var savedCount by rememberSaveable { mutableStateOf(0) } // Survives
Column {
Text("Count: $count | Saved: $savedCount")
Button(onClick = { count++ }) { Text("Inc Count") }
Button(onClick = { savedCount++ }) { Text("Inc Saved Count") }
}
}
Follow-up:
- When to use
remember
? → Temporary UI state. - When to use
rememberSaveable
? → State that must survive process death.
Q37. How does Recomposition work in Compose?
- Compose uses state-driven UI.
- When
mutableStateOf
value changes → Composable that reads it re-runs (recomposes). - Compose tries to skip unchanged parts (smart diffing).
Follow-up:
- How to optimize recomposition?
- Use
derivedStateOf
for derived values. - Hoist state higher in hierarchy.
- Use
remember
to cache expensive computations.
- Use
Q38. How do you handle side effects in Compose?
- LaunchedEffect: Runs suspend code tied to key lifecycle.
- DisposableEffect: Cleanup when leaving composition.
- SideEffect: Post a side-effect after every recomposition.
- rememberCoroutineScope: Launch coroutines manually.
Code Example:
@Composable
fun TimerExample() {
var time by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
time++
}
}
Text("Time: $time")
}
Q39. What is state hoisting in Compose?
- Moving state up from a Composable → parent controls state, Composable becomes stateless.
- Improves reusability, testability.
Code Example:
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
Counter(count) { count++ }
}
Q40. How to use Compose with ViewModel?
- ViewModel holds state as
StateFlow
/LiveData
. - Collect state in Composable using
collectAsState()
orobserveAsState()
.
Code Example:
class MyViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() { _count.value++ }
}
@Composable
fun CounterScreen(viewModel: MyViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = { viewModel.increment() }) {
Text("Count: $count")
}
}
Q41. How do you test Composables?
- Use Compose Testing APIs (
composeTestRule
). - Test tags with
Modifier.testTag
. - Assertions like
assertIsDisplayed
,assertTextEquals
.
Code Example:
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonClick() {
composeTestRule.setContent {
Counter()
}
composeTestRule.onNodeWithText("Count: 0").assertIsDisplayed()
composeTestRule.onNodeWithText("Inc Count").performClick()
composeTestRule.onNodeWithText("Count: 1").assertIsDisplayed()
}
📘 Android Advanced Tech Stack Interview Guide
Q42. Explain Dependency Injection (DI) in Android. Why Hilt/Dagger?
Concept:
- DI = providing dependencies from outside instead of creating inside.
- Improves testability, reusability, and modularity.
Hilt (recommended):
- Built on top of Dagger.
- Provides standard components (Application, Activity, ViewModel scope).
- Generates boilerplate automatically.
Code Example:
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository
}
Follow-up:
- Why Hilt over Koin?
→ Hilt is compile-time safe, faster runtime. Koin is simpler but reflection-based.
Q43. How does Room Database work? How do you handle migrations?
Room basics:
- Entity = table.
- DAO = data access object.
- Database = main entry point.
Code Example:
@Entity
data class User(@PrimaryKey val id: Int, val name: String)
@Dao
interface UserDao {
@Query("SELECT * FROM User WHERE id=:id")
fun getUser(id: Int): Flow<User>
@Insert
suspend fun insert(user: User)
}
@Database(entities = [User::class], version = 2)
abstract class AppDb : RoomDatabase() {
abstract fun userDao(): UserDao
}
Migration:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Q44. What is Paging 3? Why use it?
- Handles large lists efficiently.
- Built with Kotlin Flow & coroutines.
- Supports remote + local (Room) sources.
Code Example:
class UserPagingSource(private val api: ApiService) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val page = params.key ?: 1
val response = api.getUsers(page)
return LoadResult.Page(
data = response,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.isEmpty()) null else page + 1
)
}
}
Q45. Explain Navigation Component (XML vs Compose).
- XML Nav Component: Uses
nav_graph.xml
+ SafeArgs. - Compose Navigation: Uses
NavHost
+rememberNavController()
.
Code Example (Compose):
@Composable
fun AppNav() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(onNavigate = { navController.navigate("details") }) }
composable("details") { DetailsScreen() }
}
}
Follow-up:
- How to handle deep links?
→ Define in nav graph orNavDeepLinkBuilder
.
Q46. WorkManager vs AlarmManager vs JobScheduler.
- WorkManager: Guaranteed, deferrable, respects constraints.
- AlarmManager: Fires at exact/system-defined time, not aware of constraints.
- JobScheduler: API 21+, batch jobs, replaced by WorkManager.
Code Example:
class SyncWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
// sync logic
return Result.success()
}
}
Q47. How do you integrate Firebase in Android?
- Auth: Email, Google Sign-In.
- Firestore: Realtime DB with offline caching.
- Crashlytics: Crash reporting.
- Remote Config: Feature flags, A/B testing.
Code Example (Firestore):
val db = Firebase.firestore
db.collection("users").document("1").set(mapOf("name" to "Firoj"))
Follow-up:
- Why Remote Config over hardcoding flags?
→ Allows feature rollout without app release.
Q48. How do you secure APIs in Android?
- Always use HTTPS (TLS).
- Use OAuth2/JWT for authentication.
- Certificate pinning with OkHttp.
- Do not store secrets in APK → use Keystore or server proxy.
Code Example:
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("api.example.com", "sha256/AbCdEfGh...")
.build()
)
.build()
Q49. What is DataStore? Why use it over SharedPreferences?
- SharedPreferences: Synchronous I/O → can block main thread.
- DataStore: Built on coroutines & Flow.
- Preferences DataStore: Key-value.
- Proto DataStore: Strongly typed schema.
Code Example (Preferences):
val Context.dataStore by preferencesDataStore("settings")
val USERNAME = stringPreferencesKey("username")
suspend fun saveName(context: Context, name: String) {
context.dataStore.edit { prefs -> prefs[USERNAME] = name }
}
fun readName(context: Context): Flow<String?> =
context.dataStore.data.map { it[USERNAME] }
Q50. Explain Gradle Kotlin DSL and KSP.
- Gradle Kotlin DSL: Use Kotlin (
build.gradle.kts
) instead of Groovy.
→ Safer, autocompletion, type-safe. - KSP (Kotlin Symbol Processing): Replaces KAPT (annotation processing).
→ Faster builds, supports libraries like Room, Moshi, Hilt.
Dependency Injection (DI)
- Hilt / Dagger basics
- Why DI, scopes, ViewModel injection
- Alternatives (Koin, manual DI)
Room Database & Paging 3
- Room basics (entities, DAO, migrations)
- Paging 3 for large data lists
- Flow integration
Navigation
- Jetpack Navigation (Compose + XML)
- Deep links
- SafeArgs (XML) or
rememberNavController()
(Compose)
WorkManager & Background Tasks
- Periodic vs One-time
- Constraints (Wi-Fi, charging)
- Chained workers
Testing & CI/CD
- Unit testing (JUnit, Mockito, Kotest)
- UI testing (Espresso, Compose Test)
- CI/CD with GitHub Actions, Jenkins, Bitrise
Firebase / Analytics
- Firebase Auth, Firestore basics
- Crashlytics, Analytics integration
- Remote Config for feature flags
Networking (Retrofit + OkHttp)
- Interceptors (logging, auth)
- Caching strategies
- Error handling with sealed classes
Security
- Jetpack Security (EncryptedSharedPreferences)
- Keystore API
- Obfuscation with ProGuard/R8
Modern Android Tools
- KSP (Kotlin Symbol Processing)
- Gradle Kotlin DSL
- Jetpack DataStore (replacing SharedPreferences)