Skip to Content

Câu hỏi phỏng vấn Jetpack Library

Phần này tổng hợp các câu hỏi về Android Jetpack - bộ thư viện giúp phát triển Android app hiện đại.


1. ViewModel & LiveData

Q: ViewModel là gì? Khi nào cần dùng?

Trả lời:

ViewModel là class lưu trữ và quản lý UI-related data, survive configuration changes.

Use cases:

  • ✅ Hold UI state (loading, data, errors)
  • ✅ Business logic cho UI
  • ✅ Communicate with Repository
  • ❌ Reference đến View/Context
class UserViewModel( private val repository: UserRepository ) : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow<UserUiState> = _uiState.asStateFlow() fun loadUser(id: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } repository.getUser(id) .onSuccess { user -> _uiState.update { it.copy(isLoading = false, user = user) } } .onFailure { error -> _uiState.update { it.copy(isLoading = false, error = error.message) } } } } override fun onCleared() { super.onCleared() // Cleanup resources } } // Usage in Compose @Composable fun UserScreen(viewModel: UserViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when { uiState.isLoading -> LoadingIndicator() uiState.error != null -> ErrorMessage(uiState.error) else -> UserContent(uiState.user) } }

📚 Tìm hiểu thêm: ViewModel


Q: LiveData vs StateFlow?

Trả lời:

AspectLiveDataStateFlow
OriginsAndroid Architecture ComponentsKotlin Coroutines
LifecycleBuilt-in awareManual with operators
PlatformAndroid onlyMultiplatform
Initial valueCan be nullRequired
OperatorsLimited (map, switchMap)Rich Flow operators
TestingInstantTaskExecutorRulerunTest, Turbine
// LiveData approach class ViewModel : ViewModel() { private val _data = MutableLiveData<List<User>>() val data: LiveData<List<User>> = _data fun load() { viewModelScope.launch { _data.value = repository.getUsers() } } } // StateFlow approach (Recommended) class ViewModel : ViewModel() { val data: StateFlow<List<User>> = repository.getUsers() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) }

💡 Mẹo: Dùng WhileSubscribed(5000) để giữ flow active 5s sau configuration change, tránh refetch.


2. Room Database

Q: Room Architecture gồm những thành phần nào?

Trả lời:

Room là ORM layer trên SQLite với 3 components chính:

ComponentPurpose
EntityTable definition (data class)
DAODatabase operations (interface)
DatabaseDatabase holder, entry point
// Entity @Entity(tableName = "users") data class UserEntity( @PrimaryKey val id: String, @ColumnInfo(name = "full_name") val name: String, val email: String, val createdAt: Long = System.currentTimeMillis() ) // DAO @Dao interface UserDao { @Query("SELECT * FROM users ORDER BY createdAt DESC") fun getAll(): Flow<List<UserEntity>> @Query("SELECT * FROM users WHERE id = :id") suspend fun getById(id: String): UserEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(user: UserEntity) @Update suspend fun update(user: UserEntity) @Delete suspend fun delete(user: UserEntity) @Query("DELETE FROM users") suspend fun deleteAll() } // Database @Database( entities = [UserEntity::class, PostEntity::class], version = 2, exportSchema = true ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao abstract fun postDao(): PostDao } // Build database val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2) .build()

📚 Tìm hiểu thêm: Room Database


Q: Room Migrations hoạt động như thế nào?

Trả lời:

// Migration from version 1 to 2 val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { // Add new column database.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT") } } // Migration from 2 to 3 val MIGRATION_2_3 = object : Migration(2, 3) { override fun migrate(database: SupportSQLiteDatabase) { // Create new table database.execSQL(""" CREATE TABLE posts ( id TEXT PRIMARY KEY NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, user_id TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ) """) } } // Add migrations to builder Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .fallbackToDestructiveMigration() // Only as last resort! .build()

Testing migrations:

@RunWith(AndroidJUnit4::class) class MigrationTest { @get:Rule val helper = MigrationTestHelper( InstrumentationRegistry.getInstrumentation(), AppDatabase::class.java ) @Test fun migrate1To2() { helper.createDatabase(TEST_DB, 1).apply { execSQL("INSERT INTO users VALUES ('1', 'John')") close() } helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2) } }

3. WorkManager

Q: WorkManager là gì? Khi nào nên dùng?

Trả lời:

WorkManager là API cho deferrable, guaranteed background work.

FeatureWorkManagerServiceCoroutines
Guaranteed execution
Survives app restart
Survives device reboot
Constraints support
Immediate execution

Use cases:

  • ✅ Sync data với server
  • ✅ Upload/download files
  • ✅ Periodic cleanup tasks
  • ❌ Real-time updates (use Service)
// Worker class class SyncWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { val userId = inputData.getString("user_id") ?: return Result.failure() repository.syncUser(userId) Result.success() } catch (e: Exception) { if (runAttemptCount < 3) { Result.retry() } else { Result.failure() } } } } // Schedule work val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>() .setConstraints(constraints) .setInputData(workDataOf("user_id" to userId)) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueueUniqueWork( "sync_$userId", ExistingWorkPolicy.REPLACE, syncRequest ) // Observe work status WorkManager.getInstance(context) .getWorkInfoByIdLiveData(syncRequest.id) .observe(owner) { info -> when (info?.state) { WorkInfo.State.SUCCEEDED -> showSuccess() WorkInfo.State.FAILED -> showError() WorkInfo.State.RUNNING -> showProgress() else -> {} } }

📚 Tìm hiểu thêm: WorkManager


Q: Periodic work vs OneTime work?

Trả lời:

// One-time work val oneTimeRequest = OneTimeWorkRequestBuilder<SyncWorker>() .setInitialDelay(10, TimeUnit.MINUTES) .build() // Periodic work (minimum 15 minutes) val periodicRequest = PeriodicWorkRequestBuilder<CleanupWorker>( repeatInterval = 1, TimeUnit.HOURS, flexInterval = 15, TimeUnit.MINUTES // Can run within last 15 min of interval ) .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "cleanup", ExistingPeriodicWorkPolicy.KEEP, periodicRequest ) // Chain work WorkManager.getInstance(context) .beginWith(downloadWork) .then(processWork) .then(uploadWork) .enqueue()

4. DataStore

Q: DataStore vs SharedPreferences?

Trả lời:

AspectSharedPreferencesDataStore
APISynchronousAsynchronous (Flow)
Thread safety❌ (can block UI)
Error handlingNo exceptionsFlow catches errors
Type safetyRuntimeCompile-time (Proto)
Data formatXMLPreferences or Protocol Buffers
// Preferences DataStore val Context.dataStore by preferencesDataStore(name = "settings") // Keys object PreferencesKeys { val DARK_MODE = booleanPreferencesKey("dark_mode") val USER_NAME = stringPreferencesKey("user_name") val FONT_SIZE = floatPreferencesKey("font_size") } // Write suspend fun setDarkMode(enabled: Boolean) { context.dataStore.edit { prefs -> prefs[PreferencesKeys.DARK_MODE] = enabled } } // Read val darkModeFlow: Flow<Boolean> = context.dataStore.data .catch { exception -> if (exception is IOException) emit(emptyPreferences()) else throw exception } .map { prefs -> prefs[PreferencesKeys.DARK_MODE] ?: false } // In ViewModel class SettingsViewModel(private val dataStore: DataStore<Preferences>) : ViewModel() { val darkMode = dataStore.data .map { it[PreferencesKeys.DARK_MODE] ?: false } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) fun toggleDarkMode() { viewModelScope.launch { dataStore.edit { prefs -> prefs[PreferencesKeys.DARK_MODE] = !(prefs[PreferencesKeys.DARK_MODE] ?: false) } } } }

📚 Tìm hiểu thêm: DataStore


5. Navigation Component

Q: Navigation Component gồm những thành phần nào?

Trả lời:

ComponentPurpose
NavGraphDefine destinations và actions
NavHostEmpty container hiển thị destinations
NavControllerNavigate giữa destinations
// Navigation graph (nav_graph.xml) <navigation xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/nav_graph" app:startDestination="@id/homeFragment"> <fragment android:id="@+id/homeFragment" android:name="com.example.HomeFragment"> <action android:id="@+id/action_home_to_detail" app:destination="@id/detailFragment"/> </fragment> <fragment android:id="@+id/detailFragment" android:name="com.example.DetailFragment"> <argument android:name="itemId" app:argType="string"/> </fragment> </navigation> // Navigate with Safe Args val action = HomeFragmentDirections.actionHomeToDetail(itemId = "123") findNavController().navigate(action) // Receive arguments val args: DetailFragmentArgs by navArgs() val itemId = args.itemId

Compose Navigation:

@Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "home") { composable("home") { HomeScreen( onItemClick = { id -> navController.navigate("detail/$id") } ) } composable( route = "detail/{itemId}", arguments = listOf(navArgument("itemId") { type = NavType.StringType }) ) { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailScreen(itemId = itemId) } } }

📚 Tìm hiểu thêm: Compose Navigation


6. Paging 3

Q: Paging 3 hoạt động như thế nào?

Trả lời:

Paging 3 load data in chunks, handling loading states và errors.

// PagingSource class UserPagingSource( private val api: UserApi ) : PagingSource<Int, User>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> { return try { val page = params.key ?: 1 val response = api.getUsers(page, params.loadSize) LoadResult.Page( data = response.users, prevKey = if (page == 1) null else page - 1, nextKey = if (response.users.isEmpty()) null else page + 1 ) } catch (e: Exception) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { return state.anchorPosition?.let { position -> state.closestPageToPosition(position)?.prevKey?.plus(1) ?: state.closestPageToPosition(position)?.nextKey?.minus(1) } } } // ViewModel class UserViewModel(private val api: UserApi) : ViewModel() { val users: Flow<PagingData<User>> = Pager( config = PagingConfig( pageSize = 20, prefetchDistance = 5, enablePlaceholders = false ), pagingSourceFactory = { UserPagingSource(api) } ).flow.cachedIn(viewModelScope) } // Compose UI @Composable fun UserList(viewModel: UserViewModel) { val users = viewModel.users.collectAsLazyPagingItems() LazyColumn { items(users.itemCount) { index -> users[index]?.let { user -> UserItem(user) } } // Loading states when (users.loadState.append) { is LoadState.Loading -> item { LoadingItem() } is LoadState.Error -> item { RetryItem { users.retry() } } else -> {} } } }

7. Hilt (Dependency Injection)

Q: Hilt scopes và components?

Trả lời:

ComponentScopeLifetime
SingletonComponent@SingletonApp
ActivityRetainedComponent@ActivityRetainedScopedViewModel
ViewModelComponent@ViewModelScopedViewModel
ActivityComponent@ActivityScopedActivity
FragmentComponent@FragmentScopedFragment
// Application @HiltAndroidApp class MyApplication : Application() // Module with Singleton scope @Module @InstallIn(SingletonComponent::class) object AppModule { @Provides @Singleton fun provideDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build() } @Provides @Singleton fun provideRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() } } // ViewModel with injection @HiltViewModel class UserViewModel @Inject constructor( private val repository: UserRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() // Activity @AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: UserViewModel by viewModels() }

📚 Tìm hiểu thêm: Dependency Injection


📝 Jetpack Library Selection Guide

NeedLibrary
UI state across config changesViewModel
Local databaseRoom
Guaranteed background workWorkManager
Key-value storageDataStore
Screen navigationNavigation
Large data listsPaging 3
Dependency injectionHilt
Observe lifecycleLifecycle
Image loadingCoil

📚 Tìm hiểu thêm: Coil, Retrofit

Last updated on