current 3.0.0-beta-1
current: ^3.0.0-beta-1 copied to clipboard
A simple yet powerful state management library for Flutter. Keeps your widget build methods clean, with ZERO dependencies on other packages.
Flutter Current
A simple, lightweight state management library for Flutter
Features #
- Typed reactive properties for primitives, nullable values, lists, and maps.
- View-model-driven widgets with
CurrentWidgetandCurrentState. - Application-wide shared state with
Current. - Issue-based validation that keeps localization in the widget layer.
- Text input binding with
CurrentTextController,CurrentTextFormField, andCurrentTextFieldfor form and non-form flows. - Built-in busy state, change notifications, and event listeners for async flows.
Getting Started #
In your Flutter project, add the dependency to your pubspec.yaml.
dependencies:
current: ^3.0.0-beta-1
Tip: Consider installing the Current Flutter Snippets extension in Visual Studio Code to make creating Current classes easier.
Quick Start #
A small counter is still the fastest way to see the core pattern: keep state in a CurrentViewModel, list reactive properties in currentProps, and render that view model through a CurrentWidget.
counter_view_model.dart #
import 'package:current/current.dart';
class CounterViewModel extends CurrentViewModel {
final count = CurrentProperty.integer(
initialValue: 0,
propertyName: 'count',
);
void incrementCounter() {
count.increment();
}
@override
Iterable<CurrentProperty> get currentProps => [count];
}
counter_page.dart #
import 'package:current/current.dart';
import 'package:flutter/material.dart';
class CounterPage extends CurrentWidget<CounterViewModel> {
const CounterPage({super.key, required super.viewModel});
@override
CurrentState<CounterPage, CounterViewModel> createCurrent() {
return _CounterPageState(viewModel);
}
}
class _CounterPageState extends CurrentState<CounterPage, CounterViewModel> {
_CounterPageState(super.viewModel);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Current Example')),
body: Center(
child: Text('Count: ${viewModel.count.value}'),
),
floatingActionButton: FloatingActionButton(
onPressed: viewModel.incrementCounter,
child: const Icon(Icons.add),
),
);
}
}
main.dart #
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Current State Example',
home: CounterPage(
viewModel: CounterViewModel(),
),
);
}
}
This keeps business logic out of the widget tree without introducing a second state object. When any property in currentProps changes, the matching CurrentState rebuilds automatically.
Forms And Validation #
Current validation is issue-based. Rules return CurrentValidationIssue, not display strings. That keeps validation logic locale-agnostic and lets widgets resolve the final text using whatever localization system the app already uses.
profile_view_model.dart #
import 'package:current/current.dart';
class ProfileViewModel extends CurrentViewModel {
final displayName = CurrentProperty.string(
initialValue: '',
propertyName: 'displayName',
);
final age = CurrentProperty.integer(
initialValue: 0,
propertyName: 'age',
);
CurrentValidationGroup? _profileValidation;
CurrentValidationGroup get profileValidation =>
_profileValidation ??= CurrentValidationGroup.forProperties([
displayName,
age,
]);
@override
Iterable<CurrentProperty> get currentProps => [displayName, age];
}
profile_page.dart #
import 'package:current/current.dart';
import 'package:flutter/material.dart';
class ProfilePage extends CurrentWidget<ProfileViewModel> {
const ProfilePage({super.key, required super.viewModel});
@override
CurrentState<ProfilePage, ProfileViewModel> createCurrent() {
return _ProfilePageState(viewModel);
}
}
class _ProfilePageState extends CurrentState<ProfilePage, ProfileViewModel>
with CurrentTextControllersLifecycleMixin {
_ProfilePageState(super.viewModel);
final _formKey = GlobalKey<FormState>();
final displayNameController = CurrentTextController.string();
final ageController = CurrentTextController.integer();
@override
void bindCurrentControllers() {
displayNameController.bind(
property: viewModel.displayName,
lifecycleProvider: this,
validationBuilder: (property, context) => property.createValidation(
rules: [
(value) => value.trim().isEmpty
? CurrentValidationIssue.message(AppLocalizations.of(context)!.displayNameRequired)
: null,
],
validateOnPropertyChange: true,
),
);
ageController.bind(
property: viewModel.age,
lifecycleProvider: this,
validationBuilder: (property, _) => property.createValidation(
rules: [
(value) => value < 18
? const CurrentValidationIssue(
'profile.age.minimum', // error code if localization is based on codes
arguments: {'minimumAge': 18},
fallbackMessage: 'Must be at least 18',
)
: null,
],
validateOnPropertyChange: true,
),
validationIssues: CurrentTextControllerValidationIssues(
invalidValueIssueBuilder: _invalidAgeIssue,
),
);
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
CurrentTextFormField<String>(
controller: displayNameController,
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(labelText: 'Display name'),
),
CurrentTextFormField<int>(
controller: ageController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validationTextResolver: _resolveIssueText,
decoration: const InputDecoration(labelText: 'Age'),
),
FilledButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
// Submit the form.
}
},
child: const Text('Save profile'),
),
],
),
);
}
static String? _resolveIssueText(CurrentValidationIssue issue) {
switch (issue.code) {
case 'profile.age.minimum':
return 'Must be at least ${issue.arguments['minimumAge']} years old.';
case 'profile.age.invalid':
return 'Enter a valid whole number.';
default:
return issue.fallbackMessage;
}
}
static CurrentValidationIssue _invalidAgeIssue(String value) {
return const CurrentValidationIssue.invalidValue(
code: 'profile.age.invalid',
fallbackMessage: 'Enter a valid whole number.',
);
}
}
Choosing a field widget #
Current exposes three field-integration paths. Pick the one that matches who owns your widget tree and validation UX.
- Use
CurrentTextFormFieldwhen you are already inside aFormand want the shortest Current-specific wrapper. - Use native
TextFormFieldwithcontroller.formValidator(...)when your app already has its own field wrapper or design-system component and you only want Current to provide binding plus validation bridging. - Use
CurrentTextFieldwhen you are not using FlutterFormwidgets but still want Current-managed error visibility withAutovalidateMode-style behavior.
CurrentTextFormField<String>(
controller: displayNameController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validationTextResolver: _resolveIssueText,
decoration: const InputDecoration(labelText: 'Display name'),
);
TextFormField(
controller: ageController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: ageController.formValidator(
context: context,
resolver: _resolveIssueText,
),
decoration: const InputDecoration(labelText: 'Age'),
);
CurrentTextField<String>(
controller: displayNameController,
autovalidateMode: AutovalidateMode.onUserInteractionIfError,
validationTextResolver: _resolveIssueText,
decoration: const InputDecoration(labelText: 'Quick search'),
);
Key points:
- Register validation once, either by calling
createValidation()directly or by supplyingvalidationBuilderwhen binding a controller. - Use
CurrentValidationGroup.forProperties([...])when you want grouped validation without separately listing validators. - Use
CurrentTextFormFieldfor the shortestFormintegration, nativeTextFormFieldwithcontroller.formValidator(...)when your widget layer already exists, andCurrentTextFieldwhen you want Current-managed validation without aForm. - Let widgets resolve issue text either through a resolver or through
BuildContextwhen your localization API requires it. - Use
CurrentTextControllerValidationIssuesfor controller-generated parse or required-value failures. - Validation rules can live in the widget, the view model, or in a separate plain-Dart helper file when that keeps a larger form easier to read.
Application Wide State Management #
Use Current when you want a shared CurrentViewModel anywhere below a subtree, or across the whole app.
application_view_model.dart #
import 'package:current/current.dart';
class ApplicationViewModel extends CurrentViewModel {
final userName = CurrentProperty.nullableString(
propertyName: 'userName',
);
final signedIn = CurrentProperty.boolean(
propertyName: 'signedIn',
);
void signIn(String name) {
userName.value = name;
signedIn.value = true;
}
@override
Iterable<CurrentProperty> get currentProps => [userName, signedIn];
}
main.dart #
import 'package:current/current.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Current(
ApplicationViewModel(),
onAppStateChanged: () =>
DateTime.now().microsecondsSinceEpoch.toString(),
child: Builder(
builder: (context) {
final appViewModel = Current.viewModelOf<ApplicationViewModel>(context);
final userName = appViewModel.userName.value;
return Column(
children: [
Text('User: ${userName ?? 'Guest'}'),
TextButton(
onPressed: () => appViewModel.signIn('Taylor'),
child: const Text('Sign In'),
),
],
);
},
),
),
);
}
}
onAppStateChanged must return a unique string each time shared state changes. For production apps, a UUID generator is a common choice.
Explore The Example App #
The repository includes a larger showcase app in example/README.md. It demonstrates typed properties, form validation, controller binding, collection properties, busy state, custom events, and reference snippets in a single responsive mission-control UI.
Contributing #
This is an open source project, and contributions are welcome. Please feel free to create a new issue if you encounter any problems, or submit a pull request. For community contribution guidelines, please review the Code of Conduct.
If submitting a pull request, please ensure the following standards are met:
- Code files must be well formatted with
dart format .. - Tests must pass with
flutter test. New test cases to validate your changes are highly recommended. - Implementations must not add unnecessary project dependencies.
- Project must contain zero warnings. Running
flutter analyzemust return zero issues. - Keep docstrings and README guidance up to date when public APIs change.
Additional information #
This package has ZERO third-party package dependencies.
You can find the full API documentation here.
© 2026 Third Version Technology Ltd