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:
| Aspect | LiveData | StateFlow |
|---|---|---|
| Origins | Android Architecture Components | Kotlin Coroutines |
| Lifecycle | Built-in aware | Manual with operators |
| Platform | Android only | Multiplatform |
| Initial value | Can be null | Required |
| Operators | Limited (map, switchMap) | Rich Flow operators |
| Testing | InstantTaskExecutorRule | runTest, 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:
| Component | Purpose |
|---|---|
| Entity | Table definition (data class) |
| DAO | Database operations (interface) |
| Database | Database 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.
| Feature | WorkManager | Service | Coroutines |
|---|---|---|---|
| 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:
| Aspect | SharedPreferences | DataStore |
|---|---|---|
| API | Synchronous | Asynchronous (Flow) |
| Thread safety | ❌ (can block UI) | ✅ |
| Error handling | No exceptions | Flow catches errors |
| Type safety | Runtime | Compile-time (Proto) |
| Data format | XML | Preferences 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:
| Component | Purpose |
|---|---|
| NavGraph | Define destinations và actions |
| NavHost | Empty container hiển thị destinations |
| NavController | Navigate 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.itemIdCompose 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:
| Component | Scope | Lifetime |
|---|---|---|
SingletonComponent | @Singleton | App |
ActivityRetainedComponent | @ActivityRetainedScoped | ViewModel |
ViewModelComponent | @ViewModelScoped | ViewModel |
ActivityComponent | @ActivityScoped | Activity |
FragmentComponent | @FragmentScoped | Fragment |
// 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
| Need | Library |
|---|---|
| UI state across config changes | ViewModel |
| Local database | Room |
| Guaranteed background work | WorkManager |
| Key-value storage | DataStore |
| Screen navigation | Navigation |
| Large data lists | Paging 3 |
| Dependency injection | Hilt |
| Observe lifecycle | Lifecycle |
| Image loading | Coil |