june 1.0.2
june: ^1.0.2 copied to clipboard
June is a lightweight and modern state management library that focuses on providing a pattern very similar to Flutter's native state management.
██╗██╗ ██╗███╗ ██╗███████╗
██║██║ ██║████╗ ██║██╔════╝
██║██║ ██║██╔██╗ ██║█████╗
██ ██║██║ ██║██║╚██╗██║██╔══╝
╚█████╔╝╚██████╔╝██║ ╚████║███████╗
╚════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
Flutter state management that feels like Flutter. #
The problem with state management today #
You learned Flutter. You learned setState. It clicked.
Then you added a library and suddenly you're learning a new framework inside your framework — providers, stores, atoms, streams, reducers.
June refuses to do that.
It gives you shared, reactive state that works exactly like Flutter's own state management — just without the widget tree dependency. That's it.
At a glance #
|
Before June — state trapped in a widget
|
With June — state lives anywhere, rebuilds anything
|
Why June wins on simplicity #
| June | Provider | GetX | Bloc | |
|---|---|---|---|---|
No MaterialApp wrapper needed |
✅ | ❌ | ❌ | ❌ |
| No manual initialization | ✅ | ❌ | ✅ | ❌ |
Works with StatelessWidget |
✅ | ✅ | ✅ | ✅ |
Call setState from anywhere |
✅ | ❌ | ✅ | ❌ |
| Multiple tagged instances | ✅ | ❌ | ✅ | ❌ |
| Per-widget isolated state | ✅ | ❌ | ❌ | ❌ |
| Zero new architecture required | ✅ | ❌ | ❌ | ❌ |
Installation #
dependencies:
june: ^1.0.2
flutter pub get
Quick start — 3 steps, 3 minutes #
Step 1 — Define your state #
class CounterVM extends JuneState {
int count = 0;
}
Step 2 — Subscribe a widget #
JuneBuilder(
() => CounterVM(),
builder: (vm) => Text('${vm.count}'),
)
Step 3 — Update from anywhere #
June.getState(() => CounterVM())
..count++
..setState();
No providers. No streams. No boilerplate.
setStateworks the same way it always has — just across the whole app.
Full runnable example #
import 'package:flutter/material.dart';
import 'package:june/june.dart';
void main() => runApp(const MyApp());
// ─── State ─────────────────────────────────────────────────────────────────
class CounterVM extends JuneState {
int count = 0;
void increment() {
count++;
setState();
}
}
// ─── UI ────────────────────────────────────────────────────────────────────
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: JuneBuilder(
() => CounterVM(),
builder: (vm) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'${vm.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => June.getState(() => CounterVM()).increment(),
child: const Icon(Icons.add),
),
),
);
}
}
Patterns #
Methods on state #
Keep logic next to data. Call it from anywhere.
class CounterVM extends JuneState {
int count = 0;
void increment() {
count++;
setState();
}
void reset() {
count = 0;
setState();
}
}
// From a button, a gesture, a timer — anywhere
June.getState(() => CounterVM()).increment();
Multiple instances with tag #
One state class, many independent instances — perfect for list items, cards, feeds.
JuneBuilder(
() => CounterVM(),
builder: (vm) => Text('Default: ${vm.count}'),
)
JuneBuilder(
() => CounterVM(),
tag: 'card_a',
builder: (vm) => Text('Card A: ${vm.count}'),
)
JuneBuilder(
() => CounterVM(),
tag: 'card_b',
builder: (vm) => Text('Card B: ${vm.count}'),
)
// Update only card A — other instances are unaffected
June.getState(() => CounterVM(), tag: 'card_a')
..count++
..setState();
Per-widget local state with global: false #
For state that belongs to exactly one widget — not shared globally. Pass your own instance and June won't register it.
class SliderState extends JuneState {
double value = 0.5;
}
final sliderState = SliderState();
JuneBuilder(
() => sliderState,
global: false,
builder: (s) => Slider(
value: s.value,
onChanged: (v) {
s.value = v;
s.setState();
},
),
)
Each widget holds its own isolated state. Navigating away and back creates a fresh instance — exactly what you want for forms, sliders, and single-use UI.
Theme toggle #
JuneBuilder can wrap MaterialApp itself, rebuilding the entire app on state change.
class ThemeController extends JuneState {
bool isDark = false;
void toggle() {
isDark = !isDark;
setState();
}
}
return JuneBuilder(
() => ThemeController(),
builder: (c) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: c.isDark ? ThemeMode.dark : ThemeMode.light,
home: const HomeScreen(),
),
);
Async state / HTTP #
Call setState() when your data arrives. Works with any Future or Stream.
class UserController extends JuneState {
User? user;
bool loading = false;
Future<void> loadUser(String id) async {
loading = true;
setState();
user = await userRepository.fetch(id);
loading = false;
setState();
}
}
JuneBuilder(
() => UserController(),
builder: (c) {
if (c.loading) return const CircularProgressIndicator();
if (c.user == null) return const Text('No user loaded');
return Text('Hello, ${c.user!.name}');
},
)
Selective rebuilds with id #
Trigger only specific JuneBuilder widgets inside a shared state, leaving others untouched.
class FeedState extends JuneState {
String header = 'Latest';
List<Post> posts = [];
void updateHeader(String value) {
header = value;
setState('header'); // only rebuilds builders with id: 'header'
}
void refreshPosts(List<Post> fresh) {
posts = fresh;
setState('posts'); // only rebuilds builders with id: 'posts'
}
}
// Rebuilds only when 'header' setState is called
JuneBuilder(
() => FeedState(),
id: 'header',
builder: (s) => Text(s.header),
)
// Rebuilds only when 'posts' setState is called
JuneBuilder(
() => FeedState(),
id: 'posts',
builder: (s) => PostList(posts: s.posts),
)
Core API #
| Symbol | Role |
|---|---|
JuneState |
Base class for all state objects. Extend it, add fields and methods, call setState() to rebuild. |
JuneBuilder |
Widget that subscribes to a JuneState and rebuilds when setState() fires. |
June.getState() |
Retrieves (or lazily creates) a global singleton of a state type. Thread-safe, no setup needed. |
tag |
Namespaces multiple global instances of the same type. |
id |
Enables partial rebuilds — only JuneBuilder widgets sharing the same id will rebuild. |
global: false |
Bypasses the global registry. The builder uses your provided instance directly. |
tag vs id — when to use which #
tag |
id |
|
|---|---|---|
| Lives on | JuneBuilder & June.getState() |
JuneBuilder & setState() |
| Purpose | Pick which stored instance to use | Pick which builders to rebuild |
| Use when | Same state type needed in multiple places independently | One shared state, but only some widgets should react to a given change |
Migration #
0.8.x → 1.0.0 #
The factory function is now passed as a closure rather than an evaluated instance:
// Before (0.8.x)
June.getState(CounterVM());
// After (1.0.0+)
June.getState(() => CounterVM());
Community #
Questions, ideas, or just want to say hi?
Acknowledgements #
June was shaped by the best ideas in the Flutter ecosystem. Gratitude to the teams and communities behind Provider, GetX, Bloc, and Riverpod, and to Svelte for proving that less really is more.
Made with care for the Flutter community.