MVC & DI in Android
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
MVC pattern aims to separate responsibilities and maintain a clear division between the view, controller, and model. The model represents the data and business logic, the view handles the UI presentation, and the controller handles user interactions and updates between the model and view.
Note that in Android, the traditional MVC architecture is often modified to fit the framework's specific patterns and components.
Model-View-Controller (MVC):
Overview: MVC is one of the oldest architectural patterns, widely used in many software development paradigms.
Key Concepts: Model represents data and business logic, View displays the UI, and Controller handles user interactions and updates the Model and View.
Benefits: Separation of concerns, code reusability, and testability.
Use Cases: Traditional web applications, GUI-based applications.
? ? +-------------------+
? ? |? ? ? Activity? ? ?|
? ? +---------+---------+
? ? ? ? ? ? ? |
? ? ? ? ? ? ? |? ?Interacts with
? ? ? ? ? ? ? |
? ? +---------v---------+
? ? |? ? Controller? ? ?|
? ? +---------+---------+
? ? ? ? ? ? ? |
? ? ? ? ? ? ? |? ?Manipulates
? ? ? ? ? ? ? |
? ? +---------v---------+
? ? |? ? ? ?Model? ? ? ?|
? ? +-------------------+
In the MVC architecture:
Communication flow:
It's important to note that the View and the Controller have a one-to-one relationship, meaning that each View has its corresponding Controller.
The MVC architecture separates the concerns by keeping the View responsible for UI-related tasks, the Controller handling the logic and interaction with the View and Model, and the Model representing the data and business logic.
Benefits:
Limitations of MVC:
Implementation:
In the MVC architecture, the view and model should not have direct references to each other. The controller acts as an intermediary between the view and model, handling user interactions and updating the model and view accordingly.
Here's a simple example to demonstrate the MVC pattern in Android:
-------------------------------------------------------------------------------------
View (MainActivity.kt):
class MainActivity : AppCompatActivity() {
? ? private lateinit var controller: UserController
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_main)
? ? ? ??
? ? ? ? controller = UserController(this)
? ? ? ? // Set up UI components and event listeners
? ? ? ? val button = findViewById<Button>(R.id.button)
? ? ? ? button.setOnClickListener {
? ? ? ? ? ? controller.onButtonClick()
? ? ? ? }
? ? }
? ? fun displayUserInfo(user: User) {
? ? ? ? // Update UI to display user information
? ? ? ? val textView = findViewById<TextView>(R.id.textView)
? ? ? ? textView.text = "User ID: ${user.id}\nUser Name: ${user.name}"
? ? }
}
------------------------------------------------------------------------------------------------
Controller (UserController.kt):
class UserController(private val context: Context) {
? ? private val model: UserModel = UserModel()
? ? fun onButtonClick() {
? ? ? ? val user = model.getUser()
? ? ? ? if (context is MainActivity) {
? ? ? ? ? ? context.displayUserInfo(user)
? ? ? ? }
? ? }
}
------------------------------------------------------------------------------------------------
Model:
User.kt:
data class User(val id: Int, val name: String)
UserModel.kt
class UserModel {
? ? fun getUser(): User {
? ? ? ? // Perform some logic to fetch user data
? ? ? ? return User(1, "John Doe")
? ? }
}
------------------------------------------------------------------------------------------------
In this example, the MainActivity serves as the view, UserController acts as the controller, and UserModel represents the model. The view registers a click listener on a button, which calls the controller's onButtonClick() method. The controller interacts with the model to retrieve the user data and then updates the view by calling the displayUserInfo() method.
It's important to note that this is a simplified example and doesn't cover every aspect of a complete MVC implementation. The main idea is to demonstrate the separation of concerns between the view, controller, and model. In a real-world application, you would have more complex interactions and additional components.
Problem with above code all components are tightly coupled:
In a more modular and decoupled design, it is advisable to introduce interfaces for the controller, model, and user. This promotes loose coupling and makes it easier to swap out implementations if needed. Here's an updated example:
interface IUser {
? ? val id: Int
? ? val name: String
}
interface IUserModel {
? ? fun getUser(): IUser
}
interface IUserController {
? ? fun onButtonClick()
}
class User(private val _id: Int, private val _name: String) : IUser {
? ? override val id: Int
? ? ? ? get() = _id
? ? override val name: String
? ? ? ? get() = _name
}
class UserModel : IUserModel {
? ? override fun getUser(): IUser {
? ? ? ? // Perform some logic to fetch user data
? ? ? ? return User(1, "Jai ShreeRam")
? ? }
}
class UserController(private val context: Context,
private val model: IUserModel) : IUserController {
? ? override fun onButtonClick() {
? ? ? ? val user = model.getUser()
if (context is MainActivity) {
context.displayUserInfo(user)
}
? ? }
}
class MainActivity : AppCompatActivity() {
? ? private lateinit var controller: IUserController
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_main)
? ? ? ??
? ? ? ? val model: IUserModel = UserModel()
? ? ? ? controller = UserController(this, model)
? ? ? ? // Set up UI components and event listeners
? ? ? ? val button = findViewById<Button>(R.id.button)
? ? ? ? button.setOnClickListener {
? ? ? ? ? ? controller.onButtonClick()
? ? ? ? }
? ? }
? ? fun displayUserInfo(user: IUser) {
? ? ? ? // Update UI to display user information
? ? ? ? val textView = findViewById<TextView>(R.id.textView)
? ? ? ? textView.text = "User ID: ${user.id}\nUser Name: ${user.name}"
? ? }
}
In the above updated example, the interfaces IUser, IUserModel, and IUserController define the contract or behavior that the User, UserModel, and UserController classes must adhere to.
The UserController now takes an instance of IUserModel in its constructor, allowing different implementations of IUserModel to be injected. This promotes flexibility and makes it easier to replace the model implementation without modifying the controller.
Similarly, the MainActivity now depends on IUserController, enabling different implementations of IUserController to be used interchangeably.
By introducing interfaces, you decouple the concrete implementations from their dependencies, making the code more modular, testable, and maintainable.
There is one more problem in the above code:
The dependencies are still created from within the class, which does not fully demonstrate the benefits of Dependency Injection (DI). Allow me to explain the advantages of DI and how it helps to address the issues of tight coupling.
Dependency Injection is a design pattern that promotes loose coupling and separation of concerns in software components. It helps achieve the Dependency Inversion Principle (the D in SOLID), which states that high-level modules should not depend on low-level modules; both should depend on abstractions.
Implementation of Dependency Injection (DI) in the provided example, we can use a DI framework like Dagger, which helps with managing dependencies and injecting them into the classes.
Here's an updated example using Dagger for DI:
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'
2. Create the necessary Dagger components and modules:
@Component(modules = [AppModule::class])
interface AppComponent {
? ? fun inject(activity: MainActivity)
}
@Module
class AppModule(private val context: Context) {
? ? @Provides
? ? fun provideContext(): Context = context
? ? @Provides
? ? fun provideUserModel(): IUserModel = UserModel()
}
3. Update MainActivity to use Dagger for injection:
class MainActivity : AppCompatActivity() {
? ? @Inject
? ? lateinit var controller: IUserController
? ? override fun onCreate(savedInstanceState: Bundle?) {
? ? ? ? super.onCreate(savedInstanceState)
? ? ? ? setContentView(R.layout.activity_main)
? ? ? ? DaggerAppComponent.builder()
? ? ? ? ? ? .appModule(AppModule(applicationContext))
? ? ? ? ? ? .build()
? ? ? ? ? ? .inject(this)
? ? ? ? // Set up UI components and event listeners
? ? ? ? val button = findViewById<Button>(R.id.button)
? ? ? ? button.setOnClickListener {
? ? ? ? ? ? controller.onButtonClick()
? ? ? ? }
? ? }
? ? fun displayUserInfo(user: IUser) {
? ? ? ? // Update UI to display user information
? ? ? ? val textView = findViewById<TextView>(R.id.textView)
? ? ? ? textView.text = "User ID: ${user.id}\nUser Name: ${user.name}"
? ? }
}
Now, the MainActivity is annotated with @Inject for the IUserController dependency, and the AppComponent is used to inject the dependencies using the inject(this) method.
By using Dagger and DI, the dependencies are managed by the DI framework, allowing for easier configuration and decoupling of components. This way, you can provide different implementations of dependencies by modifying the Dagger module and ensure that the dependencies are injected correctly throughout the application.
There is advanced DI called Hilt , which is considered to easier than Dagger:
Advantage of hilt over dagger 2:
Hilt, which is built on top of Dagger 2:
HILT implementation:
Example implementation of Dependency Injection using Hilt. However, please note that Hilt requires certain setup and configuration in the project, and it might be easier to follow the official Hilt documentation for a complete integration. The example provided here will give you an idea of how DI can be implemented using Hilt in Android.
Step 1: Add Hilt dependencies
To use Hilt in your project, you need to add the necessary dependencies in your project's build.gradle file. Here's an example of how to add the dependencies:
// build.gradle (project-level)
buildscript {
? ? dependencies {
? ? ? ? // Add the Hilt plugin
? ? ? ? classpath "com.google.dagger:hilt-android-gradle-plugin:2.38.1"
? ? }
}
// build.gradle (app-level)
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
dependencies {
? ? // Add Hilt dependencies
? ? implementation "com.google.dagger:hilt-android:2.38.1"
? ? kapt "com.google.dagger:hilt-compiler:2.38.1"
}
Step 2: Enable Hilt in your Application class
Create an Application class for your project (if you don't have one already) and annotate it with @HiltAndroidApp. This annotation enables Hilt in your application.
@HiltAndroidApp
class MyApp : Application()
Step 3: Define the DI module
Create a DI module to provide the dependencies. In this case, we'll create a UserModule to provide the UserController dependency. Annotate the module with @Module and specify the scope in which the module should be installed (e.g., @InstallIn(ActivityComponent::class) if the controller is used in activities).
@Module
@InstallIn(ActivityComponent::class)
object UserModule {
? ? @Provides
? ? fun provideUserController(userModel: IUserModel): IUserController {
? ? ? ? return UserController(userModel)
? ? }
? ? @Provides
? ? fun provideUserModel(): IUserModel {
? ? ? ? return UserModel()
? ? }
}
Step 4: Inject dependencies in the UserController
Annotate the UserController with @Inject to enable dependency injection. Hilt will automatically provide the dependencies.
class UserController @Inject constructor(private val userModel: IUserModel) :
IUserController {
? ? // Rest of the UserController code
}
Step 5: Inject dependencies in the MainActivity
Annotate the MainActivity with @AndroidEntryPoint to enable Hilt injection. Then, annotate the dependency you want to inject (UserController in this case) with @Inject.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var userController: UserController
// Rest of the activity code
}
That's it! Hilt will take care of creating and providing the instances of the dependencies defined in the DI module. IUserController is correctly injected into the MainActivity using Hilt. The UserController itself depends on IUserModel, and Hilt will automatically provide an instance of UserModel as a dependency for the UserController during injection when the annotated fields are accessed.
Please note that this is a simplified example, and in a real-world scenario, you might have more complex dependencies and configurations. Make sure to follow the official Hilt documentation for a complete understanding of the setup process and more advanced usage.
Benefits of DI:
Remember that the MVC pattern aims to separate responsibilities and maintain a clear division between the view, controller, and model. The model represents the data and business logic, the view handles the UI presentation, and the controller handles user interactions and updates between the model and view.
Note that in Android, the traditional MVC architecture is often modified to fit the framework's specific patterns and components.
Thanks for reading till end. Please comment if any!