fl_observable 1.0.1 copy "fl_observable: ^1.0.1" to clipboard
fl_observable: ^1.0.1 copied to clipboard

Flutter components for dart_observable package

example/lib/main.dart

import 'dart:collection';
import 'dart:math';

import 'package:fl_observable/fl_observable.dart';
import 'package:fl_observable_example/components/info_row.dart';
import 'package:fl_observable_example/components/list_item.dart';
import 'package:fl_observable_example/snackbars.dart';
import 'package:fl_observable_example/styles/theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import 'components/number_with_controls.dart';
import 'components/segmented_item.dart';

void main() {
  // Automatically tracks any update on the observables,
  // to visualize the event you can use the [WidgetObservableLogger] widget
  ObservableGlobalLogger.enableGlobalLogger();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(final BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Observable example',
      theme: AppTheme().of(context),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

/// Contains a home screen to switch between the pages
class _MyHomePageState extends State<MyHomePage> {
  late final Rx<WidgetObservableLoggerViewType> _rxViewType = Rx<WidgetObservableLoggerViewType>(
    WidgetObservableLoggerViewType.pinnedBottom,
  );

  @override
  Widget build(final BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Observable example'),
          actions: <Widget>[
            IconButton(
              icon: const Icon(Icons.settings),
              onPressed: () {
                _showLoggerSettingsDialog(context);
              },
            ),
          ],
          bottom: const TabBar(
            tabs: <Widget>[
              Tab(text: 'Simple'),
              Tab(text: 'Collections'),
            ],
          ),
        ),
        body: _rxViewType.build(
          builder: (
            final BuildContext context,
            final WidgetObservableLoggerViewType viewType,
            final _,
          ) {
            return WidgetObservableLogger(
              viewType: viewType,
              child: const TabBarView(
                children: <Widget>[
                  _PageSimple(),
                  _PageCollections(),
                ],
              ),
            );
          },
        ),
      ),
    );
  }

  void _showLoggerSettingsDialog(final BuildContext context) {
    showDialog<void>(
      context: context,
      builder: (final BuildContext context) {
        return AlertDialog(
          backgroundColor: Colors.white,
          title: const Center(
            child: Text('Logger settings'),
          ),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              const Text('You can change the logger position and behavior'),
              const SizedBox(height: 15),
              _buildLabeledText(
                label: 'Draggable',
                message: 'You can drag the logger around the screen within the widget bounds',
              ),
              const SizedBox(height: 8),
              _buildLabeledText(
                label: 'Pinned bottom',
                message:
                    'The logger will be pinned to the bottom of the screen, the content is above the logger. The logger is NOT draggable',
              ),
              const SizedBox(height: 8),
              _buildLabeledText(
                label: 'Floating bottom',
                message:
                    'The logger will be pinned to the bottom of the screen, the content is below the logger. The logger is draggable',
              ),
              const SizedBox(height: 15),
              Center(
                child: _rxViewType.build(
                  builder: (
                    final BuildContext context,
                    final WidgetObservableLoggerViewType viewType,
                    final _,
                  ) {
                    return SegmentedButton<WidgetObservableLoggerViewType>(
                      segments: const <ButtonSegment<WidgetObservableLoggerViewType>>[
                        ButtonSegment<WidgetObservableLoggerViewType>(
                          value: WidgetObservableLoggerViewType.draggable,
                          label: WidgetSegmentedItem(text: 'Drag'),
                        ),
                        ButtonSegment<WidgetObservableLoggerViewType>(
                          value: WidgetObservableLoggerViewType.pinnedBottom,
                          label: WidgetSegmentedItem(text: 'Pinned'),
                        ),
                        ButtonSegment<WidgetObservableLoggerViewType>(
                          value: WidgetObservableLoggerViewType.floatingBottom,
                          label: WidgetSegmentedItem(text: 'Floating'),
                        ),
                      ],
                      selected: <WidgetObservableLoggerViewType>{_rxViewType.value},
                      onSelectionChanged: (final Set<WidgetObservableLoggerViewType> value) {
                        if (value.isNotEmpty) {
                          _rxViewType.value = value.first;
                        }
                      },
                      showSelectedIcon: false,
                    );
                  },
                ),
              ),
            ],
          ),
          actions: <Widget>[
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  Widget _buildLabeledText({
    required final String label,
    required final String message,
  }) {
    return RichText(
      text: TextSpan(
        style: context.textStyles.info,
        children: <InlineSpan>[
          TextSpan(
            text: '$label: ',
            style: context.textStyles.infoHighlighted,
          ),
          TextSpan(text: message),
        ],
      ),
    );
  }
}

class _PageSimple extends StatefulWidget {
  const _PageSimple();

  @override
  State<_PageSimple> createState() => _PageSimpleState();
}

class _PageSimpleState extends State<_PageSimple> {
  // Create a separate controller to manage the UI logic
  // You can use any architecture you prefer
  late final _PageSimpleController _controller = _PageSimpleController();
  late final ScrollController _scrollController = ScrollController();

  @override
  Widget build(final BuildContext context) {
    return SingleChildScrollView(
      key: const PageStorageKey<String>('simple'),
      controller: _scrollController,
      padding: const EdgeInsets.symmetric(horizontal: 20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          // Listen to the counter changes and show a snackbar every 5th change
          ObservableListener<int>(
            observable: _controller.rxCounter,
            shouldNotify: (final int counter) => counter % 5 == 0,
            onChanged: (final BuildContext context, final int counter) {
              context.showSnackbar('Counter: $counter');
            },
          ),

          const SizedBox(height: 12),
          _buildCounter(),
          Text('Operators', style: context.textStyles.title),
          const SizedBox(height: 12),
          _buildOddValue(),
          const Divider(),
          _buildMappedValue(),
          const Divider(),
          _buildMappedFilteredValue(),
          const Divider(),
          _buildCombinedValue(),
          const Divider(),
          _buildStreamValue(),
          const Divider(),
          Text('SwitchMap', style: context.textStyles.subtitle),
          const SizedBox(height: 10),
          Text(
            'You can switch and listen to a different observable based on a condition',
            style: context.textStyles.hint,
          ),
          const SizedBox(height: 20),
          _buildSwitchMapEven(),
          _buildSwitchMapOdd(),
          const SizedBox(height: 10),
          _buildSwitchMapSelector(),
          const SizedBox(height: 100),
        ],
      ),
    );
  }

  Widget _buildCounter() {
    return WidgetNumberWithControls(
      onIncrement: _controller.onCounterIncreasePressed,
      onReduce: _controller.onCounterReducePressed,
      rxNumber: _controller.rxCounter,
      title: 'Counter',
    );
  }

  Widget _buildMappedValue() {
    return _controller.rxCounterMapped.build(
      builder: (final BuildContext context, final int value, final _) {
        return WidgetInfoRow(
          info: 'Counter * 3',
          title: 'Map',
          value: '$value',
        );
      },
    );
  }

  Widget _buildOddValue() {
    return _controller.rxFilterOddNumbers.build(
      builder: (final BuildContext context, final int? value, final _) {
        return WidgetInfoRow(
          title: 'Filter',
          info: 'Last Odd number',
          value: value?.toString() ?? 'N/A',
        );
      },
    );
  }

  Widget _buildMappedFilteredValue() {
    return _controller.rxMappedFiltered.build(
      builder: (final BuildContext context, final String? value, final _) {
        return WidgetInfoRow(
          title: 'Filter + Map',
          info: 'Filter the mapped value that contains 3 or 6, and transforms it to text',
          value: value ?? 'N/A',
        );
      },
    );
  }

  Widget _buildStreamValue() {
    return _controller.fromStream.build(
      builder: (final BuildContext context, final int value, final _) {
        return WidgetInfoRow(
          title: 'From stream',
          info: 'The observable is updated from the stream. The stream is updated every second',
          value: value.toString(),
        );
      },
    );
  }

  Widget _buildSwitchMapSelector() {
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text('Switch to', style: context.textStyles.subtitle),
          _controller.rxSwitchMapSource.build(
            builder: (final BuildContext context, final bool useOdd, final _) {
              return SegmentedButton<bool>(
                segments: const <ButtonSegment<bool>>[
                  ButtonSegment<bool>(
                    value: false,
                    label: SizedBox(
                      width: 50,
                      child: Center(child: Text('Even')),
                    ),
                  ),
                  ButtonSegment<bool>(
                    value: true,
                    label: SizedBox(
                      width: 50,
                      child: Center(child: Text('Odd')),
                    ),
                  ),
                ],
                selected: <bool>{useOdd},
                onSelectionChanged: (final Set<bool> value) {
                  final bool useOdd = value.first;
                  _controller.onSwitchMapSelectorChanged(useOdd);
                },
                showSelectedIcon: false,
              );
            },
          ),
          const SizedBox(width: 10),
          _controller.rxSwitchMap.build(
            builder: (final BuildContext context, final int value, final _) {
              return Text(
                'Value:  $value',
                style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
              );
            },
          ),
        ],
      ),
    );
  }

  Widget _buildSwitchMapEven() {
    return WidgetNumberWithControls(
      onIncrement: _controller.onEvenIncreasePressed,
      onReduce: _controller.onEvenReducePressed,
      rxNumber: _controller.rxEven,
      title: 'Even',
    );
  }

  Widget _buildSwitchMapOdd() {
    return WidgetNumberWithControls(
      onIncrement: _controller.onOddIncreasePressed,
      onReduce: _controller.onOddReducePressed,
      rxNumber: _controller.rxOdd,
      title: 'Odd',
    );
  }

  Widget _buildCombinedValue() {
    return _controller.rxCombined.build(
      builder: (final BuildContext context, final String value, final _) {
        return WidgetInfoRow(
          title: 'CombineWith',
          value: value,
          info: '''The combined value of the odd and mapped filtered value
The observable only notifies when the value is updated and not on every change in the source observables
update count: ${_controller.rxCombined.updateCount}''',
        );
      },
    );
  }
}

class _PageSimpleController {
  late final RxInt _rxCounter = RxInt(0);
  late final RxBool _rxSwitchMapSource = RxBool(true);
  late final RxInt _rxEven = RxInt(0);
  late final RxInt _rxOdd = RxInt(1);

  // You can use getters to expose the immutable states
  Observable<int> get rxCounter => _rxCounter;

  Observable<int> get rxEven => _rxEven;

  Observable<int> get rxOdd => _rxOdd;

  Observable<bool> get rxSwitchMapSource => _rxSwitchMapSource;

  // For computes states don't use getter otherwise a new instance will be created each time
  late final Observable<int> rxCounterMapped = _getMapObservable();
  late final Observable<int?> rxFilterOddNumbers = _getOddFilterObservable();
  late final Observable<String?> rxMappedFiltered = _getMappedFilterObservable();
  late final Observable<String> rxCombined = _getFilteredCombinedObservable();
  late final Observable<int> fromStream = _getStreamObservable();
  late final Observable<int> rxSwitchMap = _getSwitchMapObservable();

  Observable<int> _getStreamObservable() {
    return Observable<int>.fromStream(
      stream: Stream<int>.periodic(
        const Duration(seconds: 1),
        (final int count) => count,
      ),
      initial: 0,
    );
  }

  Observable<int?> _getOddFilterObservable() {
    return _rxCounter.filter(
      (final int value) => value % 2 == 1,
    );
  }

  Observable<int> _getMapObservable() {
    return _rxCounter.map(
      (final int value) => value * 3,
    );
  }

  Observable<String?> _getMappedFilterObservable() {
    return rxCounterMapped.filter((final int value) {
      final String valueString = value.toString();
      return valueString.contains('3') || valueString.contains('6');
    }).map(
      (final int? value) {
        if (value == null) {
          return 'N/A';
        }
        return 'U:$value';
      },
    );
  }

  void onCounterIncreasePressed() {
    _rxCounter.value++;
  }

  void onCounterReducePressed() {
    _rxCounter.value--;
  }

  void onSwitchMapSelectorChanged(final bool useOddSource) {
    _rxSwitchMapSource.value = useOddSource;
  }

  Observable<int> _getSwitchMapObservable() {
    return _rxSwitchMapSource.switchMap(
      (final bool useOdd) {
        return useOdd ? _rxOdd : _rxEven;
      },
    );
  }

  void onEvenIncreasePressed() {
    _rxEven.value += 2;
  }

  void onEvenReducePressed() {
    _rxEven.value -= 2;
  }

  void onOddIncreasePressed() {
    _rxOdd.value += 2;
  }

  void onOddReducePressed() {
    _rxOdd.value -= 2;
  }

  Observable<String> _getFilteredCombinedObservable() {
    return rxFilterOddNumbers.combineWith(
      other: rxMappedFiltered,
      combiner: (final int? odd, final String? mapped) {
        return 'Odd: ${odd ?? 'N/A'}, Mapped: ${mapped ?? 'N/A'}';
      },
    );
  }
}

class _PageCollections extends StatefulWidget {
  const _PageCollections();

  @override
  State<_PageCollections> createState() => _PageCollectionsState();
}

class _PageCollectionsState extends State<_PageCollections> {
  final PageCollectionsController _controller = PageCollectionsController();

  @override
  Widget build(final BuildContext context) {
    return Scaffold(
      floatingActionButton: _buildFloatingActionButtons(),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children: <Widget>[
          Center(child: _buildTypeSelector()),
          Expanded(
            child: _buildViewForType(),
          ),
        ],
      ),
    );
  }

  Widget _buildFloatingActionButtons() {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        _controller.rxSourceList.build(
          builder: (final BuildContext context, final List<String> sourceList, final _) {
            final int length = sourceList.length;
            final bool show = length >= 50000;

            return AnimatedOpacity(
              opacity: show ? 1 : 0,
              duration: const Duration(milliseconds: 200),
              child: FloatingActionButton(
                onPressed: () {
                  if (show) {
                    _controller.clearAll();
                  }
                },
                child: const Icon(
                  Icons.delete,
                  color: Colors.red,
                ),
              ),
            );
          },
        ),
        const SizedBox(width: 10),
        FloatingActionButton(
          onPressed: () {
            _controller.onRemovePressed();
          },
          child: const Icon(Icons.remove),
        ),
        const SizedBox(width: 10),
        FloatingActionButton(
          onPressed: () {
            _controller.onAddNamePressed();
          },
          child: const Icon(Icons.add),
        ),
      ],
    );
  }

  Widget _buildTypeSelector() {
    return Center(
      child: _controller.rxViewType.build(
        builder: (final BuildContext context, final CollectionViewType buildType, final _) {
          return CupertinoSegmentedControl<CollectionViewType>(
            groupValue: buildType,
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            children: <CollectionViewType, Widget>{
              CollectionViewType.list: CollectionViewType.list.buildSelectorItem(),
              CollectionViewType.set: CollectionViewType.set.buildSelectorItem(),
              CollectionViewType.map: CollectionViewType.map.buildSelectorItem(),
            },
            onValueChanged: (final CollectionViewType? value) {
              if (value != null) {
                _controller.updateViewType(value);
              }
            },
          );
        },
      ),
    );
  }

  Widget _buildViewForType() {
    return _controller.rxViewType.build(
      builder: (final BuildContext context, final CollectionViewType viewType, final _) {
        switch (viewType) {
          case CollectionViewType.list:
            return _buildListContent();
          case CollectionViewType.set:
            return _buildSetView();
          case CollectionViewType.map:
            return _buildMapView();
        }
      },
    );
  }

  Widget _buildListSource() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Text(
            '''This is the source list, the set and the map are computed from this list
- button will remove the first ${PageCollectionsController.addCount} items
+ button will add ${PageCollectionsController.addCount} random text
The list performance will not degrade with a large number of items''',
            style: context.textStyles.hint,
          ),
        ),
        const SizedBox(height: 10),
        _controller.rxSourceListLength.build(
          builder: (final BuildContext context, final String length, final _) {
            return _buildLengthView(length);
          },
        ),
        Expanded(
          child: _controller.rxSourceList.build(
            builder: (
              final BuildContext context,
              final List<String> items,
              final _,
            ) {
              if (items.isEmpty) {
                return const Center(child: Text('No items'));
              }

              return ListView.separated(
                padding: const EdgeInsets.only(bottom: 100),
                itemCount: items.length,
                separatorBuilder: (final BuildContext context, final int index) {
                  return const Divider(
                    height: 0,
                    indent: 16,
                  );
                },
                itemBuilder: (final BuildContext context, final int index) {
                  final String item = items[index];
                  return WidgetListItem(
                    title: '$index.: $item',
                    item: item,
                    onDeletePressed: () {
                      _controller.removeItemAt(index);
                    },
                    controller: _controller,
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildListContent() {
    return _controller.rxListType.build(
      builder: (final BuildContext context, final ListType listType, final _) {
        final Widget content;
        switch (listType) {
          case ListType.source:
            content = _buildListSource();
            break;
          case ListType.filter:
            content = _buildListFiltered();
            break;
          case ListType.mapped:
            content = _buildListMapped();
            break;
        }
        return Column(
          children: <Widget>[
            SegmentedButton<ListType>(
              segments: const <ButtonSegment<ListType>>[
                ButtonSegment<ListType>(
                  value: ListType.source,
                  label: WidgetSegmentedItem(
                    text: 'Source',
                    width: 80,
                  ),
                ),
                ButtonSegment<ListType>(
                  value: ListType.filter,
                  label: WidgetSegmentedItem(
                    text: 'Filtered',
                    width: 80,
                  ),
                ),
                ButtonSegment<ListType>(
                  value: ListType.mapped,
                  label: WidgetSegmentedItem(
                    text: 'Mapped',
                    width: 80,
                  ),
                ),
              ],
              selected: <ListType>{listType},
              onSelectionChanged: (final Set<ListType> value) {
                if (value.isNotEmpty) {
                  _controller.updateListType(value.first);
                }
              },
              showSelectedIcon: false,
            ),
            Expanded(child: content),
          ],
        );
      },
    );
  }

  Widget _buildSetView() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Text(
            '''The set is updated automatically when the source list changes
When the source list is updated, only the change is re-sorted and not the entire set''',
            style: context.textStyles.hint,
          ),
        ),
        const SizedBox(height: 10),
        _controller.rxSetLength.build(
          builder: (final BuildContext context, final String length, final _) {
            return _buildLengthView(length);
          },
        ),
        Expanded(
          child: _controller.rxSortedSet.build(
            builder: (final BuildContext context, final Set<String> value, final _) {
              final List<String> items = value.toList();
              if (items.isEmpty) {
                return const Center(child: Text('No items'));
              }
              return ListView.separated(
                padding: const EdgeInsets.only(bottom: 100),
                itemCount: items.length,
                separatorBuilder: (final BuildContext context, final int index) {
                  return const Divider(
                    height: 0,
                    indent: 16,
                  );
                },
                itemBuilder: (final BuildContext context, final int index) {
                  final String item = items[index];
                  return WidgetListItem(
                    title: item,
                    item: item,
                    onDeletePressed: () {
                      _controller.removeItem(item);
                    },
                    controller: _controller,
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildMapView() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Text(
            '''Shows the count of items grouped by their length
The map is updated automatically when the source list changes
When the source list is updated, the numbers are re-calculated based on the change and not the entire list''',
            style: context.textStyles.hint,
          ),
        ),
        Expanded(
          child: _controller.rxItemCountByLength.build(
            builder: (
              final BuildContext context,
              final Map<int, int> value,
              final _,
            ) {
              final List<MapEntry<int, int>> items = value.entries.toList();
              if (items.isEmpty) {
                return const Center(child: Text('No items'));
              }

              return ListView.separated(
                padding: const EdgeInsets.only(bottom: 100),
                itemCount: items.length,
                separatorBuilder: (final BuildContext context, final int index) {
                  return const Divider(
                    height: 0,
                    indent: 16,
                  );
                },
                itemBuilder: (final BuildContext context, final int index) {
                  final MapEntry<int, int> entry = items[index];
                  return ListTile(
                    title: Text(
                      '${entry.key}: ${entry.value}',
                      style: context.textStyles.listItem.copyWith(fontSize: 16),
                    ),
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildListFiltered() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Text(
            '''The source list is filtered to show only items that contain at least 3 numbers
The list sorted alphabetically automatically when the source list changes''',
            style: context.textStyles.hint,
          ),
        ),
        const SizedBox(height: 10),
        _controller.rxFilteredListLength.build(
          builder: (final BuildContext context, final String length, final _) {
            return _buildLengthView(length);
          },
        ),
        Expanded(
          child: _controller.rxFilteredList.build(
            builder: (
              final BuildContext context,
              final List<String> items,
              final _,
            ) {
              if (items.isEmpty) {
                return const Center(child: Text('No items'));
              }

              return ListView.separated(
                padding: const EdgeInsets.only(bottom: 100),
                itemCount: items.length,
                separatorBuilder: (final BuildContext context, final int index) {
                  return const Divider(
                    height: 0,
                    indent: 16,
                  );
                },
                itemBuilder: (final BuildContext context, final int index) {
                  final String item = items[index];
                  return WidgetListItem(
                    title: '$index.: $item',
                    item: item,
                    onDeletePressed: () {
                      _controller.removeItem(item);
                    },
                    controller: _controller,
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildListMapped() {
    return Column(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: Text(
            '''The source list items are mapped based on the length
The key is the length, the value is the number of items with that length''',
            style: context.textStyles.hint,
          ),
        ),
        const SizedBox(height: 10),
        _controller.rxMappedListLength.build(
          builder: (final BuildContext context, final String length, final _) {
            return _buildLengthView(length);
          },
        ),
        Expanded(
          child: _controller.rxMappedList.build(
            builder: (
              final BuildContext context,
              final List<String> items,
              final _,
            ) {
              if (items.isEmpty) {
                return const Center(child: Text('No items'));
              }

              return ListView.separated(
                padding: const EdgeInsets.only(bottom: 100),
                itemCount: items.length,
                separatorBuilder: (final BuildContext context, final int index) {
                  return const Divider(
                    height: 0,
                    indent: 16,
                  );
                },
                itemBuilder: (final BuildContext context, final int index) {
                  final String item = items[index];
                  return WidgetListItem(
                    title: '$index.: $item',
                    item: item,
                    controller: _controller,
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildLengthView(final String length) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        color: AppTheme.secondaryColor.withOpacity(0.5),
      ),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: Center(
        child: RichText(
          text: TextSpan(
            text: 'Length: ',
            style: context.textStyles.info,
            children: <TextSpan>[
              TextSpan(text: length, style: context.textStyles.infoHighlighted),
            ],
          ),
        ),
      ),
    );
  }
}

class PageCollectionsController {
  static const int addCount = 10000;
  static const String characters = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';

  late final RxList<String> _rxSourceList = RxList<String>();
  late final Rx<CollectionViewType> _rxViewType = Rx<CollectionViewType>(CollectionViewType.list);
  late final Rx<ListType> _rxListType = Rx<ListType>(ListType.source);
  late final RxSet<String> _rxEditingItems = RxSet<String>();

  // For computes states don't use getter otherwise a new instance will be created each time
  late final Observable<String> rxSourceListLength = _createSourceLengthObservable();
  late final ObservableList<String> rxFilteredList = _createFilteredList();
  late final Observable<String> rxFilteredListLength = _createFilteredListLengthObservable();
  late final ObservableList<String> rxMappedList = _createMappedList();
  late final Observable<String> rxMappedListLength = _createMappedListLengthObservable();

  late final ObservableSet<String> rxSortedSet = _createSortedSet();
  late final Observable<String> rxSetLength = _createSetLengthObservable();
  late final ObservableMap<int, int> rxItemCountByLength = _createCountGroupedByLengthMap();

  // You can use getters to expose the immutable states
  ObservableList<String> get rxSourceList => _rxSourceList;

  Observable<ListType> get rxListType => _rxListType;

  Observable<CollectionViewType> get rxViewType => _rxViewType;

  ObservableSet<String> get rxEditingItems => _rxEditingItems;

  void clearAll() {
    _rxSourceList.clear();
  }

  String getRandomName() {
    final Random random = Random();
    final int length = random.nextInt(20) + 10;
    return String.fromCharCodes(
      Iterable<int>.generate(
        length,
        (final _) => characters.codeUnitAt(random.nextInt(characters.length)),
      ),
    );
  }

  void onAddNamePressed() {
    final List<String> names = <String>[];
    for (int i = 0; i < addCount; i++) {
      names.add(getRandomName());
    }
    _rxSourceList.addAll(names);
  }

  void removeItemAt(final int index) {
    _rxSourceList.removeAt(index);
  }

  void updateViewType(final CollectionViewType value) {
    _rxViewType.value = value;
  }

  ObservableMap<int, int> _createCountGroupedByLengthMap() {
    return _rxSourceList.transformChangeAs.map(
      transform: _transformItemsByLength,
      factory: (final Map<int, int>? items) {
        return SplayTreeMap<int, int>.of(
          items ?? <int, int>{},
          (final int a, final int b) => a.compareTo(b),
        );
      },
    );
  }

  Observable<String> _createSetLengthObservable() {
    return rxSortedSet.map((final Set<String> items) => _formatNumber(items.length));
  }

  ObservableSet<String> _createSortedSet() {
    return _rxSourceList.transformChangeAs.set(
      transform: _transformSorted,
      factory: (final Iterable<String>? items) {
        return SplayTreeSet<String>.of(
          (items ?? <String>[]),
          (final String a, final String b) => a.compareTo(b),
        );
      },
    );
  }

  Observable<String> _createSourceLengthObservable() {
    return _rxSourceList.map((final List<String> items) => _formatNumber(items.length));
  }

  String _formatNumber(final int value) {
    // Format the number with commas
    return value.toString().replaceAllMapped(
          RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
          (final Match match) => '${match[1]},',
        );
  }

  void _transformItemsByLength(
    final ObservableMap<int, int> current,
    final List<String> state,
    final ObservableListChange<String> change,
    final Emitter<ObservableMapUpdateAction<int, int>> updater,
  ) {
    final Map<int, String> removed = change.removed;
    final Iterable<String> added = change.added.values;
    final Map<int, int> changeByLength = <int, int>{};
    for (final String item in added) {
      final int length = item.length;
      changeByLength[length] = (changeByLength[length] ?? 0) + 1;
    }

    for (final String item in removed.values) {
      final int length = item.length;
      changeByLength[length] = (changeByLength[length] ?? 0) - 1;
    }

    final Map<int, int> addedItems = <int, int>{};
    final Set<int> removedKeys = <int>{};
    final UnmodifiableMapView<int, int> currentMap = current.value;

    for (final MapEntry<int, int> entry in changeByLength.entries) {
      final int key = entry.key;
      final int value = entry.value;
      final int newValue = (currentMap[key] ?? 0) + value;
      if (newValue > 0) {
        addedItems[key] = newValue;
      } else {
        removedKeys.add(key);
      }
    }

    updater(
      ObservableMapUpdateAction<int, int>(
        addItems: <int, int>{
          ...addedItems,
        },
        removeKeys: removedKeys,
      ),
    );
  }

  void _transformSorted(
    final ObservableSet<String> current,
    final _,
    final ObservableListChange<String> change,
    final Emitter<ObservableSetUpdateAction<String>> updater,
  ) {
    final Map<int, String> added = change.added;
    final Map<int, String> removed = change.removed;
    final Map<int, ObservableItemChange<String>> updated = change.updated;

    updater(
      ObservableSetUpdateAction<String>(
        addItems: <String>{
          ...added.values,
          ...updated.values.map((final ObservableItemChange<String> change) {
            return change.newValue;
          }),
        },
        removeItems: <String>{
          ...removed.values,
          ...updated.values.map((final ObservableItemChange<String> change) {
            return change.oldValue;
          }),
        },
      ),
    );
  }

  void removeItem(final String item) {
    _rxSourceList.remove(item);
  }

  void onRemovePressed() {
    final int listLength = _rxSourceList.length;
    if (listLength == 0) {
      return;
    }

    final int removeCount = min(addCount, listLength);
    // Remove first N random items
    _rxSourceList.removeIndexes(
      Iterable<int>.generate(
        removeCount,
        (final int index) => index,
      ),
    );
  }

  void toggleEditingItem(final String item) {
    if (_rxEditingItems.contains(item)) {
      _rxEditingItems.remove(item);
    } else {
      _rxEditingItems.add(item);
    }
  }

  void updateItem({
    required final String original,
    required final String updated,
  }) {
    if (original == updated) {
      return;
    }

    final int index = _rxSourceList.value.indexOf(original);
    if (index != -1) {
      _rxSourceList[index] = updated;
    }
  }

  ObservableList<String> _createFilteredList() {
    // Show items that has at least 3 number in it, and sorted alphabetically
    return _rxSourceList
        .filterItem(
          (final String item) => item.contains(RegExp(r'[0-9]{3}')),
        )
        .sorted(
          (final String a, final String b) => a.compareTo(b),
        );
  }

  ObservableList<String> _createMappedList() {
    return _rxSourceList.mapItem(
      (final String item) => item.replaceAll(RegExp(r'[0-9]'), ''),
    );
  }

  Observable<String> _createFilteredListLengthObservable() {
    return rxFilteredList.map((final List<String> items) => _formatNumber(items.length));
  }

  Observable<String> _createMappedListLengthObservable() {
    return rxMappedList.map((final List<String> items) => _formatNumber(items.length));
  }

  void updateListType(final ListType type) {
    _rxListType.value = type;
  }
}

extension ExtensionCollectionViewType on CollectionViewType {
  Widget _buildSelectorItem(final String text) {
    return WidgetSegmentedItem(text: text);
  }

  Widget buildSelectorItem() {
    switch (this) {
      case CollectionViewType.list:
        return _buildSelectorItem('List');
      case CollectionViewType.set:
        return _buildSelectorItem('Set');
      case CollectionViewType.map:
        return _buildSelectorItem('Map');
    }
  }
}

enum CollectionViewType {
  list,
  set,
  map,
}

enum ListType {
  source,
  filter,
  mapped,
}
copied to clipboard
7
likes
140
points
33
downloads

Publisher

unverified uploader

Weekly Downloads

2024.09.08 - 2025.03.23

Flutter components for dart_observable package

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

dart_observable, flutter

More

Packages that depend on fl_observable