Shard
A lightweight, zero-dependency state management solution for Flutter.
Persistence, async, caching, debounce, throttle & service locator — all in one package.
Documentation
For full documentation, tutorials, and best practices visit the official docs:
| Section | Link |
|---|---|
| Getting Started | Introduction · Installation · Quick Start |
| Essentials | Core Concepts · Widgets · Async · Caching · Observer · Locator |
| Shard Types | FutureShard · StreamShard · PersistentShard |
| Examples | Todo App · Infinite Scroll · Best Practices |
Table of Contents
Why Shard?
No code generation. No extra dependencies. A single package that covers everything you need:
- Persistent state — Automatically save and load state across app restarts with built-in serializers.
- Cache support — TTL-based in-memory caching for async shards and repositories.
- Helpers — Debounce, throttle, global observer, and
AsyncValuefor handling loading/error states gracefully. - Service locator — Register and resolve singletons effortlessly with
ShardLocator.
Quick Start
Add Shard to your project:
# pubspec.yaml
dependencies:
shard: ^1.0.1
Define your state, provide it, and use it — three simple steps:
// 1. Define your state
class CounterShard extends Shard<int> {
CounterShard() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
// 2. Provide it
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return ShardProvider<CounterShard>(
create: () => CounterShard(),
child: MaterialApp(home: CounterScreen()),
);
}
}
// 3. Use it
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// Rebuild only when state changes
ShardBuilder<CounterShard, int>(
builder: (context, count) => Text('Count: $count'),
),
ElevatedButton(
onPressed: () => context.read<CounterShard>().increment(),
child: Text('+'),
),
],
);
}
}
What's Included
| Area | What You Get |
|---|---|
| State | Shard<T> — emit(), emitForce(), lifecycle hooks, equality, observer |
| Async | FutureShard<T>, StreamShard<T> — loading / data / error with AsyncValue<T> |
| Persistence | PersistentShard<T,K>, SimplePersistentShard<T> — auto save / load with built-in serializers |
| Performance | DebounceMixin, ThrottleMixin — rate limiting built into Shard |
| Cache | CacheService, MemoryCacheService, CacheMixin — TTL-based caching for shards & repos |
| Widgets | ShardBuilder, ShardSelector, AsyncShardBuilder, MultiShardProvider |
| DI | ShardLocator — eager & lazy singletons, isRegistered(), reset() for tests |
| Observability | ShardObserver — global onChange / onError hooks |
Core Concepts
Shard follows a straightforward data flow:
ShardProvider → Injects a Shard into the widget tree
↓
ShardBuilder / Selector → Rebuilds UI when state changes
↓
Shard.emit() → Pushes new state, notifies listeners
Here's a breakdown of the key building blocks:
Shard<T>— Holds typed state. Callemit()(equality-checked) oremitForce()(always notifies).ShardProvider— Provides a shard to descendants. Use.value()constructor for existing instances.ShardBuilder— Rebuilds its child whenever the shard's state changes.ShardSelector— Rebuilds only when a selected slice of state changes.FutureShard<T>— Wraps aFuturewith automatic loading → data → error transitions and caching.StreamShard<T>— Wraps aStreamwith the sameAsyncValuepattern.PersistentShard<T,K>— Persists state to storage with full control over serialization.context.read<T>()— Access a shard without subscribing (ideal for callbacks & event handlers).
Usage Examples
FutureShard + Caching
Fetch data asynchronously with built-in caching — no boilerplate required:
class UserShard extends FutureShard<User> {
final String userId;
UserShard({required this.userId});
@override
String get cacheKey => 'user_$userId';
@override
Duration get cacheTTL => Duration(minutes: 30);
@override
Future<User> build() => api.getUser(userId);
}
class UserScreen extends StatelessWidget {
const UserScreen({super.key});
@override
Widget build(BuildContext context) {
return AsyncShardBuilder<UserShard, User>(
onLoading: (c) => CircularProgressIndicator(),
onData: (c, user) => Text(user.name),
onError: (c, e, _) => Text('$e'),
);
}
}
StreamShard
Subscribe to real-time data streams with the familiar AsyncValue pattern:
class MessagesStream extends StreamShard<List<Message>> {
final String chatId;
MessagesStream({required this.chatId});
@override
Stream<List<Message>> build() => repository.watchMessages(chatId);
}
Persistence
Keep state alive across app restarts — just pick a serializer and a storage backend:
class CounterShard extends SimplePersistentShard<int> {
CounterShard() : super(0,
storageFactory: () => SharedPreferencesStorage.getInstance(),
serializer: IntSerializer(),
);
@override
String get persistenceKey => 'counter';
void increment() => emit(state + 1);
}
Debounce & Throttle
Rate-limit expensive operations directly inside your shard:
class SearchShard extends Shard<String> {
SearchShard() : super('');
void onQueryChanged(String query) {
emit(query);
debounce(
'search',
() => performSearch(query),
duration: Duration(milliseconds: 500),
);
}
Future<void> performSearch(String query) async { /* ... */ }
}
class FeedShard extends Shard<int> {
FeedShard() : super(0);
void onScrollNearEnd() {
throttle(
'loadMore',
() => loadMore(),
duration: Duration(seconds: 1),
);
}
void loadMore() => emit(state + 1);
}
ShardSelector
Optimize rebuilds by selecting only the slice of state you care about:
class UserNameText extends StatelessWidget {
const UserNameText({super.key});
@override
Widget build(BuildContext context) {
return ShardSelector<UserShard, UserState, String>(
selector: (s) => s.name,
builder: (context, name) => Text('Hello, $name'),
);
}
}
MultiShardProvider
Provide multiple shards at once without deep nesting:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiShardProvider(
providers: [
ShardProvider<AuthShard>(create: () => AuthShard()),
ShardProvider<SettingsShard>(create: () => SettingsShard()),
ShardProvider<ThemeShard>(create: () => ThemeShard()),
],
child: MaterialApp(home: HomeScreen()),
);
}
}
ShardLocator (DI)
A simple service locator for dependency injection — supports eager and lazy singletons:
class AppBootstrap {
static void init() {
ShardLocator.registerSingleton<ApiClient>(ApiClient());
ShardLocator.registerLazySingleton<UserRepo>(
() => UserRepo(ShardLocator.get<ApiClient>()),
);
}
}
class SomeService {
UserRepo get repo => ShardLocator.get<UserRepo>();
}
CacheMixin (Repository-Level Caching)
Add caching to your repositories with a single mixin — includes stale-while-revalidate support:
class UserRepository with CacheMixin {
@override
CacheService get cacheService => MemoryCacheService();
final UserApi _api;
UserRepository(this._api);
Future<User> getUser(String id) => resolve<User>(
key: 'user_$id',
fetcher: () => _api.fetchUser(id),
ttl: Duration(minutes: 30),
onErrorReturnOldCache: true,
);
}
ShardObserver (Global Logging)
Track every state change and error across your entire app:
class AppObserver extends ShardObserver {
@override
void onChange(Shard shard, Object? previousState, Object? currentState) {
print('${shard.runtimeType}: $previousState → $currentState');
}
@override
void onError(Shard shard, Object error, StackTrace? stackTrace) {
print('${shard.runtimeType} error: $error');
}
}
void main() {
Shard.observer = AppObserver();
runApp(MyApp());
}
Requirements
- Dart
^3.10.3 - Flutter
>=1.17.0 - Zero external dependencies
License
MIT — see LICENSE.
Libraries
- Shard - A powerful, lightweight state management solution for Flutter.