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.
import 'package:flutter/material.dart';
import 'package:flutter_conditional/flutter_conditional.dart';
/// Runs the interactive conditional-selection example.
void main() {
runApp(const ConditionalExampleApp());
}
/// Root application for the `flutter_conditional` example.
final class ConditionalExampleApp extends StatelessWidget {
/// Creates the example application.
const ConditionalExampleApp({super.key});
/// Builds the Material application containing the example page.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const ConditionalExamplePage(),
);
}
}
/// Interactive demonstrations of eager and lazy conditional selection.
final class ConditionalExamplePage extends StatefulWidget {
/// Creates the example page.
const ConditionalExamplePage({super.key});
/// Creates the mutable state used by the example controls.
@override
State<ConditionalExamplePage> createState() {
return _ConditionalExamplePageState();
}
}
/// Mutable selection inputs for [ConditionalExamplePage].
final class _ConditionalExamplePageState extends State<ConditionalExamplePage> {
/// Whether the lazy single branch may read its protected value.
bool _showProtectedValue = false;
/// Whether the active lazy boolean case matches.
bool _enableLazyCase = true;
/// Nullable target used to demonstrate that `null` participates in matching.
_DemoStatus? _status;
/// Human-readable label for the current nullable [_status].
String get _statusLabel => _status?.name ?? 'null';
/// Builds every direct and builder-based selection demonstration.
@override
Widget build(BuildContext context) {
final String? protectedValue = _showProtectedValue
? 'The selected builder read this value safely.'
: null;
// Direct widget arguments are created before Conditional selects a result,
// so this example deliberately passes only safe constant widgets.
final Widget eagerSingle = Conditional.single(
condition: _showProtectedValue,
widget: const _ResultCard(
label: 'Direct single selected its eager widget.',
color: Colors.green,
),
fallback: const _ResultCard(
label: 'Direct single selected its eager fallback.',
color: Colors.blueGrey,
),
);
final Widget? optionalSingle = Conditional.optionalSingle(
condition: _showProtectedValue,
widget: const _ResultCard(
label: 'optionalSingle returned a widget.',
color: Colors.green,
),
);
// The inactive first case is ignored. When both later cases match, the
// first active match wins.
final Widget eagerFirstMatch = Conditional.multiCase(
cases: <Case>[
const Case(
condition: true,
isActive: false,
widget: _ResultCard(
label: 'An inactive case cannot be selected.',
color: Colors.red,
),
),
Case(
condition: _enableLazyCase,
widget: const _ResultCard(
label: 'The first active matching Case won.',
color: Colors.orange,
),
),
const Case(
condition: true,
widget: _ResultCard(
label: 'The later default Case won.',
color: Colors.blue,
),
),
],
fallback: const _ResultCard(
label: 'No direct Case matched.',
color: Colors.blueGrey,
),
);
// An empty candidate iterable is a no-match path and therefore uses the
// explicitly supplied fallback in version 3.
final Widget? fixedFallback = Conditional.optionalMultiCase(
cases: const <Case>[],
fallback: const _ResultCard(
label: 'An empty case list used its fixed fallback.',
color: Colors.purple,
),
);
final Widget nullableMatch = Conditional.multiMatch<_DemoStatus>(
value: _status,
values: const <Value<_DemoStatus>>[
Value<_DemoStatus>(
value: null,
widget: _ResultCard(
label: 'A null target matched a null Value.',
color: Colors.teal,
),
),
Value<_DemoStatus>(
value: _DemoStatus.ready,
widget: _ResultCard(label: 'Status: ready', color: Colors.green),
),
Value<_DemoStatus>(
value: _DemoStatus.waiting,
widget: _ResultCard(label: 'Status: waiting', color: Colors.orange),
),
],
fallback: const _ResultCard(
label: 'The status had no matching Value.',
color: Colors.blueGrey,
),
);
// A selected null widget is still a successful match, so this call does
// not continue to its later candidate or fallback.
final Widget? directMatchedNull = Conditional.optionalMultiMatch<String>(
value: 'selected',
values: const <Value<String>>[
Value<String>(value: 'selected', widget: null),
Value<String>(
value: 'selected',
widget: _ResultCard(
label: 'A later direct value must not win.',
color: Colors.red,
),
),
],
fallback: const _ResultCard(
label: 'A matched null must not use the fallback.',
color: Colors.red,
),
);
// The protected null assertion is safe because its builder is invoked only
// when the matching condition proves the value is available.
final Widget lazySingle = Conditional.singleBuilder(
context,
condition: protectedValue != null,
widgetBuilder: (BuildContext context) {
return _ResultCard(label: protectedValue!, color: Colors.green);
},
fallbackBuilder: (BuildContext context) {
return const _ResultCard(
label: 'The unsafe widget builder was not evaluated.',
color: Colors.indigo,
);
},
);
// conditionBuilder overrides the fixed false condition. The unselected
// builder remains untouched when the resolver returns false.
final Widget? optionalLazySingle = Conditional.optionalSingleBuilder(
context,
condition: false,
conditionBuilder: (BuildContext context) => _showProtectedValue,
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'A lazy condition selected optionalSingleBuilder.',
color: Colors.green,
);
},
);
final Widget lazyCase = Conditional.multiCaseBuilder(
context,
cases: <BuilderCase>[
BuilderCase.lazy(
isActive: false,
conditionBuilder: (BuildContext context) {
throw StateError('An inactive condition must not be resolved.');
},
widgetBuilder: (BuildContext context) {
throw StateError('An inactive widget must not be built.');
},
),
BuilderCase.lazy(
conditionBuilder: (BuildContext context) => _enableLazyCase,
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'A lazy BuilderCase matched.',
color: Colors.deepOrange,
);
},
),
],
fallbackBuilder: (BuildContext context) {
return const _ResultCard(
label: 'No lazy BuilderCase matched; fallback ran.',
color: Colors.blueGrey,
);
},
);
// Returning null from the first matching builder short-circuits selection.
// The throwing later builder and fallback prove they remain unevaluated.
final Widget? builderMatchedNull = Conditional.optionalMultiCaseBuilder(
context,
cases: <BuilderCase>[
BuilderCase(
condition: true,
widgetBuilder: (BuildContext context) => null,
),
BuilderCase(
condition: true,
widgetBuilder: (BuildContext context) {
throw StateError('A later matching builder must not run.');
},
),
],
fallbackBuilder: (BuildContext context) {
throw StateError('A matched null must not run the fallback.');
},
);
final Widget lazyValue = Conditional.multiMatchBuilder<_DemoStatus>(
context,
valueBuilder: (BuildContext context) => _status,
values: <BuilderValue<_DemoStatus>>[
BuilderValue<_DemoStatus>.lazy(
isActive: false,
valueBuilder: (BuildContext context) {
throw StateError('An inactive value must not be resolved.');
},
widgetBuilder: (BuildContext context) {
throw StateError('An inactive value widget must not be built.');
},
),
BuilderValue<_DemoStatus>.lazy(
valueBuilder: (BuildContext context) => null,
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'Lazy target and candidate values matched null.',
color: Colors.teal,
);
},
),
BuilderValue<_DemoStatus>(
value: _DemoStatus.ready,
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'A fixed BuilderValue matched ready.',
color: Colors.green,
);
},
),
BuilderValue<_DemoStatus>.lazy(
valueBuilder: (BuildContext context) => _DemoStatus.waiting,
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'A lazy BuilderValue matched waiting.',
color: Colors.orange,
);
},
),
],
fallbackBuilder: (BuildContext context) {
return const _ResultCard(
label: 'No lazy BuilderValue matched.',
color: Colors.blueGrey,
);
},
);
final Widget? customBuilderMatch =
Conditional.optionalMultiMatchBuilder<String>(
context,
value: ' FLUTTER ',
values: <BuilderValue<String>>[
BuilderValue<String>(
value: 'flutter',
widgetBuilder: (BuildContext context) {
return const _ResultCard(
label: 'ValueMatcher ignored case and surrounding spaces.',
color: Colors.pink,
);
},
),
],
matcher: _normalizedStringMatcher,
);
return Scaffold(
appBar: AppBar(title: const Text('flutter_conditional 3.0')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
Text(
'Change the inputs to observe first-match, fallback, null, and '
'lazy-evaluation behavior.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 12),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Protected value is available'),
subtitle: const Text(
'Controls eager and lazy single-condition examples.',
),
value: _showProtectedValue,
onChanged: _setProtectedValueVisibility,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Lazy case matches'),
subtitle: const Text('Controls direct and lazy multi-case output.'),
value: _enableLazyCase,
onChanged: _setLazyCaseEnabled,
),
Row(
children: <Widget>[
Expanded(child: Text('Match target: $_statusLabel')),
FilledButton.tonal(
onPressed: _cycleStatus,
child: const Text('Cycle target'),
),
],
),
const SizedBox(height: 20),
_ExampleSection(
title: 'Direct widget APIs',
description:
'Use these with widgets that are already safe to create. '
'Every argument expression is eager.',
children: <Widget>[
eagerSingle,
_OptionalResult(method: 'optionalSingle', result: optionalSingle),
eagerFirstMatch,
_OptionalResult(
method: 'optionalMultiCase fallback',
result: fixedFallback,
),
nullableMatch,
_OptionalResult(
method: 'optionalMultiMatch matched null',
result: directMatchedNull,
),
],
),
const SizedBox(height: 20),
_ExampleSection(
title: 'Lazy builder APIs',
description:
'Only a selected widget builder or the required fallback '
'executes. Inactive resolvers remain untouched.',
children: <Widget>[
lazySingle,
_OptionalResult(
method: 'optionalSingleBuilder',
result: optionalLazySingle,
),
lazyCase,
_OptionalResult(
method: 'optionalMultiCaseBuilder matched null',
result: builderMatchedNull,
),
lazyValue,
_OptionalResult(
method: 'optionalMultiMatchBuilder custom matcher',
result: customBuilderMatch,
),
],
),
],
),
);
}
/// Updates whether the protected lazy value is available.
void _setProtectedValueVisibility(bool value) {
setState(() {
_showProtectedValue = value;
});
}
/// Updates whether the lazy boolean case matches.
void _setLazyCaseEnabled(bool value) {
setState(() {
_enableLazyCase = value;
});
}
/// Advances the nullable match target through every demonstrated state.
void _cycleStatus() {
setState(() {
_status = switch (_status) {
null => _DemoStatus.ready,
_DemoStatus.ready => _DemoStatus.waiting,
_DemoStatus.waiting => _DemoStatus.unknown,
_DemoStatus.unknown => null,
};
});
}
/// Compares nullable strings after trimming and lowercasing both operands.
static bool _normalizedStringMatcher(String? value, String? candidate) {
return value?.trim().toLowerCase() == candidate?.trim().toLowerCase();
}
}
/// Match targets cycled by the interactive value examples.
enum _DemoStatus {
/// A completed or available state.
ready,
/// A pending state.
waiting,
/// A non-matching state used to select the fallback.
unknown,
}
/// Visually groups related conditional results.
final class _ExampleSection extends StatelessWidget {
/// Creates a labeled group of [children].
const _ExampleSection({
required this.title,
required this.description,
required this.children,
});
/// Section heading.
final String title;
/// Short explanation of the evaluation mode.
final String description;
/// Conditional results displayed in evaluation order.
final List<Widget> children;
/// Builds the section heading, explanation, and result cards.
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 4),
Text(description),
const SizedBox(height: 8),
for (final Widget child in children) ...<Widget>[
child,
const SizedBox(height: 8),
],
],
);
}
}
/// Displays whether an optional API returned a widget or `null`.
final class _OptionalResult extends StatelessWidget {
/// Creates an optional-result display for [method].
const _OptionalResult({required this.method, required this.result});
/// Method or behavior represented by this result.
final String method;
/// Nullable widget returned by the demonstrated optional API.
final Widget? result;
/// Builds [result] or a card identifying an intentional null result.
@override
Widget build(BuildContext context) {
return result ??
_ResultCard(label: '$method returned null.', color: Colors.grey);
}
}
/// Colored result used to make the selected branch visible.
final class _ResultCard extends StatelessWidget {
/// Creates a result card.
const _ResultCard({required this.label, required this.color});
/// Text describing the branch that produced this card.
final String label;
/// Background color associated with the selected behavior.
final Color color;
/// Builds the colored result surface.
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
);
}
}