A library written in pure dart to help devs set up dart projects with a proper architecture without having to write a lot of boilerplate code. Inspired by clean architecture.
Cornerstone have 2 main goals:
The first time I learned clean architecture and applied it to a real-world project, I felt increased confidence in my app maintainability. Refactor and feature additions are pleasant experience. But it's a pain to set up initially. That's why I create cornerstone. It's a library to help me set up clean architecture in a consistent and less-repetitive way. I hope you can also find it helpful too.
Most modern apps revolves around CRUD operations through an API & Data Persistence. I plan to make some utilities features to quickly adds common features to dart apps.
A mixin to make your repository persistable locally easily. I use Hive in order to achieve this. See the dartdoc in the file & implementation example in the example folder for more info and usage example.
Disclaimer: Even if I would say cornerstone is inspired from Clean Architecture, I wouldn't say that it adheres purely to clean architecture. You can see the complete app I made in the example folder for more details. It's a command line / terminal app written in pure dart.
In this section, I will focus on explaining this library interpretation. So, I recommend to read this article for more comprehensive understanding regarding clean architecture.
Platform is the "outside world". This includes server, database, 3rd party libraries, firebase, and such. It'll be better if you use interface to interacts with platform. This makes it easier to achieve layer independency. If you do, I also recommend you handle platform exceptions and transform them into your app's common exception type. This makes it easier to handle those Exceptions in repository.
I didn't use platform interface in my example project to make it simpler.
Sample platform interface:
abstract class CleanHttpClient {
FutureOr<dynamic> get(String path, {Map<String,dynamic> queryParams});
}
class DioCleanHttpClient extends CleanHttpClient {
final Dio dio;
const DioCleanHttpClient({@required this.dio});
FutureOr<dynamic> get(String path, {Map<String,dynamic> queryParams}) async {
// TODO: implement get for dio.
}
}
I don't provide the abstract for it (at least yet) because it's easier to implement it yourself to match your need.
Data source acts as the gateway to the "outside world" such as server (through API), database, and other third party libraries. Data source usually interacts with platform libraries, such as HttpClient
, Dio
, Hive
, SharedPreference
etc.
With that being said, it might be better if you make your data sources interact with platform through a platform interface. So, for example, if you need to switch from SharedPreference
to Hive
as your local storage solution, you can easily do so without directly impacts your data sources.
Remember that you need to make an abstract of your data sources to be used as repository dependency.
In Cornerstone, there are 5 kinds of abstract data sources to mix and match.
SingleGetterDataSource
MultipleGetterDataSource
CreatorDataSource
UpdaterDataSource
DeleterDataSource
For example, if you only need your data source to GET and CREATE, you can write it like:
abstract class PeopleDataSource
implements MultipleGetterDataSource<Person, Null>, CreatorDataSource<Person,Person>{}
class PeopleDataSourceImpl extends PeopleDataSource {
final Dio client;
PeopleDataSourceImpl({@required this.client});
@override
FutureOr<List<Person>> readMany({Null param}) async {
// TODO: implement readMany
}
@override
FutureOr<Person> create({Person param}) {
// TODO: implement create
}
}
See docs and example project to learn more.
Repository acts as the hub of several data sources. For example if you have more than 1 servers, repository is the place where I would put the logic of the fetching strategy (or data source calling strategy to be more precise).
Another thing it does is exception handling. In your repository, you should catch your exceptions, transform it into Failure model and returns it as Left
value with dartz. This makes you don't need to always put try catch block in the subsequent layers.
Like data source, you need to write an abstract first since your use cases will interacts with repository through interface.
Some of you might prefer to put local-persistence strategy in the data source. And it's ok since in my older projects, that's also what I did. But, after further considerations, I decided to name cornerstone's local-persistence strategy abstract class as a "Repository" instead of "Data Source". Because for my taste, persisting data can be considered as a "repository thing". Also, with this approach, I think it will make the code easier to read as well since it will produce a clearer separation of concerns like:
- Data source: retrieving data
- Repository: managing retrieved data
In my example project, I implement LocallyPersistentRepository
as a mixin. This way, you can reuse your local persistence logic, or even not using it at all if you don't want to implement persistence for your repo.
Use case is where your app's business logics and validations live. This layer should be independent regardless database, or presentation logic. The idea is that, use case will only change if your business process changes.
Use case can depend / call another use case. This way, it'll be better to keep 1 use case as single-concerned as possible.
There are several way to knit / put those layers together. First, is by using something like InheritedWidget
. But for me, I prefer to use a service locator library called Get It.
This is how I did it in my example project:
void initArchitecture() {
EquatableConfig.stringify = true;
Hive.init(Directory.current.path + '/db');
GetIt.I.registerLazySingleton(() => Dio());
GetIt.I.registerLazySingleton<PeopleDataSource>(
() => PeopleDataSourceImpl(client: GetIt.I()),
);
GetIt.I.registerLazySingleton<PeopleRepository>(
() => PeopleRepositoryImpl(dataSource: GetIt.I(), hive: Hive),
);
GetIt.I.registerLazySingleton(() => GetPeople(repo: GetIt.I()));
GetIt.I.registerLazySingleton(() => LoadPeople(repo: GetIt.I()));
GetIt.I.registerLazySingleton(() => ClearPeopleStorage(repo: GetIt.I()));
}
And then, I simply call it from main like:
void main() async {
initArchitecture();
/// Rest of the code
}
In a larger project though, it's recommended to split it into several initializator from several files to make it more readable, maybe something like:
/// Somewhere under lib/platforms
void initPlatforms() {
EquatableConfig.stringify = true;
Hive.init(Directory.current.path + '/db');
GetIt.I.registerLazySingleton(() => Dio());
}
/// Somewhere under lib/data_sources
void initDataSources() {
GetIt.I.registerLazySingleton<PeopleDataSource>(
() => PeopleDataSourceImpl(client: GetIt.I()),
);
}
/// Somewhere under lib/repositories
void initRepositories() {
GetIt.I.registerLazySingleton<PeopleRepository>(
() => PeopleRepositoryImpl(dataSource: GetIt.I(), hive: Hive),
);
}
/// Somewhere under lib/use_cases
void initUseCases() {
GetIt.I.registerLazySingleton(() => GetPeople(repo: GetIt.I()));
GetIt.I.registerLazySingleton(() => LoadPeople(repo: GetIt.I()));
GetIt.I.registerLazySingleton(() => ClearPeopleStorage(repo: GetIt.I()));
}
/// Somewhere under lib
void initArchitecture() {
initPlatforms();
initDataSources();
initRepositories();
initUseCases();
}
- Stream abstracts
- More common use cases implementations
I will appreciate any star to the github repo as well as like to the pub.dev page of this library. If you want, you can also buy me a cup of coffee by clicking the button below to motivate me creating helpful libraries in the future! Thanks for your support!
I also welcome if anyone want to collaborate with me in this project. The more the merrier!
The cornerstone (or foundation stone or setting stone) is the first stone set in the construction of a masonry foundation. All other stones will be set in reference to this stone, thus determining the position of the entire structure. (Wikipedia)