可滚动的 AlertDialog

Summary

An AlertDialog now scrolls automatically when it overflows.

Context

Before this change, when an AlertDialog widget’s contents were too tall, the display overflowed, causing the contents to be clipped. This resulted in the following issues:

  • There was no way to view the portion of the content that was clipped.
  • Most alert dialogs have buttons beneath the content to prompt users for actions. If the content overflowed, obscuring the buttons, users might be unaware of their existence.

Description of change

The previous approach listed the title and content widgets consecutively in a Row widget.

Column(
  mainAxisSize: MainAxisSize.min,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    if (title != null)
      Padding(
        padding: titlePadding ?? EdgeInsets.fromLTRB(24.0, 24.0, 24.0, content == null ? 20.0 : 0.0),
        child: DefaultTextStyle(
          style: titleTextStyle ?? dialogTheme.titleTextStyle ?? theme.textTheme.title,
          child: Semantics(
          child: title,
          namesRoute: true,
          container: true,
          ),
        ),
      ),
    if (content != null)
      Flexible(
        child: Padding(
        padding: contentPadding,
        child: DefaultTextStyle(
          style: contentTextStyle ?? dialogTheme.contentTextStyle ?? theme.textTheme.subhead,
          child: content,
        ),
      ),
    ),
    // ...
  ],
);

The new approach wraps both widgets in a SingleChildScrollView above the button bar, making both widgets part of the same scrollable and exposing the button bar at the bottom of the dialog.

Column(
  mainAxisSize: MainAxisSize.min,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    if (title != null || content != null)
      SingleChildScrollView(
        child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
         children: <Widget>[
           if (title != null)
             titleWidget,
             if (content != null)
             contentWidget,
         ],
       ),
     ),
   // ...
  ],
),

Migration guide

You might see the following issues as a result of this change:

Semantics tests might fail because of the addition of a SingleChildScrollView.
Manual testing of the Talkback and VoiceOver features show that they still exhibit the same (correct) behavior as before.
Golden tests might fail.
This change might have caused diffs in (previously passing) golden tests since the SingleChildScrollView now nests both the title and content widgets. Some Flutter projects have taken to creating semantics tests by taking goldens of semantics nodes used in Flutter’s debug build.


Any semantics golden updates that reflect the scrolling container addition are expected and these diffs should be safe to accept.

Sample resulting Semantics tree:

flutter:        ├─SemanticsNode#30 <-- SingleChildScrollView
flutter:          │ flags: hasImplicitScrolling
flutter:          │ scrollExtentMin: 0.0
flutter:          │ scrollPosition: 0.0
flutter:          │ scrollExtentMax: 0.0
flutter:          │
flutter:          ├─SemanticsNode#31 <-- title
flutter:          │   flags: namesRoute
flutter:          │   label: "Hello"
flutter:          │
flutter:          └─SemanticsNode#32 <-- contents
flutter:              label: "Huge content"
Layout changes might result because of the scroll view.
If the dialog was already overflowing, this change corrects the problem. This layout change is expected.


A nested SingleChildScrollView in AlertDialog.content should work properly if left in the code, but should be removed if unintended, since it might cause confusion.

Code before migration:

AlertDialog(
  title: Text(
    'Very, very large title that is also scrollable',
    textScaleFactor: 5,
  ),
  content: SingleChildScrollView( // will not be scrollable
    child: Text('Scrollable content', textScaleFactor: 5),
  ),
  actions: <Widget>[
    FlatButton(child: Text('Button 1'), onPressed: () {}),
    FlatButton(child: Text('Button 2'), onPressed: () {}),
  ],
)

Code after migration:

AlertDialog(
  title: Text('Very, very large title', textScaleFactor: 5),
  content: Text('Very, very large content', textScaleFactor: 5),
  actions: <Widget>[
    FlatButton(child: Text('Button 1'), onPressed: () {}),
    FlatButton(child: Text('Button 2'), onPressed: () {}),
  ],
)

Timeline

TBD - Add the version number that contains this change.

References

Design doc:

API documentation:

Relevant issue:

Relevant PRs: