Hi folks, hope you are doing great. In this blog, we will discuss and also implement Uncle Bob's Clean Architecture of software development in Android. Just stay tuned
Introduction
The actual question is "What is the need for clean architecture?". Well, the main goal of Clean Architecture is to create software that is easy to understand, test, maintain, and modify over time, while also being flexible and scalable for that purpose we differentiate our application into three layers.
Presentation layer
Domain layer
Data layer
The presentation layer contains our UI logic like user input or displaying any data to the user
The domain layer contains the business logic of the application. this layer contains the use cases, models and repositories.
The data layer deals with data whether it is stored locally or we are retrieving it from APIs
Bored with theory? let's understand it with the help of a project example
Structure of project
This is a Note taking app in which we will use Jetpack compose, Room and dagger hilt. So add their dependencies given below in ur app level build.gradle file
// Di using hilt
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
//viewModel
implementation"androidx.lifecycle:lifecycle-viewmodel-compose:$compose_ui_version"
//navigation
implementation "androidx.navigation:navigation-compose:$nav_version"
//room-database
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
After that create five packages named-
-presentation
-domain
-data
-di
-utils
Setting up the domain layer
The domain layer will contain packages
model
repository
the model package will contain a Note.kt file which contains entities
package com.android.roomdbtest.domain.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Note(
@PrimaryKey val id: Int? = null,
val title: String,
val content: String,
)
the same class will be used as our room database entities. Next, we will write our repository. The repository provides a consistent interface for accessing and manipulating the data.
package com.android.roomdbtest.domain.repository
import com.android.roomdbtest.domain.model.Note
import kotlinx.coroutines.flow.Flow
interface NoteRepository {
fun getNotes(): Flow<List<Note>>
suspend fun getNoteById(noteId: Int): Note?
suspend fun insertNote(note: Note)
suspend fun deleteNote(note: Note)
}
our domain layer is now completed let's set up our data layer
Setting up the data layer
As we are using the room database to store data locally. so this package will contain a local database and repository package. room uses dao 's as data accessing objects.
package com.android.roomdbtest.data.local
import androidx.room.*
import com.android.roomdbtest.domain.model.Note
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao{
@Query ("Select * FROM note")
fun getNotes(): Flow<List<Note>>
@Query("Select * FROM note WHERE id = :id")
suspend fun getNoteById(id:Int):Note?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertnote(note: Note)
@Delete
suspend fun deleteNote(note:Note)
}
after that set up the database class
package com.android.roomdbtest.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import com.android.roomdbtest.domain.model.Note
@Database(
entities = [Note::class],
version = 1
)
abstract class NoteDatabase : RoomDatabase() {
abstract fun noteDao() :NoteDao
companion object{
const val DATABASE_NAME = "notes_db"
}
}
we are finished setting up the room database. now let's setup the implementation of domain layer repositories
package com.android.roomdbtest.data.repository
import com.android.roomdbtest.data.local.NoteDao
import com.android.roomdbtest.domain.model.Note
import com.android.roomdbtest.domain.repository.NoteRepository
import kotlinx.coroutines.flow.Flow
class NoteRepositoryImpl
(private val noteDao: NoteDao
) : NoteRepository {
override fun getNotes(): Flow<List<Note>> {
return noteDao.getNotes()
}
override suspend fun getNoteById(noteId: Int): Note? {
return noteDao.getNoteById(noteId)
}
override suspend fun insertNote(note: Note) {
return noteDao.insertnote(note)
}
override suspend fun deleteNote(note: Note) {
return noteDao.deleteNote(note)
}
}
this class extends our domain layer repository class and overrides its function and it maps the repository to the room database
Setting up dependency injection
Dependency injection is a technique for providing dependencies to classes, rather than having them create the dependencies themselves. In our di package, there is a file AppModule.kt which provides dependencies to the app the whole time
package com.android.roomdbtest.di
import android.app.Application
import com.android.roomdbtest.data.local.NoteDatabase
import com.android.roomdbtest.domain.repository.NoteRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import androidx.room.Room
import com.android.roomdbtest.data.repository.NoteRepositoryImpl
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun provideNoteDatabase(app: Application) = Room.databaseBuilder(
app, NoteDatabase::class.java, NoteDatabase.DATABASE_NAME
).build()
@Singleton
@Provides
fun provideNoteRepository(database: NoteDatabase): NoteRepository = NoteRepositoryImpl(
database.noteDao()
)
}
Setting up the presentation layer
In the presentation layer, we will use Jetpack Compose for UI design and handling. The App will contain two screens one for displaying all the notes and the other for the insertion of notes. The navigation library will be used to switch between two screens and viewmodel class to retrieve data from the repository.
ViewModel
package com.android.roomdbtest.presentation.update_note
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.roomdbtest.domain.model.Note
import com.android.roomdbtest.domain.repository.NoteRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NoteViewModel @Inject constructor(private val repository: NoteRepository): ViewModel() {
private val _noteList = MutableStateFlow<List<Note>>(emptyList())
val noteList = _noteList.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
repository.getNotes().distinctUntilChanged().collect { listOfNotes ->
if (listOfNotes.isEmpty()) {
_noteList.value = emptyList()
Log.d("Empty", ": Empty list")
} else {
_noteList.value = listOfNotes
}
}
}
}
fun addNote(note: Note) = viewModelScope.launch { repository.insertNote(note) }
fun deleteNote(note: Note) = viewModelScope.launch { repository.deleteNote(note) }
fun getAllNotes() = viewModelScope.launch { repository.getNotes() }
}
HomeScreen(list screen)
package com.android.roomdbtest.presentation.home_screen
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.android.roomdbtest.domain.model.Note
import com.android.roomdbtest.presentation.navigation.Screens
import com.android.roomdbtest.presentation.update_note.NoteViewModel
@Composable
fun HomeScreen(noteViewModel: NoteViewModel,navController: NavController) {
val notes = noteViewModel.noteList.collectAsState().value
NoteListScreen(notes = notes, noteViewModel =noteViewModel, navController = navController )
}
@OptIn(ExperimentalMaterialApi::class)
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun NoteListScreen(
notes: List<Note>,
noteViewModel: NoteViewModel,
navController: NavController
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Notes") }
)
},
content = {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(notes) { note ->
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
elevation = 8.dp,
shape = RoundedCornerShape(16.dp),
onClick = { }
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(text = note.title, style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(8.dp))
Text(text = note.content, style = MaterialTheme.typography.body2)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
IconButton(
onClick = { noteViewModel.deleteNote(note = note) }
) {
Icon(Icons.Filled.Delete, contentDescription = "Delete")
}
}
}
}
}
}
FloatingActionButton(
onClick = { navController.navigate(Screens.UpdateNoteScreen.route)},
modifier = Modifier.align(Alignment.End),
content = {
Icon(Icons.Filled.Add, contentDescription = "Add")
}
)
}
}
)
}
Add note
package com.android.roomdbtest.presentation.update_note
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.android.roomdbtest.domain.model.Note
import com.android.roomdbtest.presentation.navigation.Screens
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun UpdateNoteScreen(
noteViewModel: NoteViewModel,
navController: NavController
)
{
var title by remember { mutableStateOf("") }
var content by remember { mutableStateOf("") }
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "Add Note") },
navigationIcon = {
IconButton(onClick = { navController.navigate(Screens.HomeScreen.route) }) {
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
}
}
)
},
content = {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text(text = "Note Title") }
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = content,
onValueChange = { content = it },
label = { Text(text = "Note Content") }
)
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = {
if (title.isNotEmpty() && content.isNotEmpty()) {
noteViewModel.addNote(note = Note(title = title, content = content))
Toast.makeText(context,"Note Added",Toast.LENGTH_SHORT).show()
}
else{
Toast.makeText(context,"Please enter note",Toast.LENGTH_SHORT).show()
}
navController.navigate(Screens.HomeScreen.route)
},
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(vertical = 12.dp)
) {
Text(text = "Save")
}
}
}
)
}
now set up the navigation graph
Screens
package com.android.roomdbtest.presentation.navigation
sealed class Screens(val route: String) {
object HomeScreen : Screens("note_list_screen")
object UpdateNoteScreen : Screens("note_create_update_screen")
}
NavGraph
package com.android.roomdbtest.presentation.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.android.roomdbtest.presentation.home_screen.HomeScreen
import com.android.roomdbtest.presentation.update_note.NoteViewModel
import com.android.roomdbtest.presentation.update_note.UpdateNoteScreen
@Composable
fun NavGraph(navController: NavHostController = rememberNavController(),
noteViewModel: NoteViewModel
)
{
NavHost(navController = navController, startDestination = Screens.HomeScreen.route ){
composable(route = Screens.HomeScreen.route){
HomeScreen( navController =navController, noteViewModel = noteViewModel )
}
composable(route = Screens.UpdateNoteScreen.route){
UpdateNoteScreen(noteViewModel = noteViewModel, navController =navController )
}
}
}
Finishing up the project
Don't forget to annotate your main activity with @AndroidEntryPoint annotation.
create a class named NoteApplication and extend it with Application() and don't forget to add .NoteApplication in your Android manifest
package com.android.roomdbtest
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class NoteApplication : Application()
Final Result
You can refer to my github repository for source code of this app