Skip to content

Instantly share code, notes, and snippets.

@KDCinfo
Created June 21, 2023 05:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KDCinfo/fb23ff106a30512154984e22ba87dcb8 to your computer and use it in GitHub Desktop.
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.
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"
}
]
}
''';
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