Dive into Dart's patterns and records

    1. Introduction

    Dart 3 introducespatterns 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
    • newswitch expressions andif-case statements.

    These features expand the choices you have when writing Dart code. In this codelab, you learn how to use them to make your code more compact, streamlined, and flexible.

    This codelab assumes you have some familiarity with Flutter and Dart. If you feel a little rusty, consider brushing up on the basics with the following resources:

    What you'll build

    This codelab creates an application that displays a JSON document in Flutter. The application simulates JSON coming from an external source. The JSON contains document data such as the modification date, title, headers, and paragraphs. You write code to neatly pack data into records so that it can be transferred and unpacked wherever your Flutter widgets need it.

    You then use patterns to build the appropriate widget when the value matches that pattern. You also see how to use patterns to destructure data into local variables.

    The final application you build in this codelab, a document with a title, the last modification date, headers and paragraphs.

    What you'll learn

    • How to create a record that stores multiple values with different types.
    • How to return multiple values from a function using a record.
    • How to use patterns to match, validate, and destructure data from records and other objects.
    • How to bind pattern-matched values to new or existing variables.
    • How to use new switch statement capabilities, switch expressions, and if-case statements.
    • How to take advantage ofexhaustiveness checking to ensure that every case is handled in a switch statement or switch expression.

    2. Set up your environment

    1. Install theFlutter SDK.
    2. Set up an editor such as Visual Studio Code (VS Code).
    3. Go through thePlatform setup steps for at least one target platform (iOS, Android, Desktop, or a web browser).

    3. Create the project

    Before diving into patterns, records, and other new features, take a moment to create a Flutter project for which you write all your code.

    Create a Flutter project

    1. Use theflutter create command to create a new project namedpatterns_codelab. The--empty flag prevents the creation of the standard counter app in thelib/main.dart file, which you'd have to remove anyway.
    flutter create --empty patterns_codelab
    1. Then, open thepatterns_codelab directory using VS Code.
    code patterns_codelab

    VS Code displaying the project created

    Set the minimum SDK version

    • Set the SDK version constraint for your project to depend on Dart 3 or above.

    pubspec.yaml

    environment:sdk:^3.0.0

    Did you know? This is your first introduction to Dart 3! The caret^ syntax can now be used for SDK constraints (although it has been supported for packages since Dart 1.8). Previously, this would be written>=3.0.0<4.0.0.

    4. Set up the project

    In this step, you create or update two Dart files:

    • Themain.dart file that contains widgets for the app, and
    • Thedata.dart file that provides the app's data.

    You will continue modifying both of these files in the subsequent steps.

    Define the data for the app

    • Create a new file,lib/data.dart, and add the following code to it:

    lib/data.dart

    import'dart:convert';classDocument{finalMap<String,Object?>_json;Document():_json=jsonDecode(documentJson);}constdocumentJson='''{  "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": false,      "text": "Learn Dart 3"    }  ]}''';

    Imagine a program receiving data from an external source, like an I/O stream or HTTP request. In this codelab, you simplify that more-realistic use case by mocking incoming JSON data with a multi-line string in thedocumentJson variable.

    The JSON data is defined in theDocument class. Later in this codelab, you add functions that return data from the parsed JSON. This class defines and initializes the_json field in its constructor.

    Did you know? You can pressCommand orControl and click functions, classes, and libraries in VS Code to see where they are defined.

    Try doing this onjsonDecode and see the editor open to the declaration in thedart:convert library.

    Run the app

    Theflutter create command creates thelib/main.dart file as part of the default Flutter file structure.

    1. To create a starting point for the application, replace the contents ofmain.dart with the following code:

    lib/main.dart

    import'package:flutter/material.dart';import'data.dart';voidmain(){runApp(constDocumentApp());}classDocumentAppextendsStatelessWidget{constDocumentApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(theme:ThemeData(),home:DocumentScreen(document:Document()),);}}classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:constText('Title goes here')),body:constColumn(children:[Center(child:Text('Body goes here'))]),);}}

    You added the following two widgets to the app:

    • DocumentApp sets up the latest version ofMaterial Design for theming the UI.
    • DocumentScreen provides the visual layout of the page using theScaffold widget.

    Tip: Splitting your UI into separate widgets reduces the size of yourbuild() methods.

    1. To make sure everything is running smoothly, run the app on your host machine by clickingRun and Debug:

    The 'Run and debug' button

    1. By default, Flutter chooses whichever target platform is available. To change the target platform, select the current platform on the Status Bar:

    The target platform selector in VS Code

    You should see an empty frame with thetitle andbody elements defined in theDocumentScreen widget:

    The application built in this step.

    5. Create and return records

    In this step, you use records to return multiple values from a function call. Then, you call that function in theDocumentScreen widget to access the values and reflect them in the UI.

    Create and return a record

    • Indata.dart, add a new getter method to the Document class calledmetadata that returns a record:

    lib/data.dart

    import'dart:convert';classDocument{finalMap<String,Object?>_json;Document():_json=jsonDecode(documentJson);(String,{DateTimemodified})getmetadata{//Addfromhere...consttitle='My Document';finalnow=DateTime.now();return(title,modified:now);}//tohere.}

    The return type for this function is a record with two fields, one with the typeString, and the other with the typeDateTime.

    The return statement constructs a new record by enclosing the two values in parenthesis,(title, modified: now).

    The first field is positional and unnamed, and the second field is namedmodified.

    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.

    Access record fields

    1. In theDocumentScreen widget, call themetadata getter method in thebuild method so that you can get your record and access its values:

    lib/main.dart

    classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){finalmetadataRecord=document.metadata;//Addthisline.returnScaffold(appBar:AppBar(title:Text(metadataRecord.$1)),//Modifythisline,body:Column(children:[//Andthefollowingline.Center(child:Text('Last modified ${metadataRecord.modified}')),],),);}}

    Themetadata getter method returns a record, which is assigned to the local variablemetadataRecord. Records are a light and easy way to return multiple values from a single function call and assign them to a variable.

    To access the individual fields composed in that record, you can use records built-in getter syntax.

    • To get a positional field (a field without a name, liketitle), use the getter on the record. This returns only unnamed fields.
    • Named fields likemodified don't have a positional getter, so you can use its name directly, likemetadataRecord.modified.

    To determine the name of a getter for a positional field, start at$1 and skip named fields. For example:

    varrecord=(named:'v','y',named2:'x','z');print(record.$1);//printsyprint(record.$2);//printsz
    1. Hot reload to see the JSON values displayed in the app. The VS Code Dart plugin hot-reloads every time you save a file.

    A screenshot of the app, which displays the title and modified date.

    You can see that each field did, in fact, maintain its type.

    • TheText() method takes a String as its first argument.
    • Themodified field is a DateTime, and is converted into aString usingstring interpolation.

    The other type-safe way to return different types of data is to define a class, which is more verbose.

    6. Match and destructure with patterns

    Records can efficiently collect different types of data and easily pass it around. Now, improve your code usingpatterns.

    A pattern represents a structure that one or more values can take, like a blueprint. Patterns compare against actual values to determine if theymatch.

    Some patterns, when they match,destructure the matched value by pulling data out of it. Destructuring lets you unpack values from an object to assign them to local variables, or perform further matching on them.

    Destructure a record into local variables

    1. Refactor thebuild method ofDocumentScreen to callmetadata and use it to initialize apattern variable declaration:

    lib/main.dart

    classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){final(title,modified:modified)=document.metadata;//ModifyreturnScaffold(appBar:AppBar(title:Text(title)),//Modifyfromhere...body:Column(children:[Center(child:Text('Last modified $modified'))]),);//Tohere.}}

    The record pattern(title, modified: modified) contains twovariable patterns that match against the fields of the record returned bymetadata.

    • The expression matches the subpattern because the result is a record with two fields, one of which is namedmodified.
    • Because they match, the variable declaration pattern destructures the expression, accessing its values and binding them to new local variables of the same types and names,String title andDateTime modified.

    There is a shorthand for when the name of a field and the variable populating it are the same. Refactor thebuild method ofDocumentScreen as follows.

    lib/main.dart

    classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){final(title,:modified)=document.metadata;//ModifyreturnScaffold(appBar:AppBar(title:Text(title)),body:Column(children:[Center(child:Text('Last modified $modified'))]),);}}

    The syntax of the variable pattern:modified is shorthand formodified: modified. If you want a new local variable of a different name, you can writemodified: localModified instead.

    1. Hot reload to see the same result as in the previous step. The behavior is exactly the same; you just made your code more concise.

    7. Use patterns to extract data

    In certain contexts, patterns don't only match and destructure but can also make adecision aboutwhat the code does, based on whether or not the pattern matches. These are calledrefutable patterns.

    The variable declaration pattern you used in the last step is anirrefutable 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:

    • Theyexpect that some values they compare against will not match.
    • They are meant toinfluence the control flow, based on whether or not the value matches.
    • Theydon'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 areonly usable when they match

    Read JSON values without patterns

    In this section, you read data without pattern matching to see how patterns can help you work with JSON data.

    • Replace the previous version ofmetadata with one that reads values from the_json map. Copy and paste this version ofmetadata into theDocument class:

    lib/data.dart

    classDocument{finalMap<String,Object?>_json;Document():_json=jsonDecode(documentJson);(String,{DateTimemodified})getmetadata{if(_json.containsKey('metadata')){//Modifyfromhere...finalmetadataJson=_json['metadata'];if(metadataJsonisMap){finaltitle=metadataJson['title']asString;finallocalModified=DateTime.parse(metadataJson['modified']asString,);return(title,modified:localModified);}}throwconstFormatException('Unexpected JSON');//tohere.}}

    This code validates that the data is structured correctly without using patterns. In a later step, you use pattern matching to perform the same validation using less code. It performs three checks before doing anything else:

    • The JSON contains the datastructure you expect:if (_json.containsKey('metadata'))
    • The data has thetype you expect:if (metadataJson is Map)
    • That the data isnot null, which is implicitly confirmed in the previous check.

    Read JSON values using a map pattern

    With a refutable pattern, you can verify that the JSON has the expected structure using amap pattern.

    • Replace the previous version ofmetadata with this code:

    lib/data.dart

    classDocument{finalMap<String,Object?>_json;Document():_json=jsonDecode(documentJson);(String,{DateTimemodified})getmetadata{if(_jsoncase{//Modifyfromhere...'metadata':{'title':Stringtitle,'modified':StringlocalModified},}){return(title,modified:DateTime.parse(localModified));}else{throwconstFormatException('Unexpected JSON');}//tohere.}}

    Here, you see a new kind of if-statement (introduced in Dart 3), theif-case. The case body only executes if the case pattern matches the data in_json. This match accomplishes the same checks you wrote in the first version ofmetadata to validate the incoming JSON. This code validates the following:

    • _json is a Map type.
    • _json contains ametadata key.
    • _json is not null.
    • _json['metadata'] is also a Map type.
    • _json['metadata'] contains the keystitle andmodified.
    • title andlocalModified are strings and aren't null.

    If the value doesn't match, thepattern refutes (refuses to continue execution) and proceeds to theelse clause. If the match is successful, the pattern destructures the values oftitle andmodified from the map and binds them to new local variables.

    For a full list of patterns, see thetable in the Patterns section of the feature specification.

    Summary: Using patterns in this step tests types, destructures, and binds values using a single statement.

    8. Prepare the app for more patterns

    So far, you address themetadata part of the JSON data. In this step, you refine your business logic a bit more in order to handle the data in theblocks list and render it into your app.

    {"metadata":{// ...},"blocks":[{"type":"h1","text":"Chapter 1"},// ...]}

    Create a class that stores data

    • Add a new class,Block, todata.dart, which is used to read and store the data for one of the blocks in the JSON data.

    lib/data.dart

    classBlock{finalStringtype;finalStringtext;Block(this.type,this.text);factoryBlock.fromJson(Map<String,dynamic>json){if(jsoncase{'type':finaltype,'text':finaltext}){returnBlock(type,text);}else{throwconstFormatException('Unexpected JSON format');}}}

    The factory constructorfromJson() uses the same if-case with a map pattern that you've seen before.

    You'll see that the JSON data looks like the expected pattern, even though it has an extra piece of information calledchecked that isn't in the pattern. This is because when you're using these kinds of patterns (called "map patterns"), they only care about the specific things you've defined in the pattern and ignore anything else in the data.

    Return a list of Block objects

    • Next, add a new function,getBlocks(), to theDocument class.getBlocks() parses the JSON into instances of theBlock class and returns a list of blocks to render in your UI:

    lib/data.dart

    classDocument{finalMap<String,Object?>_json;Document():_json=jsonDecode(documentJson);(String,{DateTimemodified})getmetadata{if(_jsoncase{'metadata':{'title':Stringtitle,'modified':StringlocalModified},}){return(title,modified:DateTime.parse(localModified));}else{throwconstFormatException('Unexpected JSON');}}List<Block>getBlocks(){//Addfromhere...if(_jsoncase{'blocks':ListblocksJson}){return[for(finalblockJsoninblocksJson)Block.fromJson(blockJson)];}else{throwconstFormatException('Unexpected JSON format');}}//tohere.}

    ThegetBlocks() function returns a list ofBlock objects, which you use later to build the UI. A familiar if-case statement performs validation and casts the value of theblocks metadata into a newList namedblocksJson (without patterns, you'd need thetoList() method to cast).

    The list literal contains acollection for in order to fill the new list withBlock objects.

    This section doesn't introduce any pattern-related features you haven't already tried in this codelab. In the next step, you prepare to render the list items in your UI.

    9. Use patterns to display the document

    You now successfully destructure and recompose your JSON data, using an if-case statement and refutable patterns. But if-case is only one of the enhancements to control flow structures that come with patterns. Now, you apply your knowledge of refutable patterns to switch statements.

    Control what's rendered using patterns with switch statements

    • Inmain.dart, create a new widget,BlockWidget, that determines the styling of each block based on itstype field.

    lib/main.dart

    classBlockWidgetextendsStatelessWidget{finalBlockblock;constBlockWidget({requiredthis.block,super.key});@overrideWidgetbuild(BuildContextcontext){TextStyle?textStyle;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;}returnContainer(margin:constEdgeInsets.all(8),child:Text(block.text,style:textStyle),);}}

    The switch statement in thebuild method switches on thetype field of theblock object.

    1. The first case statement uses aconstant string pattern. The pattern matches ifblock.type is equal to the constant valueh1.
    2. The second case statement uses alogical-or pattern with two constant string patterns as its subpatterns. The pattern matches ifblock.type matches either of the subpatternsp orcheckbox.

    Did you know?logical-and patterns (&&) can also be used.

    1. The final case is awildcard pattern,_. Wildcards in switch cases match everything else. They behave the same asdefault clauses, which are still allowed in switch statements (they're just a little more verbose).

    Wildcard patterns can be used wherever a pattern is allowed—for example, in a variable declaration pattern:var (title, _) = document.metadata;

    In this context, the wildcard doesn't bind any variable. It discards the second field.

    Note: Switch statements no longer requirebreak as of Dart 3. Non-empty cases jump to the end of the statement when they reach the end of their body.

    In the next section, you learn about more switch features after displaying theBlock objects.

    Display the document content

    Create a local variable that contains the list ofBlock objects by callinggetBlocks() in theDocumentScreen widget'sbuild method.

    1. Replace the existingbuild method inDocumentationScreen with this version:

    lib/main.dart

    classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){final(title,:modified)=document.metadata;finalblocks=document.getBlocks();//AddthislinereturnScaffold(appBar:AppBar(title:Text(title)),body:Column(children:[Text('Last modified: $modified'),//ModifyfromhereExpanded(child:ListView.builder(itemCount:blocks.length,itemBuilder:(context,index){returnBlockWidget(block:blocks[index]);},),),//tohere.],),);}}

    The lineBlockWidget(block: blocks[index]) constructs aBlockWidget widget for each item in the list of blocks returned from thegetBlocks() method.

    1. Run the application, and then you should see the blocks appearing on screen:

    The app displaying content from the 'blocks' section of the JSON data.

    10. Use switch expressions

    Patterns add a lot of capabilities toswitch andcase. To make them usable in more places, Dart hasswitch expressions. A series of cases can provide a value directly to a variable assignment or return statement.

    Convert the switch statement into a switch expression

    The Dart analyzer providesassists to help you make changes to your code.

    1. Move your cursor to the switch statement from the previous section.
    2. Click on the lightbulb to view the available assists.
    3. Select theConvert to switch expression assist.

    The 'convert to switch expression' assist available in VS Code.

    The new version of this code looks like this:

    lib/main.dart

    classBlockWidgetextendsStatelessWidget{finalBlockblock;constBlockWidget({requiredthis.block,super.key});@overrideWidgetbuild(BuildContextcontext){TextStyle?textStyle;//ModifyfromheretextStyle=switch(block.type){'h1'=>Theme.of(context).textTheme.displayMedium,'p'||'checkbox'=>Theme.of(context).textTheme.bodyMedium,_=>Theme.of(context).textTheme.bodySmall,};//tohere.returnContainer(margin:constEdgeInsets.all(8),child:Text(block.text,style:textStyle),);}}

    A switch expression looks similar to a switch statement, but it eliminates thecase keyword and uses=> to separate the pattern from the case body. Unlike switch statements, switch expressions return a value and can be used anywhere an expression can be used.

    11. Use object patterns

    Dart is an object-oriented language, so patterns apply to all objects. In this step, you switch on anobject pattern and destructure object properties to enhance the date rendering logic of your UI.

    Note: There's no migration necessary to support patterns; you can immediately start using pattern matching on your existing Dart classes.

    Extract properties from object patterns

    In this section, you improve how the last modified date is displayed using patterns.

    • Add theformatDate method tomain.dart:

    lib/main.dart

    StringformatDate(DateTimedateTime){finaltoday=DateTime.now();finaldifference=dateTime.difference(today);returnswitch(difference){Duration(inDays:0)=>'today',Duration(inDays:1)=>'tomorrow',Duration(inDays:-1)=>'yesterday',Duration(inDays:finaldays,isNegative:true)=>'${days.abs()}daysago',Duration(inDays:finaldays)=>'$daysdaysfromnow',};}

    This method returns a switch expression that switches on the valuedifference, aDuration object. It represents the span of time betweentoday and themodified value from the JSON data.

    Each case of the switch expression is using an object pattern that matches by calling getters on the object's propertiesinDays andisNegative. The syntax looks like it might be constructing a Duration object, but it's actually accessing fields on thedifference object.

    The first three cases use constant subpatterns0,1, and-1 to match the object propertyinDays and return the corresponding string.

    The last two cases handle durations beyond today, yesterday, and tomorrow:

    • If theisNegative property matches theboolean constant patterntrue, meaning the modification date was in the past, it displaysdays ago.
    • If that case doesn't catch the difference, then duration must be a positive number of days (no need to explicitly verify withisNegative: false), so the modification date is in the future and displaysdays from now.

    Add formatting logic for weeks

    • Add two new cases to your formatting function in order to identify durations longer than 7 days so that the UI can display them asweeks:

    lib/main.dart

    StringformatDate(DateTimedateTime){finaltoday=DateTime.now();finaldifference=dateTime.difference(today);returnswitch(difference){Duration(inDays:0)=>'today',Duration(inDays:1)=>'tomorrow',Duration(inDays:-1)=>'yesterday',Duration(inDays:finaldays)whendays >7=>'${days~/7}weeksfromnow',//AddfromhereDuration(inDays:finaldays)whendays <-7=>'${days.abs()~/7}weeksago',//tohere.Duration(inDays:finaldays,isNegative:true)=>'${days.abs()}daysago',Duration(inDays:finaldays)=>'$daysdaysfromnow',};}

    This code introducesguard clauses:

    • A guard clause uses thewhen keyword after a case pattern.
    • They can be used in if-cases, switch statements, and switch expressions.
    • They only add a condition to a patternafter it's matched.
    • If the guard clause evaluates to false, the entire pattern isrefuted, and execution proceeds to the next case.

    Add the newly formatted date to the UI

    1. Finally, update thebuild method inDocumentScreen to use theformatDate function:

    lib/main.dart

    classDocumentScreenextendsStatelessWidget{finalDocumentdocument;constDocumentScreen({requiredthis.document,super.key});@overrideWidgetbuild(BuildContextcontext){final(title,:modified)=document.metadata;finalformattedModifiedDate=formatDate(modified);//Addthislinefinalblocks=document.getBlocks();returnScaffold(appBar:AppBar(title:Text(title)),body:Column(children:[Text('Last modified: $formattedModifiedDate'),//ModifythislineExpanded(child:ListView.builder(itemCount:blocks.length,itemBuilder:(context,index){returnBlockWidget(block:blocks[index]);},),),],),);}}
    1. Hot reload to see the changes in your app:

    The app that displays a string 'Last modified: 2 weeks ago' using the formatDate() function.

    12. Seal a class for exhaustive switching

    Notice that you didn't use a wildcard or default case at the end of the last 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 forall of the possible valuesinDays could potentially take.

    When every case in a switch is handled, it's called anexhaustive switch. For example, switching on abool type is exhaustive when it has cases fortrue andfalse. Switching on anenum type is exhaustive when there are cases for each of the enum's values, too, because enums represent afixed number ofconstant values.

    Dart 3 extendedexhaustiveness checking to objects and class hierarchies with the new class modifiersealed. Refactor yourBlock class as a sealed superclass.

    Create the subclasses

    • Indata.dart, create three new classes—HeaderBlock,ParagraphBlock, andCheckboxBlock—that extendBlock:

    lib/data.dart

    classHeaderBlockextendsBlock{finalStringtext;HeaderBlock(this.text);}classParagraphBlockextendsBlock{finalStringtext;ParagraphBlock(this.text);}classCheckboxBlockextendsBlock{finalStringtext;finalboolisChecked;CheckboxBlock(this.text,this.isChecked);}

    Each of these classes corresponds to the differenttype values from the original JSON:'h1','p', and'checkbox'.

    Seal the superclass

    • Mark theBlock class assealed. Then, refactor the if-case as a switch expression that returns the subclass corresponding to thetype specified in the JSON:

    lib/data.dart

    sealedclassBlock{Block();factoryBlock.fromJson(Map<String,Object?>json){returnswitch(json){{'type':'h1','text':Stringtext}=>HeaderBlock(text),{'type':'p','text':Stringtext}=>ParagraphBlock(text),{'type':'checkbox','text':Stringtext,'checked':boolchecked}=>CheckboxBlock(text,checked),_=>throwconstFormatException('Unexpected JSON format'),};}}

    Thesealed keyword is aclass modifier that means you canonly 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.

    Use a switch expression in order to display widgets

    1. Update theBlockWidget class inmain.dart with a switch expression that uses object patterns for each case:

    lib/main.dart

    classBlockWidgetextendsStatelessWidget{finalBlockblock;constBlockWidget({requiredthis.block,super.key});@overrideWidgetbuild(BuildContextcontext){returnContainer(margin:constEdgeInsets.all(8),child:switch(block){HeaderBlock(:finaltext)=>Text(text,style:Theme.of(context).textTheme.displayMedium,),ParagraphBlock(:finaltext)=>Text(text),CheckboxBlock(:finaltext,:finalisChecked)=>Row(children:[Checkbox(value:isChecked,onChanged:(_){}),Text(text),],),},);}}

    In your first version ofBlockWidget, you switched on a field of aBlock object to return aTextStyle. Now, you switch an instance of theBlock object itself and match againstobject patterns that represent its subclasses, extracting the object's properties in the process.

    The Dart analyzer can check that each subclass is handled in the switch expression because you madeBlock a sealed class.

    Try it out: Try removing one of the subclass cases from the switch expression and see the error kick in:

    A warning in VS Code that warns that not all cases are exhaustively checked.

    Also note that using a switch expression here lets you pass the result directly to thechild element, as opposed to the separate return statement needed before.

    1. Hot reload to see the checkbox JSON data rendered for the first time:

    The app that displays the checkbox 'Learn Dart 3'

    Try it out: Try changing the value of thechecked key in theDocument class, where you defineddocumentJson totrue instead offalse. Hot reload again to see the UI update!

    The app that contains a checkbox 'Learn Dart 3' that is now checked.

    13. Congratulations

    You successfully experimented with patterns, records, enhanced switch and case, and sealed classes. You covered a lot of information—but only barely scratched the surface of these features. For more information on patterns, see thefeature specification.

    The different pattern types, different contexts in which they can appear, and the potential nesting of subpatterns make the possibilities in behavior seeminglyendless. But they're easy to see.

    You can imagine all kinds of ways to display content in Flutter using patterns. Using patterns, you can safely extract data in order to build your UI in a few lines of code.

    What's next?

    • Check out the documentation on patterns, records, enhanced switch and cases, and class modifiers in theLanguage section of the Dart documentation.

    Reference docs

    See the full sample code, step by step, in theflutter/codelabs repository.

    For in-depth specifications for each new feature, check out the original design docs:

    Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.