Skip to main content

Overview

Zeus uses the BLoC (Business Logic Component) pattern for state management. This provides predictable state updates and clear separation of concerns.

Why BLoC?

  • Predictable - State changes are explicit and testable
  • Scalable - Easy to add new features
  • Testable - Business logic separated from UI
  • Reactive - UI responds automatically to state changes

Core Concepts

Events

User actions or system events that trigger state changes:
abstract class WalletEvent {}

class LoadWallets extends WalletEvent {}

class CreateWallet extends WalletEvent {
  final String name;
  final String currency;
  final double initialBalance;

  CreateWallet({
    required this.name,
    required this.currency,
    required this.initialBalance,
  });
}

class DeleteWallet extends WalletEvent {
  final String id;
  DeleteWallet(this.id);
}

States

Immutable representations of the UI at a point in time:
abstract class WalletState {}

class WalletInitial extends WalletState {}

class WalletLoading extends WalletState {}

class WalletLoaded extends WalletState {
  final List<Wallet> wallets;
  WalletLoaded(this.wallets);
}

class WalletError extends WalletState {
  final String message;
  WalletError(this.message);
}

BLoC

Connects events to states:
class WalletBloc extends Bloc<WalletEvent, WalletState> {
  final CacheManager _cache;

  WalletBloc(this._cache) : super(WalletInitial()) {
    on<LoadWallets>(_onLoadWallets);
    on<CreateWallet>(_onCreateWallet);
    on<DeleteWallet>(_onDeleteWallet);
  }

  Future<void> _onLoadWallets(
    LoadWallets event,
    Emitter<WalletState> emit,
  ) async {
    emit(WalletLoading());
    try {
      final wallets = await _cache.wallets.findAll();
      emit(WalletLoaded(wallets));
    } catch (e) {
      emit(WalletError(e.toString()));
    }
  }

  Future<void> _onCreateWallet(
    CreateWallet event,
    Emitter<WalletState> emit,
  ) async {
    try {
      await _cache.createWallet(
        name: event.name,
        currency: event.currency,
        initialBalance: event.initialBalance,
      );
      
      // Reload to show updated list
      final wallets = await _cache.wallets.findAll();
      emit(WalletLoaded(wallets));
    } catch (e) {
      emit(WalletError(e.toString()));
    }
  }

  Future<void> _onDeleteWallet(
    DeleteWallet event,
    Emitter<WalletState> emit,
  ) async {
    try {
      await _cache.wallets.delete(event.id);
      
      // Reload to show updated list
      final wallets = await _cache.wallets.findAll();
      emit(WalletLoaded(wallets));
    } catch (e) {
      emit(WalletError(e.toString()));
    }
  }
}

Using BLoC in UI

Provide BLoC

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => WalletBloc(CacheManager.instance),
      child: MaterialApp(
        home: WalletPage(),
      ),
    );
  }
}

Consume State

class WalletPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Wallets')),
      body: BlocBuilder<WalletBloc, WalletState>(
        builder: (context, state) {
          if (state is WalletInitial) {
            return Center(child: Text('Press button to load'));
          }
          
          if (state is WalletLoading) {
            return Center(child: CircularProgressIndicator());
          }
          
          if (state is WalletLoaded) {
            return ListView.builder(
              itemCount: state.wallets.length,
              itemBuilder: (context, index) {
                final wallet = state.wallets[index];
                return ListTile(
                  title: Text(wallet.name),
                  subtitle: Text('${wallet.balance} ${wallet.currency}'),
                );
              },
            );
          }
          
          if (state is WalletError) {
            return Center(child: Text('Error: ${state.message}'));
          }
          
          return Container();
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read<WalletBloc>().add(LoadWallets());
        },
        child: Icon(Icons.refresh),
      ),
    );
  }
}

Best Practices

  1. Events are past tense - WalletCreated, not CreateWallet
  2. States are nouns - WalletLoaded, not LoadWallet
  3. BLoCs are pure - No side effects in mapEventToState
  4. UI is dumb - Only displays state, no business logic
  5. Test BLoCs - Test event-to-state transformations