Topic: 19 Understanding Design Patterns in Android

This is our nineteenth topic from learn android from basic to advance series

Β·

14 min read

Topic: 19 Understanding Design Patterns in Android

Hello Devs, In Today's Topic, we talk about different types of design patterns. These design patterns help you to make your Android Project Clean and Understandable. In this blog, we can explore MVC, MVVM, and Clean architecture.

MVC- Model View Controller

The Model-View-Controller (MVC) pattern is a widely used architectural pattern in software development, including Android app development. It helps in organizing code in a way that separates concerns and makes the codebase more modular, maintainable, and scalable. Here's a detailed explanation of how MVC is implemented in Android using Kotlin, from the perspective of an experienced Android developer:

  1. Model:

    • The Model represents the data and business logic of the application. It encapsulates the data, state, and behavior of the application. In Android, models typically represent the data sources, such as databases, network requests, or local storage.

    • In Kotlin, you can define model classes as simple data classes or more complex classes depending on your application requirements. For example, if you're building a task management app, you might have a Task class representing a single task.

    data class Task(val id: Int, val title: String, val description: String)
  1. View:

    • The View is responsible for rendering the user interface and displaying data to the user. In Android, views are typically implemented using XML layout files (e.g., activity_main.xml, fragment_task_detail.xml) and custom view classes (e.g., TaskAdapter for RecyclerView).

    • Views should be kept as dumb as possible, meaning they should not contain business logic. They should only display data and respond to user interactions.

  2. Controller:

    • The Controller acts as an intermediary between the Model and the View. It handles user inputs, updates the Model accordingly, and updates the View to reflect changes in the Model.

    • In Android, Activities and Fragments often serve as controllers. They receive user inputs through UI elements (e.g., button clicks, text input) and interact with the Model to update data. They also update the View to reflect changes in the Model.

    • Controllers should not contain business logic or data manipulation logic. They should delegate these responsibilities to the Model layer.

Example:

Model (Task.kt):

data class Task(val id: Int, val title: String, val description: String)

View (activity_task_list.xml):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TaskListActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/taskRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>

View (item_task.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/taskTitleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/taskDescriptionTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

</LinearLayout>

Controller (TaskListActivity.kt):

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class TaskListActivity : AppCompatActivity() {

    private lateinit var taskRecyclerView: RecyclerView
    private lateinit var taskAdapter: TaskAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_task_list)

        taskRecyclerView = findViewById(R.id.taskRecyclerView)
        taskAdapter = TaskAdapter()
        taskRecyclerView.layoutManager = LinearLayoutManager(this)
        taskRecyclerView.adapter = taskAdapter

        // Mock data
        val tasks = listOf(
            Task(1, "Task 1", "Description for Task 1"),
            Task(2, "Task 2", "Description for Task 2"),
            Task(3, "Task 3", "Description for Task 3")
        )

        taskAdapter.submitList(tasks)
    }
}

Controller (TaskAdapter.kt):

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class TaskAdapter : ListAdapter<Task, TaskAdapter.TaskViewHolder>(TaskDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_task, parent, false)
        return TaskViewHolder(view)
    }

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = getItem(position)
        holder.bind(task)
    }

    inner class TaskViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val titleTextView: TextView = itemView.findViewById(R.id.taskTitleTextView)
        private val descriptionTextView: TextView = itemView.findViewById(R.id.taskDescriptionTextView)

        fun bind(task: Task) {
            titleTextView.text = task.title
            descriptionTextView.text = task.description
        }
    }

    class TaskDiffCallback : DiffUtil.ItemCallback<Task>() {
        override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem == newItem
        }
    }
}

In this example:

  • Task class represents the Model.

  • TaskListActivity serves as the Controller.

  • TaskAdapter acts as the View to display the list of tasks.

This setup follows the MVC pattern where responsibilities are separated between the Model (data), View (UI), and Controller (logic).

MVVM- Model View ViewModel

MVVM is an architectural pattern that enhances the separation of concerns in software development, particularly in user interface code. It divides the application into three main components:

  1. Model: This component represents the data and business logic of the application. It typically includes data sources like databases, network requests, or local storage. The Model is responsible for managing data and ensuring consistency.

  2. View: The View component represents the user interface and is responsible for displaying data to the user. Unlike in the MVC pattern, Views in MVVM is not aware of the Model directly. Instead, they are bound to ViewModels.

  3. ViewModel: ViewModel acts as an intermediary between the View and the Model. It exposes data from the Model to the View via observable properties or LiveData (in Android's case) and contains presentation logic required to handle user interactions. ViewModels survive configuration changes (like screen rotations) and help in maintaining the UI state.

Implementation in Android with Kotlin:

Now, let's delve into how MVVM is implemented in an Android app using Kotlin:

  1. Model:

    • Define data classes to represent entities in your app, database access methods, or network request functions.

    • Encapsulate data-related logic, such as data validation or transformation.

  2. View:

    • Views are XML layout files (e.g., activity_main.xml, fragment_detail.xml) that define the UI components.

    • Activities, Fragments, or custom View classes represent the View layer.

    • Views observe data changes from ViewModels and update the UI accordingly.

    • Use data binding or LiveData to bind UI components to ViewModel properties.

  3. ViewModel:

    • Create ViewModel classes for each View or feature.

    • ViewModels expose data to the View layer via LiveData or observable properties.

    • They contain business logic and handle user interactions.

    • ViewModel should not contain Android framework-related code (like Context) to avoid memory leaks.

    • ViewModel instances are retained during configuration changes, such as screen rotations, using ViewModelProviders or the by viewModels() Kotlin property delegate.

Benefits of MVVM in Android:

  • Separation of Concerns: MVVM separates the UI logic from the business logic, making the codebase more modular and easier to maintain.

  • Testability: ViewModels are easier to test since they are not coupled with Android framework components. You can write unit tests for ViewModel logic independently of the Android platform.

  • Lifecycle Management: ViewModels survive configuration changes, reducing the need to manually handle state restoration.

  • Data Binding: MVVM works well with data binding libraries like Android Data Binding or Jetpack's ViewBinding, enabling easier binding of UI components to ViewModel properties.

1. Model:

data class Task(val id: Int, val title: String, val description: String)

class TaskRepository {
    // Simulated data source or network operations
    fun getTasks(): List<Task> {
        return listOf(
            Task(1, "Task 1", "Description for Task 1"),
            Task(2, "Task 2", "Description for Task 2"),
            Task(3, "Task 3", "Description for Task 3")
        )
    }
}

2. ViewModel:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class TaskViewModel : ViewModel() {
    private val taskRepository = TaskRepository()

    private val _taskList = MutableLiveData<List<Task>>()
    val taskList: LiveData<List<Task>> = _taskList

    init {
        loadTasks()
    }

    private fun loadTasks() {
        _taskList.value = taskRepository.getTasks()
    }
}

3. View (Activity):

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class TaskListActivity : AppCompatActivity() {

    private val taskViewModel: TaskViewModel by viewModels()

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: TaskAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_task_list)

        recyclerView = findViewById(R.id.taskRecyclerView)
        adapter = TaskAdapter()

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter

        // Observe task list from ViewModel
        taskViewModel.taskList.observe(this, { tasks ->
            adapter.submitList(tasks)
        })
    }
}

4. View (Adapter):

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class TaskAdapter : ListAdapter<Task, TaskAdapter.TaskViewHolder>(TaskDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_task, parent, false)
        return TaskViewHolder(view)
    }

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = getItem(position)
        holder.bind(task)
    }

    inner class TaskViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val titleTextView: TextView = itemView.findViewById(R.id.taskTitleTextView)
        private val descriptionTextView: TextView = itemView.findViewById(R.id.taskDescriptionTextView)

        fun bind(task: Task) {
            titleTextView.text = task.title
            descriptionTextView.text = task.description
        }
    }

    class TaskDiffCallback : DiffUtil.ItemCallback<Task>() {
        override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem == newItem
        }
    }
}

5. Layout (activity_task_list.xml):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TaskListActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/taskRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>

6. Layout (item_task.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/taskTitleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/taskDescriptionTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

</LinearLayout>

In this example:

  • Task represents the Model.

  • TaskRepository manages data operations.

  • TaskViewModel acts as the ViewModel.

  • TaskListActivity represents the View (Activity).

  • TaskAdapter handles RecyclerView data binding.

  • Layout files define the UI components.

This setup demonstrates the MVVM pattern in action, where the ViewModel retrieves data from the Model and provides it to the View, decoupling the UI logic from the data layer.

Clean Architecture Pattern

Clean Architecture is an architectural pattern proposed by Robert C. Martin (Uncle Bob) that emphasizes separation of concerns and independence of framework and platform. The main goal of Clean Architecture is to create a system that is easy to understand, maintain, and extend over time by dividing the codebase into distinct layers of responsibility. These layers typically include:

  1. Entities: Entities represent the business objects or domain models of the application. They encapsulate enterprise-wide business rules and are independent of any specific framework or platform.

  2. Use Cases (Interactors): Use Cases contain the application-specific business logic. They orchestrate the flow of data between entities and external systems, implementing the application's use cases or user stories.

  3. Repositories (Gateways): Repositories abstract the data sources from the use cases. They define interfaces for data operations (e.g., CRUD operations) without specifying how the data is stored or retrieved. Repositories are implemented by external frameworks or libraries, such as databases, network clients, or file systems.

  4. Frameworks and Drivers: This outermost layer consists of the frameworks and tools that interact with the external environment, such as the user interface, database, network, or external services. Android-specific components like Activities, Fragments, and Services reside in this layer.

The key principles of Clean Architecture include:

  • Dependency Rule: Dependencies flow inward, from outer layers (e.g., frameworks and drivers) to inner layers (e.g., use cases and entities). Inner layers should not depend on outer layers.

  • Separation of Concerns: Each layer has a specific responsibility and should be independent of other layers. This allows for easier testing, maintenance, and modification of individual components.

Implementation in Android with Kotlin:

Now, let's delve into how Clean Architecture can be implemented in an Android app using Kotlin:

  1. Entities:

    • Define data classes representing domain models or business objects.

    • Keep entities free from any Android-specific code or framework dependencies.

  2. Use Cases:

    • Implement Use Cases as Kotlin classes or functions that encapsulate business logic.

    • Use Cases should be independent of the Android framework and focus solely on application-specific requirements.

  3. Repositories:

    • Create Repository interfaces defining data operations required by Use Cases.

    • Implement Repository interfaces with concrete classes that interact with external data sources (e.g., Room Database, Retrofit for network requests).

  4. Frameworks and Drivers:

    • Implement Android-specific components like Activities, Fragments, and Services in this layer.

    • These components should interact with Use Cases and Repositories to perform business logic and data operations.

Benefits of Clean Architecture in Android:

  • Maintainability: Clean Architecture promotes modularity and separation of concerns, making it easier to maintain and extend the application over time.

  • Testability: Components are decoupled and independent of the Android framework, allowing for easier unit testing of business logic and data operations.

  • Flexibility: Clean Architecture enables swapping out external dependencies or frameworks without affecting the core business logic, providing flexibility to adapt to changing requirements or technology stacks.

  • Scalability: By separating concerns and defining clear boundaries between layers, Clean Architecture facilitates scalability and allows teams to work on different parts of the application concurrently.

Example:

1. Entities:

// Task.kt
data class Task(val id: Int, val title: String, val description: String)

2. Use Cases:

// GetTasksUseCase.kt
class GetTasksUseCase(private val taskRepository: TaskRepository) {
    fun execute(): List<Task> {
        return taskRepository.getTasks()
    }
}

3. Repositories:

// TaskRepository.kt
interface TaskRepository {
    fun getTasks(): List<Task>
}

4. Data Layer (Repositories Implementation):

// TaskRepositoryImpl.kt
class TaskRepositoryImpl : TaskRepository {
    override fun getTasks(): List<Task> {
        // Simulated data source or network operations
        return listOf(
            Task(1, "Task 1", "Description for Task 1"),
            Task(2, "Task 2", "Description for Task 2"),
            Task(3, "Task 3", "Description for Task 3")
        )
    }
}

5. Presentation Layer (Frameworks and Drivers):

// TaskListActivity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class TaskListActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private lateinit var adapter: TaskAdapter
    private lateinit var getTasksUseCase: GetTasksUseCase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_task_list)

        recyclerView = findViewById(R.id.taskRecyclerView)
        adapter = TaskAdapter()
        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = adapter

        // Initialize Use Case and Repository
        val taskRepository = TaskRepositoryImpl()
        getTasksUseCase = GetTasksUseCase(taskRepository)

        // Fetch tasks and update UI
        val tasks = getTasksUseCase.execute()
        adapter.submitList(tasks)
    }
}

6. Presentation Layer (Adapter):

// TaskAdapter.kt
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView

class TaskAdapter : ListAdapter<Task, TaskAdapter.TaskViewHolder>(TaskDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_task, parent, false)
        return TaskViewHolder(view)
    }

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        val task = getItem(position)
        holder.bind(task)
    }

    inner class TaskViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val titleTextView: TextView = itemView.findViewById(R.id.taskTitleTextView)
        private val descriptionTextView: TextView = itemView.findViewById(R.id.taskDescriptionTextView)

        fun bind(task: Task) {
            titleTextView.text = task.title
            descriptionTextView.text = task.description
        }
    }

    class TaskDiffCallback : DiffUtil.ItemCallback<Task>() {
        override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
            return oldItem == newItem
        }
    }
}

7. Layout Files (activity_task_list.xml, item_task.xml):

activity_task_list.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TaskListActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/taskRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</androidx.constraintlayout.widget.ConstraintLayout>

item_task.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/taskTitleTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/taskDescriptionTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="14sp"/>

</LinearLayout>

In this example:

  • Task represents the Entity.

  • GetTasksUseCase represents the Use Case.

  • TaskRepository defines the Repository interface.

  • TaskRepositoryImpl implements the Repository interface.

  • TaskListActivity serves as the Frameworks and Drivers layer, interacting with the Use Case and Repository.

  • TaskAdapter handles RecyclerView data binding.

  • Layout files define the UI components.

This setup demonstrates the Clean Architecture pattern in action, with a clear separation of concerns and independence of framework and platform-specific details.

This is one small topic that I want to discuss with you devs, the topic is ProGaurd. So what is this? Let's check out.

What is ProGuard?

ProGuard is a tool that comes with the Android SDK and is used for code shrinking, obfuscation, and optimization. It helps in reducing the size of the APK file, making it faster to download and improving app performance. Additionally, ProGuard obfuscates the code, making it harder for reverse engineers to understand and decompile your app.

How Does ProGuard Work?

ProGuard operates on Java bytecode and performs the following tasks:

  1. Code Shrinking: ProGuard identifies and removes unused classes, fields, and methods from your code. This helps in reducing the size of the APK.

  2. Code Obfuscation: ProGuard renames classes, fields, and methods to short, meaningless names, making it difficult for someone to understand the code by looking at the decompiled output.

  3. Optimization: ProGuard applies various optimizations to the code, such as inlining methods, removing unused code paths, and optimizing bytecode instructions. These optimizations improve the performance of the app.

Enabling ProGuard in Android Projects:

ProGuard is typically enabled for release builds in Android projects. To enable ProGuard, you need to set the minifyEnabled flag to true in your app's build.gradle file:

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            ...
        }
    }
}

The proguardFiles directive specifies the ProGuard rules file (proguard-rules.pro) that contains custom rules for code shrinking and obfuscation. By default, Android Studio includes a set of ProGuard rules in the proguard-android-optimize.txt file, which covers common rules for Android apps.

Writing ProGuard Rules:

The proguard-rules.pro file allows you to specify custom ProGuard rules to control the obfuscation and optimization process. Some common ProGuard rules include:

  • -keep: Specifies classes, methods, or fields that should not be obfuscated.

  • -dontwarn: Suppresses warnings for specific classes or packages.

  • -keepnames: Specifies classes, methods, or fields that should not be renamed.

  • -optimizationpasses: Specifies the number of optimization passes to be performed.

Here's an example of a basic ProGuard rules file:

# Keep all classes and their members in the specified package
-keep class com.example.app.** { *; }

# Keep all public and protected classes and their members
-keep public class * {
    public protected *;
}

# Keep all classes annotated with @Keep annotation
-keep @interface com.example.Keep

# Don't obfuscate or remove the MainActivity class
-keep class com.example.app.MainActivity { *; }

Testing with ProGuard:

Before releasing your app, it's essential to thoroughly test it with ProGuard enabled to ensure that it doesn't break any functionality. You can test the app by generating a signed APK with ProGuard enabled and then running various tests, including unit tests, integration tests, and UI tests.

Alright devs, this is the end of the blog. Remember the choice of design pattern depends on various factors such as project requirements, team expertise, scalability, and maintainability goals. OK, So our next blog is the final blog of our series. The final blog is about Testing.


Connect with Me:

Hey there! If you enjoyed reading this blog and found it informative, why not connect with me on LinkedIn? 😊 You can also follow my Instagram page for more mobile development-related content. πŸ“²πŸ‘¨β€πŸ’» Let’s stay connected, share knowledge and have some fun in the exciting world of app development! 🌟

Check out my Instagram page

Check out my LinkedIn

Β