With this episode of Mad Skills,
we're starting a series of videos dedicated to architecture.
We'll start from the ground up, the data layer.
That is, if you draw your app architecture with a data layer at the bottom.
The data layer contains application data and business logic.
The business logic is what gives value to your app.
It determines how application data must be created,
stored, and changed.
The data layer is made of repositories
that each can interact with 0, 1, or more data sources.
Data sources, much like their name suggests,
are responsible for providing data the app needs to funcion.
They may present data form the network, a local database,
files, or even, from memory.
And they should have the responsibility of working
only with one source of data,
which's typically holds a unit of business logic.
For example, articles, users, or movies.
That's data sources.
Now, repository classes are used by all the layers in your app
to interact with the data layer.
They are responsible for exposing data to the rest of the app,
but also, for centralizing changes to the data,
resolving conflicts between multiple data sources,
and containing business logic.
You should create our repository class
for each different type of data you handle in your app.
For example, you might create a movies repository
for data related to movies,
or a payments repository for payments.
Repositories can combine different data sources
and they're responsible for solving any potential conflicts between them.
If there's a clash between the data in your local database
and the server, for example, the repository should detect and fix this.
Remember that all the layers in your app
should never depend on data sources directly.
The entry points to the data layer are always their repository classes.
A common pattern in repositories is to perform one-shot calls
like create, read, update, and delete.
These can be implemented with suspend functions in Kotlin.
But also, you can be notified of data changes over time
by exposing a data stream, for example, using Kotlin Flow.
This is out of the scope of this video so, if you want to learn more,
check out our Flow guide; the link is in the decription.
Handling multiple data sources in a repository can get tricky.
You need to choose a source of truth
and make sure that it's always in a consistent state.
Let's look at a concrete example.
Let's say we have two sources of data for news.
LocalNewsDataSource depends on a room database
and can return a list of articles or update them.
RemoteNewsDataSource depends on a API client,
for example, a Retrofit client.
This one can only fetch news.
Now, the news repository would depend on those two data sources.
If we want to fetch the news from other layers,
we use the fetchNews method on the repository.
This method tries to load the news from the network first.
If it succeeds, it updates the local database.
If it fails because there was no data connection
or the server was down, it just logs the error.
Instead, you might want to show something in the UI.
Finally, in any case, it returns the result from the database
since it's our source of truth.
This case is very simple because we're using data that the user can't edit.
However, resolving conflicts can get challenging.
For example, imagine a calendar app
where a meeting is modified by two users at the same time.
So, this will require some thought,
because getting this right is important for a good offline-first user experience.
Libraries that consume remote APIs and remote databases, such as ViceStore
have caching mechanisms that you can use,
and they can even deal with conflicts.
Now, let's talk about immutability.
The data exposed by the data layer should be immutable
so that all the classes cannot tamper with it.
This would risk putting its values into an inconsistent state.
Another advantage of immutable data
is that it can be safely handled by multiple threads.
Kotlin's data classes are a perfect tool for this.
By the way, when modeling entities,
you should consider that the model returned by your database
or the remote API,
might not be what the other layers need.
For example, in this article model, if you don't need the modifications,
or the author's date of birth,
create a different model for the UI layer.
This not only makes your code neater, it provides better separation of concerns,
letting each layer define what model it needs.
Let's talk about threading now.
Calling data sources and repositories should be main-safe,
safe to call form the main thread.
This means that the repositories, our data sources,
are responsible for moving the execution of the logic to the appropriate thread
when performing long branning or blocking operations.
Something else to take into account are errors.
because data operations won't always succeed.
It's important to somehow propagate information about a failure
to the other layers.
One option is to simply let exceptions propagate.
With suspend functions,
you can grab repository calls in a regular try-catch-block
from the UI or the domain layer.
Or, if you use InFlows, you can use the catch operator.
Another option is to cath these errors in the data layer,
and expose data that can contain either a success or a failure
with a more meaningful exception.
Don't forget about dealing with errors.
They will happen.
As you saw earlier,
a repository can depend on multiple data sources.
In some cases,
you might want to have multiple levels of repositories.
This is perfectly fine.
In this example, a user repository
needs data from a logging repository and the registration repository.
Similarly, two repositories can share a data source.
All the steps are recommendations that should work well in most steps.
However, if you have a reason to deviate, feel free to do it.
FInally, let's talk about testing.
The data layer is typically easy to test.
Repositories are unit-tested normally.
You replace the data sources with fakes or mocks
and verify that your repository is correctly handling data
and calling the data sources when needed.
Testing data sources can be slightly tricky
because they depend on databases or API clients.
However, the libraries that let you do that
usually provide test artifacts or mechanisms for testing.
For example, Room provides an in-memory database implementation
that can help you test the coding data source.
In end-to-end or big tests,
you test all the layers of your app at the same time,
but you might want to use fake data
to make your test faster and more reliable.
If you use the dependency injection,
you should be able to replace data sources or repositories with fake implementations.
You can also fake your network calls with popular libraries
such as WireMock or MockWebServer.
This video is a summary of the data layer article
in the architecture guide.
For more details and code samples about best practices in the data layer,
chek out the rest of the guide here.
And that's it.
I hope you learned the difference between a repository and a data source
and how to model the data they expose.
The next video we're releasing is about the UI layer,
so subscribe if you want to be notified when it's out
or if you're from the future, check it out next.
Thanks.