Question #444MediumDart BasicsImportant

Explain clearly about Deep Copy (Hard Copy) vs Shallow Copy (Soft Copy) in flutter with some proper examples

#dart#flutter#deep-copy#shallow-copy#state-management#copyWith#immutability

Answer

Overview

When you copy an object in Dart, there are two ways it can happen:

  • Shallow Copy (Soft Copy): Copies the reference (pointer) — both variables point to the same object in memory. Changing one affects the other.
  • Deep Copy (Hard Copy): Creates a completely new object with its own memory — changing one does not affect the other.

Shallow Copy — Same Object, Different Name

A shallow copy only copies the reference (memory address), not the actual data. Both variables point to the same object.

dart
void main() {
  // Shallow copy of a List
  List<String> original = ['Apple', 'Banana', 'Cherry'];
  List<String> shallowCopy = original;  // ❌ Just copies the reference

  shallowCopy.add('Mango');

  print(original);     // [Apple, Banana, Cherry, Mango] ← ALSO changed!
  print(shallowCopy);  // [Apple, Banana, Cherry, Mango]
  print(identical(original, shallowCopy));  // true — same object in memory
}

Shallow Copy with Objects

dart
class User {
  String name;
  List<String> hobbies;
  User({required this.name, required this.hobbies});
}

void main() {
  final user1 = User(name: 'Alice', hobbies: ['Reading', 'Gaming']);
  final user2 = user1;  // ❌ Shallow copy — same object

  user2.name = 'Bob';
  user2.hobbies.add('Cooking');

  print(user1.name);     // Bob ← CHANGED! Both point to same object
  print(user1.hobbies);  // [Reading, Gaming, Cooking] ← CHANGED!
  print(identical(user1, user2));  // true
}

Problem: Modifying

text
user2
also modifies
text
user1
because they are the same object in memory.


Deep Copy — Completely New Object

A deep copy creates a brand new object with its own memory. Changes to the copy do not affect the original.

Deep Copy a List

dart
void main() {
  List<String> original = ['Apple', 'Banana', 'Cherry'];

  // ✅ Deep copy methods for List
  List<String> copy1 = List.from(original);      // Method 1
  List<String> copy2 = [...original];             // Method 2 (spread)
  List<String> copy3 = original.toList();         // Method 3

  copy1.add('Mango');

  print(original);  // [Apple, Banana, Cherry] ← NOT changed
  print(copy1);     // [Apple, Banana, Cherry, Mango]
  print(identical(original, copy1));  // false — different objects
}

Deep Copy a Map

dart
void main() {
  Map<String, int> original = {'Alice': 90, 'Bob': 85};

  // ✅ Deep copy methods for Map
  Map<String, int> copy1 = Map.from(original);    // Method 1
  Map<String, int> copy2 = {...original};          // Method 2 (spread)

  copy1['Charlie'] = 95;

  print(original);  // {Alice: 90, Bob: 85} ← NOT changed
  print(copy1);     // {Alice: 90, Bob: 85, Charlie: 95}
}

Deep Copy a Set

dart
void main() {
  Set<String> original = {'Dart', 'Flutter'};

  // ✅ Deep copy methods for Set
  Set<String> copy1 = Set.from(original);    // Method 1
  Set<String> copy2 = {...original};          // Method 2 (spread)

  copy1.add('React');

  print(original);  // {Dart, Flutter} ← NOT changed
  print(copy1);     // {Dart, Flutter, React}
}

Deep Copy Custom Objects — The Tricky Part

For custom classes,

text
List.from()
and spread only create a shallow copy of the objects inside. You need to manually deep copy each object.

Problem: Shallow Copy Inside a "Deep" List Copy

dart
class User {
  String name;
  List<String> hobbies;
  User({required this.name, required this.hobbies});
}

void main() {
  final user1 = User(name: 'Alice', hobbies: ['Reading', 'Gaming']);
  final list1 = [user1];
  final list2 = List.from(list1);  // ⚠️ List is new, but User inside is SAME

  list2[0].name = 'Bob';  // Modifying the User object

  print(list1[0].name);  // Bob ← CHANGED! Same User object
  print(identical(list1[0], list2[0]));  // true — same User in memory
}

Solution:
text
copyWith
for True Deep Copy

dart
class User {
  final String name;
  final List<String> hobbies;

  const User({required this.name, required this.hobbies});

  // ✅ Deep copy method
  User copyWith({String? name, List<String>? hobbies}) {
    return User(
      name: name ?? this.name,
      hobbies: hobbies ?? List.from(this.hobbies),  // Deep copy the list too!
    );
  }
}

void main() {
  final user1 = User(name: 'Alice', hobbies: ['Reading', 'Gaming']);
  final user2 = user1.copyWith(name: 'Bob');  // ✅ True deep copy

  print(user1.name);     // Alice ← NOT changed
  print(user2.name);     // Bob
  print(user1.hobbies);  // [Reading, Gaming]
  print(identical(user1, user2));  // false — completely different objects
}

Deep Copy with JSON (For Complex Nested Objects)

For deeply nested objects, convert to JSON and back:

dart
import 'dart:convert';

class Address {
  String city;
  String country;
  Address({required this.city, required this.country});

  Map<String, dynamic> toJson() => {'city': city, 'country': country};
  factory Address.fromJson(Map<String, dynamic> json) =>
      Address(city: json['city'], country: json['country']);
}

class User {
  String name;
  Address address;  // Nested object
  User({required this.name, required this.address});

  Map<String, dynamic> toJson() => {'name': name, 'address': address.toJson()};
  factory User.fromJson(Map<String, dynamic> json) =>
      User(name: json['name'], address: Address.fromJson(json['address']));

  // ✅ Deep copy via JSON serialization
  User deepCopy() => User.fromJson(jsonDecode(jsonEncode(toJson())));
}

void main() {
  final user1 = User(name: 'Alice', address: Address(city: 'NYC', country: 'USA'));
  final user2 = user1.deepCopy();  // ✅ Completely independent copy

  user2.name = 'Bob';
  user2.address.city = 'London';

  print(user1.name);          // Alice ← NOT changed
  print(user1.address.city);  // NYC ← NOT changed
  print(user2.name);          // Bob
  print(user2.address.city);  // London
}

Visual Representation

text
Shallow Copy (Same memory):
┌──────────┐
│ original ├──────→ [ Apple, Banana, Cherry ]
└──────────┘              ↑
┌──────────┐              │
│   copy   ├──────────────┘  (points to SAME list)
└──────────┘

Deep Copy (Different memory):
┌──────────┐
│ original ├──────→ [ Apple, Banana, Cherry ]  (Memory A)
└──────────┘
┌──────────┐
│   copy   ├──────→ [ Apple, Banana, Cherry ]  (Memory B)
└──────────┘

Real-World Flutter Example — State Management

dart
// ❌ BLoC/Riverpod Bug — Shallow copy doesn't trigger rebuild
class TodoState {
  final List<String> todos;
  TodoState(this.todos);
}

// Inside BLoC/Notifier
void addTodo(String todo) {
  state.todos.add(todo);         // ❌ Mutating same list
  emit(TodoState(state.todos));  // ❌ Same reference — BLoC thinks nothing changed!
}

// ✅ Fix — Deep copy triggers rebuild
void addTodo(String todo) {
  final newTodos = [...state.todos, todo];  // ✅ New list (deep copy + add)
  emit(TodoState(newTodos));                // ✅ New reference — BLoC detects change!
}

This is the #1 bug new Flutter developers face with BLoC/Riverpod — the UI doesn't update because the state object reference didn't change.


All Deep Copy Methods Summary

TypeShallow CopyDeep Copy
List
text
copy = original
text
List.from(original)
/
text
[...original]
/
text
.toList()
Map
text
copy = original
text
Map.from(original)
/
text
{...original}
Set
text
copy = original
text
Set.from(original)
/
text
{...original}
Custom Object
text
copy = original
text
copyWith()
method
Nested Object
text
copy = original
JSON encode → decode / manual recursive copy

Comparison Table

AspectShallow Copy (Soft Copy)Deep Copy (Hard Copy)
What is copiedReference (pointer) onlyEntire object with new memory
MemorySame memory locationNew memory location
text
identical()
check
text
true
text
false
Modify copy affects original?YesNo
SpeedInstant (just copies pointer)Slower (creates new objects)
Memory usageNo extra memoryUses extra memory
When to useRead-only sharingWhen you need independent copies
State managementCauses bugs (no rebuild)Required for BLoC/Riverpod state updates

Best Practice: In Flutter state management, always deep copy your state objects. Use spread operators (

text
[...list]
,
text
{...map}
) for collections and
text
copyWith()
for custom objects. Shallow copies are the most common source of "UI not updating" bugs.