focus_quest
A reusable gamified focus and anti-doomscroll productivity engine for Flutter.
focus_quest gives app developers the business logic for focus sessions,
rewards, streaks, levels, statistics, persistence, lifecycle handling, optional
feedback, and Riverpod state. You bring the UI: a virtual pet, garden, study
timer, charity tracker, team focus tool, or any other game layer.
Features
- Start, pause, resume, complete, cancel, reset, and restore focus sessions
- Timestamp-based countdowns that stay accurate after delayed timer ticks
- Configurable background behavior: pause, cancel, or keep running
- Points, XP, completion bonuses, partial rewards, and custom reward metadata
- Daily goal progress, current streak, longest streak, completion rate, and history
- In-memory, SharedPreferences-backed, and Hive-backed local persistence
- Storage abstraction for custom Isar, SQLite, secure storage, or backend adapters
- Riverpod notifier and immutable state for Flutter apps
- Optional haptic and audioplayers feedback hooks with a no-op default
Use Cases
focus_quest is suitable for anti-doomscroll apps where staying away from the
phone can grow a virtual forest, feed a pet, unlock collectibles, or record
charity-impact metadata. It is also useful for Pomodoro apps, study timers,
habit trackers, digital wellbeing products, and employee focus tools.
The package tracks app-level focus sessions. It does not block, inspect, or monitor other installed apps.
Supported Platforms
The core Dart logic works anywhere Flutter runs. The bundled
SharedPreferencesFocusQuestStorage depends on shared_preferences, which
supports Android, iOS, web, macOS, Windows, and Linux. Lifecycle behavior is
app-level and depends on Flutter lifecycle events from the host app.
Installation
flutter pub add focus_quest
Quick Start
import 'package:focus_quest/focus_quest.dart';
Future<void> main() async {
final controller = FocusQuestController(
storage: SharedPreferencesFocusQuestStorage(),
);
await controller.initialize();
await controller.start(
duration: const Duration(minutes: 25),
metadata: {
'category': 'study',
'task': 'Japanese vocabulary',
},
);
await controller.pause();
await controller.resume();
await controller.complete();
final state = controller.state;
print('Points: ${state.totalPoints}');
print('Streak: ${state.currentStreak}');
}
Riverpod Usage
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:focus_quest/focus_quest.dart';
class FocusButton extends ConsumerWidget {
const FocusButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(focusQuestStateProvider);
final notifier = ref.read(focusQuestStateProvider.notifier);
return ElevatedButton(
onPressed: state.status == FocusSessionStatus.running
? null
: () => notifier.start(duration: const Duration(minutes: 25)),
child: Text(state.status.name),
);
}
}
Custom Reward Strategy
Use reward metadata to drive your own game world, pet, garden, or charity UI.
class GardenRewardStrategy implements RewardStrategy {
@override
FocusReward calculate(FocusSession session, FocusQuestConfig config) {
final focusedMinutes = session.actualFocusDuration.inMinutes;
final completed = session.status == FocusSessionStatus.completed;
return FocusReward(
points: completed ? focusedMinutes + config.completionBonus : 0,
experience: focusedMinutes * 2,
metadata: {
'treeGrowth': focusedMinutes,
'petFood': focusedMinutes ~/ 5,
'charityCents': completed ? focusedMinutes : 0,
},
);
}
}
Storage Customization
Use InMemoryFocusQuestStorage in tests,
SharedPreferencesFocusQuestStorage for lightweight local persistence, or
HiveFocusQuestStorage for a Hive-backed store.
final controller = FocusQuestController(
storage: HiveFocusQuestStorage(boxName: 'my_focus_app'),
);
For a different database or backend, implement FocusQuestStorage and persist
FocusSession.toJson() plus FocusProfile.toJson().
Lifecycle Behavior
Call handleLifecycleEvent from your app lifecycle observer. The configured
background behavior decides whether the active session pauses, cancels, or keeps
running.
controller = FocusQuestController(
config: const FocusQuestConfig(
backgroundBehavior: BackgroundBehavior.pause,
maxInterruptions: 3,
),
);
await controller.handleLifecycleEvent(FocusLifecycleEvent.paused);
Elapsed and remaining time are calculated from timestamps, not only from one-second ticks. The controller prevents duplicate active timers and finalizes completed sessions once.
Optional Feedback and Audio
The package includes FocusFeedback, NoopFocusFeedback,
FlutterFocusFeedback for haptics, and AudioplayersFocusFeedback for
optional asset-based sound effects.
final controller = FocusQuestController(
feedback: AudioplayersFocusFeedback(
startedAsset: 'sounds/start.mp3',
completedAsset: 'sounds/complete.mp3',
levelUpAsset: 'sounds/level-up.mp3',
),
);
Sound assets are optional. Declare any assets you use in the host app's
pubspec.yaml.
Example Application
See example/ for a small app that initializes the controller, starts focus
sessions, handles lifecycle changes, displays progress, and persists state.
Platform Limitations
- App-level lifecycle tracking is supported.
- Device-wide app usage tracking is not included.
- App blocking is not included.
- Charity donations must be implemented by the app through its own backend or
payment/donation provider.
focus_questcan store reward metadata for that flow, but it does not transfer money.
Testing
flutter test
flutter test --coverage
cd example && flutter test
dart pub publish --dry-run
Roadmap
- Richer streak policies and calendar rules
- Share-card helpers for streaks, pets, gardens, and charity progress
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md and
CODE_OF_CONDUCT.md for project expectations.