I am a beginner in flutter and have been learning flutter for 6 months or so. I am currently working on expense manager app. I found the clean architecture pattern really interesting although it requires a lot more(!) coding than needed otherwise. I could say this authentically since I almost made a functional app and now trying to start from scratch in clean architecture. I am not entirely following the guidelines in https://pub.dev/packages/flutter_clean_architecture but rather try to follow the clean concept as such - Domain -> Data -> UI
Here is what I have done so far:
Domain
abstract class UserTransactionEntity {
final int? transactionID;
final String merchant;
final amount;
final String category;
final String paymentType;
final String paymentAccount;
final String transactionDate;
final String? transactionNotes;
final String transactionType;
///User Transaction Entity Class
UserTransactionEntity(
{required this.transactionType,
required this.merchant,
this.amount,
required this.category,
required this.paymentType,
required this.paymentAccount,
required this.transactionDate,
this.transactionNotes,
this.transactionID});
}
///UserTransaction Interface
abstract class UserTransactionRepository {
Future<List<UserTransactionEntity>> getTransactionList();
postTransaction(UserTransactionEntity transaction);
}
Base usecase
abstract class UseCase<Type, Params> {
Future<Type> call(Params params);
}
class NoParams extends Equatable {
@override
List<Object?> get props => [];
}
Usecase to fetch everything at once
class GetAllTransactions
extends UseCase<List<UserTransactionEntity>, NoParams> {
final UserTransactionRepository repository;
GetAllTransactions(this.repository);
@override
Future<List<UserTransactionEntity>> call(NoParams params) async {
print('calling GetAll Transactions UseCase');
return await repository.getTransactionList();
}
}
Second use case to post transaction
class PostTransaction extends UseCase<dynamic, UserTransactionEntity> {
final UserTransactionRepository _userTransactionRepository;
PostTransaction(this._userTransactionRepository);
@override
call(UserTransactionEntity transaction) async {
return await _userTransactionRepository.postTransaction(transaction);
}
}
Data Layer
class UserTransactionModel extends UserTransactionEntity {
UserTransactionModel(
{int? transactionID,
required String transactionType,
required String merchant,
required num amount,
required String category,
required String paymentType,
required String paymentAccount,
required String transactionDate,
String? transactionNotes})
: super(
merchant: merchant,
transactionType: transactionType,
amount: amount,
category: category,
paymentType: paymentType,
paymentAccount: paymentAccount,
transactionDate: transactionDate,
transactionNotes: transactionNotes,
transactionID: transactionID);
factory UserTransactionModel.fromMap(Map<String, dynamic> map) {
return UserTransactionModel(
merchant: map[kMerchant],
transactionType: map[kTransactionType],
amount: map[kTransactionAmount],
category: map[kCategoryName],
paymentType: map[kPaymentType],
paymentAccount: map[kPaymentAccount],
transactionDate: map[kTransactionDate],
transactionNotes: map[kTransactionNotes],
transactionID: map[kTransactionID],
);
}
Map<String, dynamic> toMap() {
_validation();
return {
kMerchant: merchant,
kTransactionAmount: amount,
kCategoryName: category,
kPaymentType: paymentType,
kPaymentAccount: paymentAccount,
kTransactionDate: transactionDate,
kTransactionNotes: transactionNotes,
kTransactionType: transactionType
};
}
@override
String toString() {
return "User Transaction {$transactionType Amount: $amount Merchant:$merchant TransactionDate: $transactionDate}";
}
void _validation() {
if (merchant == '') {
throw NullMerchantNameException(message: 'Merchant should have a name!');
}
}
}
getTransactionList() async {
final _db = await _dbProvider.database;
final result = await _db!.query(kTransactionTable);
return result;
}
Post Transaction method is using data model created from domain entity.
Future<int> postTransaction(UserTransactionModel userTransaction) async {
var _resultRow;
final _db = await _dbProvider.database;
try {
_resultRow = _db!.insert(
kTransactionTable,
userTransaction.toMap(),
);
return _resultRow;
} catch (exception) {
//TODO
throw UnimplementedError();
}
}
abstract class LocalDataSource {
postTransaction(UserTransactionModel transaction);
updateTransaction();
deleteTransaction(int transactionID);
getIncomeTransactionList();
getExpenseTransactionList();
getTransactionByDate(String date);
Future<List<TransactionCategoryEntity>> getCategoryList();
Future<List<UserTransactionEntity>> getTransactionList();
}
This is where my problem begins. Following method works fine in the 4th step when implemented in repository. Note that I am fetching entity from database.
Future<List<UserTransactionEntity>> getTransactionList();
Following method throws error when try to implement in the data repository.
postTransaction(UserTransactionModel transaction);
If I try to post this model, repository in 4th step complains that its conflicts with domain repository
'UserTransactionRepositoryImpl.postTransaction' ('dynamic Function(UserTransactionModel)') isn't a valid override of 'UserTransactionRepository.postTransaction' ('dynamic Function(UserTransactionEntity)').
If I try to update it in the Local Data Source to domain entity, then it will ultimately conflict with the method in 2nd step to post data model. If I change everything to entity, it works fine but then the data model becomes useless and I have no way to created methods to remap data fetched from DB- e.g toMAP(), FromMap() etc.
I am not sure what I am missing here. I am completely newbie in software programming but have some experience with VBA.
Let's move on to next step.
class UserTransactionRepositoryImpl implements UserTransactionRepository {
final LocalDataSource _dataSource;
UserTransactionRepositoryImpl(this._dataSource);
@override
Future<List<UserTransactionEntity>> getTransactionList() async {
try {
final List<UserTransactionEntity> transactionList =
await _dataSource.getTransactionList();
return transactionList;
} on Exception {
throw Exception;
}
}
@override
postTransaction(UserTransactionEntity transaction) async {
await _dataSource.postTransaction(transaction);
}
}
Presentation layer Finally, I have created the Presentation layer as well and isolated the UI from data layer with the help of Provider. I have set aside the UI part to the final stage unlike my previous experiments.
Questions:
So to conclude such a long writeup, here my questions are;
When we create data models extending entity, what should be the one called in UI - data model or domain entity? I can make it work with both but then data model becomes useless especially when we post data using entity and retrieve same thing. Also, this requires the entities to have proper methods to map data for SQLite. If I change to Firebase, the domain logic to format data needs change, which breaks the core principle - closed for edit and most importantly data layer affects domain layer.
Is data model used only when the data is fetched externally and needed extra work to make it look like an entity data? Or in other words, if I am just posting a transaction and read in within the app, would entity suffice?
If both should be used, how it should be done?
Finally, help me understand what went wrong.
Hopefully I was able to figure this out. I am adding the questions and answers together for ease.
Questions:
So to conclude such a long writeup, here my questions are;
[Answer]: It should be the entity. A data model's duty is to convert an external data to app's entity model. Also, it handles the additional duty of de-converting entity to outer layer format. 2. [Question]Is data model used only when the data is fetched externally and needed extra work to make it look like an entity data? Or in other words, if I am just posting a transaction and read in within the app, would entity suffice?
[Answer]: Data Model is created to parse and convert external data to entity and back. So, classes dealing with this data uses data models.
Following is an article written recently on clean architecture implementation.