What is immutable and mutable ? Difference between them ?
Answer
Definitions
Immutable
An immutable object is one whose state cannot be changed after it has been created. Once initialized with a value, it cannot be modified. Any operation that appears to modify an immutable object actually creates a new object.
Mutable
A mutable object is one whose internal state can be changed after creation. You can modify its properties, add/remove elements, or update values without creating a new object.
Comparison Table
| Aspect | Immutable | Mutable |
|---|---|---|
| Modification | Cannot be changed after creation | Can be modified after creation |
| Memory | Creates new object on change | Modifies existing object |
| Thread Safety | Inherently thread-safe | Requires synchronization |
| Performance | Memory overhead for new objects | Better for frequent updates |
| Predictability | Easier to reason about | Can have unexpected side effects |
| State Management | Preferred in Flutter/Bloc | Requires careful handling |
| Examples (Dart) | String, int, double, bool | List, Map, Set |
Immutable Types in Dart
Built-in Immutable Types
dart// String - Immutable String name = 'John'; name = name.toUpperCase(); // Creates NEW string, doesn't modify original print(name); // Output: JOHN // Numbers - Immutable int age = 25; age = age + 1; // Creates new int value print(age); // Output: 26 // Booleans - Immutable bool isActive = true; isActive = !isActive; // Creates new boolean value print(isActive); // Output: false
Creating Immutable Classes
dartclass ImmutableUser { final String name; final int age; final String email; // All fields are final - cannot be changed after construction const ImmutableUser({ required this.name, required this.age, required this.email, }); // To "modify", create a new instance with updated values ImmutableUser copyWith({ String? name, int? age, String? email, }) { return ImmutableUser( name: name ?? this.name, age: age ?? this.age, email: email ?? this.email, ); } } // Usage void main() { final user1 = ImmutableUser( name: 'Alice', age: 30, email: 'alice@example.com', ); // Cannot do: user1.name = 'Bob'; // Error: final field // Instead, create new instance final user2 = user1.copyWith(name: 'Bob'); print(user1.name); // Output: Alice (unchanged) print(user2.name); // Output: Bob (new object) }
Using @immutable Annotation
dartimport 'package:meta/meta.dart'; class Product { final String id; final String name; final double price; const Product({ required this.id, required this.name, required this.price, }); } // Compiler warning if you try to add mutable fields: class InvalidProduct { String name; // Warning: This class is marked immutable but has mutable fields InvalidProduct(this.name); }
Mutable Types in Dart
Built-in Mutable Types
dart// List - Mutable List<String> fruits = ['apple', 'banana']; fruits.add('orange'); // Modifies the same list fruits[0] = 'mango'; // Modifies existing element fruits.remove('banana'); // Removes element print(fruits); // Output: [mango, orange] // Map - Mutable Map<String, int> scores = {'Alice': 90, 'Bob': 85}; scores['Alice'] = 95; // Modifies existing value scores['Charlie'] = 88; // Adds new entry scores.remove('Bob'); // Removes entry print(scores); // Output: {Alice: 95, Charlie: 88} // Set - Mutable Set<int> numbers = {1, 2, 3}; numbers.add(4); // Modifies the set numbers.remove(1); // Removes element print(numbers); // Output: {2, 3, 4}
Creating Mutable Classes
dartclass MutableCounter { int count = 0; // Not final - can be changed void increment() { count++; // Modifies the same object } void decrement() { count--; // Modifies the same object } void reset() { count = 0; // Modifies the same object } } // Usage void main() { final counter = MutableCounter(); print(counter.count); // Output: 0 counter.increment(); print(counter.count); // Output: 1 counter.increment(); print(counter.count); // Output: 2 // Same object, different state }
final vs const Keywords
final - Single Assignment
dart// final: Value set at runtime, cannot be reassigned final currentTime = DateTime.now(); // Evaluated at runtime // currentTime = DateTime.now(); // Error: cannot reassign final List<int> numbers = [1, 2, 3]; numbers.add(4); // ✅ OK: List itself is mutable // numbers = [5, 6]; // ❌ Error: Cannot reassign the variable
const - Compile-Time Constant
dart// const: Value must be known at compile time, deeply immutable const pi = 3.14159; // Compile-time constant // const now = DateTime.now(); // Error: Not a compile-time constant const List<int> primes = [2, 3, 5, 7]; // primes.add(11); // Error: Cannot modify const list // primes = [2, 3, 5]; // Error: Cannot reassign
Comparison
dart// final: Runtime constant, shallow immutability final list1 = [1, 2, 3]; list1.add(4); // ✅ Allowed // const: Compile-time constant, deep immutability const list2 = [1, 2, 3]; // list2.add(4); // ❌ Error: Cannot modify // Mixed usage final list3 = const [1, 2, 3]; // final reference to const list // list3.add(4); // ❌ Error: List is const list3 = [4, 5, 6]; // ✅ Can reassign the variable... wait, no! // Actually, this is an error because list3 is final
Immutability in Flutter State Management
Why Immutability Matters in Flutter
Flutter rebuilds widgets when object references change, not when object contents change. Immutable state makes this detection trivial.
BLoC Pattern with Immutable State
dartimport 'package:flutter_bloc/flutter_bloc.dart'; // Immutable state class class CounterState { final int count; const CounterState(this.count); // Create new state with updated value CounterState copyWith({int? count}) { return CounterState(count ?? this.count); } } // Events abstract class CounterEvent {} class IncrementEvent extends CounterEvent {} class DecrementEvent extends CounterEvent {} // BLoC class CounterBloc extends Bloc<CounterEvent, CounterState> { CounterBloc() : super(CounterState(0)) { on<IncrementEvent>((event, emit) { // Emit NEW state, don't modify existing emit(CounterState(state.count + 1)); }); on<DecrementEvent>((event, emit) { emit(CounterState(state.count - 1)); }); } }
Provider with Immutable Data
dartimport 'package:flutter/foundation.dart'; class TodoState { final List<String> todos; const TodoState(this.todos); // Return new state with updated list TodoState addTodo(String todo) { return TodoState([...todos, todo]); // New list } TodoState removeTodo(int index) { final newTodos = List<String>.from(todos); newTodos.removeAt(index); return TodoState(newTodos); // New state } } class TodoProvider extends ChangeNotifier { TodoState _state = const TodoState([]); TodoState get state => _state; void addTodo(String todo) { _state = _state.addTodo(todo); // Replace with new state notifyListeners(); } void removeTodo(int index) { _state = _state.removeTodo(index); // Replace with new state notifyListeners(); } }
Creating Immutable Collections
Unmodifiable List
dartimport 'dart:collection'; // Create unmodifiable list from mutable list final mutableList = [1, 2, 3]; final immutableList = UnmodifiableListView(mutableList); // immutableList.add(4); // Error: Unsupported operation mutableList.add(4); // ✅ OK: Original list is still mutable print(immutableList); // Output: [1, 2, 3, 4] - reflects original
Unmodifiable Map
dartfinal mutableMap = {'a': 1, 'b': 2}; final immutableMap = UnmodifiableMapView(mutableMap); // immutableMap['c'] = 3; // Error: Unsupported operation
Using Freezed for Immutability
dartimport 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; class User with _$User { const factory User({ required String name, required int age, required String email, }) = _User; } // Usage - completely immutable with copyWith void main() { final user1 = User(name: 'Alice', age: 30, email: 'alice@example.com'); final user2 = user1.copyWith(age: 31); // New instance print(user1.age); // 30 print(user2.age); // 31 print(user1 == user2); // false (different instances) }
Performance Considerations
When to Use Immutable
dart// ✅ Good: Infrequent updates, state management class AppConfig { final String apiUrl; final int timeout; final bool debugMode; const AppConfig({ required this.apiUrl, required this.timeout, required this.debugMode, }); }
When to Use Mutable
dart// ✅ Good: Frequent updates, large collections class ChatMessageBuffer { final List<Message> messages = []; void addMessage(Message message) { messages.add(message); // Efficient for frequent additions } void clearOldMessages() { messages.removeWhere((m) => m.timestamp.isBefore(cutoffTime)); } }
Best Practices
Prefer Immutability:
- Use for all class fields by defaulttext
final - Use constructors when possibletext
const - Implement for state updatestext
copyWith() - Use immutable state in BLoC/Provider/Riverpod
Use Mutability When:
- Performance is critical (large, frequently-updated collections)
- Working with builders or temporary data structures
- Interfacing with APIs that require mutable objects
Avoid Common Pitfalls:
- Don't expose mutable collections from immutable classes
- Don't confuse (reference immutability) with deep immutabilitytext
final - Don't create unnecessary copies of large objects
Learning Resources
- Dart Immutability Guide
- Freezed Package
- Flutter State Management Best Practices
- @immutable Annotation
Key Takeaway: In Flutter, prefer immutability for state management. It makes apps more predictable, easier to debug, and naturally supports Flutter's reactive rebuild mechanism. Use
fields,textfinalconstructors, and code generation tools like Freezed to enforce immutability without boilerplate.textconst