flutter_conditional 3.0.0 copy "flutter_conditional: ^3.0.0" to clipboard
flutter_conditional: ^3.0.0 copied to clipboard

A Flutter package for readable conditional widget rendering with value matching, branching, and lazy builders.

example/lib/main.dart

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,
          ),
        ),
      ),
    );
  }
}
5
likes
160
points
107
downloads

Documentation

API reference

Publisher

verified publishercoderave.dev

Weekly Downloads

A Flutter package for readable conditional widget rendering with value matching, branching, and lazy builders.

Repository (GitHub)
View/report issues
Contributing

Topics

#flutter #widget #conditional-rendering #builder #ui

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on flutter_conditional