DEV Community

Adam McNeilly
Adam McNeilly

Posted on

The Repository Pattern: Properly Organizing Your Data Layer

Originally published on Android Essence.

How to properly architect your application is a concern we as developers constantly face. There's unfortunately no one size fits all answer to it, and sometimes we don't even know where to begin. I've learned along my Android journey that the answer can also vary depending on what portion of your app you're trying to organize. Of course, you might say, it depends.

When it comes to your data layer, though, there are some really good tips on how to write clean, maintainable code. One of them is the Repository Pattern, and I'd like to provide a quick walk through of what it is and why it's important.

TL;DR

The repository pattern is a way to organize your code such that your ViewModel or Presenter class doesn't need to care about where your data comes from. It only cares about how to request data and what it gets back.

A Bad Example

Let's first look at what happens to our code when we don't implement the repository pattern. Let's say I have a detail page for a Pokemon (yes, I love using Pokemon examples), and I want to fetch the data from a retrofit service. I might end up with a ViewModel looking something like this:

class DetailActivityViewModel(
    private val pokemonAPI: PokemonAPI
) : BaseObservableViewModel() {
    ...

    init {
        job = CoroutineScope(dispatcherProvider.IO).launch {
            ...

            val pokemon = pokemonAPI.getPokemonDetailAsync(pokemonName).await()

            ...
        }
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

While this may not look awful, it actually poses an interesting limitation. What if you were asked to fetch from a GraphQL API instead? Or even a local database? What if you wanted a mix, or to A/B test multiple approaches?

Depending on which one of those you chose, this gets really ugly. First and foremost, you'd have to add the relevant properties to your ViewModel, and then your ViewModel class eventually becomes bloated with data layer work that really doesn't belong there anymore.

If you've ever found yourself in this spot, even if you haven't yet hit this limitation (some people only use a Retrofit API and that's fine), you may want to consider helping your future self with the repository pattern.

Repository Interface

Going back up to the TL;DR, your ViewModel shouldn't care where the information comes from. In many programming problems where we don't care about the implementation of something, we can put the contract of what we do care about in an interface.

We can start there by defining our interface for what our data fetching behavior should be:

interface PokemonRepository {
    suspend fun getPokemon(): PokemonResponse
    suspend fun getPokemonDetail(pokemonName: String): Pokemon
}
Enter fullscreen mode Exit fullscreen mode

Once we've defined that, we should update our ViewModel to use this interface:

class DetailActivityViewModel(
    private val repository: PokemonRepository
) : BaseObservableViewModel() {
    ...

    init {
        job = CoroutineScope(dispatcherProvider.IO).launch {
            ...

            val pokemon = repository.getPokemonDetail(pokemonName)

            ...
        }
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

Now we're in a good place to take control of where our data comes from, and update it as needed without worrying about updating our ViewModel.

The Implementation

If you were someone who was only using one data source, like a Retrofit API, you only have to worry about creating one implementation, which can be done with some heavy copy and paste. In the Pokedex example, we converted our implementation to this:

open class PokemonRetrofitService(
    private val api: PokemonAPI
): PokemonRepository {
    override suspend fun getPokemon(): PokemonResponse {
        return api.getPokemonAsync().await()
    }

    override suspend fun getPokemonDetail(pokemonName: String): Pokemon {
        return api.getPokemonDetailAsync(pokemonName).await()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can just update our call site for creating the ViewModel to use this implementation:

private val viewModelFactory = object : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        val pokemonAPI = ...
        val repository = PokemonRetrofitService(pokemonAPI)

        return DetailActivityViewModel(repository) as T
    }
}
Enter fullscreen mode Exit fullscreen mode

Multiple Implementation Example

This pattern is especially important if you want to consider multiple implementations for fetching data. A common example may be that you want to also fetch data from a local database, in addition to your network service.

Let's consider an example where we have an explicit offline mode that fetches data from a database. If your code is already using the repository pattern, we don't need to worry about updating the ViewModel.

All we would need to do is create our DatabasePokemonService and then we can conditionally pass that into the ViewModel:

private val viewModelFactory = object : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        val repository = getPokemonRepository()

        return DetailActivityViewModel(repository) as T
    }
}

private fun getPokemonRepository(): PokemonRepository {
    if (offlineMode()) {
        return DatabasePokemonService()
    } else {
        return RetrofitPokemonService()
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing Benefits

When you move your data fetching code into an interface, you also make unit testing a lot easier! Our unit tests for the ViewModel don't have to worry about the data implementation either, we can just use Mockito to mock the interface and stub test data that way:

class DetailActivityViewModelTest {
    ...

    private val mockRepository = mock(PokemonRepository::class.java)

    @Test
    fun loadData() {
        val testPokemon = Pokemon(name = "Adam", types = listOf(TypeSlot(type = Type("grass"))))

        whenever(mockRepository.getPokemonDetail(anyString())).thenReturn(testPokemon)

        ...
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

Now we don't have to worry about creating a room database in our unit tests, or a retrofit service. Dealing with an interface can help avoid all of those struggles.

Resources

I hope you found this helpful in understanding how to properly organize your data fetching code. If you want to see the repository pattern in action (although I don't do this A/B testing scenario), you should check out this Pokedex project on GitHub.

If you like analyzing the code directly, you can see the repository pattern implemented in this pull request.

Top comments (0)