Post

Design Patterns

A design pattern is a reusable solution to common problems in software development. There are different types of design patterns, such as creational patterns, which focus on how objects are created and ensure efficient and controlled creation, and architectural patterns, which define the overall structure and organization of an application to improve maintainability and scalability.

Design Patterns

Architectural Design Patterns

An architectural design pattern is a general, reusable solution to a commonly occurring problem in software architecture. It defines how software components should be organized and interact — focusing on the high-level structure of an application.

Clean Architecture

Clean Architecture is a software design pattern that emphasizes separation of concerns, testability, and maintainability by organizing code into distinct layers. In Android development, it’s often structured into the following layers:

Presentation Layer

  • Handles the UI and user interactions.
  • Uses ViewModels or MVI/MVVM patterns.
  • Communicates with the domain layer to perform actions or fetch data.

Domain Layer

  • Contains business logic and application rules.
  • Independent of any frameworks or libraries.
  • Includes use cases (interactors) that encapsulate specific functionality.

Data Layer

  • Manages data sources (e.g., local database, network API).
  • Implements repository interfaces defined in the domain layer.

clean architecture

Benefits:

  • Testable: Each layer can be tested independently.
  • Maintainable: Clear boundaries make the code easier to update or refactor.
  • Scalable: Easy to add new features or switch data sources without affecting other layers.

Example: Get User Name

Domain Layer:

1
2
3
4
5
6
7
8
9
class GetUserNameUseCase(private val userRepository: UserRepository) {
    fun execute(): String {
        return userRepository.getUserName()
    }
}

interface UserRepository {
    fun getUserName(): String
}

Data Layer:

1
2
3
4
5
class UserRepositoryImpl : UserRepository {
    override fun getUserName(): String {
        return "tawhidmonowar" // could be from API or DB
    }
}

Presentation Layer (ViewModel):

1
2
3
4
5
class UserViewModel(private val getUserNameUseCase: GetUserNameUseCase) {
    fun showUserName(): String {
        return getUserNameUseCase.execute()
    }
}

MVVM (Model–View–ViewModel)

MVVM is a design pattern that separates UI (View), business logic (ViewModel), and data (Model). It is widely used in Android. MVVM is an architectural pattern that divides an application into three core components:

Model–View–ViewModel

  • View: This layer handles the user interface (UI) elements and their layout. It displays data to the user and captures user interactions.
  • ViewModel: This layer acts as the intermediary between the Model and the View. It prepares the data for the View in a consumable way, handles business logic, and exposes observable data streams to the View.
  • Model: This layer represents the data of a application. It interacts with data sources like databases or network APIs.

Example: Task Manager

1) Task (Model)

1
2
3
4
5
data class Task(
    val id: Int,
    val title: String,
    val isDone: Boolean = false
)

2) TaskViewModel (ViewModel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class TaskViewModel : ViewModel() {

    private var nextId = 1

    // Empty task list initially
    var tasks by mutableStateOf(listOf<Task>())
        private set

    // Add a new task
    fun addTask(title: String) {
        if (title.isNotBlank()) {
            val newTask = Task(id = nextId++, title = title.trim())
            tasks = tasks + newTask
        }
    }

    // Toggle completion status
    fun toggleTaskDone(taskId: Int) {
        tasks = tasks.map { task ->
            if (task.id == taskId) task.copy(isDone = !task.isDone) else task
        }
    }

    // Delete a task
    fun deleteTask(taskId: Int) {
        tasks = tasks.filter { it.id != taskId }
    }
}

3) TaskScreen (Compose UI/View)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
    var newTaskTitle by remember { mutableStateOf("") }
    val tasks = viewModel.tasks

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text("Task Manager", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))

        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = newTaskTitle,
                onValueChange = { newTaskTitle = it },
                placeholder = { Text("Enter new task") },
                modifier = Modifier.weight(1f)
            )
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = {
                viewModel.addTask(newTaskTitle)
                newTaskTitle = ""
            }) {
                Text("Add")
            }
        }

        Spacer(modifier = Modifier.height(24.dp))

        if (tasks.isEmpty()) {
            Text(
                text = "No tasks yet. Add one above!",
                style = MaterialTheme.typography.bodyLarge
            )
        } else {
            LazyColumn {
                items(tasks) { task ->
                    TaskItem(
                        taskTitle = task.title,
                        isDone = task.isDone,
                        onToggle = { viewModel.toggleTaskDone(task.id) },
                        onDelete = { viewModel.deleteTask(task.id) }
                    )
                }
            }
        }
    }
}

@Composable
fun TaskItem(
    taskTitle: String,
    isDone: Boolean,
    onToggle: () -> Unit,
    onDelete: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp)
            .clickable { onToggle() },
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = isDone,
            onCheckedChange = { onToggle() }
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(
            text = taskTitle,
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.weight(1f)
        )
        IconButton(onClick = onDelete) {
            Icon(Icons.Default.Delete, contentDescription = "Delete Task")
        }
    }
}

4) MainActivity

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            TaskAppTheme {
                val viewModel: TaskViewModel = viewModel()
                TaskScreen(viewModel)
            }
        }
    }
}

MVI (Model-View-Intent)

MVI, the most recent addition to the MV* family, structures an application into three components.

  • Model: Represents the states for the UI
  • View: Represents the UI itself
  • Intent: Represents actions triggering state updates

The following diagram provides a comprehensive overview of the MVI architecture.

mvi

Example: Task Manager

1) Task (Model)

1
2
3
4
5
data class Task(
    val id: Int,
    val title: String,
    val isDone: Boolean = false
)

2) TaskState (State)

1
2
3
4
data class TaskState(
    val tasks: List<Task> = emptyList(),
    val input: String = ""
)

3) Intents (User actions)

1
2
3
4
5
6
sealed class TaskIntent {
    data class AddTask(val title: String) : TaskIntent()
    data class ToggleTaskDone(val taskId: Int) : TaskIntent()
    data class DeleteTask(val taskId: Int) : TaskIntent()
    data class UpdateInput(val input: String) : TaskIntent()
}

4) ViewModel (State + Intent processing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class TaskViewModel : ViewModel() {

    private var nextId = 1

    private val _state = MutableStateFlow(TaskState())
    val state: StateFlow<TaskState> = _state.asStateFlow()

    fun process(intent: TaskIntent) {
        when (intent) {
            is TaskIntent.AddTask -> addTask(intent.title)
            is TaskIntent.ToggleTaskDone -> toggleTaskDone(intent.taskId)
            is TaskIntent.DeleteTask -> deleteTask(intent.taskId)
            is TaskIntent.UpdateInput -> updateInput(intent.input)
        }
    }

    private fun addTask(title: String) {
        if (title.isBlank()) return

        _state.update { currentState ->
            val newTask = Task(id = nextId++, title = title.trim())
            currentState.copy(
                tasks = currentState.tasks + newTask,
                input = ""
            )
        }
    }

    private fun toggleTaskDone(taskId: Int) {
        _state.update { currentState ->
            currentState.copy(
                tasks = currentState.tasks.map {
                    if (it.id == taskId) it.copy(isDone = !it.isDone) else it
                }
            )
        }
    }

    private fun deleteTask(taskId: Int) {
        _state.update { currentState ->
            currentState.copy(
                tasks = currentState.tasks.filter { it.id != taskId }
            )
        }
    }

    private fun updateInput(input: String) {
        _state.update { currentState ->
            currentState.copy(input = input)
        }
    }
}

5) Composable UI TaskScreen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
    val state by viewModel.state.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text("Task Manager (MVI)", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))

        Row(
            modifier = Modifier.fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = state.input,
                onValueChange = { viewModel.process(TaskIntent.UpdateInput(it)) },
                placeholder = { Text("Enter new task") },
                modifier = Modifier.weight(1f)
            )
            Spacer(modifier = Modifier.width(8.dp))
            Button(
                onClick = { viewModel.process(TaskIntent.AddTask(state.input)) }
            ) {
                Text("Add")
            }
        }

        Spacer(modifier = Modifier.height(24.dp))

        if (state.tasks.isEmpty()) {
            Text(
                text = "No tasks yet. Add one above!",
                style = MaterialTheme.typography.bodyLarge
            )
        } else {
            LazyColumn {
                items(state.tasks) { task ->
                    TaskItem(
                        taskTitle = task.title,
                        isDone = task.isDone,
                        onToggle = { viewModel.process(TaskIntent.ToggleTaskDone(task.id)) },
                        onDelete = { viewModel.process(TaskIntent.DeleteTask(task.id)) }
                    )
                }
            }
        }
    }
}

@Composable
fun TaskItem(
    taskTitle: String,
    isDone: Boolean,
    onToggle: () -> Unit,
    onDelete: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 8.dp)
            .clickable { onToggle() },
        verticalAlignment = Alignment.CenterVertically
    ) {
        Checkbox(
            checked = isDone,
            onCheckedChange = { onToggle() }
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(
            text = taskTitle,
            style = MaterialTheme.typography.bodyLarge,
            modifier = Modifier.weight(1f)
        )
        IconButton(onClick = onDelete) {
            Icon(Icons.Default.Delete, contentDescription = "Delete Task")
        }
    }
}

6) MainActivity

1
2
3
4
5
6
7
8
9
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val viewModel: TaskViewModel = viewModel()
            TaskScreen(viewModel)
        }
    }
}

Creational Design Patterns

Creational design patterns are a category of design patterns focused on object creation mechanisms. They provide flexible ways to create objects, helping to control how objects are instantiated, while hiding the creation logic from the client.

This post is licensed under CC BY 4.0 by the author.