К содержимому

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.

осторожно

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.

осторожно

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!