jintent 2.2.0
jintent: ^2.2.0 copied to clipboard
jintent is a Flutter package that provides an architecture for managing state changes in your application using the concept of intents.
JIntent #
Lightweight, explicit Intent + State + Side Effect architecture for Flutter (MVI-inspired)
Table of Contents #
- Overview
- Why JIntent?
- Quick Start
- Core Concepts
- Architecture
- Minimal Example
- Side Effects
- Concurrency & Ordering
- Observability
- Testing
- Comparison
- Migration (if any)
- Roadmap (Short Term)
- Contributing
- License
Overview #
JIntent provides a simple, explicit way to:
- Represent immutable UI state (JState).
- Trigger domain actions via Intents (JIntent subclasses).
- Update state in a single, predictable point (JController).
- Emit one-off side effects (navigation, dialogs, toasts) without polluting state (JEffect / side effect channels).
Goal: Clarity and testability with minimal boilerplate.
Why JIntent? #
| Problem in typical apps | How JIntent helps |
|---|---|
| Mixed UI + logic | Controller centralizes state transitions |
| Side effects duplicated | Dedicated side effect stream/channel |
| Hard to test flows | Intents are discrete, testable units |
| Race conditions | (Document your chosen intent handling policy) |
| State mutation | Immutability via copyWith patterns |
Quick Start #
Add dependency:
dependencies:
jintent: ^X.Y.Z
Import:
import 'package:jintent/jintent.dart';
Create State:
@immutable
class CounterState extends JState {
final int counter;
const CounterState({required this.counter});
@override
CounterState copyWith({int? newStateCounter}) =>
CounterState(counter: newStateCounter ?? counter);
@override
List<Object?> get props => [counter];
factory CounterState.initialState() => const CounterState(counter: 0);
}
Declare Intents:
class DecrementUseCase extends JSyncUseCase<int, int> {
@override
Either<Exception, int> run(int currentValue) {
final newValue = currentValue - 1;
if (newValue < -10) {
return Left(Exception('Value cannot be less than -10'));
}
return Right(newValue);
}
}
Controller:
class CounterController extends JController<CounterState> {
// With injection dependence
final _getCurrentCounterValueIntent = Di.sl<GetCurrentCounterValueIntent>();
final _incrementIntent = Di.sl<IncrementIntent>();
final _decrementIntent = Di.sl<DecrementIntent>();
// other common to Creation
// final _getCurrentCounterValueIntent = GetCurrentCounterValueIntent()
// final _incrementIntent = IncrementIntent();
// final _decrementIntent = DecrementIntent();
CounterController(super.initialState);
void loadCounter() {
intent(_getCurrentCounterValueIntent);
}
void increment() => intent(_incrementIntent);
void decrement() => intent(_decrementIntent);
@override
void onInit() {}
}
UI:
import 'package:counter/src/presentation/counter/controllers/controller.dart';
import 'package:counter/src/presentation/counter/presentation/counter_effect_handler.dart';
import 'package:counter/src/presentation/counter/states/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:jintent/jintent.dart';
class CounterView extends ConsumerStatefulWidget {
const CounterView({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _CounterViewState();
}
class _CounterViewState extends ConsumerState<CounterView> {
final throttler = JThrottler(const Duration(milliseconds: 200));
late final CounterController _counterController;
JSideEffectHandler<CounterState> get _sideEffectHandler =>
CounterEffectHandler(_counterController);
@override
void initState() {
super.initState();
_counterController = ref.read(couterControllerProvider.notifier);
}
@override
Widget build(BuildContext context) {
final counter = ref.watch(
couterControllerProvider.select((value) => value.counter),
);
return JEffectListener(
controller: _counterController,
handler: _sideEffectHandler,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
label: const Text('Increment'),
heroTag: 'Increment',
onPressed: () => throttler.call(_counterController.increment),
tooltip: 'Increment',
icon: const Icon(Icons.exposure_plus_1_outlined),
),
const SizedBox(height: 10),
FloatingActionButton.extended(
label: const Text('Decrement'),
heroTag: 'Decrement',
onPressed: () => throttler.call(_counterController.decrement),
tooltip: 'Decrement',
icon: const Icon(Icons.exposure_minus_1_sharp),
),
],
),
),
);
}
}
Core Concepts #
- JState: Immutable snapshot of UI data.
- JIntent: User or system intention (e.g., SubmitLogin, LoadItems).
- JController<S, I>: Receives intents, updates state, emits side effects.
- Side Effect: One-shot event (navigation, toast, analytics).
- Effect Channel / Stream: Decoupled delivery of ephemeral events.
Architecture #
Flow (simplified): User Action -> Intent -> Controller.handle -> (New State) + (Optional Side Effect)
State updates propagate to UI via a ValueListenable/Stream. Side effects consumed once by a listener (e.g. using a StreamBuilder or dedicated hook).
β‘ Side Effects (Modern Overview) #
Side effects (JEffect) model transient events (navigation, dialogs, toasts) outside of state.
Key types:
- JEffect
- JFireAndForgetEffect (no return value)
- JResultEffect
Quick example:
final confirmed = await controller.emitAndWaitSideEffect(
DeleteDialogEffect(itemName: 'File.txt'),
);
if (confirmed) {
controller.intent(DeleteItemIntent(...));
}
If an awaited effect has no handler, the default strategy completes it silently (warnAndAutoComplete).
Configure a different strategy with:
JEffectsConfig().unhandledStrategy = UnhandledEffectStrategy.throwError;
Concurrency & Ordering #
JIntent processes intents in a predictable, sequential order by default.
- Queue (FIFO): Intents are enqueued and handled one at a time, ensuring state transitions are applied in the order they were received.
- No Parallel Mutations: This avoids race conditions and makes state changes easy to reason about.
- Custom Policies: Advanced users can implement custom intent handling strategies (e.g., debouncing, throttling, dropping, or merging intents) by extending the controller or using middleware.
Best practice:
Document your chosen concurrency policy in your controller, especially if you change the default behavior. This helps maintain clarity and prevents subtle bugs in complex flows.
Observability #
JIntent provides production-ready observability features for monitoring, debugging, and understanding your application's behavior.
Structured JSON Logging #
import 'package:jintent/jintent.dart';
void main() {
final logger = JStructuredLogger(
serviceName: 'my-app',
version: '1.0.0',
minLevel: LogLevel.info,
);
logger.info('User logged in', context: {
'userId': '12345',
'action': 'login',
});
runApp(MyApp());
}
All logs are output as JSON for easy parsing:
{
"timestamp": "2025-10-15T12:34:56.789Z",
"level": "INFO",
"message": "User logged in",
"service": "my-app",
"version": "1.0.0",
"context": {
"userId": "12345",
"action": "login"
}
}
Correlation IDs #
Track user actions across multiple operations:
// Wrap user actions in a correlation context
await CorrelationContext.runWithCorrelation(() async {
await controller.dispatch(LoginIntent());
// Access the correlation ID anywhere
final id = CorrelationContext.current;
logger.info('Processing', context: {'correlationId': id});
});
Metrics Collection #
Collect operational and performance metrics:
void main() {
// Enable metrics
JMetrics.enable();
JMetrics.attachToObserver();
runApp(MyApp());
}
// Metrics are automatically tracked for:
// - Intent dispatches
// - State changes
// - Effect emissions
// - Execution timings
Manual metric recording:
// Counter
JMetrics.incrementCounter('user.login');
// Gauge
JMetrics.recordGauge('active.users', userCount);
// Timer
final timerId = JMetrics.startTimer('api.request');
await performOperation();
JMetrics.stopTimer(timerId);
Complete Observability Example #
class LoginIntent extends JIntent<AuthState> {
final JStructuredLogger logger;
@override
Future<void> onInvoke() async {
await CorrelationContext.runWithCorrelation(() async {
final correlatedLogger = logger.withContext(
CorrelationContext.asContext ?? {},
);
correlatedLogger.info('Login attempt started');
final timerId = JMetrics.startTimer('login.duration');
try {
final user = await loginUseCase.execute();
JMetrics.incrementCounter('login.success');
JMetrics.stopTimer(timerId, tags: {'status': 'success'});
correlatedLogger.info('Login successful');
update((state) => state.copyWith(user: user));
} catch (e) {
JMetrics.incrementCounter('login.failed');
JMetrics.stopTimer(timerId, tags: {'status': 'failed'});
correlatedLogger.error('Login failed', error: e);
emitSideEffect(ShowErrorEffect(e.toString()));
}
});
}
}
π For comprehensive observability documentation, see docs/OBSERVABILITY_GUIDE.md
Advanced Features (Phase 4) #
DevTools Overlay #
Real-time monitoring of JIntent operations directly in your app:
MaterialApp(
builder: (context, child) {
return JDevToolsOverlay(
enabled: kDebugMode,
child: child!,
);
},
)
Features:
- Real-time intent/state/effect visualization
- Event timeline with metadata
- Metrics dashboard
- Toggle on/off with FAB
- Zero performance impact when hidden
π See docs/DEVTOOLS_POC.md
Undo/Redo (Experimental) #
Add undo/redo capabilities to your controllers:
class MyController extends JController<MyState>
with UndoRedoMixin<MyState> {
MyController() : super(MyState.initial());
@override
void onInit() {
enableUndoRedo(maxHistorySize: 50);
}
}
// In your intent
updateWithUndo((state) => state.copyWith(value: newValue));
// Undo/redo
controller.undo(); // Returns bool
controller.redo(); // Returns bool
Alternative command pattern approach also available for fine-grained control.
Plugin Ecosystem #
Extend JIntent with custom behavior:
class AnalyticsPlugin {
void install() {
JObserver.onIntentDispatched = (intent) {
analytics.logEvent('intent_dispatched',
parameters: {'type': intent.runtimeType.toString()});
};
}
}
Extensibility points:
- Observer hooks (intents, states, effects)
- Custom dispatchers (priority, debouncing)
- Custom effect handlers
- Middleware pattern
π See docs/PLUGIN_HOOKS.md
Performance #
JIntent is designed for high performance:
- Intent Processing: < 0.5ms (P50)
- State Updates: > 10,000/sec
- Memory Overhead: < 1KB per controller
- Binary Size: +42KB to APK
π See docs/PERFORMANCE.md
Testing #
Recommended strategy:
- Given initial state
- When: dispatch intent
- Await completion
- Assert new state + captured side effects
Comparison (High Level) #
| Feature | JIntent | Bloc | Redux | Plain Riverpod |
|---|---|---|---|---|
| Side effects channel | Yes | Yes (via Bloc) | Middleware needed | Provider-dependent |
| Boilerplate | Low | Medium | High | Low |
| Immutable state | Yes | Yes | Yes | Depends |
| Intent semantics | Explicit classes | Events | Actions | Method calls |
| Concurrency control | (Document) | Per event loop | Middleware | Custom |
Migration #
If upgrading from 1.x to 2.x:
- Update import paths.
- Adjust side effect API rename.
- See CHANGELOG for removed symbols. (Provide MIGRATION.md if many items.)
Roadmap #
Completed β #
- [β] Phase 0: Discovery and documentation baseline
- [β] Phase 1: CI/CD, testing infrastructure, ADRs
- [β] Phase 2: Security baseline, API patterns, data layer
- [β] Phase 3: Structured logging, metrics, correlation IDs, integration tests
- [β] Phase 4: DevTools overlay, undo/redo, performance docs, plugin hooks
Recent Additions (Phase 4) #
- [β] DevTools overlay for real-time monitoring
- [β] Undo/redo experimental support
- [β] Performance benchmarks and optimization guide
- [β] Plugin hooks documentation
- [β] 95%+ OWASP compliance documentation
Future Enhancements #
- β Advanced examples (debounce, pagination, streaming)
- β Automated benchmark suite in CI
- β Community plugin ecosystem
- β Chrome DevTools integration
Documentation #
Core Documentation #
- Effects Guide - Comprehensive side effects system documentation
- Mapper Reader - Data mapping utilities
Advanced Features (Phase 4) #
- DevTools PoC - Real-time monitoring overlay documentation
- Plugin Hooks - Extensibility guide for building plugins
- Performance Guide - Benchmarks, optimization, profiling
Observability (Phase 3) #
- Observability Guide - Structured logging, metrics, correlation IDs
- Observability Example - Complete working example
Security & Best Practices #
- Security Guide - OWASP ASVS compliance, input validation, secure state management
- Error Handling Guide - Either patterns, exception handling, global error handlers
- API Versioning - Semantic versioning policy, breaking changes, deprecation process
- Data Layer Guide - Repository patterns, mappers, caching strategies
Code Examples #
- Validation Examples - Input validation patterns and reusable validators
- Error Handling Examples - Practical error handling patterns
Governance & Architecture #
- Documentation Index - Complete documentation navigation
- ADR-000 to ADR-009 - Architecture Decision Records
- Executive Summary - Project overview and roadmap
Contributing Guidelines #
Thank you for your interest in contributing to this project.
This document sets the official policies and guidelines for collaboration.
1. Communication #
- Before starting any development, please open an issue to discuss the proposal.
- All contributions must align with the project's vision, objectives, and standards.
2. Git Workflow #
-
Use branch names with the following format:
feature/<short-description>fix/<short-description>chore/<short-description>
-
Example:
feature/offline-sync -
Commit messages must follow the Conventional Commits standard:
feat: New featurefix: Bug fixdocs: Documentationrefactor: Internal refactor, no functional changetest: Adding or updating tests
3. Code Quality #
- Every change must include:
- Unit and/or integration tests.
- An entry in CHANGELOG.md under
[Unreleased]. - Compliance with the projectβs linting and formatting rules.
CI/CD Pipeline #
- All PRs trigger automated checks via GitHub Actions:
- Code formatting (
dart format) - Static analysis (
flutter analyze) - Test suite with coverage report
- Coverage threshold enforcement (β₯80%)
- Code formatting (
- PRs cannot merge until all checks pass.
- Coverage reports are available as artifacts in the workflow runs.
4. Pull Requests #
- PRs must be clear, concise, and focused only on related changes.
- The PR description should include:
- The problem it solves.
- The changes introduced.
- Instructions for testing.
- At least one reviewer must approve the PR, and all automated checks must pass.
5. Versioning and Releases #
- We follow Semantic Versioning (SemVer):
- MAJOR: Breaking changes.
- MINOR: Backward-compatible new features.
- PATCH: Bug fixes.
- All releases must be documented in CHANGELOG.md.
6. Code of Conduct #
All interactions must follow the Code of Conduct, fostering a professional, inclusive, and respectful environment.
License #
MIT Β© 2025 TodoFlutter.com
FAQ #
Q: How do I avoid duplicate side effects after Hot Reload?
A: Keep effects listener registration inside initState and cancel in dispose; do not re-emit past effects (channel is one-shot).
Q: Can I dispatch intents from inside another intent?
A: Prefer composing functions or scheduling a new intent after current finishes to maintain linear flow.
Q: Does JIntent support cancellation?
A: (Document if implemented; show API or mark as planned.)