Android Interview Series 2024 ā Part 8 (Android architecture)
Android Interview Series 2024 ā Part 8 (Android architecture) ź“ė Ø
This is Part 8 of the android interview question series. This part will focus on Android architecture.
Android Interview Series 2024
- Part 1 ā Android basics
- Part 2 ā Android experts
- Part 3 ā Java basics
- Part 4 ā Kotlin basics
- Part 5 ā Kotlin coroutines
- Part 6 ā Kotlin Flows
- Part 7 ā Jetpack Compose
- Part 8 ā Android architecture & framework ->Ā You are here
1. Can you explain the MVC and MVP patterns? What are the main differences and why are they not used in Android development?
- MVCĀ is theĀ Model-View-ControllerĀ architecture where model refers to the data model classes. The view refers to the xml files and the controller handles the business logic. The issue with this architecture is unit testing. The model can be easily tested since it is not tied to anything. The controller is tightly coupled with the android apis making it difficult to unit test. Modularity & flexibility is a problem since the view and the controller are tightly coupled. If we change the view, the controller logic should also be changed. Maintenance is also an issue.
- MVP architecture:Ā Model-View-Presenter architecture. MVP separates concerns by using aĀ PresenterĀ to handle business logic, theĀ ViewĀ (often an Activity or Fragment) to display UI, and theĀ ModelĀ to manage data. In this setup, the Presenter is responsible for updating the View based on the Modelās data and handling user actions, making it testable and reducing the burden on the Android lifecycle-aware View. This separation improves testability since the Presenter can be tested independently of the Android framework. But theĀ PresenterĀ does not inherently respond to lifecycle events like configuration changes (e.g., screen rotations), which means that extra handling is often required to manage these situations.
2. What is MVVM architecture in android?
MVVM (Model-View-View Model) architecture:Ā MVVM leveragesĀ ViewModel, a lifecycle-aware component that holds and processes data for the UI, separating it from theĀ ViewĀ (Activity/Fragment). TheĀ ModelĀ represents the data layer and interacts with the ViewModel, which then updates the View using LiveData, DataBinding, or StateFlow. The ViewModel handles data and business logic, while the View observes changes and updates UI reactively, which is lifecycle-aware and thus avoids memory leaks and configuration issues.
3. What are the main advantages and disadvantages of using MVVM in Android development?
Advantages include:
- MVVM encourages a clear separation between the UI (View), business logic (ViewModel), and data (Model). This makes the codebase more modular, organized, and easier to maintain.
- The ViewModel is lifecycle-aware, meaning it retains its data across configuration changes like screen rotations. This lifecycle management reduces memory leaks and simplifies handling UI-related data.
- The ViewModel contains the business logic and is decoupled from the Android UI framework, making it easier to test independently. The Model and ViewModel can be unit-tested, which improves code quality and reliability.
- MVVM enables a reactive approach where the View observes data changes in the ViewModel, allowing for automatic UI updates without manual intervention. Using LiveData, Flow, or StateFlow, the View automatically reacts to data changes, which can result in a more responsive and dynamic UI.
Some disadvantages might include:
- MVVM, along with components like LiveData, DataBinding, and Flow, requires a deeper understanding of reactive programming and lifecycle management, which can be challenging for developers new to these concepts.
- Using DataBinding in MVVM can reduce some code but also add boilerplate, especially if implemented extensively. Additionally, debugging can be more complex with DataBinding, as errors may not always be immediately apparent.
4. How do you manage the āfat ViewModelā problem in MVVM?
In MVVM, the ViewModel can easily become āfatā if too much logic is placed there, especially as the app grows. Handling multiple ViewModel responsibilities can lead to complex, hard-to-maintain code if not carefully managed. This is called the āfat ViewModelā problem. In order to avoid that,
- Use the Repository Pattern: The Repository acts as a single source of truth for data, abstracting data access layers (local databases, network, cache) from the ViewModel. By offloading data-fetching and manipulation responsibilities to the Repository, the ViewModel only needs to manage UI data, reducing its workload.
- Delegate Business Logic to Use Cases or Interactors: Use Cases are components that encapsulate a specific piece of business logic. By moving business logic out of the ViewModel into Use Cases, each Use Case is responsible for a single action, keeping the ViewModel focused on UI state and interactions. This approach makes it easier to test logic independently and enhances reusability.
- Utilize Separate State Management Classes: State management classes manage UI states and UI events, helping organize complex UI-related data. If your ViewModel has many UI states, creating a specific state management class can centralize the handling of UI states, reducing clutter in the ViewModel.
- Use Event Wrappers for One-Time Events: Event wrappers (e.g., SingleLiveEvent, Event classes) handle events that should only be consumed once, like navigation triggers or toast messages. By using event wrappers, the ViewModel doesnāt need complex logic to manage one-time events, simplifying its responsibilities and making UI event handling more predictable.
5. What is MVI architecture and what are itās core concepts?
MVI (Model-View-Intent)Ā is an architecture pattern that is inspired by functional and reactive programming principles.
- TheĀ ModelĀ in MVI represents the applicationās state and data. It holds all the information needed to render the UI at any given time.
- TheĀ ViewĀ in MVI is responsible for rendering the UI based on the Modelās state. The View receives the state updates from the Model and re-renders itself accordingly, which means the View is entirely reactive and doesnāt hold any logic.
- IntentsĀ represent user actions or events, such as button clicks, text input, or system events. Intents are like requests to change the state of the application. They are dispatched from the View to the Model, which processes the intent and updates the state accordingly.
Key principles of MVI
- MVI promotes a unidirectional data flow: Intents are sent from the View to the Model, which processes them and returns a new state back to the View. This clear flow helps to prevent unpredictable state changes and race conditions.
- The state in MVI is usually managed in one central location (often a single state object), ensuring there is a single source of truth for the UI. This single state object holds all relevant data for rendering the UI, which allows for easy testing, debugging, and state persistence, especially in cases where configuration changes or complex UI flows are involved.
- MVI encourages immutable states, meaning each change creates a new state rather than modifying the existing one. This immutability helps prevent unintended side effects and makes the state transitions easy to trace.
6. What are some scenarios where MVI might be a better fit than MVVM?
Jetpack Compose aligns well with MVI principles. The declarative nature of Compose reduces the complexity of handling user interactions and ensures that the UI stays in sync with the application state.
- In MVI, the UI state is managed by the ViewModel, often usingĀ
StateFlow
Ā orĀLiveData
. This state remains immutable, ensuring UI updates are predictable. Composables observe the state and recompose automatically when the state changes. - Event/Intent: User interactions, like button clicks, are captured as events (Intents) and sent to the ViewModel for handling. Intents guide the ViewModel on how to respond to user actions, such as fetching data or adding a user, and the ViewModel adjusts the state based on these actions. This structured flow ensures smooth transitions in state and UI updates.
- Effects handle one-time actions like showing a snackbar or navigation. MVI manages effects using channels, allowing the ViewModel to dispatch them without affecting the appās overall state.
7. Can you describe Clean Architecture? What layers would you typically create in a Clean Architecture setup, and whatās the purpose of each?
In Android development,Ā clean architectureĀ design approach provides a structured way to organize code into layers, each with its own responsibilities and dependencies, making the application more maintainable, flexible, and testable. Typically, Clean Architecture consists of four layers:
- Presentation Layer: Responsible for the UI and the communication between the user and the app. This is where you handle user input, present data to the user, and manage UI state. Eg: activities, fragments, composables etc.
- Domain Layer: Contains theĀ business logicĀ andĀ use casesĀ (or interactors) of the application. Executes business rules and logic without directly interacting with data sources or the UI. This layer is agnostic to platform or framework, making it suitable for pure unit testing. Eg: repository, entity, use cases etc.
- Data Layer: Responsible for managing data sources and implementing theĀ Repository pattern. It provides data to the Domain Layer by implementing the Repository interfaces defined in the Domain. Eg: Retrofit, Room db, Paging source etc.
- Framework and UI Layer: Houses platform-specific elements and external frameworks that your application depends on, such as Android SDK components, dependency injection frameworks (like Dagger or Hilt), and navigation components.
8. How do you handle communication between different layers in a Clean Architecture setup?
- Communication from Presentation Layer to Domain Layer: can happen usingĀ Use Cases. Use Cases are oftenĀ
suspend
Ā functions (or returnĀ FlowĀ orĀ LiveData) to support asynchronous operations. The Presentation Layer then collects the data or subscribes to updates and updates the UI accordingly.
// Presentation Layer (ViewModel) calls a Use Case
class ProfileViewModel(private val getUserProfile: GetUserProfile) : ViewModel() {
val userProfile = MutableLiveData<User>()
fun loadProfile(userId: String) {
viewModelScope.launch {
val result = getUserProfile(userId)
userProfile.value = result
}
}
}
- Communication from Domain Layer to Data Layer: The Domain Layer interacts with data through theĀ Repository pattern. The Repository interface is defined in the Domain Layer, while the actual implementation resides in the Data Layer. The Domain Layer doesnāt know where the data is coming from (e.g., network, database). It simply calls theĀ
UserRepository
Ā interface, which the Data Layer implements.
// Domain Layer (Use Case)
class GetUserProfile(private val userRepository: UserRepository) {
suspend operator fun invoke(userId: String): User {
return userRepository.getUserById(userId)
}
}
// Data Layer (Repository Implementation)
class UserRepositoryImpl(private val apiService: ApiService, private val userDao: UserDao) : UserRepository {
override suspend fun getUserById(userId: String): User {
// Decide whether to fetch data from network or local database
return apiService.getUserById(userId)
}
}
- Communication within the Data Layer:Ā The Repository in the Data Layer manages communication between various data sources (e.g., remote API, local database, cache).Data Sources: The Data Layer might have separateĀ Data SourcesĀ for handling network requests, local database interactions, and cache. These data sources are abstracted within the Repository, so the Repository decides the source and manages caching logic if necessary.
// Data Layer: Example of combining data sources
class UserRepositoryImpl(
private val apiService: ApiService,
private val userDao: UserDao
) : UserRepository {
override suspend fun getUserById(userId: String): User {
return try {
// Fetch from remote API
val userDto = apiService.getUserById(userId)
// Save to local database for caching
userDao.insertUser(userDto.toUserEntity())
userDto.toUser() // Convert DTO to domain model
} catch (exception: Exception) {
// Fallback to local database if network fails
userDao.getUserById(userId).toDomainModel()
}
}
}
- Data Flow from Data Layer Back to Domain and Presentation Layers:Ā The Data Layer can expose data asĀ Flows,Ā LiveData, orĀ suspend functionsĀ that the Domain Layer or Presentation Layer can observe or collect.
// Data Layer exposes data as Flow
override fun getUserById(userId: String): Flow<User> = flow {
val userEntity = userDao.getUserById(userId)
emit(userEntity.toDomainModel()) // Emit as domain model
}
9. Can you explain the Repository pattern and how it helps with code organization and separation of concerns?
- The Repository Pattern is a design pattern used to abstract and centralize data access logic, which helps with code organization and separation of concerns. It serves as a bridge between the Domain Layer (business logic) and the Data Layer (data sources such as local databases, remote APIs, and cache). This allows the applicationās core logic to remain independent of specific data source implementations, making the codebase easier to maintain, test, and scale.
- The Repository Pattern abstracts data access, hiding the details of data sources from the rest of the app.
- Separates concerns by isolating data handling logic, keeping the UI and Domain layers focused on their specific roles.
- Improves testability by allowing you to mock the Repository for unit tests.
- Centralizes data management and caching, making it easier to implement and maintain complex data operations.
10. How would you design a Repository to interact with a local database and a remote API in an Android application?
// Domain Layer - Repository Interface
interface ProductRepository {
suspend fun getProducts(): List<Product>
}
// Data Layer - Repository Implementation
class ProductRepositoryImpl(
private val apiService: ApiService,
private val productDao: ProductDao
) : ProductRepository {
override suspend fun getProducts(): List<Product> {
return try {
val products = apiService.fetchProducts()
productDao.insertProducts(products) // Cache data
products
} catch (e: Exception) {
productDao.getProducts() // Retrieve from local database on error
}
}
}
In this example:
- The
ProductRepository
interface is defined in the Domain Layer, which is agnostic to where the data comes from. - The
ProductRepositoryImpl
in the Data Layer fetches data from an API and caches it locally. If the API call fails, it retrieves data from the local database. - This pattern allows the rest of the app to call
getProducts()
without knowing or managing data retrieval or caching strategies.