Senior Mobile Developer (Android) Interview Questions: Complete Guide

Milad Bonakdar
Author
Master advanced Android development with essential interview questions covering architecture patterns, performance optimization, dependency injection, testing, security, and system design for senior developers.
Introduction
Senior Android developers are expected to architect scalable, maintainable applications while ensuring high performance and code quality. This role demands deep expertise in Android frameworks, architectural patterns, dependency injection, testing strategies, and the ability to make informed technical decisions.
This comprehensive guide covers essential interview questions for Senior Android Developers, spanning advanced Kotlin concepts, architectural patterns, performance optimization, dependency injection, testing, and system design. Each question includes detailed answers, rarity assessment, and difficulty ratings.
Advanced Kotlin & Language Features (5 Questions)
1. Explain Kotlin Coroutines and their advantages over threads.
Answer: Coroutines are lightweight concurrency primitives that allow writing asynchronous code in a sequential manner.
- Advantages over Threads:
- Lightweight: Can create thousands of coroutines without performance issues
- Structured Concurrency: Parent-child relationship ensures proper cleanup
- Cancellation Support: Built-in cancellation propagation
- Exception Handling: Structured exception handling
- Key Components:
- CoroutineScope: Defines lifecycle
- Dispatchers: Control execution context (Main, IO, Default)
- suspend functions: Can be paused and resumed
class UserRepository(private val api: ApiService) {
suspend fun getUser(id: Int): Result<User> = withContext(Dispatchers.IO) {
try {
val user = api.fetchUser(id)
Result.success(user)
} catch (e: Exception) {
Result.failure(e)
}
}
}
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
fun loadUser(id: Int) {
viewModelScope.launch {
when (val result = repository.getUser(id)) {
is Result.Success -> _user.value = result.data
is Result.Error -> handleError(result.exception)
}
}
}
}Rarity: Very Common Difficulty: Hard
2. What are Sealed Classes and when should you use them?
Answer: Sealed classes represent restricted class hierarchies where all subclasses are known at compile time.
- Benefits:
- Exhaustive
whenexpressions - Type-safe state management
- Better than enums for complex data
- Exhaustive
- Use Cases: Representing states, results, navigation events
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
fun loadUser() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.getUser()
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// In UI
when (val state = uiState.value) {
is UiState.Loading -> showLoading()
is UiState.Success -> showData(state.data)
is UiState.Error -> showError(state.message)
// Compiler ensures all cases are handled
}Rarity: Common Difficulty: Medium
3. Explain Kotlin Flow and how it differs from LiveData.
Answer: Flow is Kotlin's cold asynchronous stream that emits values sequentially.
- Flow vs LiveData:
- Flow: Cold stream, supports operators, not lifecycle-aware, more flexible
- LiveData: Hot stream, lifecycle-aware, Android-specific, simpler for UI
- Flow Types:
- Flow: Cold stream (starts on collection)
- StateFlow: Hot stream with current state
- SharedFlow: Hot stream for events
class UserRepository {
// Cold Flow - starts when collected
fun getUsers(): Flow<List<User>> = flow {
val users = api.fetchUsers()
emit(users)
}.flowOn(Dispatchers.IO)
// StateFlow - hot, holds state
private val _userState = MutableStateFlow<List<User>>(emptyList())
val userState: StateFlow<List<User>> = _userState.asStateFlow()
// SharedFlow - hot, for events
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
}
class UserViewModel : ViewModel() {
val users = repository.getUsers()
.map { it.filter { user -> user.isActive } }
.catch { emit(emptyList()) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
// Collect in Activity/Fragment
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { users ->
updateUI(users)
}
}
}Rarity: Very Common Difficulty: Hard
4. What are Inline Functions and when should you use them?
Answer: Inline functions copy the function body to the call site, avoiding function call overhead.
- Benefits:
- Eliminates lambda allocation overhead
- Allows non-local returns from lambdas
- Better performance for higher-order functions
- Use Cases: Higher-order functions with lambda parameters
- Trade-off: Increases code size
// Without inline - creates lambda object
fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("Time: ${end - start}ms")
}
// With inline - no lambda object created
inline fun measureTimeInline(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("Time: ${end - start}ms")
}
// noinline - prevents specific parameter from being inlined
inline fun performOperation(
inline operation: () -> Unit,
noinline logger: () -> Unit
) {
operation()
saveLogger(logger) // Can store noinline lambda
}
// crossinline - allows inline but prevents non-local returns
inline fun runAsync(crossinline block: () -> Unit) {
thread {
block() // Can't return from outer function
}
}Rarity: Medium Difficulty: Hard
5. Explain Delegation in Kotlin.
Answer: Delegation allows an object to delegate some of its responsibilities to another object.
- Class Delegation:
bykeyword - Property Delegation: Lazy, observable, delegates
- Benefits: Code reuse, composition over inheritance
// Class delegation
interface Repository {
fun getData(): String
}
class RemoteRepository : Repository {
override fun getData() = "Remote data"
}
class CachedRepository(
private val remote: Repository
) : Repository by remote {
private var cache: String? = null
override fun getData(): String {
return cache ?: remote.getData().also { cache = it }
}
}
// Property delegation
class User {
// Lazy initialization
val database by lazy {
Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
}
// Observable property
var name: String by Delegates.observable("Initial") { prop, old, new ->
println("$old -> $new")
}
// Custom delegate
var token: String by PreferenceDelegate("auth_token", "")
}
class PreferenceDelegate(
private val key: String,
private val defaultValue: String
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return sharedPreferences.getString(key, defaultValue) ?: defaultValue
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
sharedPreferences.edit().putString(key, value).apply()
}
}Rarity: Medium Difficulty: Medium
Architecture Patterns (6 Questions)
6. Explain MVVM architecture and its benefits.
Answer: MVVM (Model-View-ViewModel) separates UI logic from business logic.
- Model: Data layer (repositories, data sources)
- View: UI layer (Activities, Fragments, Composables)
- ViewModel: Presentation logic, survives configuration changes
- Benefits: Testable, separation of concerns, lifecycle-aware
// Model
data class User(val id: Int, val name: String, val email: String)
// Repository
class UserRepository(
private val api: ApiService,
private val dao: UserDao
) {
suspend fun getUser(id: Int): User {
return dao.getUser(id) ?: api.fetchUser(id).also {
dao.insertUser(it)
}
}
}
// ViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<User>>(UiState.Loading)
val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Error")
}
}
}
}
// View (Fragment)
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showUser(state.data)
is UiState.Error -> showError(state.message)
}
}
}
}
}Rarity: Very Common Difficulty: Medium
7. What is Clean Architecture and how do you implement it in Android?
Answer: Clean Architecture separates code into layers with clear dependencies.
- Presentation: UI, ViewModels
- Domain: Use Cases, Business Logic, Entities
- Data: Repositories, Data Sources (API, Database)
- Dependency Rule: Inner layers don't know about outer layers
// Domain Layer - Entities
data class User(val id: Int, val name: String, val email: String)
// Domain Layer - Repository Interface
interface UserRepository {
suspend fun getUser(id: Int): Result<User>
}
// Domain Layer - Use Case
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(id: Int): Result<User> {
return repository.getUser(id)
}
}
// Data Layer - Repository Implementation
class UserRepositoryImpl(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource
) : UserRepository {
override suspend fun getUser(id: Int): Result<User> {
return try {
val localUser = localDataSource.getUser(id)
if (localUser != null) {
Result.success(localUser)
} else {
val remoteUser = remoteDataSource.fetchUser(id)
localDataSource.saveUser(remoteUser)
Result.success(remoteUser)
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
// Presentation Layer - ViewModel
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
fun loadUser(id: Int) {
viewModelScope.launch {
when (val result = getUserUseCase(id)) {
is Result.Success -> handleSuccess(result.data)
is Result.Failure -> handleError(result.exception)
}
}
}
}Rarity: Common Difficulty: Hard
8. Explain Dependency Injection and Dagger/Hilt.
Answer: Dependency Injection provides dependencies to classes instead of creating them internally.
- Benefits: Testability, loose coupling, reusability
- Dagger: Compile-time DI framework
- Hilt: Simplified Dagger for Android
// Define dependencies with Hilt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app_db")
.build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao {
return database.userDao()
}
}
// Repository with injected dependencies
@Singleton
class UserRepository @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) {
suspend fun getUser(id: Int): User {
return userDao.getUser(id) ?: apiService.fetchUser(id).also {
userDao.insertUser(it)
}
}
}
// ViewModel with injected repository
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
// ViewModel logic
}
// Activity/Fragment
@AndroidEntryPoint
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
}Rarity: Very Common Difficulty: Hard
9. What is the Repository pattern and why use it?
Answer: Repository pattern abstracts data sources, providing a clean API for data access.
- Benefits:
- Single source of truth
- Centralized data logic
- Easy to switch data sources
- Testable
- Implementation: Coordinates between multiple data sources
interface UserRepository {
suspend fun getUsers(): Flow<List<User>>
suspend fun getUser(id: Int): User?
suspend fun saveUser(user: User)
suspend fun deleteUser(id: Int)
}
class UserRepositoryImpl @Inject constructor(
private val remoteDataSource: UserRemoteDataSource,
private val localDataSource: UserLocalDataSource,
private val cacheDataSource: UserCacheDataSource
) : UserRepository {
override suspend fun getUsers(): Flow<List<User>> = flow {
// Emit cached data first
val cachedUsers = cacheDataSource.getUsers()
if (cachedUsers.isNotEmpty()) {
emit(cachedUsers)
}
// Fetch from local database
val localUsers = localDataSource.getUsers()
if (localUsers.isNotEmpty()) {
emit(localUsers)
cacheDataSource.saveUsers(localUsers)
}
// Fetch from remote
try {
val remoteUsers = remoteDataSource.fetchUsers()
localDataSource.saveUsers(remoteUsers)
cacheDataSource.saveUsers(remoteUsers)
emit(remoteUsers)
} catch (e: Exception) {
// If remote fails, we already emitted cached/local data
}
}
override suspend fun getUser(id: Int): User? {
return cacheDataSource.getUser(id)
?: localDataSource.getUser(id)
?: remoteDataSource.fetchUser(id)?.also {
localDataSource.saveUser(it)
cacheDataSource.saveUser(it)
}
}
}Rarity: Very Common Difficulty: Medium
10. Explain the Single Activity architecture.
Answer: Single Activity architecture uses one Activity with multiple Fragments, managed by Navigation Component.
- Benefits:
- Simplified navigation
- Shared ViewModels between fragments
- Better animations
- Easier deep linking
- Navigation Component: Handles fragment transactions, back stack, arguments
// Navigation graph (nav_graph.xml)
<navigation>
<fragment
android:id="@+id/homeFragment"
android:name="com.app.HomeFragment">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.app.DetailFragment">
<argument
android:name="userId"
app:argType="integer" />
</fragment>
</navigation>
// MainActivity - single activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = findNavController(R.id.nav_host_fragment)
setupActionBarWithNavController(navController)
}
}
// HomeFragment - navigate to detail
class HomeFragment : Fragment() {
fun navigateToDetail(userId: Int) {
val action = HomeFragmentDirections.actionHomeToDetail(userId)
findNavController().navigate(action)
}
}
// DetailFragment - receive arguments
class DetailFragment : Fragment() {
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val userId = args.userId
viewModel.loadUser(userId)
}
}Rarity: Common Difficulty: Medium
11. What is MVI (Model-View-Intent) architecture?
Answer: MVI is a unidirectional data flow architecture inspired by Redux.
- Components:
- Model: Represents UI state
- View: Renders state, emits intents
- Intent: User actions/events
- Benefits: Predictable state, easier debugging, time-travel debugging
// State
data class UserScreenState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
// Intent (user actions)
sealed class UserIntent {
data class LoadUser(val id: Int) : UserIntent()
object RetryLoading : UserIntent()
}
// ViewModel
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow(UserScreenState())
val state: StateFlow<UserScreenState> = _state.asStateFlow()
fun processIntent(intent: UserIntent) {
when (intent) {
is UserIntent.LoadUser -> loadUser(intent.id)
is UserIntent.RetryLoading -> retryLoading()
}
}
private fun loadUser(id: Int) {
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true, error = null)
try {
val user = repository.getUser(id)
_state.value = _state.value.copy(
isLoading = false,
user = user
)
} catch (e: Exception) {
_state.value = _state.value.copy(
isLoading = false,
error = e.message
)
}
}
}
}
// View
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Send intent
viewModel.processIntent(UserIntent.LoadUser(123))
// Observe state
viewLifecycleOwner.lifecycleScope.launch {
viewModel.state.collect { state ->
render(state)
}
}
}
private fun render(state: UserScreenState) {
when {
state.isLoading -> showLoading()
state.error != null -> showError(state.error)
state.user != null -> showUser(state.user)
}
}
}Rarity: Medium Difficulty: Hard
Performance & Optimization (5 Questions)
12. How do you optimize RecyclerView performance?
Answer: Multiple strategies improve RecyclerView scrolling performance:
- ViewHolder Pattern: Reuse views (built-in)
- DiffUtil: Efficient list updates
- Stable IDs: Override
getItemId()andsetHasStableIds(true) - Prefetching: Increase prefetch distance
- Image Loading: Use libraries like Glide/Coil with proper sizing
- Avoid Heavy Operations: Don't perform expensive calculations in
onBindViewHolder - Nested RecyclerViews: Set
setRecycledViewPool()andsetHasFixedSize(true)
class UserAdapter : ListAdapter<User, UserViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return UserViewHolder(binding)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun getItemId(position: Int): Long {
return getItem(position).id.toLong()
}
}
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
// In Fragment/Activity
recyclerView.apply {
setHasFixedSize(true)
adapter = userAdapter
// Increase prefetch
(layoutManager as? LinearLayoutManager)?.initialPrefetchItemCount = 4
}
// Update list efficiently
userAdapter.submitList(newUsers)Rarity: Very Common Difficulty: Medium
13. How do you detect and fix memory leaks in Android?
Answer: Memory leaks occur when objects are held in memory longer than needed.
- Common Causes:
- Context leaks (Activity/Fragment references)
- Static references
- Anonymous inner classes
- Listeners not unregistered
- Coroutines not cancelled
- Detection Tools:
- LeakCanary library
- Android Studio Memory Profiler
- Heap dumps
// BAD - Memory leak
class MyActivity : AppCompatActivity() {
companion object {
var instance: MyActivity? = null // Static reference leaks
}
private val handler = Handler() // Implicit reference to Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = this
// Anonymous class holds Activity reference
handler.postDelayed({
// Activity might be destroyed
}, 10000)
}
}
// GOOD - No memory leak
class MyActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use weak reference if needed
val weakRef = WeakReference(this)
handler.postDelayed({
weakRef.get()?.let { activity ->
// Safe to use activity
}
}, 10000)
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null) // Clean up
}
}
// ViewModel - survives configuration changes
class MyViewModel : ViewModel() {
private val job = Job()
private val scope = CoroutineScope(Dispatchers.Main + job)
override fun onCleared() {
super.onCleared()
job.cancel() // Cancel coroutines
}
}Rarity: Very Common Difficulty: Medium
14. How do you optimize app startup time?
Answer: Faster startup improves user experience:
- Lazy Initialization: Initialize objects only when needed
- Avoid Heavy Work in Application.onCreate():
- Move to background thread
- Defer non-critical initialization
- Content Providers: Minimize or lazy-load
- Reduce Dependencies: Fewer libraries = faster startup
- App Startup Library: Structured initialization
- Baseline Profiles: Ahead-of-time compilation hints
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Critical initialization only
initCrashReporting()
// Defer non-critical work
lifecycleScope.launch {
delay(100)
initAnalytics()
initImageLoader()
}
// Background initialization
CoroutineScope(Dispatchers.IO).launch {
preloadData()
}
}
}
// App Startup library
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics {
return Analytics.getInstance(context).apply {
initialize()
}
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
// Lazy initialization
class MyActivity : AppCompatActivity() {
private val database by lazy {
Room.databaseBuilder(this, AppDatabase::class.java, "db").build()
}
// Only created when accessed
private val heavyObject by lazy {
createHeavyObject()
}
}Rarity: Common Difficulty: Medium
15. How do you handle bitmap loading and caching efficiently?
Answer: Efficient image handling is crucial for performance:
- Libraries: Glide, Coil (handle caching automatically)
- Manual Optimization:
- Downsampling (load smaller images)
- Memory cache (LruCache)
- Disk cache
- Bitmap pooling
// Using Glide
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.override(width, height) // Resize
.into(imageView)
// Manual implementation with LruCache
class ImageCache {
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
private val cacheSize = maxMemory / 8
private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
fun getBitmap(key: String): Bitmap? {
return memoryCache.get(key)
}
fun putBitmap(key: String, bitmap: Bitmap) {
if (getBitmap(key) == null) {
memoryCache.put(key, bitmap)
}
}
}
// Downsample large images
fun decodeSampledBitmap(file: File, reqWidth: Int, reqHeight: Int): Bitmap {
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeFile(file.path, this)
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
inJustDecodeBounds = false
BitmapFactory.decodeFile(file.path, this)
}
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val (height: Int, width: Int) = options.run { outHeight to outWidth }
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}Rarity: Common Difficulty: Hard
16. What is ANR and how do you prevent it?
Answer: ANR (Application Not Responding) occurs when the main thread is blocked for too long.
- Causes:
- Heavy computation on main thread
- Network calls on main thread
- Database operations on main thread
- Deadlocks
- Prevention:
- Move heavy work to background threads
- Use coroutines with proper dispatchers
- Avoid synchronized blocks on main thread
- Use WorkManager for background tasks
// BAD - Blocks main thread
class BadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ANR risk!
val data = fetchDataFromNetwork() // Blocks UI thread
processLargeFile() // Blocks UI thread
}
}
// GOOD - Background processing
class GoodActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadData() // Handled in ViewModel with coroutines
}
}
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
// Automatically on Main dispatcher
showLoading()
// Switch to IO dispatcher for network
val data = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
// Switch to Default dispatcher for heavy computation
val processed = withContext(Dispatchers.Default) {
processData(data)
}
// Back to Main for UI update
updateUI(processed)
}
}
}Rarity: Common Difficulty: Easy
Testing (3 Questions)
17. How do you write unit tests for ViewModels?
Answer: ViewModels should be tested in isolation with mocked dependencies.
class UserViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private lateinit var viewModel: UserViewModel
private lateinit var repository: UserRepository
@Before
fun setup() {
repository = mockk()
viewModel = UserViewModel(repository)
}
@Test
fun `loadUser success updates state`() = runTest {
// Given
val user = User(1, "John", "john@example.com")
coEvery { repository.getUser(1) } returns Result.success(user)
// When
viewModel.loadUser(1)
// Then
val state = viewModel.uiState.value
assertTrue(state is UiState.Success)
assertEquals(user, (state as UiState.Success).data)
}
@Test
fun `loadUser failure updates error state`() = runTest {
// Given
val exception = Exception("Network error")
coEvery { repository.getUser(1) } returns Result.failure(exception)
// When
viewModel.loadUser(1)
// Then
val state = viewModel.uiState.value
assertTrue(state is UiState.Error)
assertEquals("Network error", (state as UiState.Error).message)
}
}
// MainDispatcherRule for testing coroutines
@ExperimentalCoroutinesApi
class MainDispatcherRule : TestWatcher() {
private val testDispatcher = StandardTestDispatcher()
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}Rarity: Very Common Difficulty: Medium
18. What is the difference between Unit, Integration, and UI tests?
Answer:
- Unit Tests:
- Test individual components in isolation
- Fast, run on JVM
- Mock dependencies
- Example: ViewModel, Repository, Use Case tests
- Integration Tests:
- Test how components work together
- May require Android framework
- Example: Repository with real database
- UI Tests (Instrumented):
- Test user interactions
- Run on device/emulator
- Slower but verify actual behavior
- Example: Espresso tests
// Unit test - JVM
class UserRepositoryTest {
@Test
fun `getUser returns cached user`() {
val cache = mockk<UserCache>()
val api = mockk<ApiService>()
val repository = UserRepository(api, cache)
every { cache.getUser(1) } returns User(1, "John", "john@example.com")
val user = runBlocking { repository.getUser(1) }
assertEquals("John", user.name)
verify(exactly = 0) { api.fetchUser(any()) } // API not called
}
}
// Integration test - Android
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
userDao = database.userDao()
}
@Test
fun insertAndGetUser() = runBlocking {
val user = User(1, "John", "john@example.com")
userDao.insertUser(user)
val retrieved = userDao.getUser(1)
assertEquals(user, retrieved)
}
}
// UI test - Espresso
@RunWith(AndroidJUnit4::class)
class UserActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(UserActivity::class.java)
@Test
fun displayUserName() {
onView(withId(R.id.nameText))
.check(matches(withText("John Doe")))
onView(withId(R.id.loadButton))
.perform(click())
onView(withId(R.id.progressBar))
.check(matches(isDisplayed()))
}
}Rarity: Common Difficulty: Easy
19. How do you test Coroutines and Flow?
Answer: Use testing libraries designed for coroutines.
class FlowTest {
@Test
fun `flow emits correct values`() = runTest {
val flow = flow {
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
val values = flow.toList()
assertEquals(listOf(1, 2, 3), values)
}
@Test
fun `stateFlow updates correctly`() = runTest {
val stateFlow = MutableStateFlow(0)
val job = launch {
stateFlow.emit(1)
stateFlow.emit(2)
}
advanceUntilIdle() // Process all coroutines
assertEquals(2, stateFlow.value)
job.cancel()
}
@Test
fun `turbine library for flow testing`() = runTest {
val flow = flowOf(1, 2, 3)
flow.test {
assertEquals(1, awaitItem())
assertEquals(2, awaitItem())
assertEquals(3, awaitItem())
awaitComplete()
}
}
}Rarity: Common Difficulty: Medium
Security & Best Practices (2 Questions)
20. How do you secure sensitive data in Android?
Answer: Multiple layers protect app and user data:
- EncryptedSharedPreferences: Encrypt preferences
- Keystore: Secure key storage
- Network Security: HTTPS, certificate pinning
- ProGuard/R8: Code obfuscation
- Root Detection: Detect compromised devices
// EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit()
.putString("auth_token", token)
.apply()
// Keystore for encryption keys
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"MyKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(keyGenParameterSpec)
val secretKey = keyGenerator.generateKey()
// Certificate pinning with OkHttp
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()Rarity: Common Difficulty: Hard
21. What are Android best practices for production apps?
Answer: Key practices for production-ready apps:
- Architecture: Use MVVM/Clean Architecture
- Dependency Injection: Hilt for testability
- Error Handling: Proper exception handling, user-friendly messages
- Logging: Structured logging, crash reporting (Firebase Crashlytics)
- Testing: Unit, integration, and UI tests
- Performance: Monitor with Firebase Performance
- Security: Encrypt sensitive data, use HTTPS
- Offline Support: Cache data, handle network errors
- Accessibility: Support TalkBack, content descriptions
- Localization: Support multiple languages
Rarity: Common Difficulty: Medium




