In Dart, when a Cart class exposes its items read-only, how does returning List.unmodifiable(_items) differ from returning UnmodifiableListView(_items)?
answer
- snapshot versus window
- copy is O(n) per call
- view is O(1), reads through
- view shows later cart changes
- both shallow: items still mutable
basics
~20 sList.unmodifiable copies the items into a new frozen list, so each call costs O(n) and callers hold a snapshot; UnmodifiableListView wraps the private list in O(1), so callers see later additions. Both block list writes, and neither freezes the items themselves.
solid answer
~40 sBoth stop callers from calling `add`, `remove` or `[]=` on what they receive, but they differ in **when the data is read**. `List.unmodifiable(_items)` copies every element into a new unmodifiable list: O(n) per call, and the caller's result is a **snapshot** that never changes, even when the cart does. `UnmodifiableListView(_items)` from `dart:collection` is a thin wrapper: O(1), no copy, and every read goes to the live `_items`, so a caller that kept the view sees items added later. Choose the view for a cheap live read-only window on internal state, the copy when callers need a stable value, for example to compare an old and new state. Neither protects the `CartItem` objects: if they have mutable fields, callers can still change them.
code
dart · 24 linesimport 'dart:collection';
class CartItem {
CartItem(this.name);
final String name;
}
class Cart {
final List<CartItem> _items = [];
List<CartItem> get snapshot => List.unmodifiable(_items);
late final UnmodifiableListView<CartItem> items = UnmodifiableListView(_items);
void add(CartItem item) => _items.add(item);
}
void main() {
final cart = Cart()..add(CartItem('tea'));
final before = cart.snapshot;
final live = cart.items;
cart.add(CartItem('milk'));
print(before.length); // 1: the copy is a snapshot
print(live.length); // 2: the view reads through
}go deeper
Know that both options stop add and remove, and that one copies while the other wraps the original list.
Explain snapshot versus live view with their O(n) and O(1) costs, and show why both are shallow for mutable CartItem objects.
Choose per call site: views for cheap getters on mutable models, copies for state comparison and iteration during changes; cache hot views.
Decide whether models should be mutable with views or immutable with copy-on-write, weighing allocation cost against the class of bugs each design rules out.
## The setup A shopping cart keeps its items in a private growable list and exposes them read-only: ```dart import 'dart:collection'; class Cart { final List<CartItem> _items = []; // Option A: a copy List<CartItem> get itemsCopy => List.unmodifiable(_items); // Option B: a view UnmodifiableListView<CartItem> get itemsView => UnmodifiableListView(_items); void add(CartItem item) => _items.add(item); } ``` Both getters prevent `cart.itemsCopy.add(...)` and `cart.itemsView.add(...)`: each throws `UnsupportedError`. The differences are in **cost**, **freshness** and **identity**. ## What each one does **`List.unmodifiable(_items)`** creates a new list, copies every element reference into it, and marks it unmodifiable. - Cost: O(n) time and memory on **every** call to the getter. - Freshness: a **snapshot**. Items added to the cart afterwards do not appear in it. - Independence: later changes to `_items` cannot affect it. **`UnmodifiableListView(_items)`** creates a small wrapper object that forwards `length` and `[]` to `_items` and throws on writes. - Cost: O(1); nothing is copied. - Freshness: **live**. A caller that stored the view sees items added later. - Coupling: the view is only as stable as the list behind it. | Aspect | `List.unmodifiable` | `UnmodifiableListView` | |---|---|---| | Copies elements | yes, O(n) | no, O(1) | | Sees later cart changes | no | yes | | Blocks list writes | yes | yes | | Protects element objects | no | no | | Library | `dart:core` | `dart:collection` | ## Live views have a sharp edge Because a view reads through, iterating it while the cart changes behaves exactly like iterating `_items` while it changes: adding to or removing from a list during a `for`-in loop over it throws a `ConcurrentModificationError`. If callers may iterate while something else calls `cart.add`, give them a copy. A view can also surprise code that compares states. If a caller stores `previous = cart.itemsView` and later compares it with the current contents, both read the same live list, so "previous" has silently become current. A snapshot copy is what makes before-and-after comparisons meaningful. ## Neither one freezes the items Both options are **shallow**. They freeze the list's length and slots, not the objects in the slots. If `CartItem` has a mutable `int quantity`, then `cart.itemsView.first.quantity = 99` succeeds with either option. To protect the items too: 1. make `CartItem` immutable, with `final` fields and a `copyWith` that returns a new item; 2. change quantities through `Cart` methods that replace the item in `_items`. ## Why not just return `Iterable<CartItem>`? A common shortcut is `Iterable<CartItem> get items => _items;`. It hides `add` from the analyzer, but the object handed out is still the private growable list: - `(cart.items as List<CartItem>).add(x)` compiles and succeeds, mutating the cart's internals; - a caller can keep the reference and see later changes, like a view, but without any write protection; - callers who need indexing must call `toList()`, paying for a copy anyway. Returning an unmodifiable object is what actually enforces read-only access. A narrower return type can sit on top of it as documentation, but it is not a substitute. ## Choosing - **Default to the view** for a getter on a mutable model: cheap, read-only, and always current. - **Use a copy** when the caller needs a stable value: an old state to compare against, or a list to iterate while the cart may change. - **Cache the view** if the getter is hot: `late final UnmodifiableListView<CartItem> items = UnmodifiableListView(_items);` creates one wrapper that stays valid for the cart's lifetime. - In Flutter, a `ChangeNotifier` cart that exposes a view and calls `notifyListeners()` after each change is a common, correct shape (the Flutter docs' simple state-management `CartModel` does exactly this): widgets rebuild and read the current items through the view.
- How would you stop callers of a Dart Cart from changing an item's quantity, not just the list?Make `CartItem` immutable, with `final` fields and a `copyWith`, and route quantity changes through a `Cart` method that replaces the item in the private list. An unmodifiable list or view only freezes the slots; it cannot stop writes to mutable objects inside them.
- Why might a Dart cart getter cache its UnmodifiableListView instead of creating one per call?Creating a view is cheap but still allocates a wrapper on every access. Because the view reads through to the private list, one instance stays correct for the object's lifetime, so `late final items = UnmodifiableListView(_items)` gives callers the same object each time and avoids repeated allocations.
saying these in an interview costs you the question
- UnmodifiableListView copies the list so later changes are not visible
- List.unmodifiable is as cheap as a view because it is lazy
- Exposing items read-only also stops callers changing an item's fields
- A view is always safer than a copy for comparing old and new state
- UnmodifiableListView lives in dart:core alongside List