Created
June 21, 2023 05:47
-
-
Save KDCinfo/fb23ff106a30512154984e22ba87dcb8 to your computer and use it in GitHub Desktop.
Files from Dart's codelab: Dive into Dart 3's OO Language Enhancements including patterns, records, enhanced switch and case, and sealed classes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
/// In this codelab, you simplify that more-realistic use case by | |
/// mocking incoming JSON data with a multi-line string in the documentJson variable. | |
/// | |
/// https://codelabs.developers.google.com/codelabs/dart-patterns-records#5 | |
/// | |
class Document { | |
final Map<String, Object?> _json; | |
Document() : _json = jsonDecode(documentJson); | |
/// Summary: | |
/// Records are comma-delimited field lists enclosed in parentheses. | |
/// Record fields can each have a different type, so records can collect multiple types. | |
/// Records can contain both named and positional fields, like argument lists in a function. | |
/// Records can be returned from a function, so they enable you to return multiple values from a function call. | |
/// | |
/// The other type-safe way to return different types of data is to define a class, which is more verbose. | |
/// | |
(String, {DateTime modified}) get metadata { | |
// const title = 'My Document'; | |
// final now = DateTime.now(); | |
// return (title, modified: now); | |
/// In certain contexts, patterns don't only match and destructure but can also | |
/// make a decision about what the code does, based on whether or not the pattern matches. | |
/// These are called `refutable` patterns. | |
/// | |
/// The variable declaration pattern you used in the last step is an `irrefutable` pattern: | |
/// the value must match the pattern or it's an error and destructuring won't happen. | |
/// Think of any variable declaration or assignment; | |
/// you can't assign a value to a variable if they're not the same type. | |
/// | |
/// Refutable patterns, on the other hand, are used in control flow contexts: | |
/// - They expect that [some values they compare against will not match]. | |
/// - They are meant to [influence the control flow], based on whether or not the value matches. | |
/// - They [don't interrupt execution with an error] if they don't match, they just move to the next statement. | |
/// - They can destructure and [bind variables that are only usable when they match] | |
/// This code validates that the data is structured correctly without using patterns. [Change #1] | |
// if (_json.containsKey('metadata')) { // Modify from here... | |
// final metadataJson = _json['metadata']; | |
// if (metadataJson is Map) { | |
// final title = metadataJson['title'] as String; | |
// final localModified = | |
// DateTime.parse(metadataJson['modified'] as String); | |
// return (title, modified: localModified); | |
// } | |
// } | |
// throw const FormatException('Unexpected JSON'); // to here | |
/// With a refutable pattern, you can verify that the JSON has | |
/// the expected structure using a map pattern. [Change #2] | |
/// | |
/// Here, you see a new kind of if-statement (introduced in Dart 3), the if-case. | |
/// The case body only executes if the case pattern matches the data in _json. | |
if (_json // Modify from here... | |
/// If the value doesn't match, | |
/// the pattern refutes (refuses to continue execution) and proceeds to the else clause. | |
/// | |
/// For a full list of patterns, see the table in the Patterns section of the feature specification. | |
/// https://github.com/dart-lang/language/blob/main/accepted/future-releases/0546-patterns/feature-specification.md#patterns | |
case { | |
'metadata': { | |
'title': String title, | |
'modified': String localModified, | |
} | |
}) { | |
return (title, modified: DateTime.parse(localModified)); | |
} else { | |
throw const FormatException('Unexpected JSON'); | |
} // to here | |
} | |
/// The `getBlocks()` function returns a list of `Block` objects, which you use later | |
/// in order to build the UI. A familiar `if-case` statement performs validation and | |
/// casts the value of the `blocks`' metadata into a new `List` named `blocksJson`. | |
List<SealedBlock> getBlocks() { | |
// Add from here... [Change #3] | |
if (_json case {'blocks': List blocksJson}) { | |
/// Without patterns, you'd need the `toList()` method to cast. | |
return [ | |
/// The list literal contains a collection for in order to fill the new list with Block objects. | |
/// https://dart.dev/language/collections#collection-operators | |
for (final blockJson in blocksJson) SealedBlock.fromJson(blockJson), | |
]; | |
} else { | |
throw const FormatException('Unexpected JSON format'); | |
} | |
} // to here. | |
// I kept these similar functions separate to keep the flows of the codelab clear. | |
List<UnsealedBlock> getUnsealedBlocks() { | |
if (_json case {'blocks': List blocksJson}) { | |
return [ | |
for (final blockJson in blocksJson) UnsealedBlock.fromJson(blockJson), | |
]; | |
} else { | |
throw const FormatException('Unexpected JSON format'); | |
} | |
} | |
} | |
class UnsealedBlock { | |
UnsealedBlock(this.type, this.text); | |
final String type; | |
final String text; | |
factory UnsealedBlock.fromJson(Map<String, dynamic> json) { | |
/// Notice that the json matches the map pattern, even though | |
/// one of the keys, `checked`, is not accounted for in the pattern. | |
/// | |
/// Map patterns ignore any entries in the map object | |
/// that aren't explicitly accounted for in the pattern. | |
if (json case {'type': final type, 'text': final text}) { | |
return UnsealedBlock(type, text); | |
} else { | |
throw const FormatException('Unexpected JSON format'); | |
} | |
} | |
} | |
/// The `sealed` keyword is a class modifier that means you can | |
/// only `extend` or `implement` this class **in the same library**. | |
/// | |
/// Since the analyzer knows the subtypes of this class, it reports | |
/// an error if a switch fails to cover one of them and isn't exhaustive. | |
sealed class SealedBlock { | |
SealedBlock(); | |
factory SealedBlock.fromJson(Map<String, Object?> json) { | |
/// | |
/// WARNING: If you remove one of these cases, | |
/// the app will run-time error in the | |
/// debug console in a potentially infinite loop. | |
/// | |
return switch (json) { | |
{'type': 'h1', 'text': String text} => HeaderBlock(text), | |
{'type': 'p', 'text': String text} => ParagraphBlock(text), | |
{'type': 'checkbox', 'text': String text, 'checked': bool checked} => | |
CheckboxBlock(text, checked), | |
_ => throw const FormatException('Unexpected JSON format'), | |
}; | |
} | |
} | |
class HeaderBlock extends SealedBlock { | |
final String text; | |
HeaderBlock(this.text); | |
} | |
class ParagraphBlock extends SealedBlock { | |
final String text; | |
ParagraphBlock(this.text); | |
} | |
class CheckboxBlock extends SealedBlock { | |
final String text; | |
final bool isChecked; | |
CheckboxBlock(this.text, this.isChecked); | |
} | |
const documentJson = ''' | |
{ | |
"metadata": { | |
"title": "My Document", | |
"modified": "2023-05-10" | |
}, | |
"blocks": [ | |
{ | |
"type": "h1", | |
"text": "Chapter 1" | |
}, | |
{ | |
"type": "p", | |
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." | |
}, | |
{ | |
"type": "checkbox", | |
"checked": true, | |
"text": "Learn Dart 3" | |
} | |
] | |
} | |
'''; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
import 'data.dart'; | |
void main() { | |
runApp(const DocumentApp()); | |
} | |
/// Codelab: Dive into Dart 3's new Records and Patterns. | |
/// | |
/// > https://codelabs.developers.google.com/codelabs/dart-patterns-records | |
/// > `flutter create --empty patterns_codelab` | |
/// | |
/// > In this course you will experiment with patterns, records, enhanced switch and case, and sealed classes. | |
/// > You will cover a lot of information --- yet only barely scratch the surface of these features. | |
/// | |
/// - Dart 3 introduces patterns to the language, a major new category of grammar. | |
/// Beyond this new way to write Dart code, | |
/// there are several other language enhancements, including | |
/// | |
/// - Records for bundling data of different types, | |
/// - class modifiers for controlling access, and | |
/// - new switch expressions and if-case statements. | |
/// | |
/// For more information on patterns, see the feature specification. | |
/// https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md | |
/// | |
/// For more specific information, see the individual feature specifications: | |
/// - Records => https://github.com/dart-lang/language/blob/master/accepted/future-releases/records/records-feature-specification.md<br> | |
/// - Flutter Bloc analogy of Dart 3 Records --- 'cubits for classes'. | |
/// - Patterns => https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/feature-specification.md<br> | |
/// - Exhaustiveness checking => https://github.com/dart-lang/language/blob/master/accepted/future-releases/0546-patterns/exhaustiveness.md<br> | |
/// - Sealed classes => https://github.com/dart-lang/language/blob/master/accepted/future-releases/sealed-types/feature-specification.md | |
class DocumentApp extends StatelessWidget { | |
const DocumentApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData(useMaterial3: true), | |
home: DocumentScreen( | |
document: Document(), | |
), | |
); | |
} | |
} | |
class DocumentScreen extends StatelessWidget { | |
final Document document; | |
const DocumentScreen({ | |
required this.document, | |
super.key, | |
}); | |
/// Switch on 'an object pattern' and 'destructure object properties' | |
/// to enhance the date rendering logic of your UI. | |
/// | |
/// This method returns a `switch expression` that | |
/// switches on the value `differenceObj`, a Duration object. | |
/// | |
String formatDate(DateTime dateTime) { | |
final today = DateTime.now(); | |
final differenceObj = dateTime.difference(today); | |
// final differenceObj = dateTime.difference(today).inDays; | |
// final differenceObj = dateTime.difference(today).isNegative; | |
return switch (differenceObj) { | |
Duration(inDays: 0) => 'today', | |
Duration(inDays: 1) => 'tomorrow', | |
Duration(inDays: -1) => 'yesterday', | |
// | |
// Add from here - Modify #5 | |
/// | |
/// This code introduces guard clauses: | |
/// - A guard clause uses the `when` keyword after a case pattern. | |
/// - They can be used in `if-cases`, `switch statements`, and `switch expressions`. | |
/// - They only add a condition to a pattern after it's matched. | |
/// - If the guard clause evaluates to false, | |
/// the entire pattern is refuted, and execution proceeds to the next case. | |
/// | |
/// --- `(inDays: final days)` --> Tricky, tricky!! | |
/// | |
Duration(inDays: final daysA) when daysA > 7 => '${daysA ~/ 7} weeks from now', | |
Duration(inDays: final daysB) when daysB < -7 => '${daysB.abs() ~/ 7} weeks ago', | |
// to here. | |
Duration(inDays: final daysC, isNegative: true) => '${daysC.abs()} days ago', | |
Duration(inDays: final daysD) => '$daysD days from now', | |
/// Notice that you didn't use a wildcard or default case at the end of this switch. | |
/// Though it's good practice to always include a case for values that might fall through, | |
/// it's ok in a simple example like this since you know the cases you defined account for | |
/// all of the possible values inDays could potentially take. | |
/// | |
/// When every case in a switch is handled, it's called an exhaustive switch. | |
/// For example, switching on a bool type is exhaustive when it has cases for true and false. | |
/// Switching on an enum type is exhaustive when there are cases for each of the | |
/// enum's values, too, because enums represent a fixed number of constant values. | |
/// | |
/// Dart 3 extended exhaustiveness checking to objects and class hierarchies | |
/// with the new class modifier `sealed`. | |
}; | |
} | |
@override | |
Widget build(BuildContext context) { | |
// final metadataRecord = document.metadata; // Add this line. #1 | |
// final (title, modified: modified) = document.metadata; // Modify #2 | |
final (title, :modified) = document.metadata; // Modify #3 | |
final formattedModifiedDate = formatDate(modified); // Add this line - Modify #6 | |
DateTime dateTimeNow = DateTime.now(); | |
// bool useSealed = dateTimeNow.difference(dateTimeNow).inDays == 0; | |
bool useSealed = dateTimeNow.difference(dateTimeNow).inDays != 0; | |
final blocks = document.getBlocks(); // Modify #4 | |
final unsealedBlocks = document.getUnsealedBlocks(); // Modify #4 | |
return Scaffold( | |
appBar: AppBar( | |
centerTitle: true, | |
// title: Text(metadataRecord.$1), // Modify this line, #1 | |
title: Text(title), // Modify #2 | |
), | |
body: Column( | |
children: [ | |
Center( | |
child: Text( | |
// 'Last modified ${metadataRecord.modified}', // And this one. #1 | |
// 'Last modified $modified', // Modify #2 | |
// 'Last modified ${formatDate(modified)}', // Modify #6 [Me] | |
'Last modified $formattedModifiedDate', // Modify #6 [codelab] (better approach!) | |
), | |
), | |
// | |
const Text( | |
'Approach #1 - Uses spread operator for small lists', | |
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), | |
), | |
const Text( | |
'Not efficient for large lists. ' | |
'Flutter will create and keep all widgets in memory, even if they are not currently displayed on screen.' | |
'NOTE: `Expanded would work better with smaller lists.', | |
style: TextStyle(color: Colors.blue), | |
), | |
if (useSealed) ...blocks.map((block) => SealedBlockWidget(block: block)), | |
if (!useSealed) ...unsealedBlocks.map((block) => UnsealedBlockWidget(block: block)), | |
// | |
const Text( | |
'Approach #2 - Uses ListView.builder for larger lists', | |
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), | |
), | |
const Text( | |
'More efficient for large lists. ' | |
'ListView.builder only creates widgets that are actually visible on the screen. ' | |
'Flutter will automatically recycle the widgets when they are scrolled off-screen.', | |
style: TextStyle(color: Colors.blue), | |
), | |
const Text( | |
'NOTE: The Expanded widget forces its child to fill available vertical space, ' | |
'which can lead to layout issues when used with scrollable widgets like ListView.builder in a Column. ' | |
'Instead, consider using Flexible or ensuring your list is not within an infinitely heighted parent, ' | |
'or if Column is necessary, ensure ListView.builder is the last widget in your children list.', | |
style: TextStyle(color: Colors.blue), | |
), | |
// Expanded( | |
Flexible( | |
child: ColoredBox( | |
// color: Colors.blueAccent, | |
color: Colors.transparent, | |
child: ListView.builder( | |
itemCount: blocks.length, | |
itemBuilder: (context, index) => useSealed | |
? Center(child: SealedBlockWidget(block: blocks[index])) | |
: Center(child: UnsealedBlockWidget(block: unsealedBlocks[index])), | |
), | |
), | |
), // to here. | |
// const Text( | |
// 'Test 3', | |
// style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), | |
// ), | |
], | |
), | |
); | |
} | |
} | |
class SealedBlockWidget extends StatelessWidget { | |
/// The `sealed` keyword is a class modifier that means you can only extend or implement | |
/// this class **in the same library**. Since the analyzer knows the subtypes of this class, | |
/// it reports an error if a `switch` fails to cover one of them and isn't exhaustive. | |
final SealedBlock block; | |
const SealedBlockWidget({ | |
required this.block, | |
super.key, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
/// | |
/// Using `exhaustive switching`. | |
return Container( | |
color: ColorsX.random(), | |
margin: const EdgeInsets.all(8), | |
child: switch (block) { | |
/// Note that using a switch expression here lets you | |
/// pass the result directly to the child element, as | |
/// opposed to the separate return statement needed before. | |
/// (Unsure what this means because the `UnsealedBlockWidget` | |
/// just has a `Text(block.text)` as the child element.) | |
/// | |
HeaderBlock(:final text) => Text( | |
text, | |
style: Theme.of(context).textTheme.displayMedium, | |
), | |
ParagraphBlock(:final text) => Text(text), | |
CheckboxBlock(:final text, :final isChecked) => Row( | |
children: [ | |
Checkbox(value: isChecked, onChanged: (_) {}), | |
Text(text), | |
], | |
), | |
}); | |
} | |
} | |
class UnsealedBlockWidget extends StatelessWidget { | |
final UnsealedBlock block; | |
const UnsealedBlockWidget({ | |
required this.block, | |
super.key, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
TextStyle? textStyle; | |
/// Switch statement. | |
// switch (block.type) { | |
// case 'h1': | |
// textStyle = Theme.of(context).textTheme.displayMedium; | |
// case 'p' || 'checkbox': | |
// textStyle = Theme.of(context).textTheme.bodyMedium; | |
// case _: | |
// textStyle = Theme.of(context).textTheme.bodySmall; | |
// } | |
/// Switch expression. | |
/// | |
/// Unlike switch statements, switch expressions return a value and can be used anywhere an expression can be used. | |
/// | |
/// IMO | |
/// - More concise than longer `if` conditionals, although could get a bit ugly. | |
/// - Perhaps as much a competitor to ternaries. | |
/// | |
/// Using `non-exhaustive switching`. | |
textStyle = switch (block.type) { | |
'h1' => Theme.of(context).textTheme.displayMedium, | |
'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium, | |
_ => Theme.of(context).textTheme.bodySmall | |
}; | |
/// if-case statement chain. | |
// if (block.type case 'h1') { | |
// textStyle = Theme.of(context).textTheme.displayMedium; | |
// } else if (block.type case 'p' || 'checkbox') { | |
// textStyle = Theme.of(context).textTheme.bodyMedium; | |
// } else if (block.type case _) { | |
// textStyle = Theme.of(context).textTheme.bodySmall; | |
// } | |
/// Using `non-exhaustive switching`. | |
return Container( | |
color: ColorsX.random(), | |
margin: const EdgeInsets.all(8), | |
child: Text( | |
block.text, | |
style: textStyle, | |
), | |
); | |
} | |
} | |
extension ColorsX on Colors { | |
static Color random() { | |
// return Color((math.Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(0.3); // GCP | |
return Color((math.Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(0.3); // SO | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment