publisher_subscriber 0.0.2
publisher_subscriber: ^0.0.2 copied to clipboard
A simple, minimal, and opinionated state management library for Flutter.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:publisher_subscriber/publisher_subscriber.dart';
// --- Concrete Publisher Implementations ---
class GlobalCounter extends Publisher<int> {
GlobalCounter() : super(0);
void increment() => setState(state + 1);
}
class ScopedCounter extends Publisher<int> {
ScopedCounter() : super(0);
void increment() => setState(state + 1);
}
// --- APP ENTRY POINT & RECIPE DEFINITIONS ---
void main() {
GlobalPublisherObserver.on<ScopedCounter>(
(state) => debugPrint('ScopedCounter changed: $state'),
);
GlobalPublisherObserver.on<GlobalCounter>(
(state) => debugPrint('GlobalCounter changed: $state'),
);
runApp(const MainApp());
}
final globalCounterRecipe = Publisher.global(() => GlobalCounter());
final scopedCounterRecipe = Publisher.scoped(() => ScopedCounter());
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Simple State Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: Colors.grey.shade100,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black87,
backgroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade300),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
),
),
),
home: const HomePage(),
);
}
}
// --- UI (VIEWS) ---
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home Page')),
body: Subscriber(
builder: (context) {
// Access is now consistent for both types using the context.
final globalCounter = context.read(globalCounterRecipe);
final scopedCounter = context.read(scopedCounterRecipe);
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_CounterDisplay(
title: 'GLOBAL COUNTER',
subtitle: '(State persists across all pages)',
count: globalCounter.state,
onPressed: globalCounter.increment,
),
const SizedBox(height: 40),
_CounterDisplay(
title: 'SCOPED COUNTER',
subtitle: '(State is disposed when leaving this page)',
count: scopedCounter.state,
onPressed: scopedCounter.increment,
),
const SizedBox(height: 40),
ElevatedButton(
child: const Text('Go to Second Page'),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const SecondPage()),
);
},
),
],
),
),
);
},
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Subscriber(
builder: (context) {
// We only read the global recipe here.
final globalCounter = context.read(globalCounterRecipe);
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_CounterDisplay(
title: 'GLOBAL COUNTER',
subtitle: '(State was preserved from Home Page)',
count: globalCounter.state,
onPressed: globalCounter.increment,
),
const SizedBox(height: 40),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'The Scoped Counter does not exist on this page. It was disposed when HomePage was replaced.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54),
),
),
const SizedBox(height: 40),
ElevatedButton(
child: const Text('Go Back to Home Page'),
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
);
},
),
],
),
),
);
},
),
);
}
}
// --- SHARED WIDGETS ---
class _CounterDisplay extends StatelessWidget {
const _CounterDisplay({
required this.title,
required this.subtitle,
required this.count,
required this.onPressed,
});
final String title;
final String subtitle;
final int count;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 16),
Text('$count', style: Theme.of(context).textTheme.displayMedium),
const SizedBox(height: 16),
ElevatedButton(onPressed: onPressed, child: const Text('Increment')),
],
),
);
}
}