idle_flutter
idle_flutter is a lightweight Flutter binding layer for idle_core + idle_save.
It stays framework-agnostic (no Riverpod/BLoC lock-in) and focuses on two things by default:
safe lifecycle + offline persistence, and safe UI rebuild behavior.
Documentation in this repository is always written in English.
Core Goals
- Framework neutral: Use any state management you want. No forced coupling.
- Mistake-proof persistence: Avoid lost saves or double-applied offline progress.
- UI performance safety: Selector equality by default, optional rebuild warnings in debug.
- Mobile/Desktop first: Web support only when explicitly targeted.
Quick Start
1) Define state and reducer
import 'package:idle_core/idle_core.dart';
class GameState extends IdleState {
final int gold;
final int rate;
const GameState({required this.gold, required this.rate});
GameState copyWith({int? gold, int? rate}) =>
GameState(gold: gold ?? this.gold, rate: rate ?? this.rate);
@override
Map<String, dynamic> toJson() => {'gold': gold, 'rate': rate};
static GameState fromJson(Map<String, dynamic> json) => GameState(
gold: (json['gold'] as num?)?.toInt() ?? 0,
rate: (json['rate'] as num?)?.toInt() ?? 1,
);
}
class UpgradeRate extends IdleAction {
final int delta;
const UpgradeRate(this.delta);
}
GameState reducer(GameState state, IdleAction action) {
if (action is IdleTickAction) {
return state.copyWith(gold: state.gold + state.rate);
}
if (action is UpgradeRate) {
return state.copyWith(rate: state.rate + action.delta);
}
return state;
}
2) Choose a store
import 'package:idle_flutter/idle_flutter.dart';
import 'package:idle_save/idle_save.dart';
final store = SharedPreferencesStore('idle_save');
final controller = IdleFlutterController<GameState>(
config: IdleConfig<GameState>(
dtMs: 1000,
maxOfflineMs: 60 * 60 * 1000,
maxTicksTotal: 100000,
),
reducer: reducer,
initialState: const GameState(gold: 0, rate: 1),
stateDecoder: GameState.fromJson,
store: store,
migrator: Migrator(latestVersion: 1),
);
3) Bind to widgets
IdleControllerHost<GameState>(
controller: controller,
loading: const SizedBox.shrink(),
builder: (context, c) {
return IdleSelector<GameState, int>(
controller: c,
selector: (c) => c.state.gold,
builder: (context, gold, c) => Text('gold=$gold'),
);
},
)
Safe Lifecycle + Offline Design
Core idea
- Persist lastSeenMs: track the last active time of the app.
- Apply offline progress exactly once: restore, apply offline window, then immediately save.
- Debounce + max interval: saves never get starved by continuous ticks.
Behavior summary
- App start: load save -> apply offline -> save immediately
- App background: capture lastSeenMs -> save immediately
- App resume: apply offline -> save immediately
- Normal runtime: debounced save + max interval to guarantee periodic saves
Related parameters
autosaveDebounce: default 1s; delay after last activity before savingautosaveMaxInterval: default 10s; ensures a save happens even with constant ticks
final controller = IdleFlutterController<GameState>(
...
autosaveDebounce: const Duration(milliseconds: 300),
autosaveMaxInterval: const Duration(seconds: 5),
);
API Reference
IdleFlutterController
Role
- Flutter lifecycle wiring for
IdleEngine - Persistence + migration management
- Debounced autosave and offline safety
Constructor parameters
config:IdleConfig(dtMs, maxOfflineMs, maxTicksTotal)reducer:IdleReducer(game logic)initialState: initial statestateDecoder: decoder for saved statestore: primarySaveStorebackupStore: optional backupSaveStorecodec: save codec (default JSON)migrator: migration chainnowMs: time provider for testsautosaveDebounce: debounce delayautosaveMaxInterval: max save intervalstartTickingImmediately: auto start on init
Key properties
state: current statevalue:ValueListenablevaluelastSeenMs: last active timestampready: start completeddisposed: disposed state
Key methods
start(): load + restore + offline applydispatch(IdleAction): apply actiontick({count}): manual tickflush(): force savedispose(): release lifecycle and final save
IdleControllerHost
Safe wrapper for start()/dispose() lifecycle.
startOnInit: auto start on initdisposeOnUnmount: dispose controller on unmountrebuildOnChange: rebuild subtree on change
IdleBuilder
AnimatedBuilder wrapper for full rebuilds.
IdleSelector
Rebuilds only when selected values change.
- Default equality uses
listEquals/mapEquals/setEqualsfor collections - Other types use
== - Use
shouldRebuildto provide custom logic
IdleSelector<GameState, List<int>>(
controller: controller,
selector: (c) => c.state.someList,
shouldRebuild: (prev, next) => prev.length != next.length,
builder: ...,
)
Debug rebuild warning (optional)
IdleSelector<GameState, int>(
controller: controller,
selector: (c) => c.state.gold,
debug: const IdleSelectorDebug(
label: 'GoldText',
maxRebuilds: 60,
interval: Duration(seconds: 10),
),
builder: ...,
)
- Only in debug mode
- Logs a warning if rebuild count exceeds the threshold
Stores (SaveStore)
SharedPreferencesStore
final store = SharedPreferencesStore('idle_save');
- Works on Web/Mobile/Desktop
- Simple and reliable default choice
PathFileStore
final store = await PathFileStore.documents('idle_save.json');
- File-backed on Mobile/Desktop
- On Web, falls back to in-memory store (not persistent)
Web Support Policy
idle_flutter is Mobile/Desktop focused by default.
If Web is a target, prefer:
SharedPreferencesStoreor a customSaveStore- A web-specific persistence strategy
If Web is not a target, explicitly state Mobile/Desktop focus in your app docs.
Update Policy
This package follows an SDK-like update policy:
- Stable APIs
- Predictable upgrades
- Clear migration guidance