Architecture Patterns
Xây dựng kiến trúc tốt giúp ứng dụng dễ test, bảo trì và mở rộng. Bài này hướng dẫn các patterns tốt nhất khi làm việc với Compose.
1. Unidirectional Data Flow (UDF)
Phần tiêu đề “1. Unidirectional Data Flow (UDF)”Nguyên tắc
Phần tiêu đề “Nguyên tắc”- State flows down (từ ViewModel xuống UI)
- Events flow up (từ UI lên ViewModel)
┌─────────────────────────────────────────┐│ ││ ViewModel ────────▶ State ──▶ UI ││ ▲ │ ││ │ ▼ ││ └────── Events ◀───── User ││ │└─────────────────────────────────────────┘Implementation
Phần tiêu đề “Implementation”// UI State - represents what to displaydata class HomeUiState( val items: List<Item> = emptyList(), val isLoading: Boolean = false, val error: String? = null)
// ViewModel - manages stateclass HomeViewModel : ViewModel() { private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
fun onRefresh() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } try { val items = repository.getItems() _uiState.update { it.copy(items = items, isLoading = false) } } catch (e: Exception) { _uiState.update { it.copy(error = e.message, isLoading = false) } } } }}
// UI - displays state and sends events@Composablefun HomeScreen(viewModel: HomeViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState()
HomeContent( uiState = uiState, onRefresh = viewModel::onRefresh )}2. UI State Pattern
Phần tiêu đề “2. UI State Pattern”Sealed class cho các states
Phần tiêu đề “Sealed class cho các states”sealed interface HomeUiState { object Loading : HomeUiState data class Success(val items: List<Item>) : HomeUiState data class Error(val message: String) : HomeUiState}
@Composablefun HomeScreen(uiState: HomeUiState) { when (uiState) { is HomeUiState.Loading -> LoadingIndicator() is HomeUiState.Success -> ItemList(uiState.items) is HomeUiState.Error -> ErrorMessage(uiState.message) }}Data class với multiple fields
Phần tiêu đề “Data class với multiple fields”data class HomeUiState( val items: List<Item> = emptyList(), val isLoading: Boolean = false, val isRefreshing: Boolean = false, val error: String? = null, val selectedItem: Item? = null)3. State Holders
Phần tiêu đề “3. State Holders”Screen-level State Holder
Phần tiêu đề “Screen-level State Holder”class HomeScreenState( val listState: LazyListState, val scaffoldState: ScaffoldState, private val scope: CoroutineScope) { val showFab: Boolean get() = listState.firstVisibleItemIndex > 0
fun scrollToTop() { scope.launch { listState.animateScrollToItem(0) } }
fun showSnackbar(message: String) { scope.launch { scaffoldState.snackbarHostState.showSnackbar(message) } }}
@Composablefun rememberHomeScreenState( listState: LazyListState = rememberLazyListState(), scaffoldState: ScaffoldState = rememberScaffoldState(), scope: CoroutineScope = rememberCoroutineScope()) = remember(listState, scaffoldState, scope) { HomeScreenState(listState, scaffoldState, scope)}
@Composablefun HomeScreen() { val screenState = rememberHomeScreenState() // Use screenState}4. Event Handling Patterns
Phần tiêu đề “4. Event Handling Patterns”Events as functions
Phần tiêu đề “Events as functions”@Composablefun ProductScreen( uiState: ProductUiState, onAddToCart: (Product) -> Unit, onNavigateToDetail: (String) -> Unit, onRefresh: () -> Unit) { // UI implementation}Events as sealed class
Phần tiêu đề “Events as sealed class”sealed interface ProductEvent { data class AddToCart(val product: Product) : ProductEvent data class NavigateToDetail(val productId: String) : ProductEvent object Refresh : ProductEvent}
class ProductViewModel : ViewModel() { fun onEvent(event: ProductEvent) { when (event) { is ProductEvent.AddToCart -> addToCart(event.product) is ProductEvent.NavigateToDetail -> { /* Navigation handled by UI */ } is ProductEvent.Refresh -> refresh() } }}
@Composablefun ProductScreen( viewModel: ProductViewModel, onNavigate: (String) -> Unit) { val uiState by viewModel.uiState.collectAsState()
ProductContent( uiState = uiState, onEvent = { event -> when (event) { is ProductEvent.NavigateToDetail -> onNavigate(event.productId) else -> viewModel.onEvent(event) } } )}5. ViewModel trong Compose
Phần tiêu đề “5. ViewModel trong Compose”Basic usage
Phần tiêu đề “Basic usage”@Composablefun UserScreen( viewModel: UserViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState() UserContent(uiState)}Với Hilt
Phần tiêu đề “Với Hilt”@HiltViewModelclass UserViewModel @Inject constructor( private val userRepository: UserRepository) : ViewModel() { // ...}
@Composablefun UserScreen( viewModel: UserViewModel = hiltViewModel()) { // ...}Với SavedStateHandle
Phần tiêu đề “Với SavedStateHandle”@HiltViewModelclass DetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: Repository) : ViewModel() { private val id: String = checkNotNull(savedStateHandle["id"])
val uiState = repository.getItem(id) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)}6. Navigation Patterns
Phần tiêu đề “6. Navigation Patterns”Stateless screens
Phần tiêu đề “Stateless screens”// Screen composable không biết về navigation@Composablefun ProfileScreen( uiState: ProfileUiState, onEditClick: () -> Unit, onLogoutClick: () -> Unit)
// Navigation wrapper xử lý navigation@Composablefun ProfileRoute( navController: NavController, viewModel: ProfileViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsState()
ProfileScreen( uiState = uiState, onEditClick = { navController.navigate("edit") }, onLogoutClick = { viewModel.logout() navController.navigate("login") { popUpTo(0) { inclusive = true } } } )}NavHost setup
Phần tiêu đề “NavHost setup”@Composablefun AppNavHost(navController: NavHostController) { NavHost(navController, startDestination = "home") { composable("home") { HomeRoute(navController) } composable("profile") { ProfileRoute(navController) } composable( route = "detail/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType }) ) { DetailRoute(navController) } }}7. Dependency Injection với Compose
Phần tiêu đề “7. Dependency Injection với Compose”Hilt setup
Phần tiêu đề “Hilt setup”// Application class@HiltAndroidAppclass MyApp : Application()
// Activity@AndroidEntryPointclass MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyAppTheme { AppNavHost() } } }}
// ViewModel@HiltViewModelclass HomeViewModel @Inject constructor( private val repository: HomeRepository) : ViewModel()
// In Composable@Composablefun HomeScreen( viewModel: HomeViewModel = hiltViewModel())Provide dependencies với CompositionLocal
Phần tiêu đề “Provide dependencies với CompositionLocal”val LocalAnalytics = staticCompositionLocalOf<Analytics> { error("No Analytics provided")}
@Composablefun App() { CompositionLocalProvider( LocalAnalytics provides AnalyticsImpl() ) { AppNavHost() }}
// Usage@Composablefun SomeScreen() { val analytics = LocalAnalytics.current analytics.logScreen("some_screen")}8. Layers và Separation
Phần tiêu đề “8. Layers và Separation”Recommended Structure
Phần tiêu đề “Recommended Structure”app/├── ui/│ ├── home/│ │ ├── HomeScreen.kt # UI composables│ │ ├── HomeViewModel.kt # ViewModel│ │ └── HomeUiState.kt # UI state model│ ├── detail/│ └── theme/├── domain/│ ├── model/ # Domain models│ ├── repository/ # Repository interfaces│ └── usecase/ # Use cases (optional)└── data/ ├── repository/ # Repository implementations ├── remote/ # API, Network └── local/ # Room, DataStoreUI Layer independence
Phần tiêu đề “UI Layer independence”// UI không biết về data layer@Composablefun UserCard( user: User, // Domain model onFollowClick: () -> Unit)
// ViewModel chuyển đổi data → UI stateclass UserViewModel @Inject constructor( private val userRepository: UserRepository) : ViewModel() { val users = userRepository.getUsers() .map { users -> users.map { it.toUiModel() } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Pattern | Mục đích |
|---|---|
| UDF | State down, Events up |
| UI State | Single source of truth cho UI |
| State Holder | Quản lý UI state phức tạp |
| ViewModel | Business logic, survive config changes |
| Stateless Screens | Dễ test, reusable |
| DI | Loose coupling, testability |
Architecture Checklist
Phần tiêu đề “Architecture Checklist”- UI State là single source of truth
- State flows down, Events flow up
- Screen composables stateless (nhận state, emit events)
- ViewModel xử lý business logic
- Navigation logic tách khỏi UI
- Dependencies được inject (Hilt/Koin)