Zum Hauptinhalt springen

Why Immutability

What is Immutability?

Immutability is when all fields of an Object are final or late final. They are set exactly once upon construction.

Immutability is desireable for many different reasons

  • Value equality rather than reference equality
  • Local reasoning about a piece of code
    • A far distant piece of code can't obtain a reference and change the object from underneath you
  • Easier to reason about for asynchronous and parallel tasks
    • Other code can't mutate your object in between operations
  • Safety of APIs
    • What you pass into a method cannot be changed by the callee / caller

A copyWith method helps with reducing verbosity when creating a new object with just a few things changed.

Copying is more efficient than you might think, since dart can reuse any references to sub-objects that have not changed.

danger

Make sure your objects are deeply immutable, otherwise you'll have to implement some sort of deep copy mechanism.

Best Practices

You can use any package you want to create immutable state.

For immutable objects:

For immutable collections (Map, Set, List):

It is highly recommended to use freezed, since it has several nice additions beyond just making immutable objects including:

  • A generated copyWith method
  • Deep copy (copyWith on nested freezed objects)
  • Union types
  • Union mapping functions

You do not need to use code generation to work with immutable state, but it makes it much easier.

danger

If you want to use the built-in collections, make sure to enforce a discipline of making copies of collections when updating them. The issue with not copying a collection is that riverpod determines whether to emit a new state based on whether the reference to the object has changed. If you just call a method that mutates an object, the reference is the same.

Using immutable state

Immutable state is best fit for using a Notifier . A Notifier allows you to expose an interface through which you can 'mutate' the state. You cannot mutate the state from outside the class you define that extends Notifier. This enforces a separation of concerns and keeps business logic outside of your UI.

Here is an example of a simple immutable settings class for changing an app theme.



class ThemeNotifier extends _$ThemeNotifier {

ThemeSettings build() => const ThemeSettings(
mode: ThemeMode.light,
primaryColor: Colors.blue,
);

void toggle() {
state = state.copyWith(mode: state.mode.toggle);
}

void setDarkTheme() {
state = state.copyWith(mode: ThemeMode.dark);
}

void setLightTheme() {
state = state.copyWith(mode: ThemeMode.light);
}

void setSystemTheme() {
state = state.copyWith(mode: ThemeMode.system);
}

void setPrimaryColor(Color color) {
state = state.copyWith(primaryColor: color);
}
}


class ThemeSettings with _$ThemeSettings {
const factory ThemeSettings({
required ThemeMode mode,
required Color primaryColor,
}) = _ThemeSettings;
}

extension ToggleTheme on ThemeMode {
ThemeMode get toggle {
switch (this) {
case ThemeMode.dark:
return ThemeMode.light;
case ThemeMode.light:
return ThemeMode.dark;
case ThemeMode.system:
return ThemeMode.system;
}
}
}

To use this code, remember to import freezed_annotation add the part directive and run build_runner to generate the freezed classes!