Android- WorkManager in Clean Architecture

Sourav Saha
6 min readMay 27, 2024

--

Clean Architecture is nothing new in Android ! It has been a long time since we are playing and implementing it. And As android engineers, we often need to use WorkManager. But then the question comes, where do this little piece of component of Android belong in the perspective of a clean architecture?

I was working with work manager for one of my projects a few days ago and ran into the problem myself. Where do I attach the workmanager? Let’s dive deep.

We know a clean architecture has 3 main layers.

  1. Domain layer
  2. Data layer
  3. Presentation layer

Now, I know what many of you are thinking, where are framework and use case layers?? In terms of clean architecture, the Use-case layer is actually a part of domain layer and Framework layer, we can merge it into presentation layer. So we are going to keep things simple here using 3 layers.

Now, I am not going to deep dive into clean architecture basics, rather I will try to explain where do I affix WorkManager in Clean. But for the starters,

A domain layer encapsulates the business logic, in terms of android, it will only have code that is pure kotlin and no android specific code! In other words, it is actually “What” we will be getting from Backend (Or Local Db may be !). Like, if you want to get a list of post online, the domain layer should only contain the request-response models and the skeleton function without body of “What” data we need to get!

A data layer is responsible for getting the data from backend. It completes the domain layer. In other words, it explains “How” we are going to get the data from rather than pointing to What we are getting. So it will contain the actual implementation of the skeleton that we declared in Domain layer.

A Presentation layer is responsible for showing the data to users, more like the name suggests! It “Presents” the data to user. This contains the activity, composable files and the viewmodels.

Now according to our discussion above we can be sure of one thing, a workmanager can’t be a part of the domain layer ! Cause, it is not pure kotlin thing rather pure android component and also it is not related to business logic! So, that leaves us two choices

  1. Data layer
  2. Presentation layer

Now, it totally depends on your specific scenario which layer the WorkManager will belong to. But in normal cases where you get some small chunk of data from backend or something, the WorkManager should belong to the data layer. That is what we are going to depict here.

For starters, Let’s declare what we will be doing in our demo project!

We will be getting some data from

https://jsonplaceholder.typicode.com/

We will be needing Hilt as dependency injection library (Or any other DI library is also fine!). We will be using Hilt here! So lets add the dependency

("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44"

We will be needing some other libraries for hilt to incorporate with work manager.

implementation ("androidx.work:work-runtime-ktx:2.7.1")
implementation ("androidx.hilt:hilt-work:1.0.0")

Now, Since we are going to use retrofit for getting data from an api, let’s implement that using hilt.

Here is our AppModule class. We want the retrofit instance to be available through the lifecycle of the whole application. So, the AppModule will be Singleton scoped!

Also, here is an interesting part, Workmanager initialisation needs a context to be initialized. Since we want to make the Workmanager part of our data layer and and we don’t have the context at the data layer, we are going to provide the context from our hilt module. So that it can access the AppContext provided by hilt and we then, inject the initialized WorkManager to our data layer.

Here is our AppModule Class.

package com.example.workmanagerinclean.di

import android.content.Context
import androidx.work.WorkManager
import com.example.workmanagerinclean.data.remote.Api

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create
import javax.inject.Singleton


@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
@Singleton
fun provideRetrofit(): Api {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
return Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
.create()
}


@Provides
@Singleton
fun provideWorkManager(@ApplicationContext context: Context): WorkManager {
return WorkManager.getInstance(context)
}
}

The best part is, you can use @ApplicationContext and get app context here by default.

And, now the api class for retrofit

package com.example.workmanagerinclean.data.remote

import com.example.workmanagerinclean.domain.response_model.Post
import retrofit2.Response
import retrofit2.http.GET


interface Api {

@GET("posts")
suspend fun getPosts(): Response<List<Post>>
}

Now, in the domain layer, we will add our skeleton functions to get posts from api.

so, the package structure for domain layer will be like the image

here is our PostRepository Interface and Data class Post

package com.example.workmanagerinclean.domain.repository

import kotlinx.coroutines.flow.Flow

interface PostRepository {
suspend fun getPost(): Flow<Pair<String,String>>
}
package com.example.workmanagerinclean.domain.response_model

data class Post(
val id: Long,
val title: String,
val body: String,
val userId: Long
)

For the Data layer, we will be making package structure like below-

Now, for the moment of truth xD, we are writing our Worker class. We will be getting the data from our api, so the worker needs to have the api instance from retrofit. So we will inject the api into our worker.

So, here is our worker class

package com.example.workmanagerinclean.data.remote

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext

@HiltWorker
class GetPostWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted params: WorkerParameters,
private val api: Api
) :
CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
delay(2000)
return withContext(Dispatchers.IO) {
try {
val response = api.getPosts()
if (response.isSuccessful) {

val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(List::class.java).lenient()
val jsonString = jsonAdapter.toJson(response.body()?.take(10))
val data = Data
.Builder()
.putString("responseString", jsonString)
.build()
Result.success(data)
} else {
Result.failure(
Data.Builder().putString("failure", response.errorBody()?.string()).build()
)
}
} catch (e: Exception) {
e.printStackTrace()
Result.failure(Data.Builder().putString("failure", e.message).build())
}
}
}
}

we are using Moshi to get the response into the form of a json string since we can only return specific types from work manager. Since we are injecting api instance into the worker , it has to be annotated with @HiltWorker

PostRepoImpl class ->

package com.example.workmanagerinclean.data.remote.repository

import androidx.lifecycle.asFlow
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.example.workmanagerinclean.data.remote.GetPostWorker
import com.example.workmanagerinclean.domain.repository.PostRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class PostRepoImpl @Inject constructor(private val workManager: WorkManager) : PostRepository {
override suspend fun getPost(): Flow<Pair<String,String>> {
val getPostWorkRequest: OneTimeWorkRequest =
OneTimeWorkRequest.Builder(GetPostWorker::class.java).build()
workManager.enqueue(getPostWorkRequest)
return workManager.getWorkInfoByIdLiveData(getPostWorkRequest.id).asFlow().map {
if (it.state == WorkInfo.State.SUCCEEDED) {
val res = it.outputData.getString("responseString") ?: ""
Pair("Success",res)
} else {
Pair("Failure","")
}
}
}
}

here, we are injecting our Workmanager instance injected. Which will be provided by our AppModule. And we are going to return the flow we will be getting from the WorkManager. And to get the data from Work Manager, we observe the flow (Converted from Livedata) from Work Manager

We are converting our livedata from workmanager to Flow for better compatibility to use in MVI design pattern in Clean architecture with Jetpack Compose. Since this article is not about MVI or Jetpack compose. I am skipping details on that

And finally, Let’s talk about how we are going to design our ViewModel for the UI to fetch the data.

package com.example.workmanagerinclean.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import com.example.workmanagerinclean.domain.repository.PostRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject


@HiltViewModel
class PostViewModel @Inject constructor(private val repo: PostRepository) : ViewModel() {

init {
getPost()
}

private fun getPost() {
viewModelScope.launch {
repo.getPost().collect{
if(it.first == "Success"){
val outPut = it.second
}
}
}

}

fun onEvent(event: MainUiEvent) {
when (event) {
is MainUiEvent.GetPost -> {
getPost()
}
}
}
}

We are fetching the data in Viewmodel which is annotated with HitlViewModel.

So, Here is how we can place WorkManager in our Clean architecture. So here are the keys

  1. Design your app module for Hilt and provide WorkManager instance using app context from app module
  2. Use hilt-worker library to inject anything into Worker class.
  3. Get your Worker working in Data layer to do the job you want your worker to do

Full code is available in my Github Repo. Feel free to clone and check.

Github link : https://github.com/ssjsaha/WorkmanagerinClean

And a clap in this post would be really very … umm ‘Inspiring

Happy Androiding :)

--

--

Sourav Saha
Sourav Saha

Written by Sourav Saha

Programming , Travel , Mobile app ,Family and Dogs . Well that almost defines me!!