Immutable Dart collections via the builder pattern.

Last update: May 23, 2022

Built Collections for Dart

Build Status

Introduction

Built Collections are immutable collections using the builder pattern.

Each of the core SDK collections is split in two: a mutable builder class and an immutable "built" class. Builders are for computation, "built" classes are for safely sharing with no need to copy defensively.

Immutable collections work particularly well with immutable values. See built_value.

You can read more about built_collection on medium.

Design

Built Collections:

  • are immutable, if the elements/keys/values used are immutable;
  • are comparable;
  • are hashable;
  • use copy-on-write to avoid copying unnecessarily.

See below for details on each of these points.

Recommended Style

A project can benefit greatly from using Built Collections throughout. Methods that will not mutate a collection can accept the "built" version, making it clear that no mutation will happen and completely avoiding the need for defensive copying.

For code that is public to other projects or teams not using Built Collections, prefer to accept Iterable where possible. That way your code is compatible with SDK collections, Built Collections and any other collection implementation that builds on Iterable.

It's okay to accept List, Set or Map if needed. Built Collections provide efficient conversion to their SDK counterparts via BuiltList.toList, BuiltListMultimap.toMap, BuiltSet.toSet, BuiltMap.toMap and BuiltSetMultimap.toMap.

Built Collections are Immutable

Built Collections do not offer any methods that modify the collection. In order to make changes, first call toBuilder to get a mutable builder.

In particular, Built Collections do not implement or extend their mutable counterparts. BuiltList implements Iterable, but not List. BuiltSet implements Iterable, but not Set. BuiltMap, BuiltListMultimap and BuiltSetMultimap share no interface with the SDK collections.

Built Collections can contain mutable elements. However, this use is not recommended, as mutations to the elements will break comparison and hashing.

Built Collections are Comparable

Core SDK collections do not offer equality checks by default.

Built Collections do a deep comparison against other Built Collections of the same type, only. Hashing is used to make repeated comparisons fast.

Built Collections are Hashable

Core SDK collections do not compute a deep hashCode.

Built Collections do compute, and cache, a deep hashCode. That means they can be stored inside collections that need hashing, such as hash sets and hash maps. They also use the cached hash code to speed up repeated comparisons.

Built Collections Avoid Copying Unnecessarily

Built Collections and their builder and helper types collaborate to avoid copying unless it's necessary.

In particular, BuiltList.toList, BuiltListMultimap.toMap, BuiltSet.toSet, BuiltMap.toMap and BuiltSetMultimap.toMap do not make a copy, but return a copy-on-write wrapper. So, Built Collections can be efficiently and easily used with code that needs core SDK collections but does not mutate them.

When you want to provide a collection that explicitly throws when a mutation is attempted, use BuiltList.asList, BuiltListMultimap.asMap, BuiltSet.asSet, BuiltSetMultimap.asMap and BuiltMap.asMap.

Features and bugs

Please file feature requests and bugs at the issue tracker.

GitHub

https://github.com/google/built_collection.dart
Comments
  • 1. Set/Map/Multimap: Custom equality?

    Any concern about support package:collection's Equality class, optionally?

    I have some use cases where I'd like to use BuiltCollection(s), but the keys I don't have control over and don't really want to write wrapper objects just to change the identity.

    If you're OK, I can send PRs. :)

    Reviewed by matanlurey at 2016-11-08 03:47
  • 2. feature request: expose interfaces that other classes can implement

    I have written a class Superset that implements all methods of BuiltSet. I would like to actually write implements BuiltSet (for the static analysis hints during development), but that would break this line in your code.

    Related(?) issue: https://github.com/dart-lang/site-www/issues/382

    Btw: I can contribute a pull request for this (and the other issues I opened recently). I guess you assign yourself to an issue if you started working on it?

    Reviewed by pschiffmann at 2017-11-07 16:55
  • 3. Add option to use HashSet and HashMap instead of default Set (LinkedHashSet) and Map (LinkedHashMap)

    Currently BuiltSet and BuiltMap use Dart's default Set and Map collections, that are not efficient in inserting or deletion large amounts of data. I would like to have ability to specify underlying Set and Map collection implementation, at least as a bool flag to use HashSet and HashMap. Another option would be to use HashSet and HashMap as default backend for those collections. What do you think about it?

    Reviewed by ranquild at 2016-10-18 08:11
  • 4. ListBuilder.replace(Iterable iterable) accepts dynamic Iterable

    Is this intentional to pass Iterable<dynamic> iterable or it could be reused generic E here like?

    void replace(Iterable<E> iterable) {...}
    
    Reviewed by audkar at 2019-08-30 10:14
  • 5. Concurrent modification during iteration: _LinkedHashMap len:0

    • Dart SDK Version (dart --version) Flutter 1.5.4-hotfix.2 • channel stable • https://github.com/flutter/flutter.git Framework • revision 7a4c33425d (9 weeks ago) • 2019-04-29 11:05:24 -0700 Engine • revision 52c7a1e849 Tools • Dart 2.3.0 (build 2.3.0-dev.0.5 a1668566e5) (I'm using flutter to developer a mobile app)

    First, I'm not sure if this is a issue of built_collection, or dart SDK, or my code. But I feel like it's more likely a issue of built_collection so I want to report it here first.

    So here is what happened: I'm randomly seeing this error in my code, both in JIT & Compiled AOT app.

    [VERBOSE-2:ui_dart_state.cc(148)] Unhandled Exception: Exception: Concurrent modification during iteration: _LinkedHashMap len:0.
    #0      _CompactIterator.moveNext (dart:collection-patch/compact_hash.dart:443:7)
    #1      Iterable.firstWhere (dart:core/iterable.dart)
    #2      DiscussionParser._isMutedDiscussionItem (package:munin/providers/bangumi/discussion/parser/DiscussionParser.dart:157:53)
    #3      DiscussionParser.processDiscussionItems (package:munin/providers/bangumi/discussion/parser/DiscussionParser.dart:173:12)
    #4      processDiscussion (package:munin/providers/bangumi/discussion/parser/isoldate.dart:15:8)
    #5      _IsolateConfiguration.apply (package:flutter/src/foundation/isolates.dart:110:16)
    #6      _spawn.<anonymous closure> (package:flutter/src/foundation/isolates.dart:117:45)
    <asynchronous suspension>
    #7      Timeline.timeSync (dart:developer/timeline.dart:161:22)
    #8      _spawn (package:flutter/src/foundation/isolates.dart:115:18)
    <asynchronous suspension>
    #9      _startIsolate.<anonymo<…>
    flutter: Another exception was thrown: Exception: Concurrent modification during iteration: _LinkedHashMap len:0.
    

    here is the corresponding code and here is the corresponding built_value class

    I basically pass a nested built_value class into a new isolate, and check a value in BuiltMapA.BuiltMapB.values.firstWhere, more specifically, I believe while I saw this error BuiltMapB is an empty BuiltMap.

    I think it's more likely due to an issue in built_value because

    1. I don't think I'm modifying this BuiltMap in my code during iteration, and it happened randomly (1 time out of 100?), but once it started happenning I can steadily reproduce it until I restart the app.
    2. It always happens in the same line, same column of my code, which is related to built_value: BuiltMapA.BuiltMapB.values.firstWhere

    Any thoughts on what might be the root cause?

    Reviewed by edwardez at 2019-07-02 03:13
  • 6. Serializers spread across projects?

    Hey @davidmorgan, could you possibly give me a little guidance with my serializer problem: My main serializers.dart file is in a package called core (dart native), my flutter app imports all the classes & serializers from the core project.

    I've just made a BlobSerializer so I can store Uint8List data in Firestore, however this requires me to import the package cloud_firestore_platform_interface so I can access the Blob interface, this package requires the flutter SDK so cannot imported by my core project.

    I've created the new serializer in my app project, but I'm unsure how to load it. Can my app can add to the existing serializers?

    One solution could be to move all the serializers to the app project out of core?

    Reviewed by jimmyff at 2020-08-19 14:51
  • 7. Cannot rebuild a BuiltSet entry in BuiltMap

    It appears that if you try to rebuild a BuiltSet which is an entry of BuiltMap, it does not rebuild as expected. The changes are not applied.

    main.dart

    import 'package:built_collection/built_collection.dart';
    import 'package:built_value_map_entry_rebuilder/models.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    void main() {
      test('BuiltMap entry update', () {
        final joe = Person((b) => b..name = 'Joe');
        final luke = Person((b) => b..name = 'Luke');
    
        final org = Organization((b) => b
          ..name = 'Foolandia'
          ..people = MapBuilder({
            'mobile': BuiltSet<Person>({joe, luke}),
          }));
    
        final orgBuilder = org.toBuilder();
    
        /// Initial works
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({joe, luke})));
    
        /// Weird rebuild works
        orgBuilder.people['mobile'] = (orgBuilder.people['mobile']!.toBuilder()
              ..removeWhere((e) => e.name == 'Joe'))
            .build();
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({luke})));
    
        /// Rebuild fails, expected to work
        orgBuilder.people['mobile']!
            .rebuild((b) => b..removeWhere((e) => e.name == 'Luke'));
    
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({})));
      });
    }
    

    models.dart

    import 'package:built_value/built_value.dart';
    import 'package:built_value/serializer.dart';
    import 'package:built_collection/built_collection.dart';
    
    part 'models.g.dart';
    
    abstract class Person implements Built<Person, PersonBuilder> {
      static Serializer<Person> get serializer => _$personSerializer;
    
      Person._();
      factory Person([void Function(PersonBuilder) updates]) = _$Person;
    
      String get name;
    }
    
    abstract class Organization
        implements Built<Organization, OrganizationBuilder> {
      static Serializer<Organization> get serializer => _$organizationSerializer;
    
      Organization._();
      factory Organization([void Function(OrganizationBuilder) updates]) =
          _$Organization;
    
      String get name;
    
      BuiltMap<String, BuiltSet<Person>> get people;
    }
    

    built_value_test.dart

    import 'package:built_collection/built_collection.dart';
    import 'package:built_value_map_entry_rebuilder/models.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    void main() {
      test('BuiltMap entry update', () {
        final joe = Person((b) => b..name = 'Joe');
        final luke = Person((b) => b..name = 'Luke');
    
        final org = Organization((b) => b
          ..name = 'Foolandia'
          ..people = MapBuilder({
            'mobile': BuiltSet<Person>({joe, luke}),
          }));
    
        final orgBuilder = org.toBuilder();
    
        /// Initial works
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({joe, luke})));
    
        /// Weird rebuild works
        orgBuilder.people['mobile'] = (orgBuilder.people['mobile']!.toBuilder()
              ..removeWhere((e) => e.name == 'Joe'))
            .build();
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({luke})));
    
        /// Rebuild fails, expected to work
        orgBuilder.people['mobile']!
            .rebuild((b) => b..removeWhere((e) => e.name == 'Luke'));
    
        expect(orgBuilder.people['mobile'], equals(BuiltSet<Person>({})));
      });
    }
    

    Log

    Expected: _BuiltSet<Person>:[]
      Actual: _BuiltSet<Person>:[
                _$Person:Person {  
                name=Luke,  
              }
              ]
       Which: at location [0] is _BuiltSet<Person>:[
                _$Person:Person {  
                name=Luke,  
              }
              ] which longer than expected
    
    package:test_api                                   expect
    expect
    package:flutter_test/src/widget_tester.dart:441
    main.<fn>
    test/built_value_test.dart:38
    2
    
    ✖ BuiltMap entry update
    Exited (1)
    
    Reviewed by lukepighetti at 2021-07-27 21:46
  • 8. BuiltMap not supported?

    So getting a Map from Firestore is basically always a Map<String, dynamic>. I was hoping that BuiltMap also supported mixed maps, as I'd opt to use BuiltMaps instead of Map, internally. But it doesn't look like it?

    Reviewed by larssn at 2019-07-21 15:42
  • 9. Add asCollection() methods to BuiltCollection classes.

    Closes https://github.com/google/built_collection.dart/issues/77

    1.2.0

    • Add asList, asMap, and asSet to the built collection classes.

    This change is Reviewable

    Reviewed by matanlurey at 2016-11-09 23:03
  • 10. Consider "asList", "asMap", and "asSet"

    I like this library, but one thing stopping me from using it is that I don't want to require other users to use built_collection, so I tend to only use it as an implementation detail and pass a toList/toMap/toSet as part of my API.

    I would use asList/Map/Set, where they could be, for example:

    List<T> asList() => new List<T>.unmodifiable(_backingList);
    
    Reviewed by matanlurey at 2016-11-08 03:52
  • 11. Add const BuiltList.empty()

    This would allow us to use it as a default for a named parameter.

    Related to https://github.com/google/built_collection.dart/issues/164 which is blocked by missing language features.

    Reviewed by cbenhagen at 2021-02-14 08:22
  • 12. Docs for `asList` misleading

    It says "This differs from toList where mutations are explicitly disallowed." ... whic needs 'where' replacing with 'in that' to be correct, or even better a total rewrite to be clear.

    Same for other similar methods.

    Reviewed by davidmorgan at 2022-04-06 11:44
  • 13. MapBuilder dont fail in compilation time if generics does not match on constructor

    Currently you will have a runtime exception if you try to build a map with the wrong types.

    This will allow the developer to know at compile time if what he is intending to do will work or not, avoiding bugs.

    Reviewed by danielgomezrico at 2022-01-28 17:58
  • 14. Why is the return operator[] on BuiltSetMultimap nullable?

    It seems it can never return null. I imagine the API was based on SetMultiMap from package:collection before, but it seems that the return type of that one is not nullable.

    Reviewed by Pacane at 2021-10-04 15:08
  • 15. (Question) how to rebuild every value in BuiltList?

    I'm new to built list, there's this thing I cant understand, so I have a model called AdressShipmentModel

    In that model i have this value (I'm using freezed package)

    @freezed
    abstract class AdressShipmentModel with _$AdressShipmentModel {
      const factory AdressShipmentModel({
        required int addrId,
        required String label,
        required String receiverName,
        required String receiverPhone,
        required String address,
        required String isDefault,
        required int locationId,
        required String locationName,
        required String latlng,
        @Default(false) @JsonKey(ignore: true) bool isChoosenAddress,
      }) = _AdressShipmentModel;
    
      factory AdressShipmentModel.fromJson(Map<String, dynamic> json) => _$AdressShipmentModelFromJson(json);
    }
    

    The goal is to modify the model that choose by user is default by parameter isChoosenAddress

    By this code I'm able to modify the choosen address to true if isDefault parameter is "1".

      initialData() {
        List<AdressShipmentModel> oldValue = value.toList();
        List<AdressShipmentModel> newValue = [];
        oldValue.forEach((e) {
          if (e.isDefault == "1") {
            newValue.add(e.copyWith(isChoosenAddress: true));
          } else {
            newValue.add(e.copyWith(isChoosenAddress: false));
          }
        });
    
        return ChoosenAddressEntities._(BuiltList<AdressShipmentModel>(newValue));
      }
    

    But this was the old method of modifying list, and there's really no need for built_collection package if I do it like this.

    When I use .rebuild(), it will not modify list as I wanted, both of the isChoosenAddress was set to false. (Not modified) image

    Is there's something I do wrong? Any kind of explanation will be so much appreciated! Thank you :)

    This was my full code of logic:

    import 'package:built_collection/built_collection.dart';
    import 'package:equatable/equatable.dart';
    import '/infrastructure/models/settings/address_shipment_model.dart';
    
    class ChoosenAddressEntities extends Equatable {
      final BuiltList<AdressShipmentModel> value;
      const ChoosenAddressEntities._(this.value);
      factory ChoosenAddressEntities(List<AdressShipmentModel> listOfCart) =>
          ChoosenAddressEntities._(BuiltList<AdressShipmentModel>(listOfCart));
    
      updateChoosenAddress({
        required int id,
      }) {
        int index = _getAddressByIndex(id);
        BuiltList<AdressShipmentModel> newValue = value.rebuild((e) {
          e[index] = e[index].copyWith(isChoosenAddress: true);
    
          return _replaceRange(index: index, newAddressItem: e[index]);
        })
          ..forEach((e) {
            if (e.addrId != index) {
              e.copyWith(isChoosenAddress: false);
            }
          });
        return ChoosenAddressEntities._(newValue);
      }
    
      initialData() {
        List<AdressShipmentModel> oldValue = value.toList();
        List<AdressShipmentModel> newValue = [];
        oldValue.forEach((e) {
          if (e.isDefault == "1") {
            newValue.add(e.copyWith(isChoosenAddress: true));
          } else {
            newValue.add(e.copyWith(isChoosenAddress: false));
          }
        });
    
        return ChoosenAddressEntities._(BuiltList<AdressShipmentModel>(newValue));
      }
    
      int _getAddressByIndex(int id) {
        return value.indexOf(value.firstWhere((e) => e.addrId == id));
      }
    
      BuiltList<AdressShipmentModel> _replaceRange({
        required int index,
        AdressShipmentModel? newAddressItem,
      }) {
        return value.rebuild(
          (a) => a
            ..replaceRange(
              index,
              index + 1,
              newAddressItem != null ? [newAddressItem] : [],
            ),
        );
      }
    
      @override
      // TODO: implement props
      List<Object?> get props => [value];
    }
    
    Reviewed by linxkaa at 2021-08-19 14:35
  • 16. Tighten generics

    e.g. making replace taking an Iterable and having a separate method replaceFrom.

    It's a breaking API change, so we likely want to do all related improvements in one shot.

    Reviewed by davidmorgan at 2021-06-09 09:31

Related

Form builder image picker - Form builder image picker for flutter

form_builder_image_picker Field for picking image(s) from Gallery or Camera for

Jan 28, 2022
Responsive-Ui-builder - The responsive ui builder package contains widgets that helps you to create your UI responsive
Responsive-Ui-builder - The responsive ui builder package contains widgets that helps you to create your UI responsive

Responsive Ui Builder Getting Started The responsive ui builder package contains

Feb 1, 2022
Functional extensions to Dart collections.

collection_ext A set of extension methods for Dart collections, designed for the purpose of making it easier to write concise, functional-programming-

Dec 28, 2021
Immutable value types, enum classes, and serialization.

Built Values for Dart Introduction Built Value provides: Immutable value types; EnumClass, classes that behave like enums; JSON serialization. Immutab

May 19, 2022
Github Trending app built with Flutter+Redux+Built(Immutable Data)

Github Trending app built with Flutter+Redux+Built(Immutable Data)

May 13, 2020
Dart wrapper via `dart:js` for webusb

Dart wrapper via dart:js for https://wicg.github.io/webusb/ Features canUseUsb g

Jan 25, 2022
A Dart Build Plugin that uploads debug symbols for Android, iOS/macOS and source maps for Web to Sentry via sentry-cli

Sentry Dart Plugin A Dart Build Plugin that uploads debug symbols for Android, iOS/macOS and source maps for Web to Sentry via sentry-cli. For doing i

Apr 26, 2022
Package your Flutter app into OS-specific bundles (.dmg, .exe, etc.) via Dart or the command line.

flutter_distributor Package your Flutter app into OS-specific bundles (.dmg, .exe, etc.) via Dart or the command line. The flutter_distributor source

May 26, 2022
A builder for extracting a package version into code

Include the version of your package in our source code. Add build_version to pubspec.yaml. Also make sure there is a version field. name: my_pkg versi

Jan 12, 2022
Jan 29, 2022
QR.Flutter is a Flutter library for simple and fast QR code rendering via a Widget or custom painter.
QR.Flutter is a Flutter library for simple and fast QR code rendering via a Widget or custom painter.

QR.Flutter is a Flutter library for simple and fast QR code rendering via a Widget or custom painter. Need help? Please do not submit an issue for a "

May 12, 2022
💳 A Flutter package for making payments via credo central. Provides support for both Android and iOS

?? Credo Package for Flutter TODO: Put a short description of the package here that helps potential users know whether this package might be useful fo

Dec 26, 2021
Sangre - Sangre streams your backend queries in realtime to your clients minimizing the load via diffs

Sangre Sangre streams your backend queries in realtime to your clients minimizin

May 10, 2022
Helper app to run code on Aliucord iOS via websocket.

aliucordebug A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you started if

Jan 25, 2022
Flutter implementation for ExotikArch via a simple todos CRUD application.
Flutter implementation for ExotikArch via a simple todos CRUD application.

ExotikArch - Flutter setState on steriods ⚡ ExotikArch for Flutter. Deliver production apps fast with flutter. Global State Management Navigation Serv

Apr 7, 2022
A dart package for pattern matching.

A dart package for pattern matching. Inspired by match in Rust and when in Kotlin Usage var x = 2; var result = match( x, { 1: () => "Its a on

Dec 30, 2021
A starter kit for beginner learns with Bloc pattern, RxDart, sqflite, Fluro and Dio to architect a flutter project. This starter kit build an App Store app as a example
A starter kit for beginner learns with Bloc pattern, RxDart, sqflite, Fluro and Dio to architect a flutter project. This starter kit build an App Store app as a example

Flutter Starter Kit - App Store Example A starter kit for beginner learns with Bloc pattern, RxDart, sqflite, Fluro and Dio to architect a flutter pro

May 24, 2022
constructing... Flutter, Ganache, Truffle, Remix, Getx Pattern, Infura, GetX, Blockchain

constructing... Flutter, Ganache, Truffle, Remix, Getx Pattern, Infura, GetX, Blockchain

May 16, 2022