8.9 KiB
CRUD BloC
CRUD Bloc Pattern utilities for Flutter.
This package defines a set of classes that can be used to implement the CRUD Bloc Pattern.
- Data Source
- Repository
- Use Case
- Create
- Get
- Get All
- Update
- Update All
- Delete
- Delete All
- Search
- Bloc
- Standard (C R U D), you have to choose the responsiblity of the bloc for each use case. For example, you can have a cubit that only handles the creation of an entity, and another cubit that only handles the deletion of an entity. Each cubit can only have one operation responsibility of each type, for example you can't have a cubit that handles get and get all.
- Advanced, you can set every use case to be handled by the bloc. This is useful if you want to have a single bloc that handles all the operations of an entity.
Usage
Create your entity using Wyatt Architecture and Equatable
import 'package:equatable/equatable.dart';
import 'package:wyatt_architecture/wyatt_architecture.dart';
class User extends Entity with EquatableMixin {
final String? id;
final String name;
final String email;
final String phone;
const User({
required this.name,
required this.email,
required this.phone,
this.id,
});
@override
List<Object?> get props => [id, name, email, phone];
}
Then, you can create your model for this entity with Freezed, JsonSerializable or any other library.
import 'package:crud_bloc_example/user_entity.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
@JsonSerializable()
class UserModel extends User {
UserModel({
super.id,
required super.name,
required super.email,
required super.phone,
});
factory UserModel.fromJson(Map<String, dynamic> json) =>
_$UserModelFromJson(json);
Map<String, dynamic> toJson() => _$UserModelToJson(this);
@override
String toString() =>
'UserModel(id: $id, name: $name, email: $email, phone: $phone)';
}
This exemple uses JsonSerializable to generate the model, but you can use any other library. Or you can even create your own model without any library.
Then you have to create a CRUD Cubit. Each Crud cubit can only handle one operation of each type. For example, you can't have a cubit that handles get and get all. (See the CrudAdvancedCubit
for this use case).
You also have to create a modelIdentifier
that will be used to identify your model. This is used to identify the model in the state of the cubit. For example, if you have a list of users, you can use the id of the user to identify it in the state. It's up to you to choose how you want to identify your model.
import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart';
/// A [CrudCubit] for [User].
class UserCubit extends CrudCubit<User> {
final CrudRepository<User> crudRepository;
UserCubit(this.crudRepository);
@override
DefaultCreate<User>? get createOperation => Create(crudRepository);
@override
DefaultDelete? get deleteOperation => Delete(crudRepository);
@override
DefaultRead? get readOperation => GetAll(crudRepository);
@override
DefaultUpdate? get updateOperation => Update(crudRepository);
@override
ModelIdentifier<User> get modelIdentifier => ModelIdentifier(
getIdentifier: (user) => user.id ?? '',
);
}
In this example, the
modelIdentifier
is the id of the user. But you can use any other property of the user to identify it. But make sure that the property you use is unique.
In this example, the
CrudCubit
handlesCreate
,Delete
,GetAll
andUpdate
operations. But you can choose which operation you want to handle. For example, you can have a cubit that only handles the creation of an entity, and another cubit that only handles the deletion of an entity.
Then you can configure the data source and the repository.
In Memory
The base implementation of the data source is the CrudInMemoryDataSourceImpl
. This data source stores the data in memory. This is useful for testing.
final CrudDataSource crudDataSource = CrudDataSourceInMemoryImpl(
id: (operation, json) => json['id'] as String?,
);
The
id
parameter is used to get the id of the raw data. You can use theoperation
parameter to get the operation that is being performed. This is useful if you want to have different ids for each operation. For example, you can have a different id for the creation and the update of an entity.
Firestore
You can use the CrudFirestoreDataSourceImpl
to store your data in Firestore.
Make sure to add the wyatt_crud_bloc_firestore
package to your dependencies.
final CrudDataSource crudDataSource = CrudDataSourceFirestoreImpl(
'users',
id: (_, json) => json['id'] as String,
fromFirestore: (document, snapshot) => document.data() ?? {},
toFirestore: (object, options) => object,
);
The
id
parameter is used to get the id of the raw data. You can use theoperation
parameter to get the operation that is being performed. This is useful if you want to have different ids for each operation. For example, you can have a different id for the creation and the update of an entity.
The
fromFirestore
andtoFirestore
parameters are used to convert the data from and to Firestore.
Finally, you can use the CrudRepositoryImpl
to create your repository.
final CrudRepository<User> userRepository = CrudRepositoryImpl(
crudDataSource: crudDataSource,
modelMapper: ModelMapper(
fromJson: (json) => UserModel.fromJson(json ?? {}),
toJson: (user) => UserModel(
id: user.id,
name: user.name,
email: user.email,
phone: user.phone,
).toJson(),
),
);
The
modelMapper
parameter is used to convert the data from and to the model. In thetoJson
make sure to use the same data structure as the one used in the data source. For example the previousid()
function is used to get the id of the raw data, ifid
is missing from the raw data, theid()
function will not works.
And in your widget tree you can use the CrudBuilder
to build your UI.
...
BlocBuilder<UserCubit, CrudState>(
buildWhen: (previous, current) {
if (current is CrudLoading && current is! CrudReading) {
return false;
}
return true;
},
builder: (context, state) {
return Expanded(
child: CrudBuilder.typed<CrudListLoaded<User?>>(
state: state,
builder: ((context, state) {
return ListView.builder(
itemCount: state.data.length,
itemBuilder: (context, index) {
final user = state.data.elementAt(index);
return ListTile(
title: Text(user?.name ?? 'Error'),
subtitle: Text(user?.email ?? 'Error'),
onTap: () {
context
.read<UserCubit>()
.delete(id: (user?.id)!);
},
onLongPress: () {
context.read<UserCubit>().update(
single: UpdateParameters(
id: user?.id ?? '',
raw: {
'email': '${user?.id}@updated.io',
}),
);
},
);
},
);
}),
initialBuilder: (context, state) =>
const Center(child: CircularProgressIndicator()),
loadingBuilder: (context, state) =>
const Center(child: CircularProgressIndicator()),
errorBuilder: (context, state) => Text("Error: $state"),
),
);
},
),
...
This piece of code is used to build a list of users. When you tap on a user, it will delete it. When you long press on a user, it will update it. Also, the loading state is not always displayed, it will only be displayed when the state is
CrudReading
, so when the state isCrudUpdating
orCrudDeleting
, the loading state will not be displayed.