Lesson 20 · 20 of 20
Project: order processing · Dart
Combine Dart sealed classes, generics, async and JSON in a small order processing system: the logic that sits behind a Flutter app.

This program brings the course together in a small order processing layer, the kind a Flutter app keeps underneath its screens. It starts with a sealed, generic `Result<T>` type: an operation ends either in Ok, holding a value, or in Err, holding a reason. That way expected failures (out of stock, payment declined) are data you handle, not exceptions that escape.
Order.fromJson uses an initializer list and, inside it, a for with a list pattern: each JSON line is an array [sku, qty, cents] that is destructured and stored as a (String, int, int) record. total adds up quantity times price with fold, and toJson returns the summary to be sent on later.
process is the simulated asynchronous service: it waits 10 ms per line, looks for lines whose product lacks enough units in the stock map, and returns an Err if it finds one or if the total exceeds 100 euros (10000 cents); otherwise, an Ok. Being async, its type is Future<Result<Order>>.
main decodes the orders and processes all of them in parallel with Future.wait(orders.map(process)). Even though each takes a different time, the results arrive in order, so the output never changes. The switch over the sealed Result is exhaustive and extracts value or reason; then if (r case Ok(:final value)) keeps the accepted ones and jsonEncode produces the report, calling each order's toJson().
Classes, records, patterns, generics, async and JSON, all in 34 lines. In DMarlon Studio, Dart is the language of the Flutter targets, and this is exactly the kind of code that goes into the model and logic layer of those apps, kept apart from the widgets that display it.
Code · main.dart
import 'dart:convert';// Either a value or the reason it failed: a switch must handle both.sealed class Result<T> {}class Ok<T> extends Result<T> { final T value; Ok(this.value); }class Err<T> extends Result<T> { final String reason; Err(this.reason); }class Order { final String id; final List<(String sku, int qty, int cents)> lines; Order.fromJson(Map<String, dynamic> json) : id = json['id'], lines = [for (final [String sku, int qty, int cents] in json['lines']) (sku, qty, cents)]; int get total => lines.fold(0, (sum, line) => sum + line.$2 * line.$3); Map<String, dynamic> toJson() => {'id': id, 'total': total};}const stock = {'MUG': 5, 'TEE': 0, 'CAP': 3};Future<Result<Order>> process(Order order) async { // a simulated service await Future.delayed(Duration(milliseconds: 10 * order.lines.length)); final missing = order.lines.where((line) => (stock[line.$1] ?? 0) < line.$2); if (missing.isNotEmpty) return Err('${order.id}: no stock of ${missing.first.$1}'); return order.total > 10000 ? Err('${order.id}: payment declined') : Ok(order);}const incoming = '[{"id": "A-1", "lines": [["MUG", 2, 1250]]}, {"id": "A-2", "lines": [["CAP", 1, 1500], ' '["TEE", 1, 1999]]}, {"id": "A-3", "lines": [["CAP", 3, 4000]]}, {"id": "A-4", "lines": [["MUG", 1, 1250]]}]';Future<void> main() async { final orders = [for (final json in jsonDecode(incoming)) Order.fromJson(json)]; final results = await Future.wait(orders.map(process)); // all in parallel for (final result in results) print(switch (result) { Ok(:final value) => 'OK ${value.id}: ${value.total} cents', Err(:final reason) => 'ERROR $reason', }); final accepted = [for (final r in results) if (r case Ok(:final value)) value]; print(jsonEncode({'accepted': accepted, 'revenue': accepted.fold(0, (s, o) => s + o.total)}));}Output · dart run main.dart
OK A-1: 2500 cents
ERROR A-2: no stock of TEE
ERROR A-3: payment declined
OK A-4: 1250 cents
{"accepted":[{"id":"A-1","total":2500},{"id":"A-4","total":1250}],"revenue":3750}Key points
- A sealed
Resultturns expected failures into data. - List patterns read compact JSON safely.
Future.waitprocesses in parallel and keeps the order.- The exhaustive
switchforces you to handle both success and failure. toJsonandjsonEncodeprepare the output for another API.
Exercise
Add a rejected field to the final report with the list of reasons for the rejected orders, gathered with another if (r case Err(:final reason)).