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.
dartvoid 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
dartclass 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
also modifiestextuser2because they are the same object in memory.textuser1
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
dartvoid 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
dartvoid 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
dartvoid 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()Problem: Shallow Copy Inside a "Deep" List Copy
dartclass 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: textcopyWith
for True Deep Copy
text
copyWithdartclass 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:
dartimport '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
textShallow 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
| Type | Shallow Copy | Deep Copy |
|---|---|---|
| List | text | text text text |
| Map | text | text text |
| Set | text | text text |
| Custom Object | text | text |
| Nested Object | text | JSON encode → decode / manual recursive copy |
Comparison Table
| Aspect | Shallow Copy (Soft Copy) | Deep Copy (Hard Copy) |
|---|---|---|
| What is copied | Reference (pointer) only | Entire object with new memory |
| Memory | Same memory location | New memory location |
text | text | text |
| Modify copy affects original? | Yes | No |
| Speed | Instant (just copies pointer) | Slower (creates new objects) |
| Memory usage | No extra memory | Uses extra memory |
| When to use | Read-only sharing | When you need independent copies |
| State management | Causes 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]) for collections andtext{...map}for custom objects. Shallow copies are the most common source of "UI not updating" bugs.textcopyWith()