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

Architecture & Design Patterns

Phần này tổng hợp các câu hỏi về Architecture patternsDesign principles trong Android development.


Q: MVVM pattern là gì? Giải thích từng layer?

Phần tiêu đề “Q: MVVM pattern là gì? Giải thích từng layer?”

Trả lời:

MVVM là architecture pattern tách biệt UI logic khỏi business logic:

flowchart LR
V["View<br/>(Activity/Fragment/<br/>Composable)"] <--> VM["ViewModel<br/>(UI State +<br/>Business Logic)"]
VM <--> M["Model<br/>(Repository/<br/>Data Sources)"]
style V fill:#e3f2fd
style VM fill:#fff3e0
style M fill:#e8f5e9
Layer Responsibility Android Components
Model Data + business logic Repository, Room, Retrofit
View Display UI, handle input Activity, Fragment, Composable
ViewModel Hold UI state, transform data ViewModel class
// Model (Repository)
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): User = api.fetchUser(id)
}
// ViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
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)
}
}
}
}
// View (Compose)
@Composable
fun UserScreen(viewModel: UserViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserContent(uiState.user)
is UiState.Error -> ErrorText(uiState.message)
}
}

Lợi ích:

  • ✅ Separation of concerns
  • ✅ Testable business logic
  • ✅ Survives configuration changes

📚 Tìm hiểu thêm: MVVM trong Android


Q: ViewModel survive configuration change như thế nào?

Phần tiêu đề “Q: ViewModel survive configuration change như thế nào?”

Trả lời:

ViewModelStore được retain bởi ViewModelStoreOwner (Activity/Fragment) qua configuration changes.

flowchart TD
A["Activity Created"] --> B["ViewModelStore Created"]
B --> C["ViewModel Created"]
C --> D["Configuration Change"]
D --> E["Activity Destroyed"]
E --> F["Activity Recreated"]
F --> G["Same ViewModelStore"]
G --> H["Same ViewModel Instance"]
style D fill:#fff3e0
style H fill:#c8e6c9

Internal mechanism:

  1. ViewModelStore lưu trữ ViewModel instances theo key
  2. Khi configuration change, NonConfigurationInstances giữ reference đến ViewModelStore
  3. Activity mới nhận lại cùng ViewModelStore
// ViewModel tồn tại qua rotation
class MainViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data = _data.asStateFlow()
init {
loadData() // Called only once, not on rotation
}
override fun onCleared() {
// Called when Activity is truly finishing
}
}

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


Trả lời:

MVI thêm unidirectional data flow với Intent (user actions):

Aspect MVVM MVI
Data Flow Bidirectional Unidirectional
State Multiple LiveData/Flows Single immutable state
Events Direct function calls Intent objects
Complexity Simpler More structured
flowchart LR
V["View"] -->|"Intent"| VM["ViewModel"]
VM -->|"new State"| V
VM --> M["Model"]
M --> VM
style V fill:#e3f2fd
style VM fill:#fff3e0
// Intent (User Actions)
sealed class UserIntent {
data class LoadUser(val id: String) : UserIntent()
object Refresh : UserIntent()
data class UpdateName(val name: String) : UserIntent()
}
// State (Single source of truth)
data class UserState(
val isLoading: Boolean = false,
val user: User? = null,
val error: String? = null
)
// ViewModel processes intents
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow(UserState())
val state = _state.asStateFlow()
fun processIntent(intent: UserIntent) {
when (intent) {
is UserIntent.LoadUser -> loadUser(intent.id)
is UserIntent.Refresh -> refresh()
is UserIntent.UpdateName -> updateName(intent.name)
}
}
private fun loadUser(id: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
try {
val user = repository.getUser(id)
_state.update { it.copy(isLoading = false, user = user) }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}
}
// View sends intents
@Composable
fun UserScreen(viewModel: UserViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.processIntent(UserIntent.LoadUser("123"))
}
Button(onClick = { viewModel.processIntent(UserIntent.Refresh) }) {
Text("Refresh")
}
}

Khi nào dùng MVI:

  • ✅ Complex state với nhiều interactions
  • ✅ Need time-travel debugging
  • ✅ Team prefers strict patterns

Trả lời:

Clean Architecture chia app thành layers với dependency rule: outer layers depend on inner layers.

flowchart TB
subgraph Presentation["Presentation Layer"]
UI["UI<br/>(Compose/XML)"]
VM["ViewModel"]
end
subgraph Domain["Domain Layer"]
UC["Use Cases"]
RE["Repository<br/>Interfaces"]
EN["Entities"]
end
subgraph Data["Data Layer"]
RI["Repository<br/>Implementation"]
DS["Data Sources<br/>(API, DB)"]
end
UI --> VM
VM --> UC
UC --> RE
RI -.-> RE
RI --> DS
style Domain fill:#e8f5e9
Layer Contents Dependencies
Domain Use Cases, Entities, Repository interfaces None (pure Kotlin)
Data Repository impl, Data Sources, Mappers Domain
Presentation UI, ViewModels Domain
// Domain Layer - Pure Kotlin, no Android dependencies
data class User(val id: String, val name: String)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun saveUser(user: User)
}
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(id: String): Result<User> {
return runCatching { repository.getUser(id) }
}
}
// Data Layer
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: String): User {
return try {
api.fetchUser(id).also { dao.insert(it.toEntity()) }
} catch (e: Exception) {
dao.getById(id)?.toUser() ?: throw e
}
}
}
// Presentation Layer
class UserViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
fun loadUser(id: String) {
viewModelScope.launch {
getUserUseCase(id)
.onSuccess { _user.value = it }
.onFailure { _error.value = it.message }
}
}
}

📚 Tìm hiểu thêm: Clean Architecture


Trả lời:

DI là pattern cung cấp dependencies từ bên ngoài thay vì tạo trong class.

// ❌ Without DI - tight coupling
class UserViewModel {
private val repository = UserRepository(
UserApi(RetrofitClient.create()),
UserDatabase.getInstance(context).userDao()
)
}
// ✅ With DI - loose coupling
class UserViewModel(
private val repository: UserRepository // Injected
) : ViewModel()

Hilt vs Koin:

Aspect Hilt Koin
Type Compile-time Runtime
Error Detection Compile time Runtime
Performance Faster (no reflection) Slower (uses reflection)
Setup More boilerplate Simpler
Google Support ✅ Official Community

Hilt Example:

// Module
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideUserRepository(
api: UserApi,
dao: UserDao
): UserRepository = UserRepositoryImpl(api, dao)
}
// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()
// Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity()

Koin Example:

// Module
val appModule = module {
single<UserRepository> { UserRepositoryImpl(get(), get()) }
viewModel { UserViewModel(get()) }
}
// Application
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
modules(appModule)
}
}
}
// Usage
class UserViewModel(
private val repository: UserRepository
) : ViewModel()

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


Trả lời:

Repository là abstraction layer giữa data sources và business logic:

flowchart LR
VM["ViewModel"] --> R["Repository"]
R --> A["Remote<br/>(API)"]
R --> L["Local<br/>(Room)"]
R --> C["Cache<br/>(Memory)"]
style R fill:#fff3e0

Responsibilities:

  • Abstract data sources
  • Implement caching strategy
  • Handle offline-first logic
  • Map DTOs to domain entities
class UserRepository(
private val api: UserApi,
private val dao: UserDao,
private val cache: UserCache
) {
// Single source of truth pattern
fun getUsers(): Flow<List<User>> = flow {
// 1. Emit cached data first
cache.getUsers()?.let { emit(it) }
// 2. Fetch from network
try {
val users = api.fetchUsers()
dao.insertAll(users.map { it.toEntity() })
cache.setUsers(users)
emit(users)
} catch (e: Exception) {
// 3. Fallback to local DB
emit(dao.getAll().map { it.toUser() })
}
}
// Offline-first with NetworkBoundResource
fun getUser(id: String): Flow<Resource<User>> = networkBoundResource(
query = { dao.getById(id).map { it?.toUser() } },
fetch = { api.fetchUser(id) },
saveFetchResult = { dao.insert(it.toEntity()) },
shouldFetch = { it == null || cache.isExpired(id) }
)
}

Trả lời:

Principle Description Android Example
Single Responsibility One class, one job Separate ViewModel, Repository
Open/Closed Open for extension, closed for modification Use interfaces
Liskov Substitution Subtypes replaceable Repository implementations
Interface Segregation Small, focused interfaces Separate Dao interfaces
Dependency Inversion Depend on abstractions Inject interfaces, not implementations
// Single Responsibility
class UserRepository { /* only data operations */ }
class UserValidator { /* only validation */ }
class UserMapper { /* only mapping */ }
// Open/Closed - extend via interfaces
interface DataSource<T> {
suspend fun get(id: String): T?
suspend fun save(item: T)
}
class ApiDataSource : DataSource<UserDto> { /* ... */ }
class LocalDataSource : DataSource<UserEntity> { /* ... */ }
// Dependency Inversion
class UserViewModel(
private val repository: UserRepository // Interface, not UserRepositoryImpl
) : ViewModel()

Trả lời:

Mỗi component chỉ làm một việc:

Component Concern Should NOT
Activity/Fragment Host UI, handle navigation Business logic, data fetching
Composable Render UI Direct API calls
ViewModel UI state, orchestrate Database access directly
Repository Data operations UI updates
Use Case Business rules Data persistence
// ❌ Bad - Activity does everything
class BadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val users = Retrofit.create(UserApi::class.java)
.fetchUsers() // Network in Activity!
Room.databaseBuilder(this, AppDb::class.java, "db")
.build().userDao().insertAll(users) // DB in Activity!
}
}
// ✅ Good - Separation of concerns
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val users by viewModel.users.collectAsStateWithLifecycle()
LazyColumn {
items(users) { UserItem(it) }
}
}
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users = repository.getUsers().stateIn(
viewModelScope, SharingStarted.WhileSubscribed(), emptyList()
)
}

Scenario Recommended
Small app, simple state MVVM
Complex state, many interactions MVI
Large team, maintainability Clean Architecture
Quick prototype MVVM + Koin
Production app, Google stack MVVM + Hilt
Multiple data sources Repository pattern

📚 Tìm hiểu thêm: MVVM, Clean Architecture