flutter_conditional 3.0.0
flutter_conditional: ^3.0.0 copied to clipboard
A Flutter package for readable conditional widget rendering with value matching, branching, and lazy builders.
flutter_conditional
Readable, predictable conditional widget selection for Flutter.
flutter_conditional provides eager APIs for already-safe widgets and genuinely
lazy builder APIs for widgets, conditions, and match values that must only be
evaluated when selection reaches them. Boolean branches, value matching,
inactive candidates, nullable results, custom equality, and fallbacks all share
the same first-match rules.
Developed with 💙 and maintained by coderave
Important notes #
- A direct
Widgetargument is eager. Dart evaluates it beforeConditionalruns, even when that branch is not selected. Use a builder API when constructing an unselected widget could throw, read unavailable state, perform work, or cause another side effect. - Builder APIs are genuinely lazy in 3.0. Only the selected widget builder or, when nothing matches, the fallback builder executes. Inactive candidates are skipped before their condition or value resolvers run.
- 3.0.0 is an intentional breaking release. Branch selection inputs are now
required, fallbacks cover every no-match path, and
nullparticipates in value matching. See the migration guide.
Install #
flutter pub add flutter_conditional
The package requires Dart 3.8 and Flutter 3.32 or newer.
Quick start #
Use a builder when the selected widget reads data that may be unavailable:
import 'package:flutter/widgets.dart';
import 'package:flutter_conditional/flutter_conditional.dart';
Widget buildAccountLabel(BuildContext context, String? displayName) {
return Conditional.singleBuilder(
context,
condition: displayName != null,
widgetBuilder: (BuildContext context) {
return Text('Signed in as ${displayName!}');
},
fallbackBuilder: (BuildContext context) {
return const Text('Signed out');
},
);
}
Only one of the two builders runs. See the runnable example for fixed widgets, lazy builders, ordered cases, nullable matches, custom matching, and fallbacks.
Eager widgets and lazy builders #
The direct methods accept widget values:
return Conditional.single(
condition: isReady,
widget: const Text('Ready'),
fallback: const Text('Waiting'),
);
This is ideal for constant widgets or values that are already safe to create.
It does not defer the expressions passed to widget or fallback.
For example, this is unsafe when profile is null:
return Conditional.single(
condition: profile != null,
widget: Text(profile!.displayName), // Evaluated before selection.
fallback: const Text('No profile'),
);
Moving the access into widgetBuilder defers it until that branch wins:
final Profile? currentProfile = profile;
return Conditional.singleBuilder(
context,
condition: currentProfile != null,
widgetBuilder: (BuildContext context) {
return Text(currentProfile!.displayName);
},
fallbackBuilder: (BuildContext context) {
return const Text('No profile');
},
);
The builder guarantee applies to fallback builders and every multi-branch widget builder as well.
Boolean branches #
multiCase returns the widget from the first active case whose condition is
true:
return Conditional.multiCase(
cases: <Case>[
const Case(
condition: false,
widget: Text('Skipped'),
),
Case(
condition: hasWarning,
widget: const Text('Warning'),
),
const Case(
condition: true,
widget: Text('Default active case'),
),
],
fallback: const Text('No active case matched'),
);
Set isActive: false to remove a candidate from selection without rebuilding
the collection. Direct Case widgets are still eager Dart arguments.
Use BuilderCase for lazy widgets and BuilderCase.lazy to defer the condition
itself:
return Conditional.multiCaseBuilder(
context,
cases: <BuilderCase>[
BuilderCase.lazy(
isActive: featureEnabled,
conditionBuilder: (BuildContext context) {
return MediaQuery.sizeOf(context).width >= 600;
},
widgetBuilder: (BuildContext context) {
return const Text('Wide feature layout');
},
),
BuilderCase(
condition: true,
widgetBuilder: (BuildContext context) {
return const Text('Compact layout');
},
),
],
);
An inactive BuilderCase.lazy does not evaluate conditionBuilder. Conditions
are checked in order and selection stops after the first match.
For a single builder branch, pass conditionBuilder when the condition itself
must be resolved lazily. It takes precedence over the fixed condition value.
Value matching #
multiMatch<T> compares a nullable target against each active candidate using
== and returns the first match:
return Conditional.multiMatch<String?>(
value: selectedStatus,
values: const <Value<String?>>[
Value<String?>(value: 'ready', widget: Text('Ready')),
Value<String?>(value: 'waiting', widget: Text('Waiting')),
Value<String?>(value: null, widget: Text('No status')),
],
fallback: const Text('Unknown status'),
);
Unlike 2.x, null is a valid target and can match a null candidate.
Use BuilderValue<T> to defer the widget and BuilderValue<T>.lazy to defer a
candidate value. multiMatchBuilder can also resolve its target lazily:
return Conditional.multiMatchBuilder<Brightness>(
context,
valueBuilder: (BuildContext context) => Theme.of(context).brightness,
values: <BuilderValue<Brightness>>[
BuilderValue<Brightness>(
value: Brightness.dark,
widgetBuilder: (BuildContext context) {
return const Text('Dark theme');
},
),
BuilderValue<Brightness>.lazy(
valueBuilder: (BuildContext context) => Brightness.light,
widgetBuilder: (BuildContext context) {
return const Text('Light theme');
},
),
],
);
The target resolver runs at most once and only when an active candidate exists. Candidate resolvers run in order until the first match. Inactive and later candidates are never resolved.
Custom matching #
Supply a ValueMatcher<T> when == is not the desired equality rule. Its
arguments are the target value followed by the candidate value:
return Conditional.multiMatch<String>(
value: searchTerm,
values: const <Value<String>>[
Value<String>(value: 'flutter', widget: Text('Matched Flutter')),
],
matcher: (String? value, String? candidate) {
return value?.trim().toLowerCase() == candidate?.trim().toLowerCase();
},
fallback: const Text('No match'),
);
Custom matchers are supported by direct, optional, builder, and optional builder multi-match methods.
Fallback, inactive, and nullable-result rules #
Selection follows the same rules across every method:
| Situation | Behavior |
|---|---|
| More than one active candidate matches | The first match wins |
| A candidate is inactive | It is skipped before lazy resolvers or builders run |
A selected widget or builder result is null |
Selection stops; fallback and later candidates are skipped |
| No active candidate matches | The supplied fallback is selected |
| No match and no fallback in a non-optional method | Returns SizedBox.shrink() |
| No match and no fallback in an optional method | Returns null |
Target and candidate are both null |
They match under default equality |
The matched-null rule matters when using optional methods. A first matching
branch that deliberately returns null is still a successful match; selection
does not continue in search of a non-null widget.
Every no-match path now uses the supplied fallback, including empty iterables, all-inactive candidates, and a nullable target with no matching null candidate.
Complete API index #
| Eager widgets | Lazy widget builders | Result |
|---|---|---|
Conditional.single |
Conditional.singleBuilder |
Non-null Widget |
Conditional.optionalSingle |
Conditional.optionalSingleBuilder |
Widget? |
Conditional.multiCase |
Conditional.multiCaseBuilder |
Non-null Widget |
Conditional.optionalMultiCase |
Conditional.optionalMultiCaseBuilder |
Widget? |
Conditional.multiMatch<T> |
Conditional.multiMatchBuilder<T> |
Non-null Widget |
Conditional.optionalMultiMatch<T> |
Conditional.optionalMultiMatchBuilder<T> |
Widget? |
Supporting values:
Casestores a required fixed condition, a required nullable widget result, and optional activity.BuilderCasestores a fixed condition and lazy widget builder;BuilderCase.lazyalso defers the condition.Value<T>stores a required nullable candidate value, a required nullable widget result, and optional activity.BuilderValue<T>stores a fixed candidate and lazy widget builder;BuilderValue<T>.lazyalso defers the candidate value.ConditionBuilder,OptionalValueBuilder<T>,OptionalWidgetBuilder, andValueMatcher<T>describe the supported resolver functions.
The example guide maps these APIs to the runnable controls in example/lib/main.dart.
Version 3 migration #
Version 3 deliberately corrects observable 2.x behavior instead of preserving the old eager builder conversion and fallback edge cases. Read MIGRATION.md for required-argument changes, before-and-after code, null matching, custom matching, and exact evaluation-order guarantees.
Contributing #
See CONTRIBUTING.md for package development, tests, 100% coverage, documentation, examples, and pull-request requirements.