How to focus effective business logic & implement more expandable & simplify modules & reduce the boiler-plate code using the Module-Pattern to make Android App.

Lukoh Nam
13 min readDec 7, 2022

--

Here are the effective ways to focus on your business logic & simplify modules & reduce the boiler-plate code & using the Module-Pattern(Called Module Pattern by Lukoh). A well-designed/implemented Mobile App using Module-Pattern(that is, well-designed App Architecture) can be extended to new apps faster and easier by applying/adapting new UI layouts and business logic. It helps you focus your business logic and create better services, and you can quickly deliver user-friendly features to your users.

Photo by Kelly Sikkema on Unsplash

It enables/helps Code-Implementation to get simplified and improved Code Readability & Quality and contribute Coding Productivity and Efficiency.

🧤 Let’s dive into how to use the Module-Pattern to concentrate on your Business-Logic and improve/increate your Coding Productivity and Efficiency.

☑️ Before stepping into it, you’ve to know or learn four prerequisites below:

App Architecture

Dependency Injection with Dagger2

● Object-Oriented Programming

Coroutines-Flow in Kotlin

❗️✔️ I recommend you to read or learn above 4 techs in advance to understand this article

App Architecture using the Module-Pattern

Communication between the layers of a App Architecture on Android. Each layer should talk only with their immediate friends.

In this case, if you look at the App Architecture scheme:

  • The UI can only communicate with the ViewModel
  • The ViewModel can only communicate with the UseCase
  • The UseCase can only communicate with the Repository
  • The Repository can only communicate with the Datasource
Diagram of the App Architecture.

👉 In this way, Each area communicates with the next immediate and never with others and UI module can receive updates whenever the data from remote-source changes or the user’s data in the repository is changed.

Use the Module Pattern in your ViewModel

Handle ViewModel events

UI actions that originate from the ViewModel — ViewModel events — should always result in a UI state update. This complies with the principles of Unidirectional Data Flow. It makes events reproducible after configuration changes and guarantees that UI actions won’t be lost. Optionally, you can also make events reproducible after process death if you use the saved state module.

Mapping UI actions to UI state is not always a simple process, but it does lead to simpler logic. Your thought process shouldn’t end with determining how to make the UI navigate to a particular screen, for example. You need to think further and consider how to represent that user flow in your UI state. In other words: don’t think about what actions the UI needs to make; think about how those actions affect the UI state.

Key Point: ViewModel events should always result in a UI state update.

Consuming events can trigger state updates

Consuming certain ViewModel events in the UI might result in other UI state updates. For example, when showing transient messages on the screen to let the user know that something happened, the UI needs to notify the ViewModel to trigger another state update when the message has been shown on the screen. The event that happens when the user has consumed the message (by dismissing it or after a timeout) can be treated as “user input” and as such, the ViewModel should be aware of that. In this situation, the UI state can be modeled.

✍️ I implemented this pattern for getting clean and simple code and also reduced the boiler-plate code in my private project. I designed/implemented all ViewModel-Module to get same structure shaped like below. So by using it, I was able to focus on effective business logic and improve code readability, quality and extendability. Please refer to below:

class GetFeedViewModel
@AssistedInject
constructor(
useCase: GetFeedUseCase,
@Assisted private val params: Params
) : MediatorViewModel(useCase, params) {
@AssistedFactory
interface AssistedVMFactory {
fun create(params: Params): GetFeedViewModel
}

companion object {
fun provideFactory(assistedFactory: AssistedVMFactory, params: Params) =
object :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(params) as T
}
}
}
}
class GetCommentsViewModel
@AssistedInject
constructor(
useCase: GetCommentsUseCase,
@Assisted private val params: Params
) : MediatorViewModel(useCase, params) {
@AssistedFactory
interface AssistedVMFactory {
fun create(params: Params): GetCommentsViewModel
}

companion object {
fun provideFactory(assistedFactory: AssistedVMFactory, params: Params) =
object :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(params) as T
}
}
}
}
class PostFeedViewModel
@AssistedInject
constructor(
useCase: PostFeedUseCase,
@Assisted private val params: Params
) : MediatorViewModel(useCase, params) {
@AssistedFactory
interface AssistedVMFactory {
fun create(params: Params): PostFeedViewModel
}

companion object {
fun provideFactory(assistedFactory: AssistedVMFactory, params: Params) =
object :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(params) as T
}
}
}
}
class PostBookmarkViewModel
@AssistedInject
constructor(
useCase: PostBookmarkUseCase,
@Assisted private val params: Params
) : MediatorViewModel(useCase, params) {
@AssistedFactory
interface AssistedVMFactory {
fun create(params: Params): PostBookmarkViewModel
}

companion object {
fun provideFactory(assistedFactory: AssistedVMFactory, params: Params) =
object :
ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return assistedFactory.create(params) as T
}
}
}
}

⚠️ Warning: Someone might think that it would be better to move the interface and companion object blocks to the MediatorViewModel class as the parent class. But can’t do it. Because Dagger2 does not allow/support it. I bet you will see the Dagger error on compiling time if you put the interface and companion object blocks into MediatorViewModel class. Please refer to this link: Assisted Injection

You can implement ViewModel modules very quickly and easily using this pattern because the ViewModel module follows almost the same structural shape. I made all my effort to get the identical code implemented in all ViewModel modules.

Please refer to below link if you have no knowledge of Dagger2 as DI(Dependency Injection).

https://developer.android.com/training/dependency-injection/dagger-basics

Use the Module Pattern in your UseCase

Classes belonging to the Domain layer in App Architecture are commonly called use cases or interactors. Each use case should have responsibility over a single functionality.

The UseCase Class sits between the UI layer and the data layer.

The UseCase Class is responsible for encapsulating complex business logic, or simple business logic that is reused by multiple ViewModels. You use it when needed-for example, to handle complexity or favor reusability.

A UseCase module provides the following benefits:

  • It avoids code duplication.
  • It improves readability in classes that use domain layer classes.
  • It improves the testability of the app.
  • It avoids large classes by allowing you to split responsibilities.

To keep these classes simple and lightweight, each use case should only have responsibility over a single functionality, and they should not contain mutable data. You should instead handle mutable data in your UI or data layers.

The reason why I implemented UseCase will be explained in detail in the next article, “How to handle your internal business logic using ViewModel and UseCase”.

✍️ I implemented this pattern for getting clean and simple code and also reduced the boiler-plate code in my private project. I designed/implemented all UseCase-Module to get same structure shaped like below. So by using it, I was able to focus on effective business logic and improve code readability, quality and extendability. Please refer to below:

@Singleton
class GetFeedUseCase
@Inject
constructor(override val repository: GetFeedRepository) : RepoUseCase(repository)
@Singleton
class GetCommentsUseCase
@Inject
constructor(override val repository: GetCommentsRepository) : RepoUseCase(repository)
@Singleton
class PostFeedUseCase
@Inject
constructor(override val repository: PostFeedRepository) : RepoUseCase(repository)
@Singleton
class PostBookmarkUseCase
@Inject
constructor(override val repository: PostBookmarkRepository) : RepoUseCase(repository)

You can implement UseCase modules very quickly and easily using this pattern because the UseCase module follows almost the same structural shape. I made all my effort to get the identical code implemented in all UseCase modules.

Please refer to below link if you have no knowledge of Dagger2 as DI(Dependency Injection).

https://developer.android.com/training/dependency-injection/dagger-basics

Use the Module Pattern in your Repository

The data layer is made of repositories that each can contain zero to many data sources. You should create a repository class for each different type of data you handle in your app.

Repository classes are responsible for the following tasks:

  • Exposing data to the rest of the app.
  • Centralizing changes to the data.
  • Resolving conflicts between multiple data sources.
  • Abstracting sources of data from the rest of the app.
  • Containing business logic.

The entry points to the data layer are always the repository classes.

✍️ I implemented this pattern for getting clean and simple code and also reduced the boiler-plate code in my private project. I designed/implemented all Repository-Module to get same structure shaped like below. So by using it, I was able to focus on effective business logic and improve code readability, quality and extendability. Please refer to below:

@Singleton
class GetFeedRepository
@Inject
constructor() : Repository<Resource>() {
@Inject
lateinit var pagingSource: FeedPagingSource

override fun handle(viewModelScope: CoroutineScope, query: Query) = object :
PagingDataMediator<PagingData<Feed>>(viewModelScope, false) {
override fun load() = Pager(
config = PagingConfig(
pageSize = ITEM_COUNT,
prefetchDistance = ITEM_COUNT.div(2),
enablePlaceholders = true
)
) {
pageSize = ITEM_COUNT
pagingSource.setPagingParam(query)
pagingSource
}.flow.cachedIn(viewModelScope)
}.asSharedFlow

override fun invalidatePagingSource() {
pagingSource.invalidate()
}
}
@Singleton
class GetCommentsRepository
@Inject
constructor() : Repository<Resource>() {
@Inject
lateinit var pagingSource: CommentsPagingSource

override fun handle(viewModelScope: CoroutineScope, query: Query) = object :
PagingDataMediator<PagingData<Comment>>(viewModelScope, false) {
@Suppress("UNCHECKED_CAST")
override fun load() = Pager(
config = PagingConfig(
pageSize = ITEM_COUNT,
prefetchDistance = ITEM_COUNT.div(2),
initialLoadSize = ITEM_COUNT
)
) {
pageSize = ITEM_COUNT
pagingSource.setPagingParam(query)
pagingSource
}.flow.cachedIn(viewModelScope)
}.asSharedFlow

override fun invalidatePagingSource() {
pagingSource.invalidate()
}
}
@Singleton
class PostFeedRepository
@Inject
constructor() : Repository<Resource>() {
override fun handle(viewModelScope: CoroutineScope, query: Query) = object :
DataMediator<FeedResponse>(viewModelScope, false) {
override fun load() = restAPI.postFeed(query.args[0] as FeedSchema)
}.asSharedFlow
}
@Singleton
class PostBookmarkRepository
@Inject
constructor() : Repository<Resource>() {
override fun handle(viewModelScope: CoroutineScope, query: Query) = object :
DataMediator<BookmarkResponse>(viewModelScope, false) {
override fun load() = restAPI.postBookmark(query.args[0] as BookmarkSchema)
}.asSharedFlow
}

You can implement Repository modules very quickly and easily using this pattern because the Repository module follows almost the same structural shape. I made all my effort to get the identical code implemented in all Repository modules.

Please refer to below link if you have no knowledge of Dagger2 as DI(Dependency Injection).

https://developer.android.com/training/dependency-injection/dagger-basics

Use the Module Pattern in your PagingSource as DataSource

Each data source class should have the responsibility of working with only one source of data, which can be a file, a network source, or a local database. Data source classes are the bridge between the application and the system for data operations.

Other layers in the hierarchy should never access data sources directly; the entry points to the data layer are always the repository classes. State holder classes (see the UI layer guide) or use case classes (see the domain layer guide) should never have a data source as a direct dependency. Using repository classes as entry points allows the different layers of the architecture to scale independently.

The data exposed by this layer should be immutable so that it cannot be tampered with by other classes, which would risk putting its values in an inconsistent state. Immutable data can also be safely handled by multiple threads.

✍️ I implemented this pattern for getting clean and simple code and also reduced the boiler-plate code in my private project. I designed/implemented all PagingSource-Module to get same structure shaped like below. So by using it, I was able to focus on effective business logic and improve code readability, quality and extendability. Please refer to blow:

@Singleton
class FeedPagingSource
@Inject
constructor() : BasePagingSource<Int, FeedResponse, Feed>() {
@Suppress("UNCHECKED_CAST", "KotlinConstantConditions")
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Feed> {
val offset = params.key ?: 0
val next = pagingResponse?.next
val nextKey: Int?

return try {
nextOffset = offset
nextKey = request(params, next, offset)
when {
errorMessage == PAGING_EMPTY -> LoadResult.Error(Throwable(errorMessage))
pagingResponse?.payload?.isNotEmpty()!! -> {
LoadResult.Page(
data = pagingResponse?.payload!!,
prevKey = null,
nextKey = nextKey
)
}
nextKey == null -> {
errorMessage = PAGING_END
LoadResult.Error(Throwable(errorMessage))
}
else -> LoadResult.Error(Throwable(errorMessage))
}
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
} catch (exception: Exception) {
// Handle errors in this block
return LoadResult.Error(exception)
}
}

override fun getRefreshKey(state: PagingState<Int, Feed>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(pageSize) ?: anchorPage?.nextKey?.minus(pageSize)
}
}

override fun remakeQuery(next: String): Query {
TODO("Not yet implemented")
}
}
@Singleton
class GetCommentsPagingSource
@Inject
constructor() : BasePagingSource<Int, CommentsResponse, Comment>() {
private var pagingResponse: CommentsResponse? = null

@Suppress("UNCHECKED_CAST", "KotlinConstantConditions")
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Comment> {
val offset = params.key ?: 0
val nextKey: Int?

return try {
nextOffset = offset
request(params, PARAMETER1)
nextKey = handleKey(params, pagingResponse?.next, false)
when {
errorMessage == PAGING_EMPTY -> LoadResult.Error(Throwable(errorMessage))
pagingResponse?.payload?.isNotEmpty()!! -> {
LoadResult.Page(
data = pagingResponse?.payload!!,
prevKey = null,
nextKey = nextKey
)
}
nextKey == null -> {
errorMessage = PAGING_END
LoadResult.Error(Throwable(errorMessage))
}
else -> LoadResult.Error(Throwable(errorMessage))
}
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
} catch (exception: Exception) {
// Handle errors in this block
return LoadResult.Error(exception)
}
}

override fun getRefreshKey(state: PagingState<Int, Comment>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(pageSize) ?: anchorPage?.nextKey?.minus(pageSize)
}
}

override fun remakeQuery(next: String): Query {
val page = next.substring(
next.indexOf(PARAMETER1) + 4,
next.indexOf(PARAMETER2)
)

return Query(
query.args[0] as String,
query.args[1] as String,
page.toInt(),
query.args[3] as Int
)
}

@Suppress("UNCHECKED_CAST")
override suspend fun requestAPI(query: Query) {
restAPI.getComments(
query.args[0] as String,
query.args[1] as String,
query.args[2] as Int,
query.args[3] as Int
).collectLatest { response ->
pagingResponse = handleResponse(response)
pagingResponse?.let {
errorMessage = if (it.count == 0 || it.payload.isEmpty())
PAGING_EMPTY
else
PAGING_NORMAL
}
}
}
}
@Singleton
class SearchedFeedPagingSource
@Inject
constructor() : BasePagingSource<Int, FeedResponse, Feed>() {
@Suppress("UNCHECKED_CAST", "KotlinConstantConditions")
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Feed> {
val offset = params.key ?: 0
val nextKey: Int?

return try {
nextOffset = offset
request(params, PARAMETER)
nextKey = handleKey(params, pagingResponse?.next, false)
when {
errorMessage == PAGING_EMPTY -> LoadResult.Error(Throwable(errorMessage))
pagingResponse?.payload?.isNotEmpty()!! -> {
LoadResult.Page(
data = pagingResponse?.payload!!,
prevKey = null,
nextKey = nextKey
)
}
nextKey == null -> {
errorMessage = PAGING_END
LoadResult.Error(Throwable(errorMessage))
}
else -> LoadResult.Error(Throwable(errorMessage))
}
} catch (exception: IOException) {
return LoadResult.Error(exception)
} catch (exception: HttpException) {
return LoadResult.Error(exception)
} catch (exception: Exception) {
// Handle errors in this block
return LoadResult.Error(exception)
}
}

override fun getRefreshKey(state: PagingState<Int, Feed>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}

override fun remakeQuery(next: String): Query {
val page = next.substring(
next.indexOf(PARAMETER1) + 4,
next.indexOf(PARAMETER2)
)

@Suppress("UNCHECKED_CAST")
return Query(
query.args[0] as String,
page.toInt(),
query.args[2] as List<String>
)
}

@Suppress("UNCHECKED_CAST")
override suspend fun requestAPI(query: Query) {
restAPI.getSearchedFeed(
query.args[0] as String,
query.args[1] as Int,
ITEM_COUNT
).collectLatest { response ->
pagingResponse = handleResponse(response)
pagingResponse?.let {
errorMessage = if (it.count == 0 || it.payload.isEmpty())
PAGING_EMPTY
else
PAGING_NORMAL
}
}
}
}

You can implement PagingSource modules very quickly and easily using this pattern because the PagingSource module follows almost the same structural shape. I made all my effort to get the identical code implemented in all PagingSource modules.

Please refer to below link if you have no knowledge of Dagger2 as DI(Dependency Injection).

https://developer.android.com/training/dependency-injection/dagger-basics

☑️ Conclusion

The module pattern allows you to build better code faster, focus more on your business logic, and make your features more extensible and flexible. It helps you focus your business logic and create better services, and you can quickly deliver user-friendly features to your users. It also increases/contributes coding productivity and improves efficiency.

However, the Patterns are not a panacea. Of the Three phases below, Module-Patterns do almost nothing in the analysis. Module-Patterns, as the name implies, have their biggest impact in the design/implement phase of making App.

App development can be divided into three stages:

  • Analysis — Understand the problem to be solved; gather user requirements
  • Design — Choose the data structures, algorithms, classes, and patterns that will model the problem; possibly prototype the system, especially if it. has a large user interface component
  • Code — Implement the design in code

To be more precise:

  1. Module-Patterns helps you analyze/implement the more abstract areas of a service by providing concrete, well-tested solutions.
  2. Module-Patterns helps you make code faster by providing a clearer picture of how you are implementing the design.
  3. Module-Patterns encourages code reuse and accommodate change by supplying well-tested mechanisms for delegation[1] and composition[2], and other non-inheritance based reuse techniques.
  4. Module-Patterns encourages more legible and maintainable code by following well-understood paths.
  5. Module-Patterns increasingly provides a common language and terminology for App developer.

[1] Delegation: This pattern is one where a given object provides an interface to a set of operations. However, the actual work for those operations is performed by one or more other objects.
[2] composition: Creating objects with other objects as members. Composition should be used when a ‘has-a’ relationship applies.

Photo by Oleg Savenok on Unsplash

👉❗️🧤 Well designed/implemented Mobile App using the Module-Pattern can make to the new extendable App more quickly and easily by getting the new UI Layout and the business logics applied. Just change the these factors. So making the App Architecture with Module-Patterns is very important to run your services. You can expand your service more faster than others, and add features flexibly If you can make the App-Architecture blueprint.

Thank you so much for reading and I hope this article was helpful. If you have something to say to me, please leave a comment, and if this article was helpful, please share and clicks 👏 👏 👏 👏 👏.

PS :

I’m scheduled to write a couple of tech article about Android Techs.

🔰 Next article coming soon! —”How to transmit the logs about the patterns, interests of users to Firebase log Server for Data Analysis and Machine Learning by scrolling on RecyclerView: for making Data-Driven Decisions”.

I’m looking for a new job. Let me know or email me if you’re interested in my experience and techs. Please refer to my LinkedIn link below:

LinkedIn : https://www.linkedin.com/in/lukoh-nam-68207941/

Email : lukoh.nam@gmail.com

--

--