Photo by Sincerely Media on Unsplash
Implementing a Repository in our App with MVVM & DI in Android: (Day 07)
In our last blog post, we implemented Retrofit into our app, so at this point we have implemented and injected our RoomDB instance as well as Retrofit. If we look at our architecture pattern image again, it shows us that the next step would be implementing a Repository that would depend on both Models as well as Remote Data Source.
Just to brush up old concepts, here's why we need a repository in the first place.
Abstraction of Data Source: The primary role of the Repository is to abstract the origin of the data that the ViewModel consumes. This means that the ViewModel doesn't need to know where the data comes from. It could be from a local database, a remote API, shared preferences, or any other data source.
Single Source of Truth: By centralizing the data logic in the Repository, you ensure that there's a single source of truth in your application. This means that all parts of your app that need data will get a consistent set of data.
Decoupling: MVVM promotes separation of concerns. By using a Repository, you further decouple the data source logic from the ViewModel. This makes the ViewModel more focused on its primary responsibility: managing and processing data for the View.
Now that we have a basic understanding of the core reason behind creating a repository class, let's go ahead and create a repository class that will handle our login and signup workflows.
The code to this blog post can be found here
Step 1
Start by creating a simple class and name it as UserRepository
, as already discussed above, this class will be dependent on two components, i.e. our UserDAO
(which will be our local database) and WebService
(which will provide access to remote databases or APIs)
class UserRepository @Inject constructor(val webService: WebService, val userDAO: UserDAO) {
//TODO Code to be added here in future.
}
Note:
The @Inject
before the constructor exists because we'll be using constructor injection to provide the webService
and the userDAO
. This is because it's a general practice to prefer constructor injection over field injection wherever possible. We used field injection in our MainActivity
only because we didn't have access to it's constructor. To know more, consider reading the following blog post
Field Injection in Dagger2: DI (Day 2)
Step 2
Now we need to create a new Repository module in our di
package, this will hold all our future repository injection provider methods.
@Module
class RepositoryModule {
@Provides
@Singleton
fun provideUserRepository(webservice: WebService, dao: UserDAO): UserRepository {
return UserRepository(webService = webservice, userDAO = dao)
}
}
The code above is pretty much self-explanatory. We've simply injected an instance of UserRepository into our dagger graph with webService
and userDAO
which we had already injected in our previous post.
Step 3
Now that our repository has access to webService
and userDAO
, let's go ahead and try to write methods that will access the database and APIs.
class UserRepository @Inject constructor(val webService: WebService, val userDAO: UserDAO) {
private val _userDataResponse = MutableLiveData<UserDataResponse>()
val userDataResponse: LiveData<UserDataResponse> get() = _userDataResponse
fun storeUserData(userData: UserData){
userDAO.insertUsers(userData = userData)
}
fun fetchAllUserData() {
webService.fetchAllUsers().enqueue(object : Callback<UserDataResponse> {
override fun onResponse(call: Call<UserDataResponse>, response: Response<UserDataResponse>) {
if (response.isSuccessful) {
_userDataResponse.postValue(response.body())
Log.e("API_RESPONSE", "Success: ${Gson().toJson(response.body())}")
} else {
val errorString = response.errorBody()?.string()
Log.e("API_RESPONSE", "Unsuccessful: $errorString")
}
}
override fun onFailure(call: Call<UserDataResponse>, t: Throwable) {
// Handle failure
Log.e("API_RESPONSE", "Failure: ${t.message}")
}
})
}
}
Most of the code here has been borrowed from what we had written in the MainActivity earlier and doesn't need any explanation.
The only part which is new to us is the use of LiveData and Mutable LiveData above. So let's go ahead and understand why was it used, and what it does.
What is LiveData?
LiveData:
What is it? Think of
LiveData
as a container that holds some data, like a box. But this isn't just any box; it's a special box that can notify you whenever the data inside it changes.Why is it special? This box is smart. It knows when you're around (like when your app is open and active) and only then will it notify you about changes. If you're not around (like when your app is in the background), it won't bother you.
MutableLiveData:
What is it? It's a type of
LiveData
, but with an extra feature: you can change the data inside it. With the regularLiveData
, the box is sealed, and you can't change its contents. But withMutableLiveData
, you can open the box and put new things inside.When to use it? When you want to change the data inside the box from within your app's code.
In our code above, we used _userDataResponse
to update the data whenever we got a response from our API call. But we used userDataResponse
as the exposed member of the class that can be accessed by the UI to observe changes to _userDataResponse
.
The reason behind doing this was simply that while _userDataResponse
is a MutableLiveData i.e. it can be modified, and the userDataResponse
is a simple LiveData which will prevent UI operation from being able to modify the contents ensuring data integrity.
Step 4
Go ahead and now add the Repository Module to our AppComponent, so that we can later use it in our dagger graph.
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, DatabaseModule::class, RepositoryModule::class])
interface AppComponent : AndroidInjector<MyApp> {
@Component.Factory
interface Factory {
fun create(@BindsInstance application: Application): AppComponent
}
}
Step 5
Now that our repository code is ready to broadcast data to other components, let's go to our MainActivity and try to observe it.
class MainActivity : DaggerAppCompatActivity() {
@Inject
lateinit var userRepository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val insertButton = findViewById<Button>(R.id.insert)
insertButton.setOnClickListener {
fetchUserDataList()
}
userRepository.userDataResponse.observe(this, Observer {
Log.e("Observed", "Some data received in observer ${Gson().toJson(it)}")
})
}
fun storeDataExample(){
val data = UserData()
data.emailId = "testemail@test.com"
data.firstName = "test name"
data.password = "test password"
data.uid = "12312"
CoroutineScope(Dispatchers.IO).launch {
userRepository.storeUserData(userData = data)
}
}
fun fetchUserDataList(){
userRepository.fetchAllUserData()
}
}
Explanation
Notice that we have removed our older field injections for
webService
anduserDAO
, instead, now we only have a single field injection for the repository which helps keep the code uncluttered and easy to read.We are calling the
fetchAllUserData
method when the insert button is clicked, but we are not storing the response of the API call here, instead we are observing it asynchronously using LiveData.
LiveData observer in the MainActivity
.observe(this, Observer {...})
method is used to watch the LiveData for changes. It takes two parameters:
A
LifecycleOwner
(this
in this case, which refers to theMainActivity
).An
Observer
that defines what should be done when the LiveData's data changes.
How It Functions:
Initialization: When the
MainActivity
is created, the observer is set up to watch theuserDataResponse
LiveData.Data Change: Whenever the data inside
userDataResponse
changes (for instance, after a network request fetches new user data), all active observers are notified.Observer Action: Once notified, the lambda function inside the observer is executed. In this case, it logs the new data.
Lifecycle Respect: If the
MainActivity
goes to the background (like when the user navigates away or the screen is turned off), the observer won't receive updates, even if the data changes. This is because LiveData respects the lifecycle and doesn't push updates to inactive components. Once theMainActivity
becomes active again, it will start receiving updates.Activity Destruction: If the
MainActivity
is destroyed (like when the user finishes the activity or the system reclaims resources), the observer is automatically unregistered, ensuring there are no memory leaks.
Conclusion
And that's it, if you run the code again, you'll see similar results as our last blog post, but now our code is much cleaner and we also have a much streamlined separation of scopes and workflows across repository, API and database modules and of course the UI components.
But please not that this is still not the complete architecture, we are still missing a major piece of the puzzle which is MVVM i.e. the ViewModel class. This class sits between the UI and the repository and makes sure that logic specific to how data is shown on the UI is neither on the UI class nor on the repository class but in a third class with only this business logic as its purpose.
In our next blog post we'll try to understand in more detail the need for a ViewModel and how should it be implemented. Till then, Happy Coding!