Android- WorkManager in Clean Architecture
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.
- Domain layer
- Data layer
- 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
- Data layer
- 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
- Design your app module for Hilt and provide WorkManager instance using app context from app module
- Use hilt-worker library to inject anything into Worker class.
- 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 :)