DEV Community

Louie John
Louie John

Posted on • Edited on

Dart Immutability with copyWith

Before we talk about copyWith let's first understand what is Immutability in programming.

Immutability

In programming, this is the state of an object where we don't allow changes after initialization.

Immutability-is-no-changes

Having an immutable object allows predictability and maintainability in our code. You no longer need to concern yourself about which area might be affected if you alter the object.

Now, Dart is rich in this immutability feature. For object immutability, we can use its method copyWith.

copyWith

An instance method of a Class that returns a new instance with the properties from the initialized Class. These returned properties can be either modified or not.

Here's an example with a Shirt class:

enum ShirtSize {
  S,
  M,
  L,
  XL,
}

enum ShirtType {
  streetWear,
  formal,
  casual,
}

class Shirt {
  final String id;
  final ShirtType type;
  final ShirtSize size;

  const Shirt({
    required this.id,
    required this.type,
    required this.size,
  });

  @override
  String toString() => "ID: ${id} - Type: ${type} - Size: ${size}";

  Shirt copyWith({String? id, ShirtType? type, ShirtSize? size}) => Shirt(
        id: id ?? this.id,
        type: type ?? this.type,
        size: size ?? this.size,
      );
}
Enter fullscreen mode Exit fullscreen mode

We created a class Shirt with an instance method of copyWith. The copyWith method returns a new instance of Shirt, allowing overriding of the properties id, type, and size.

Let's say we have a collection of casual shirts that we want to add to our list.

void main() {
  final List<Shirt> casualShirts = [];
  final casualSmall = Shirt(
    id: "0ABC1",
    type: ShirtType.casual,
    size: ShirtSize.S,
  );
  final casualMedium = casualSmall.copyWith(size: ShirtSize.M);
  final casualLarge = casualSmall.copyWith(size: ShirtSize.L);
  final casualExtraLarge = casualSmall.copyWith(size: ShirtSize.XL);

  casualShirts.addAll([
    casualSmall,
    casualMedium,
    casualLarge,
    casualExtraLarge,
  ]);
}
Enter fullscreen mode Exit fullscreen mode

In the example above, we first have a casual shirt with a Small size. This will act as our base instance in creating the remaining sizes. We can use the copyWith method to modify only the size property from our base instance while retaining the id and type properties.

After adding all our casual shirts to our list, let's try printing our list of casual shirts to verify.

...

/// Check the override method toString 
/// in our Shirt Class.

casualShirts.forEach(print);

ID: 0ABC1 - Type: ShirtType.casual - Size: ShirtSize.S
ID: 0ABC1 - Type: ShirtType.casual - Size: ShirtSize.M
ID: 0ABC1 - Type: ShirtType.casual - Size: ShirtSize.L
ID: 0ABC1 - Type: ShirtType.casual - Size: ShirtSize.XL
Enter fullscreen mode Exit fullscreen mode

Using copyWith prevents external code from needing to know the actual implementation, keeping coupling loose. I hope with the example that we had, you can see the advantage of using this pattern when working with data in larger apps.

Top comments (0)