Essentials

Core Concepts

Learn the fundamental concepts of Shard state management.

Shard

Shard<T> is a generic shard that holds typed state. It extends ChangeNotifier and provides state management with built-in lifecycle management and mixin support.

Creating a Shard

class CounterShard extends Shard<int> {
  CounterShard() : super(0);
  
  void increment() {
    emit(state + 1);
  }
  
  void decrement() {
    emit(state - 1);
  }
}

State Updates

Use emit() to update state. This will notify all listeners:

void updateValue(int newValue) {
  emit(newValue);
}

State Equality Check

By default, emit() performs an equality check before updating state. If the new state equals the current state, no notification is triggered. This prevents unnecessary widget rebuilds and improves performance.

class CounterShard extends Shard<int> {
  CounterShard() : super(0);

  void increment() => emit(state + 1);  // Notifies listeners
  void emitSame() => emit(state);       // Skipped - no rebuild
}

Custom Equality

Override stateEquals() to provide custom equality logic for complex state objects:

class UserShard extends Shard<UserState> {
  UserShard() : super(UserState.empty());

  @override
  bool stateEquals(UserState a, UserState b) {
    // Only compare specific fields
    return a.id == b.id && a.name == b.name;
  }
}

Force Emit

Use emitForce() to bypass equality check and always notify listeners:

void forceRefresh() {
  emitForce(state);  // Always notifies, even if state is same
}
emitForce() is useful when your state object is mutable and has been modified in place, or when you need to force a UI refresh.

Lifecycle

Override onInit() to perform initialization when the shard is created, and dispose() to clean up resources when the shard is disposed:

class TimerShard extends Shard<int> {
  Timer? _timer;
  
  TimerShard() : super(0);
  
  @override
  void onInit() {
    super.onInit();
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      emit(state + 1);
    });
  }
  
  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

Observer Methods

Shard provides built-in observer methods that you can override to handle state changes and errors:

class CounterShard extends Shard<int> {
  CounterShard() : super(0);

  @override
  void onChange(int previousState, int currentState) {
    print('State: $previousState$currentState');
    super.onChange(previousState, currentState);
  }

  @override
  void onError(Object error, StackTrace? stackTrace) {
    print('Error: $error');
    super.onError(error, stackTrace);
  }

  void increment() => emit(state + 1);
}

Use addError to report errors without stopping execution:

void riskyOperation() {
  try {
    // risky code
  } catch (e, st) {
    addError(e, st);  // Reports error, doesn't block
  }
  emit(state + 1);  // Still executes
}

Learn more about Observing a Shard for advanced observer patterns and global error handling.

Additional Helpers

Shard provides built-in debounce and throttle mixins to optimize state updates and reduce unnecessary operations.

Debounce

Delays execution until after a period of inactivity. Perfect for search inputs and form validation.

class SearchShard extends Shard<SearchState> {
  SearchShard() : super(SearchState(query: '', results: []));
  
  void updateQuery(String query) {
    // Update UI immediately
    emit(state.copyWith(query: query));
    
    // Debounce actual search
    debounce('search', () {
      performSearch(query);
    }, duration: const Duration(milliseconds: 500));
  }
  
  Future<void> performSearch(String query) async {
    if (query.isEmpty) {
      emit(state.copyWith(results: []));
      return;
    }
    
    final results = await searchApi(query);
    emit(state.copyWith(results: results));
  }
}

Throttle

Limits execution to once per time period. The first call executes immediately, subsequent calls are ignored until the period expires. Perfect for API calls and scroll events.

class InfiniteScrollShard extends Shard<ScrollState> {
  void loadMore() {
    throttle('loadMore', () {
      _performLoadMore();
    }, duration: const Duration(seconds: 1));
  }
  
  Future<void> _performLoadMore() async {
    if (state.isLoading || !state.hasMore) return;
    
    emit(state.copyWith(isLoading: true));
    
    try {
      final items = await fetchItems(state.page + 1);
      emit(state.copyWith(
        items: [...state.items, ...items],
        page: state.page + 1,
        isLoading: false,
      ));
    } catch (e) {
      emit(state.copyWith(isLoading: false, error: e.toString()));
    }
  }
}

Next Steps

Now that you understand core concepts, learn about Persistent Shard to add state persistence.