Last updated:
0 purchases
entity store
English | 日本語
EntityStore Package #
Introduction #
EntityStore enhances Flutter application development by offering state management centered around entities. This library encapsulates the application's business logic within immutable entities and maintains UI consistency through centralized state management.
The following TodoTile component example demonstrates how EntityStore connects UI components with their state.
class TodoTile extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
// Monitor a specific Todo
final todo = context.watchOne<int, Todo>(todoId)!;
// Toggle the completion state of Todo
return CheckboxListTile(
title: Text(todo.name),
value: todo.isDone,
onChanged: (bool? value) {
if (value != null) {
todoRepository.save(todo.toggle());
}
},
);
}
}
copied to clipboard
Features #
Reactive UI Synchronization: EntityStore reflects state changes reactively on the UI. In the code example above, the watchOne method is used to monitor a specific Todo entity, and updates the checkbox display when its state changes.
Concise State Updates: The state of an entity is updated through a new instance only when necessary. Toggling the completion state of a Todo is easily done by calling todo.toggle(), and the result is persisted through the repository.
Central Handling of Entities: EntityStore focuses on the entities that form the core part of the application, allowing developers to concentrate on business logic.
Flexible Database Integration: EntityStore facilitates the integration with external data sources. By swapping out repository implementations, it is possible to connect with Firebase Firestore, local databases, or other data storages.
Boilerplate Reduction: The use of pre-prepared repository implementations eliminates the need for developers to write repetitive database operation code. This accelerates the development process and eases the maintenance of applications.
Installation #
To introduce the EntityStore package into your Flutter project, add the following to your pubspec.yaml file:
dependencies:
entity_store: latest_version
copied to clipboard
Usage #
The following sample code demonstrates the implementation of a Todo application using EntityStore.
Definition of the Todo Entity #
An entity encapsulates core business logic in your application and has a unique identifier (ID). This enables entities to be identifiable throughout the application, maintaining data integrity. In EntityStore, entities are designed to be immutable. This is important for detecting changes in entity states and ensuring they are appropriately reflected in the UI.
The Todo class is an example that implements this concept. Each Todo item has a unique ID, a name, and a completion status attribute. The create factory method allows you to create a new Todo instance with a random name. The toggle method is used to toggle the completion status of a Todo, returning a new Todo instance with the updated status.
class Todo implements Entity<int> {
// Entity attributes must be immutable.
@override
final int id;
final String name;
final bool isDone;
Todo(this.id, this.name, this.isDone);
// Create a new Todo entity
factory Todo.create(int id) {
return Todo(
id,
getRandomTodoName(),
false,
);
}
// Toggle the completion status of a Todo entity
Todo toggle() {
return Todo(
id,
name,
!isDone,
);
}
}
copied to clipboard
This immutable design approach allows each instance of Todo to be reusable, and state changes can only be made through the creation of new instances, preventing unexpected side effects and making state management more predictable. It also reduces complexity when detecting entity changes and updating the UI, improving the maintainability of the application.
Implementation of the Todo Repository #
One of the powerful features of EntityStore is the implementation of the repository pattern. Following this pattern, the TodoRepository extends LocalStorageRepository and defines its own handling of saving, reading, and deleting entities by overriding inherited methods.
The TodoRepository is a specific implementation of the LocalStorageRepository for Todo entities. It overrides the fromJson and toJson methods for reading and saving JSON data, making it easy to convert between entities and data storage.
class TodoRepository extends LocalStorageRepository<int, Todo> {
TodoRepository(super.controller, super.localStorageHandler);
@override
Todo fromJson(Map<String, dynamic> json) {
// Create a Todo entity from JSON
}
@override
Map<String, dynamic> toJson(Todo entity) {
// Convert a Todo entity to JSON
}
}
copied to clipboard
About LocalStorageHandler
The EntityStore package provides InMemoryStorageHandler by default, which is a simple storage handler that stores data only in memory. This allows for easy state management during development and testing. However, in a real application, you can implement a custom LocalStorageHandler to store data in the device's local storage.
class InMemoryStorageHandler extends LocalStorageHandler {
// Data operations in memory
}
copied to clipboard
Setup of EntityStore #
To set up EntityStore, initialize EntityStoreNotifier or EntityStoreController, and associate them with the repository as follows:
final entityStoreNotifier = EntityStoreNotifier();
final entityStoreController = EntityStoreController(entityStoreNotifier);
final storageHandler = InMemoryStorageHandler();
final todoRepository = TodoRepository(entityStoreController, storageHandler);
copied to clipboard
While the code snippet above uses global variables for simplicity, it is recommended to use dependency injection (DI) using containers like Riverpod's Provider or GetIt for a more robust design. By doing so, you can efficiently resolve dependencies needed by different parts of your application, improving testability and code reusability.
Example:
final entityStoreProvider = Provider((ref) => EntityStoreNotifier());
final entityStoreControllerProvider = Provider((ref) => EntityStoreController(ref.watch(entityStoreProvider)));
// ...
// In other parts of the application
final entityStoreNotifier = ref.watch(entityStoreProvider);
final entityStoreController = ref.watch(entityStoreControllerProvider);
copied to clipboard
By adopting this approach, you make dependencies required by various parts of your application more explicit and achieve a design that is resilient to changes and extensions.
Usage in UI #
Setup of EntityStoreProviderScope
The first step in using the EntityStore package is to set up an EntityStoreProviderScope at the top level of your application. This creates a foundation for monitoring and sharing state changes throughout the entire application.
class MyApp extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
// Place the EntityStoreProviderScope at the root of the app
return EntityStoreProviderScope(
entityStoreNotifier: entityStoreNotifier,
child: MaterialApp(
// ...
),
);
}
}
copied to clipboard
Monitoring State and Updating UI
You can use selectAll to retrieve the IDs of all Todo entities and display them as a list in the UI. This way, you monitor the entire Todo list and update the list whenever a new Todo is added.
context.watchOne monitors specific entities. By utilizing these methods, changes in state are automatically reflected in the UI.
class MyHomePage extends StatefulWidget {
// ...
@override
Widget build(BuildContext context) {
// Get IDs of all Todos
final todoIds = context.selectAll<int, Todo, List<int>>(
(value) => value.ids.toList(),
);
// ...
}
}
copied to clipboard
Performing Entity Operations and UI Consistency
When you manipulate entities through the repository, the state in EntityStore is updated, and related UI components are automatically updated.
class TodoTile extends StatelessWidget {
// ...
@override
Widget build(BuildContext context) {
// Monitor a specific Todo
final todo = context.watchOne<int, Todo>(todoId);
// Toggle the completion status of Todo
return CheckboxListTile(
title: Text(todo.name),
value: todo.isDone,
onChanged: (bool? value) {
if (value != null) {
// Update the entity through the repository
todoRepository.save(todo.toggle());
}
},
);
}
}
copied to clipboard
Details of State Management Methods and Usage Examples #
The watch, select, and read methods provided by EntityStore are essential tools for monitoring application state and updating the UI accordingly. These methods help maintain synchronization between the state managed by EntityStore and UI components.
Usage of the watch Method #
You can use the watchAll method to monitor only incomplete Todos in real-time and update the list whenever changes occur.
final activeTodos = context.watchAll<int, Todo>(
(todo) => !todo.isDone,
);
ListView.builder(
itemCount: activeTodos.length,
itemBuilder: (context, index) {
final todo = activeTodos.values.elementAt(index);
return ListTile(
title: Text(todo.name),
leading: Checkbox(
value: todo.isDone,
onChanged: (bool? checked) {
// Logic to update the completion status of Todo
},
),
);
},
);
copied to clipboard
You can use the watchOne method to monitor changes in the state of a specific Todo entity and update only that Todo.
final todo = context.watchOne<int, Todo>(todoId);
if (todo != null) {
return CheckboxListTile(
title: Text(todo.name),
value: todo.isDone,
onChanged: (bool? newValue) {
// Logic to update the state of Todo
},
);
}
copied to clipboard
Usage of the select Method #
You can use the selectAll method to retrieve specific data (e.g., names) from the entire Todo list and display only that data.
final todoNames = context.selectAll<int, Todo, List<String>>(
(todos) => todos.values.map((todo) => todo.name).toList(),
);
ListView(
children: todoNames.map((name) => Text(name)).toList(),
);
copied to clipboard
You can use the selectOne method to retrieve specific data from a single Todo and update a widget that displays that data when the name of the Todo changes.
final todoName = context.selectOne<int, Todo, String>(
todoId,
(todo) => todo.name,
);
Text(todoName ?? 'No name');
copied to clipboard
Usage of the read Method #
You can use the readAll method to retrieve all Todos once during the initial load of the screen and use the data without triggering a rebuild.
final allTodos = context.readAll<int, Todo>();
// Display all Todos during the initial load
ListView(
children: allTodos.values.map((todo) => Text(todo.name)).toList(),
);
copied to clipboard
You can use the readOne method to perform a one-time data read based on a user action, such as displaying Todo details in a dialog.
final todo = context.readOne<int, Todo>(todoId);
// Display a dialog triggered by a user action
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(todo?.name ?? 'No name'),
content: Text('Completed: ${todo?.isDone}'),
);
},
);
copied to clipboard
By using these methods, you can efficiently read and update state in EntityStore while ensuring that changes in the state of your application are reflected in the UI as needed.
Usage of Repositories #
Repositories are designed to abstract data source operations and separate business logic from data access code. In EntityStore, operations on entities performed through repositories are automatically reflected in the UI. This means that when you save, update, or delete entities through a repository, these changes are automatically propagated to the UI, allowing users to immediately see the latest state.
This behavior is achieved by using reactive methods like watch, select, etc., in combination with EntityStore, which ensures synchronization between the state managed by EntityStore and UI components. Below are basic repository operations and their usage examples.
findById
Get an entity with the specified ID.
// Example: Finding a Todo by ID
var result = await todoRepository.findById(todoId);
if (result.isOk) {
var todo = result.ok;
// Perform Todo-related operations here
}
copied to clipboard
findAll
Retrieve all entities.
// Example: Get all Todos that meet specific criteria
var result = await todoRepository.query()
.where('isComplete', isEqualTo: false)
.findAll();
if (result.isOk) {
var todos = result.ok;
// Perform operations on the Todo list here
}
copied to clipboard
findOne
Retrieve the first entity that matches the specified criteria.
// Example: Find the first Todo that meets specific criteria
var result = await todoRepository.query()
.where('isComplete', isEqualTo: false)
.findOne();
if (result.isOk) {
var todo = result.ok;
// Perform Todo-related operations here
}
copied to clipboard
count
Count the number of entities that match the specified criteria.
// Example: Count the number of incomplete Todos
var result = await todoRepository.query()
.where('isComplete', isEqualTo: false)
.count();
if (result.isOk) {
var activeCount = result.ok;
// Perform operations using activeCount here
}
copied to clipboard
save
Save or update an entity.
// Example: Save a new Todo
var newTodo = Todo.create(name: 'New Task');
var result = await todoRepository.save(newTodo);
if (result.isOk) {
// Handle the success of the save operation here
}
copied to clipboard
delete
Delete an entity.
// Example: Delete a Todo
var result = await todoRepository.delete(todoId);
if (result.isOk) {
// Handle the success of the delete operation here
}
copied to clipboard
upsert
Create a new entity if it doesn't exist or update it if it does.
// Example: Upsert a Todo
var result = await todoRepository.upsert(
todoId,
creater: () => Todo.create(name: 'New Task'),
updater: (existingTodo) => existingTodo.copyWith(isComplete: true),
);
if (result.isOk) {
// Handle the success of the upsert operation here
}
copied to clipboard
Through these operations, the application can efficiently manage data within EntityStore and maintain data consistency.
License #
This project is released under the MIT license. Please refer to the LICENSE file for details.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.