Always create new state objects instead of mutating existing ones:
// ❌ Bad
class BadShard extends Shard<List<String>> {
BadShard() : super([]);
void addItem(String item) {
state.add(item); // Mutating state
notifyListeners();
}
}
// ✅ Good
class GoodShard extends Shard<List<String>> {
GoodShard() : super([]);
void addItem(String item) {
emit([...state, item]); // New list
}
}
For complex state, create dedicated state classes:
// ✅ Good
class AppState {
final String? user;
final List<String> items;
final bool isLoading;
final String? error;
const AppState({
this.user,
this.items = const [],
this.isLoading = false,
this.error,
});
AppState copyWith({
String? user,
List<String>? items,
bool? isLoading,
String? error,
}) {
return AppState(
user: user ?? this.user,
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
class AppShard extends Shard<AppState> {
AppShard() : super(const AppState());
}
Only include what's necessary in state:
// ❌ Bad - includes derived data
class TodoState {
final List<Todo> todos;
final int activeCount; // Can be computed
final int completedCount; // Can be computed
}
// ✅ Good - compute derived data
class TodoState {
final List<Todo> todos;
}
class TodoShard extends Shard<TodoState> {
int get activeCount => state.todos.where((t) => !t.completed).length;
int get completedCount => state.todos.where((t) => t.completed).length;
}
Each shard should have a single responsibility:
// ❌ Bad - too many responsibilities
class AppShard extends Shard<AppState> {
// Handles todos, users, settings, etc.
}
// ✅ Good - focused responsibilities
class TodoShard extends Shard<TodoState> {
// Only handles todos
}
class UserShard extends Shard<UserState> {
// Only handles users
}
Keep business logic in shards, not in widgets:
// ❌ Bad
class TodoScreen extends StatelessWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
final shard = context.read<TodoShard>();
if (title.trim().isEmpty) return; // Logic in widget
shard.addTodo(title);
},
child: Text('Add'),
);
}
}
// ✅ Good
class TodoShard extends Shard<TodoState> {
void addTodo(String title) {
if (title.trim().isEmpty) return; // Logic in shard
emit(state.copyWith(
todos: [...state.todos, Todo(id: '1', title: title)],
));
}
}
// ❌ Bad - rebuilds on any state change
ShardBuilder<TodoShard, TodoState>(
builder: (context, state) {
return Text('Count: ${state.todos.length}');
},
)
// ✅ Good - only rebuilds when count changes
ShardSelector<TodoShard, TodoState, int>(
selector: (state) => state.todos.length,
builder: (context, count) {
return Text('Count: $count');
},
)
ShardBuilder<TodoShard, TodoState>(
buildWhen: (prev, curr) => prev.todos.length != curr.todos.length,
builder: (context, state) {
return Text('Todo count changed');
},
)
// ❌ Bad
ShardBuilder<TodoShard, TodoState>(
builder: (context, state) {
return Text(state.searchQuery); // Only needs searchQuery
},
)
// ✅ Good
ShardSelector<TodoShard, TodoState, String>(
selector: (state) => state.searchQuery,
builder: (context, query) {
return Text(query);
},
)
Only persist what's necessary:
class TodoState {
final List<Todo> todos;
final String searchQuery; // Don't persist this
Map<String, dynamic> toJson() {
return {
'todos': todos.map((t) => t.toJson()).toList(),
// searchQuery is not persisted
};
}
}
When state structure changes, handle migration:
@override
TodoState deserialize(String data) {
final json = jsonDecode(data) as Map<String, dynamic>;
// Handle old format
if (!json.containsKey('todos')) {
return TodoState(todos: []);
}
// Handle version migration
if (json['version'] == 1) {
return _migrateFromV1(json);
}
return TodoState.fromJson(json);
}
Adjust debounce duration based on update frequency:
// Frequent updates (search)
debounceDuration: const Duration(milliseconds: 500)
// Less frequent updates (form data)
debounceDuration: const Duration(milliseconds: 1000)
class TodoShard extends PersistentShard<TodoState> {
@override
void onLoadError(Object error, StackTrace? stackTrace) {
// Log error
log('Error loading todos: $error');
// Optionally show user notification
// Or retry logic
}
@override
void onSaveError(Object error, StackTrace? stackTrace) {
// Log error
log('Error saving todos: $error');
// Optionally retry
Future.delayed(Duration(seconds: 1), () {
saveState();
});
}
}
void main() {
test('CounterShard increments correctly', () {
final shard = CounterShard();
expect(shard.state, 0);
shard.increment();
expect(shard.state, 1);
shard.increment();
expect(shard.state, 2);
});
}
Don't forget to dispose shards in tests:
test('CounterShard increments correctly', () {
final shard = CounterShard();
expect(shard.state, 0);
shard.increment();
expect(shard.state, 1);
shard.dispose(); // Clean up
});
test('TodoShard adds todo correctly', () {
final shard = TodoShard();
expect(shard.state.todos.length, 0);
shard.addTodo('Test todo');
expect(shard.state.todos.length, 1);
expect(shard.state.todos[0].title, 'Test todo');
});
lib/
features/
todos/
models/
todo.dart
todo_state.dart
shard/
todo_shard.dart
widgets/
todo_list_screen.dart
todo_item.dart
// todos/todos.dart
export 'models/models.dart';
export 'shard/todo_shard.dart';
export 'widgets/widgets.dart';
class DataState {
final List<Item> items;
final bool isLoading;
final String? error;
DataState({
this.items = const [],
this.isLoading = false,
this.error,
});
}
class DataShard extends Shard<DataState> {
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, error: null));
try {
final items = await fetchItems();
emit(state.copyWith(items: items, isLoading: false));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
}
void toggleTodo(String id) {
// Update UI immediately
emit(state.copyWith(
todos: state.todos.map((todo) {
if (todo.id == id) {
return todo.copyWith(completed: !todo.completed);
}
return todo;
}).toList(),
));
// Sync with server in background
syncWithServer(id).catchError((error) {
// Revert on error
toggleTodo(id); // Toggle back
});
}
copyWith methodsShardSelector for performanceFollowing these practices will help you build maintainable, performant Flutter apps with Shard.