Visualizing a Reactive Android Application Through Modular Architecture

oussama ben wafi
4 min readSep 19, 2023

--

Designing a modular Android application can yield numerous benefits: improved build times, structured code separation, and enhanced scalability. As we venture through each module, we’ll relate it to our architectural image for a cohesive understanding, and we’ll mention the dependency of each module to better understand the graph and unidirectional dependency.

1. The Domain Module: Center of the World

At our architectural core lies the domain module, emblematic of the app’s business logic.

Module: :domain

Example:

// Entity
data class User(val id: Int, val name: String)
// UseCase
class GetUserUseCase(private val userRepository: UserRepository) {
suspend fun execute(userId: Int): User {
return userRepository.getUser(userId)
}
}

build.gradle.kts:

dependencies { //… other common dependencies}

2. Data Module: Enveloping the Domain

Next, our image reflects the data layer around the domain, harmonizing data from various sources.

Module: :data

class UserRepository @Inject constructor(
private val userLocalDataSource: UserLocalDataSource,
private val userRemoteDataSource: UserRemoteDataSource
) {
suspend fun getUser(userId: Int): User {
// Logic to decide whether to fetch from local or remote source
}
}

build.gradle.kts:

dependencies {
implementation(project(“:domain”))
implementation(project(“:datasource”))
implementation(project(“:network”))
}

3. DataSource Module: Storage and Retrieval

Further out, our image depicts data sources that facilitate actual storage interactions.

Module: :datasource

class UserLocalDataSource @Inject constructor(private val userDao: UserDao) : LocalDataSource {
override suspend fun getUser(id: Int) = userDao.getUser(id)
}

build.gradle.kts:

dependencies {
// Dependencies for Room and other local storage utilities
}

Let’s get back to data to expose our local data source functions, now data become agnostic from how data source work.

Module: :data

interface LocalDataSource { suspend fun getUserLocal(userId: Int): User? suspend fun saveUserLocal(user: User) // … any other local data functions }

4. Network Module: The Web Beyond

Approaching the image’s boundary, the network module manages our external data interactions.

Module: :network

// Retrofit setup
val retrofit = Retrofit.Builder()
.baseUrl(“https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
interface UserApi {
@GET(“users/{id}”)
suspend fun getUser(@Path(“id”) id: Int): User
}
class UserRemoteDataSource @Inject constructor(private val userApi: UserApi) : RemoteDataSource {
override suspend fun getUser(id: Int) = userApi.getUser(id)
}

build.gradle.kts:

dependencies {
// Retrofit and other network dependencies
}

Let’s get back to data to expose our remote function call, with that interface, data module create abstraction and does not need to know about the actual remote data source, how it work and what is it as library: retrofit or something else.

Module: :data

interface RemoteDataSource { suspend fun getUserRemote(userId: Int): User? // … any other remote data functions }

5. Analytics Module: Gateways to Understanding

Sitting within our image’s “Gateways” layer, the analytics module serves as an interface to log and monitor app events.
Analytics module will depends to domain module to expose an interface that will be accessible for any other module, especially UI module that need to Log some events into the viewModel. So let’s get back to the domain module and add the interface, and the useCase consuming it.
In fact, at first domain need to depend to analytics because it is the core of our application, but this dependency will create a strong coupling, and break the dependency rule that external layer should depend only the inner layer.
That’s why we will inverse the dependency by exposing in interface into domain layer, and analytics module, does not matter which library it uses, will depends on domain and implements the domain interface.

Module: :analytics

Example:


class AnalyticsServiceImpl @Inject constructor(private val analyticsSdk: AnalyticsSdk) : AnalyticsService {
override fun logEvent(event: String) {
analyticsSdk.logEvent(event)
}
}

build.gradle.kts:

dependencies { 
implementation(project(":domain"))
// External SDK or library for analytics
}

Module: :domain

interface AnalyticsService {
fun logEvent(event: String)
}

usecase:

// UseCase
class LogUseCase(private val analyticsService: AnalyticsService) {
suspend fun execute(event: String) {
return analyticsService.log(event)
}
}

6. Presentation Module: User’s Touchpoint

The outermost layer in our image embodies the UI, responding to user inputs and data transformations.

Module: :ui

@HiltViewModel
class UserViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val analyticsService: AnalyticsService
) : ViewModel() {
private val _state = MutableStateFlow<User?>(null)
val state: StateFlow<User?> get() = _state
fun getUser(userId: Int) {
viewModelScope.launch {
_state.value = getUserUseCase.execute(userId)
analyticsService.logEvent(“User data fetched for ID $userId”)
}
}
}

build.gradle.kts:

dependencies {
implementation(project(“:domain”))
//… Jetpack Compose and other UI dependencies
}

Conclusion

Mapping modules to our architectural image offers clarity, tying together abstract design and concrete code. Through a journey from core entities to the user interface, interspersed with external interactions and understanding via analytics, this architectural visualization fosters effective development and collaboration.

--

--

oussama ben wafi
oussama ben wafi

Written by oussama ben wafi

Android, Kotlin , java & football

No responses yet