Bỏ qua để đến nội dung

Jetpack Libraries

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.


Trả lời:

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

flowchart LR
subgraph "Activity Lifecycle"
A1["onCreate"] --> A2["onDestroy<br/>(rotation)"]
A2 --> A3["onCreate"]
A3 --> A4["onDestroy<br/>(finish)"]
end
subgraph "ViewModel Lifecycle"
V1["Created"] --> V2["Active"]
V2 --> V3["Cleared"]
end
A1 -.-> V1
A4 -.-> V3
style V2 fill:#c8e6c9

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


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.


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

Phần tiêu đề “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:

flowchart LR
A["Repository"] --> B["DAO"]
B --> C["Room Database"]
C --> D["SQLite"]
E["Entity"]
E -.-> C
style B fill:#fff3e0
style C fill:#e8f5e9
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?

Phần tiêu đề “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)
}
}

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


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()

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


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

Phần tiêu đề “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.itemId
@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


Trả lời:

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

flowchart LR
A["PagingSource"] --> B["Pager"]
B --> C["PagingData Flow"]
C --> D["LazyPagingItems"]
D --> E["LazyColumn"]
style B fill:#fff3e0
// 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 -> {}
}
}
}

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


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

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