This example demonstrates a complete todo app with:
PersistentShardclass Todo {
final String id;
final String title;
final bool completed;
final DateTime createdAt;
Todo({
required this.id,
required this.title,
this.completed = false,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();
Todo copyWith({
String? id,
String? title,
bool? completed,
DateTime? createdAt,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
completed: completed ?? this.completed,
createdAt: createdAt ?? this.createdAt,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'completed': completed,
'createdAt': createdAt.toIso8601String(),
};
}
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'] as String,
title: json['title'] as String,
completed: json['completed'] as bool? ?? false,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
);
}
}
class TodoState {
final List<Todo> todos;
final String searchQuery;
const TodoState({
required this.todos,
this.searchQuery = '',
});
TodoState copyWith({
List<Todo>? todos,
String? searchQuery,
}) {
return TodoState(
todos: todos ?? this.todos,
searchQuery: searchQuery ?? this.searchQuery,
);
}
Map<String, dynamic> toJson() {
return {
'todos': todos.map((todo) => todo.toJson()).toList(),
// searchQuery is not persisted
};
}
factory TodoState.fromJson(Map<String, dynamic> json) {
final List<dynamic> todosJson = json['todos'] as List<dynamic>;
return TodoState(
todos: todosJson
.map((todoJson) => Todo.fromJson(todoJson as Map<String, dynamic>))
.toList(),
searchQuery: '',
);
}
}
import 'dart:developer';
import 'package:shard/shard.dart';
import '../models/todo.dart';
import '../models/todo_state.dart';
import '../serializers/todo_serializer.dart';
import '../storage/shared_preferences_storage.dart';
class PersistentTodoShard extends PersistentShard<TodoState> {
@override
String get persistenceKey => 'todos';
PersistentTodoShard()
: super(
const TodoState(todos: []),
storageFactory: () => SharedPreferencesStorage.create(),
serializer: TodoSerializer(),
autoSave: true,
autoLoad: true,
debounceDuration: const Duration(milliseconds: 500),
);
// Getters
String get searchQuery => state.searchQuery;
List<Todo> get todos => state.todos;
List<Todo> get activeTodos => state.todos.where((todo) => !todo.completed).toList();
List<Todo> get completedTodos => state.todos.where((todo) => todo.completed).toList();
int get totalCount => state.todos.length;
int get activeCount => activeTodos.length;
int get completedCount => completedTodos.length;
// Actions
void addTodo(String title) {
if (title.trim().isEmpty) return;
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title.trim(),
);
emit(state.copyWith(todos: [...state.todos, newTodo]));
}
void removeTodo(String id) {
emit(state.copyWith(
todos: state.todos.where((todo) => todo.id != id).toList(),
));
}
void toggleTodo(String id) {
emit(
state.copyWith(
todos: state.todos.map((todo) {
if (todo.id == id) {
return todo.copyWith(completed: !todo.completed);
}
return todo;
}).toList(),
),
);
}
void clearCompleted() {
emit(state.copyWith(
todos: state.todos.where((todo) => !todo.completed).toList(),
));
}
void updateSearchQuery(String query) {
debounce('search', () {
emit(state.copyWith(searchQuery: query));
}, duration: const Duration(milliseconds: 500));
}
@override
void onLoadError(Object error, StackTrace? stackTrace) {
log('Error loading todos: $error');
log('Stack trace: $stackTrace');
}
@override
void onSaveError(Object error, StackTrace? stackTrace) {
log('Error saving todos: $error');
log('Stack trace: $stackTrace');
}
}
import 'dart:convert';
import 'package:shard/shard.dart';
import '../models/todo_state.dart';
class TodoSerializer implements StateSerializer<TodoState> {
@override
String serialize(TodoState state) {
return jsonEncode(state.toJson());
}
@override
TodoState deserialize(String data) {
final json = jsonDecode(data) as Map<String, dynamic>;
return TodoState.fromJson(json);
}
}
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shard/shard.dart';
class SharedPreferencesStorage implements StateStorage {
final SharedPreferences _prefs;
SharedPreferencesStorage(this._prefs);
static Future<SharedPreferencesStorage> create() async {
final prefs = await SharedPreferences.getInstance();
return SharedPreferencesStorage(prefs);
}
@override
Future<void> save(String key, String value) async {
await _prefs.setString(key, value);
}
@override
Future<String?> load(String key) async {
return _prefs.getString(key);
}
@override
Future<void> clear() async {
await _prefs.clear();
}
}
import 'package:flutter/material.dart';
import 'package:shard/shard.dart';
import 'shard/persistent_todo_shard.dart';
import 'widgets/todo_list_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Shard Todo Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: ShardProvider<PersistentTodoShard>(
create: () => PersistentTodoShard(),
child: const TodoListScreen(),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:shard/shard.dart';
import '../models/todo.dart';
import '../models/todo_state.dart';
import '../shard/persistent_todo_shard.dart';
import 'todo_item.dart';
import 'todo_input.dart';
class TodoListScreen extends StatefulWidget {
const TodoListScreen({super.key});
@override
State<TodoListScreen> createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
TodoFilter _currentFilter = TodoFilter.all;
final _searchController = TextEditingController();
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shard Todo Example'),
actions: [
// Show todo count using ShardSelector
ShardSelector<PersistentTodoShard, TodoState, int>(
selector: (state) => state.todos.length,
builder: (context, count) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text('$count todos'),
),
);
},
),
],
),
body: SafeArea(
child: Column(
children: [
// Search bar
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search todos...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onChanged: (value) {
context.read<PersistentTodoShard>().updateSearchQuery(value);
},
),
),
// Filter chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
_buildFilterChip(TodoFilter.all, 'All'),
const SizedBox(width: 8),
_buildFilterChip(TodoFilter.active, 'Active'),
const SizedBox(width: 8),
_buildFilterChip(TodoFilter.completed, 'Completed'),
],
),
),
// Todo input
TodoInput(
onAdd: (title) {
context.read<PersistentTodoShard>().addTodo(title);
},
),
// Todo list
Expanded(
child: ShardBuilder<PersistentTodoShard, TodoState>(
builder: (context, state) {
final filteredTodos = _getFilteredTodos(state);
if (filteredTodos.isEmpty) {
return Center(
child: Text('No todos yet'),
);
}
return ListView.builder(
itemCount: filteredTodos.length,
itemBuilder: (context, index) {
final todo = filteredTodos[index];
return TodoItem(
todo: todo,
onToggle: () => context.read<PersistentTodoShard>().toggleTodo(todo.id),
onDelete: () => context.read<PersistentTodoShard>().removeTodo(todo.id),
);
},
);
},
),
),
// Stats and actions
Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ShardSelector<PersistentTodoShard, TodoState, int>(
selector: (state) =>
state.todos.where((t) => !t.completed).length,
builder: (context, shard, activeCount) {
return Text('Active: $activeCount');
},
),
ElevatedButton(
onPressed: () {
context.read<PersistentTodoShard>().clearCompleted();
},
child: Text('Clear Completed'),
),
],
),
),
],
),
),
);
}
Widget _buildFilterChip(TodoFilter filter, String label) {
final isSelected = _currentFilter == filter;
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() {
_currentFilter = filter;
});
}
},
);
}
List<Todo> _getFilteredTodos(TodoState state) {
var filtered = state.todos;
// Apply search filter
if (state.searchQuery.isNotEmpty) {
filtered = filtered
.where((todo) => todo.title
.toLowerCase()
.contains(state.searchQuery.toLowerCase()))
.toList();
}
// Apply status filter
switch (_currentFilter) {
case TodoFilter.all:
return filtered;
case TodoFilter.active:
return filtered.where((todo) => !todo.completed).toList();
case TodoFilter.completed:
return filtered.where((todo) => todo.completed).toList();
}
}
}
enum TodoFilter { all, active, completed }
ShardSelector for performanceCheck out the Infinite Scroll Example to see throttle in action.