Built Value for Dart

repository·master·Indexed 21 days ago

https://github.com/google/built_value.dart

A Dart library for creating immutable value types, enum-like classes (EnumClass), and robust JSON serialization. It utilizes code generation via built_value_generator and build_runner to eliminate boilerplate for complex data models. Key features include support for generics and interfaces, decoupled models for client/server architectures, and lifecycle hooks (@BuiltValueHook) for validation and default values.

Tokens
5.8K
Snippets
17
Records
32
Agent score
75%

What's inside built_value.dart

  1. Introduction to Built Value for Dart

    master

    Built Value is a library for Dart that provides:

    • Immutable value types: Classes that are considered interchangeable if their fields have the same values.
    • EnumClass: Classes that behave like enums but are full objects that can hold code and implement interfaces.
    • JSON serialization: Support for serializing complete data models including Built Values, Enum Classes, and Built Collections.

    Note: Immutable collections are provided by the separate built_collection package.

  2. Serialization features in Built Value

    master

    Built Value's serialization support is designed for robust, evolving data models. Key characteristics include:

    • Object-Oriented Design: Supports full use of generics and interfaces.
    • Decoupled Models: Allows different class models (e.g., Client vs Server) to map to the same underlying data.
    • Evolution Support: Supports adding/removing optional fields or switching fields from optional to required without breaking compatibility.
    • Validation: First-class support for ensuring data validity via Built Values.
    • Pluggable: Supports custom serializers for your own types and plugins that run before/after all serializers.
  3. How Enum Classes work

    master

    Enum Classes provide enum-like features for object-oriented design in Dart. Unlike standard enums, they are real classes that can hold code and implement interfaces.

    Key features:

    • Constants have name and toString and can be used in switch statements.
    • A generated values method returns all enum values in a BuiltSet (an immutable set).
    • A generated valueOf method allows looking up an enum value by its String name.
  4. Explore built_value usage examples

    master

    The example directory contains several practical implementations of built_value to help you understand its application in different contexts:

    • Basic usage: A simple demonstration of defining and using value types.
    • Client and server: A more complex example showing how to use built_value in a full-stack architecture (chat application).
    • End to end tests: Demonstrates how to write tests that cover the entire lifecycle of value types and their interactions.
  5. Explore built_value.dart usage examples

    master

    The built_value_generator example repository provides several reference implementations to help you understand how to use the library in different contexts:

    • Basic usage: A simple demonstration of defining and using value types.
    • Client and server: A more complex example showing how to use built_value in a full-stack application context (e.g., chat application).
    • End to end tests: Examples of how to write tests that cover the entire lifecycle of your value types.

    You can find the source code for the basic usage in example/lib/example.dart.

    https://github.com/google/built_value.dart/blob/master/example/lib/example.dart
  6. Run Codegen with build_runner

    master

    Built Value uses code generation to handle boilerplate. You must add built_value_generator and build_runner as dev dependencies in your pubspec.yaml.

    To perform a one-off build of generated files:

    dart run build_runner build

    To continuously watch your source files and automatically update generated output when changes occur:

    dart run build_runner watch
  7. Validate, process, and set defaults in Value Types

    master

    Built Value provides several ways to handle logic during the instantiation of a value type:

    Validation on instantiation

    Use the private constructor to perform arbitrary checks. If a check fails, you can throw an error.

    abstract class MyValue {
      MyValue._() {
        if (field < 0) {
          throw ArgumentError(field, 'field', 'Must not be negative.');
        }
      }
    }

    Processing fields (Hooks)

    Use @BuiltValueHook(finalizeBuilder: true) to run logic immediately before a builder is built (e.g., sorting a list).

    abstract class MyValue {
      @BuiltValueHook(finalizeBuilder: true)
      static void _sortItems(MyValueBuilder b) =>
          b..items.sort();
    }

    Setting default values

    Use @BuiltValueHook(initializeBuilder: true) to run logic whenever a builder is created.

    abstract class MyValue {
      @BuiltValueHook(initializeBuilder: true)
      static void _setDefaults(MyValueBuilder b) =>
          b
            ..name = 'defaultName'
            ..count = 0;
    }
  8. Run the built_value chat example

    master

    The chat example consists of a client (compiled to dart2js) and a server (running on Dart VM). To run the full application, you must launch both the web server and the backend server in separate terminal windows.

    1. Start the web server: pub serve

    2. Start the backend server: dart bin/main.dart

    Once both are running, access the application in a modern browser at http://localhost:26199.

    pub serve
    dart bin/main.dart
  9. How Built and Builder classes work together

    master

    The core pattern of built_value involves two cooperating classes: a Built class representing an immutable value and a Builder class used to construct or modify that value.

    1. Built<V, B>: An immutable instance. To create a new instance based on an existing one, use rebuild(Function(B) updates). To get a mutable version, use toBuilder().
    2. Builder<V, B>: A mutable object used to configure the value. Use replace(V value) to overwrite the builder with an existing instance, update(Function(B)? updates) to apply changes, and build() to produce the final immutable Built instance.

    Note: You typically do not implement these classes manually; the built_value_generator creates the boilerplate for you based on your class definitions.

    // Conceptual usage of the generated pattern
    final myValue = MyClass((b) => b..field = 'value').build();
    
    // Rebuilding an existing value
    final updatedValue = myValue.rebuild((b) => b..field = 'new value');
    
    // Using a builder manually
    final builder = myValue.toBuilder();
    builder.field = 'manual change';
    final newValue = builder.build();
  10. Understand FullType for generic serialization

    master

    The FullType class represents a Dart type, including its generic parameters and nullability. This is used by serializers to correctly identify types and instantiate builders for generic collections.

    • FullType.unspecified: Represents a type with no information.
    • FullType.object: Represents Object.
    • withNullability(bool): Returns a new FullType marked as nullable or non-nullable.

    Example of representing BuiltList<String>:

    const type = FullType(BuiltList, [FullType(String)]);
  11. Use JsonObject for flexible JSON fields

    master

    The JsonObject class is a wrapper designed for use in built_value fields when you need to represent arbitrary JSON values (bool, List, Map, num, or String).

    Key characteristics:

    • Serialization: When serialized, it maps directly onto standard JSON values.
    • Equality: It provides deep equality and hashing for List and Map contents using DeepCollectionEquality.
    • Immutability: List and Map values are wrapped in UnmodifiableListView and UnmodifiableMapView. Note that while the wrapper is unmodifiable, you must ensure the original reference is not updated, as a copy is not made.
    • Experimental: This is an experimental feature; the API may change without a major version increase.

    To create a JsonObject, use the factory constructor with a valid JSON type. If the type is invalid, an ArgumentError is thrown.

    // Creating a JsonObject from various types
    final boolJson = JsonObject(true);
    final listJson = JsonObject([1, 2, 3]);
    final mapJson = JsonObject({'key': 'value'});
    final stringJson = JsonObject('hello');
    final numJson = JsonObject(42);