├── lib ├── bloc │ ├── bloc.dart │ ├── bloc-prov-tree.dart │ └── bloc-prov.dart ├── blocs │ ├── blocs.dart │ ├── auth-bloc.dart │ └── pref-bloc.dart ├── models │ └── contact.dart ├── routes.dart ├── theme │ └── style.dart ├── services │ └── exampleapi.dart ├── screens │ ├── example1 │ │ ├── components │ │ │ └── body.dart │ │ ├── example-bloc.dart │ │ └── examplescreen1.dart │ └── example2 │ │ ├── components │ │ └── body.dart │ │ ├── example2-bloc.dart │ │ └── examplescreen2.dart ├── main.dart └── components │ └── roundedalertdialog.dart └── README.md /lib/bloc/bloc.dart: -------------------------------------------------------------------------------- 1 | abstract class Bloc {} -------------------------------------------------------------------------------- /lib/blocs/blocs.dart: -------------------------------------------------------------------------------- 1 | export 'auth-bloc.dart'; 2 | export 'pref-bloc.dart'; -------------------------------------------------------------------------------- /lib/blocs/auth-bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/bloc/bloc.dart'; 2 | 3 | class AuthBloc extends Bloc { 4 | AuthBloc(); 5 | } -------------------------------------------------------------------------------- /lib/blocs/pref-bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/bloc/bloc.dart'; 2 | 3 | class PrefBloc extends Bloc { 4 | PrefBloc(); 5 | } -------------------------------------------------------------------------------- /lib/models/contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | class Contact { 4 | final String avatarUrl; 5 | final String name; 6 | 7 | Contact({@required this.avatarUrl, @required this.name}); 8 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Code Organization Example App 2 | 3 | This project only includes a lib, but if you wish to use it in your project you can just copy the lib contents here to your project. 4 | 5 | This is the new organization setup that works great with large and small projects, article [here](https://medium.com/flutter-community/flutter-code-organization-revised-b09ad5cef7f6) 6 | -------------------------------------------------------------------------------- /lib/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:example/screens/example1/examplescreen1.dart'; 3 | import 'package:example/screens/example2/examplescreen2.dart'; 4 | 5 | final Map routes = { 6 | "/": (BuildContext context) => ExScreen1(), 7 | "/ExScreen2": (BuildContext context) => ExScreen2(), 8 | }; 9 | -------------------------------------------------------------------------------- /lib/theme/style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ThemeData appTheme() { 4 | return ThemeData( 5 | primaryColor: Colors.white, 6 | accentColor: Colors.orange, 7 | hintColor: Colors.white, 8 | dividerColor: Colors.white, 9 | buttonColor: Colors.white, 10 | scaffoldBackgroundColor: Colors.black, 11 | canvasColor: Colors.black, 12 | ); 13 | } -------------------------------------------------------------------------------- /lib/services/exampleapi.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:http/http.dart' as http; 3 | 4 | Future exampleApi(String orgid) async { 5 | http.Response response = await http.get( 6 | Uri.encodeFull("https://www.example.com/api"), 7 | ); 8 | print("Respone ${response.body.toString()}"); 9 | //Returns 'true' or 'false' as a String 10 | return response.body; 11 | } -------------------------------------------------------------------------------- /lib/screens/example1/components/body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Body extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Center( 7 | child: RaisedButton( 8 | onPressed: () { 9 | Navigator.pop(context); 10 | }, 11 | child: Text('Go back!'), 12 | ), 13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /lib/screens/example2/components/body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Body extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Center( 7 | child: RaisedButton( 8 | onPressed: () { 9 | Navigator.pop(context); 10 | }, 11 | child: Text('Go back!'), 12 | ), 13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /lib/screens/example1/example-bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:example/models/contact.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:example/bloc/bloc.dart'; 7 | 8 | class ExampleBloc extends Bloc { 9 | StreamSubscription _audioPlayerStateSubscription; 10 | 11 | Stream get example => _exampleSubject.stream; 12 | Sink get exampleSink => _exampleSubject.sink; 13 | final StreamController _exampleSubject = StreamController(); 14 | 15 | ExampleBloc(); 16 | 17 | void dispose() { 18 | _exampleSubject.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/screens/example2/example2-bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:example/models/contact.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:example/bloc/bloc.dart'; 7 | 8 | class Example2Bloc extends Bloc { 9 | StreamSubscription _audioPlayerStateSubscription; 10 | 11 | Stream get example => _exampleSubject.stream; 12 | Sink get exampleSink => _exampleSubject.sink; 13 | final StreamController _exampleSubject = StreamController(); 14 | 15 | Example2Bloc(); 16 | 17 | void dispose() { 18 | _exampleSubject.close(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:example/theme/style.dart'; 3 | import 'package:example/routes.dart'; 4 | import 'package:example/bloc/bloc-prov-tree.dart'; 5 | import 'package:example/bloc/bloc-prov.dart'; 6 | import 'package:example/blocs/blocs.dart'; 7 | import 'blocs/blocs.dart'; 8 | 9 | void main() { 10 | runApp(ExampleApp()); 11 | } 12 | class ExampleApp extends StatelessWidget { 13 | @override 14 | Widget build(BuildContext context) { 15 | return BlocProviderTree( 16 | blocProviders: [ 17 | BlocProvider(bloc: AuthBloc()), 18 | BlocProvider(bloc: PrefBloc()), 19 | ], 20 | child: MaterialApp( 21 | title: 'ExampleApp', 22 | theme: appTheme(), 23 | initialRoute: '/', 24 | routes: routes, 25 | ), 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/screens/example1/examplescreen1.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:example/screens/example1/components/body.dart'; 3 | import 'package:example/screens/example1/example-bloc.dart'; 4 | import 'package:example/bloc/bloc-prov.dart'; 5 | 6 | class ExScreen1 extends StatefulWidget { 7 | @override 8 | _ExScreen1State createState() => _ExScreen1State(); 9 | } 10 | 11 | class _ExScreen1State extends State { 12 | ExampleBloc exampleBloc; 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | 18 | exampleBloc = ExampleBloc(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | exampleBloc.dispose(); 24 | 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return BlocProvider( 31 | bloc: exampleBloc, 32 | child: Scaffold( 33 | appBar: AppBar( 34 | title: Text("First Screen"), 35 | ), 36 | body: Body(), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/screens/example2/examplescreen2.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:example/screens/example2/components/body.dart'; 3 | import 'package:example/screens/example2/example2-bloc.dart'; 4 | import 'package:example/bloc/bloc-prov.dart'; 5 | 6 | class ExScreen2 extends StatefulWidget { 7 | @override 8 | _ExScreen2State createState() => _ExScreen2State(); 9 | } 10 | 11 | class _ExScreen2State extends State { 12 | Example2Bloc example2Bloc; 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | 18 | example2Bloc = Example2Bloc(); 19 | } 20 | 21 | @override 22 | void dispose() { 23 | example2Bloc.dispose(); 24 | 25 | super.dispose(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return BlocProvider( 31 | bloc: Example2Bloc(), 32 | child: Scaffold( 33 | appBar: AppBar( 34 | title: Text("Second Screen"), 35 | ), 36 | body: Body(), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/bloc/bloc-prov-tree.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:example/bloc/bloc-prov.dart'; 3 | 4 | /// A Flutter [Widget] that merges multiple [BlocProvider] widgets into one widget tree. 5 | /// 6 | /// [BlocProviderTree] improves the readability and eliminates the need 7 | /// to nest multiple [BlocProviders]. 8 | /// 9 | /// By using [BlocProviderTree] we can go from: 10 | /// 11 | /// ```dart 12 | /// BlocProvider( 13 | /// bloc: BlocA(), 14 | /// child: BlocProvider( 15 | /// bloc: BlocB(), 16 | /// child: BlocProvider( 17 | /// value: BlocC(), 18 | /// child: ChildA(), 19 | /// ) 20 | /// ) 21 | /// ) 22 | /// ``` 23 | /// 24 | /// to: 25 | /// 26 | /// ```dart 27 | /// BlocProviderTree( 28 | /// blocProviders: [ 29 | /// BlocProvider(bloc: BlocA()), 30 | /// BlocProvider(bloc: BlocB()), 31 | /// BlocProvider(bloc: BlocC()), 32 | /// ], 33 | /// child: ChildA(), 34 | /// ) 35 | /// ``` 36 | /// 37 | /// [BlocProviderTree] converts the [BlocProvider] list 38 | /// into a tree of nested [BlocProvider] widgets. 39 | /// As a result, the only advantage of using [BlocProviderTree] is improved 40 | /// readability due to the reduction in nesting and boilerplate. 41 | class BlocProviderTree extends StatelessWidget { 42 | /// The [BlocProvider] list which is converted into a tree of [BlocProvider] widgets. 43 | /// The tree of [BlocProvider] widgets is created in order meaning the first [BlocProvider] 44 | /// will be the top-most [BlocProvider] and the last [BlocProvider] will be a direct ancestor 45 | /// of the `child` [Widget]. 46 | final List blocProviders; 47 | 48 | /// The [Widget] and its descendants which will have access to every [Bloc] provided by `blocProviders`. 49 | /// This [Widget] will be a direct descendent of the last [BlocProvider] in `blocProviders`. 50 | final Widget child; 51 | 52 | const BlocProviderTree({ 53 | Key key, 54 | @required this.blocProviders, 55 | @required this.child, 56 | }) : assert(blocProviders != null), 57 | assert(child != null), 58 | super(key: key); 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | Widget tree = child; 63 | for (final blocProvider in blocProviders.reversed) { 64 | tree = blocProvider.copyWith(tree); 65 | } 66 | return tree; 67 | } 68 | } -------------------------------------------------------------------------------- /lib/bloc/bloc-prov.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:example/bloc/bloc.dart'; 3 | 4 | /// A Flutter widget which provides a bloc to its children via `BlocProvider.of(context)`. 5 | /// It is used as a DI widget so that a single instance of a bloc can be provided 6 | /// to multiple widgets within a subtree. 7 | class BlocProvider extends InheritedWidget { 8 | /// The [Bloc] which is to be made available throughout the subtree 9 | final T bloc; 10 | 11 | /// The [Widget] and its descendants which will have access to the [Bloc]. 12 | final Widget child; 13 | 14 | BlocProvider({ 15 | Key key, 16 | @required this.bloc, 17 | this.child, 18 | }) : assert(bloc != null), 19 | super(key: key, child: child); 20 | 21 | /// Method that allows widgets to access the bloc as long as their `BuildContext` 22 | /// contains a `BlocProvider` instance. 23 | static T of(BuildContext context) { 24 | final type = _typeOf>(); 25 | final BlocProvider provider = context 26 | .ancestorInheritedElementForWidgetOfExactType(type) 27 | ?.widget as BlocProvider; 28 | 29 | if (provider == null) { 30 | throw FlutterError( 31 | """ 32 | BlocProvider.of() called with a context that does not contain a Bloc of type $T. 33 | No ancestor could be found starting from the context that was passed to BlocProvider.of<$T>(). 34 | This can happen if the context you use comes from a widget above the BlocProvider. 35 | This can also happen if you used BlocProviderTree and didn\'t explicity provide 36 | the BlocProvider types: BlocProvider(bloc: $T()) instead of BlocProvider<$T>(bloc: $T()). 37 | The context used was: $context 38 | """, 39 | ); 40 | } 41 | return provider?.bloc; 42 | } 43 | 44 | /// Clone the current [BlocProvider] with a new child [Widget]. 45 | /// All other values, including [Key] and [Bloc] are preserved. 46 | BlocProvider copyWith(Widget child) { 47 | return BlocProvider( 48 | key: key, 49 | bloc: bloc, 50 | child: child, 51 | ); 52 | } 53 | 54 | /// Necessary to obtain generic [Type] 55 | /// https://github.com/dart-lang/sdk/issues/11923 56 | static Type _typeOf() => T; 57 | 58 | @override 59 | bool updateShouldNotify(BlocProvider oldWidget) => false; 60 | } -------------------------------------------------------------------------------- /lib/components/roundedalertdialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | // Examples can assume: 10 | // enum Department { treasury, state } 11 | 12 | /// A material design dialog. 13 | /// 14 | /// This dialog widget does not have any opinion about the contents of the 15 | /// dialog. Rather than using this widget directly, consider using [AlertDialog] 16 | /// or [SimpleDialog], which implement specific kinds of material design 17 | /// dialogs. 18 | /// 19 | /// See also: 20 | /// 21 | /// * [AlertDialog], for dialogs that have a message and some buttons. 22 | /// * [SimpleDialog], for dialogs that offer a variety of options. 23 | /// * [showDialog], which actually displays the dialog and returns its result. 24 | /// * 25 | class Dialog extends StatelessWidget { 26 | /// Creates a dialog. 27 | /// 28 | /// Typically used in conjunction with [showDialog]. 29 | const Dialog({ 30 | Key key, 31 | this.child, 32 | this.insetAnimationDuration: const Duration(milliseconds: 100), 33 | this.insetAnimationCurve: Curves.decelerate, 34 | }) : super(key: key); 35 | 36 | /// The widget below this widget in the tree. 37 | /// 38 | /// {@macro flutter.widgets.child} 39 | final Widget child; 40 | 41 | /// The duration of the animation to show when the system keyboard intrudes 42 | /// into the space that the dialog is placed in. 43 | /// 44 | /// Defaults to 100 milliseconds. 45 | final Duration insetAnimationDuration; 46 | 47 | /// The curve to use for the animation shown when the system keyboard intrudes 48 | /// into the space that the dialog is placed in. 49 | /// 50 | /// Defaults to [Curves.fastOutSlowIn]. 51 | final Curve insetAnimationCurve; 52 | 53 | Color _getColor(BuildContext context) { 54 | return Theme.of(context).dialogBackgroundColor; 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return new AnimatedPadding( 60 | padding: MediaQuery.of(context).viewInsets + 61 | const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), 62 | duration: insetAnimationDuration, 63 | curve: insetAnimationCurve, 64 | child: new MediaQuery.removeViewInsets( 65 | removeLeft: true, 66 | removeTop: true, 67 | removeRight: true, 68 | removeBottom: true, 69 | context: context, 70 | child: new Center( 71 | child: new ConstrainedBox( 72 | constraints: const BoxConstraints(minWidth: 280.0), 73 | child: new Material( 74 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10.0)), side: BorderSide(color: Colors.white, width: 1.0, style: BorderStyle.solid)), 75 | elevation: 30.0, 76 | color: _getColor(context), 77 | type: MaterialType.card, 78 | child: child, 79 | ), 80 | ), 81 | ), 82 | ), 83 | ); 84 | } 85 | } 86 | 87 | /// A material design alert dialog. 88 | /// 89 | /// An alert dialog informs the user about situations that require 90 | /// acknowledgement. An alert dialog has an optional title and an optional list 91 | /// of actions. The title is displayed above the content and the actions are 92 | /// displayed below the content. 93 | /// 94 | /// If the content is too large to fit on the screen vertically, the dialog will 95 | /// display the title and the actions and let the content overflow. Consider 96 | /// using a scrolling widget, such as [ListView], for [content] to avoid 97 | /// overflow. 98 | /// 99 | /// For dialogs that offer the user a choice between several options, consider 100 | /// using a [SimpleDialog]. 101 | /// 102 | /// Typically passed as the child widget to [showDialog], which displays the 103 | /// dialog. 104 | /// 105 | /// ## Sample code 106 | /// 107 | /// This snippet shows a method in a [State] which, when called, displays a dialog box 108 | /// and returns a [Future] that completes when the dialog is dismissed. 109 | /// 110 | /// ```dart 111 | /// Future _neverSatisfied() async { 112 | /// return showDialog( 113 | /// context: context, 114 | /// barrierDismissible: false, // user must tap button! 115 | /// builder: (BuildContext context) { 116 | /// return new AlertDialog( 117 | /// title: new Text('Rewind and remember'), 118 | /// content: new SingleChildScrollView( 119 | /// child: new ListBody( 120 | /// children: [ 121 | /// new Text('You will never be satisfied.'), 122 | /// new Text('You\’re like me. I’m never satisfied.'), 123 | /// ], 124 | /// ), 125 | /// ), 126 | /// actions: [ 127 | /// new FlatButton( 128 | /// child: new Text('Regret'), 129 | /// onPressed: () { 130 | /// Navigator.of(context).pop(); 131 | /// }, 132 | /// ), 133 | /// ], 134 | /// ); 135 | /// }, 136 | /// ); 137 | /// } 138 | /// ``` 139 | /// 140 | /// See also: 141 | /// 142 | /// * [SimpleDialog], which handles the scrolling of the contents but has no [actions]. 143 | /// * [Dialog], on which [AlertDialog] and [SimpleDialog] are based. 144 | /// * [showDialog], which actually displays the dialog and returns its result. 145 | /// * 146 | class CustomAlertDialog extends StatelessWidget { 147 | /// Creates an alert dialog. 148 | /// 149 | /// Typically used in conjunction with [showDialog]. 150 | /// 151 | /// The [contentPadding] must not be null. The [titlePadding] defaults to 152 | /// null, which implies a default that depends on the values of the other 153 | /// properties. See the documentation of [titlePadding] for details. 154 | const CustomAlertDialog({ 155 | Key key, 156 | this.title, 157 | this.titlePadding, 158 | this.content, 159 | this.contentPadding: const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), 160 | this.actions, 161 | this.semanticLabel, 162 | }) : assert(contentPadding != null), 163 | super(key: key); 164 | 165 | /// The (optional) title of the dialog is displayed in a large font at the top 166 | /// of the dialog. 167 | /// 168 | /// Typically a [Text] widget. 169 | final Widget title; 170 | 171 | /// Padding around the title. 172 | /// 173 | /// If there is no title, no padding will be provided. Otherwise, this padding 174 | /// is used. 175 | /// 176 | /// This property defaults to providing 24 pixels on the top, left, and right 177 | /// of the title. If the [content] is not null, then no bottom padding is 178 | /// provided (but see [contentPadding]). If it _is_ null, then an extra 20 179 | /// pixels of bottom padding is added to separate the [title] from the 180 | /// [actions]. 181 | final EdgeInsetsGeometry titlePadding; 182 | 183 | /// The (optional) content of the dialog is displayed in the center of the 184 | /// dialog in a lighter font. 185 | /// 186 | /// Typically, this is a [ListView] containing the contents of the dialog. 187 | /// Using a [ListView] ensures that the contents can scroll if they are too 188 | /// big to fit on the display. 189 | final Widget content; 190 | 191 | /// Padding around the content. 192 | /// 193 | /// If there is no content, no padding will be provided. Otherwise, padding of 194 | /// 20 pixels is provided above the content to separate the content from the 195 | /// title, and padding of 24 pixels is provided on the left, right, and bottom 196 | /// to separate the content from the other edges of the dialog. 197 | final EdgeInsetsGeometry contentPadding; 198 | 199 | /// The (optional) set of actions that are displayed at the bottom of the 200 | /// dialog. 201 | /// 202 | /// Typically this is a list of [FlatButton] widgets. 203 | /// 204 | /// These widgets will be wrapped in a [ButtonBar], which introduces 8 pixels 205 | /// of padding on each side. 206 | /// 207 | /// If the [title] is not null but the [content] _is_ null, then an extra 20 208 | /// pixels of padding is added above the [ButtonBar] to separate the [title] 209 | /// from the [actions]. 210 | final List actions; 211 | 212 | /// The semantic label of the dialog used by accessibility frameworks to 213 | /// announce screen transitions when the dialog is opened and closed. 214 | /// 215 | /// If this label is not provided, a semantic label will be infered from the 216 | /// [title] if it is not null. If there is no title, the label will be taken 217 | /// from [MaterialLocalizations.alertDialogLabel]. 218 | /// 219 | /// See also: 220 | /// 221 | /// * [SemanticsConfiguration.isRouteName], for a description of how this 222 | /// value is used. 223 | final String semanticLabel; 224 | 225 | @override 226 | Widget build(BuildContext context) { 227 | final List children = []; 228 | String label = semanticLabel; 229 | 230 | if (title != null) { 231 | children.add(new Padding( 232 | padding: titlePadding ?? 233 | new EdgeInsets.fromLTRB( 234 | 24.0, 24.0, 24.0, content == null ? 20.0 : 0.0), 235 | child: new DefaultTextStyle( 236 | style: Theme.of(context).textTheme.title, 237 | child: new Semantics(child: title, namesRoute: true), 238 | ), 239 | )); 240 | } else { 241 | switch (defaultTargetPlatform) { 242 | case TargetPlatform.iOS: 243 | label = semanticLabel; 244 | break; 245 | case TargetPlatform.android: 246 | case TargetPlatform.fuchsia: 247 | label = semanticLabel ?? 248 | MaterialLocalizations.of(context)?.alertDialogLabel; 249 | } 250 | } 251 | 252 | if (content != null) { 253 | children.add(new Flexible( 254 | child: new Padding( 255 | padding: contentPadding, 256 | child: new DefaultTextStyle( 257 | style: Theme.of(context).textTheme.subhead, 258 | child: content, 259 | ), 260 | ), 261 | )); 262 | } 263 | 264 | if (actions != null) { 265 | children.add(new ButtonTheme.bar( 266 | child: new ButtonBar( 267 | children: actions, 268 | ), 269 | )); 270 | } 271 | 272 | Widget dialogChild = new IntrinsicWidth( 273 | child: new Column( 274 | mainAxisSize: MainAxisSize.min, 275 | crossAxisAlignment: CrossAxisAlignment.stretch, 276 | children: children, 277 | ), 278 | ); 279 | 280 | if (label != null) 281 | dialogChild = 282 | new Semantics(namesRoute: true, label: label, child: dialogChild); 283 | 284 | return new Dialog(child: dialogChild); 285 | } 286 | } 287 | 288 | /// An option used in a [SimpleDialog]. 289 | /// 290 | /// A simple dialog offers the user a choice between several options. This 291 | /// widget is commonly used to represent each of the options. If the user 292 | /// selects this option, the widget will call the [onPressed] callback, which 293 | /// typically uses [Navigator.pop] to close the dialog. 294 | /// 295 | /// The padding on a [SimpleDialogOption] is configured to combine with the 296 | /// default [SimpleDialog.contentPadding] so that each option ends up 8 pixels 297 | /// from the other vertically, with 20 pixels of spacing between the dialog's 298 | /// title and the first option, and 24 pixels of spacing between the last option 299 | /// and the bottom of the dialog. 300 | /// 301 | /// ## Sample code 302 | /// 303 | /// ```dart 304 | /// new SimpleDialogOption( 305 | /// onPressed: () { Navigator.pop(context, Department.treasury); }, 306 | /// child: const Text('Treasury department'), 307 | /// ) 308 | /// ``` 309 | /// 310 | /// See also: 311 | /// 312 | /// * [SimpleDialog], for a dialog in which to use this widget. 313 | /// * [showDialog], which actually displays the dialog and returns its result. 314 | /// * [FlatButton], which are commonly used as actions in other kinds of 315 | /// dialogs, such as [AlertDialog]s. 316 | /// * 317 | class SimpleDialogOption extends StatelessWidget { 318 | /// Creates an option for a [SimpleDialog]. 319 | const SimpleDialogOption({ 320 | Key key, 321 | this.onPressed, 322 | this.child, 323 | }) : super(key: key); 324 | 325 | /// The callback that is called when this option is selected. 326 | /// 327 | /// If this is set to null, the option cannot be selected. 328 | /// 329 | /// When used in a [SimpleDialog], this will typically call [Navigator.pop] 330 | /// with a value for [showDialog] to complete its future with. 331 | final VoidCallback onPressed; 332 | 333 | /// The widget below this widget in the tree. 334 | /// 335 | /// Typically a [Text] widget. 336 | final Widget child; 337 | 338 | @override 339 | Widget build(BuildContext context) { 340 | return new InkWell( 341 | onTap: onPressed, 342 | child: new Padding( 343 | padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24.0), 344 | child: child), 345 | ); 346 | } 347 | } 348 | 349 | /// A simple material design dialog. 350 | /// 351 | /// A simple dialog offers the user a choice between several options. A simple 352 | /// dialog has an optional title that is displayed above the choices. 353 | /// 354 | /// Choices are normally represented using [SimpleDialogOption] widgets. If 355 | /// other widgets are used, see [contentPadding] for notes regarding the 356 | /// conventions for obtaining the spacing expected by Material Design. 357 | /// 358 | /// For dialogs that inform the user about a situation, consider using an 359 | /// [AlertDialog]. 360 | /// 361 | /// Typically passed as the child widget to [showDialog], which displays the 362 | /// dialog. 363 | /// 364 | /// ## Sample code 365 | /// 366 | /// In this example, the user is asked to select between two options. These 367 | /// options are represented as an enum. The [showDialog] method here returns 368 | /// a [Future] that completes to a value of that enum. If the user cancels 369 | /// the dialog (e.g. by hitting the back button on Android, or tapping on the 370 | /// mask behind the dialog) then the future completes with the null value. 371 | /// 372 | /// The return value in this example is used as the index for a switch statement. 373 | /// One advantage of using an enum as the return value and then using that to 374 | /// drive a switch statement is that the analyzer will flag any switch statement 375 | /// that doesn't mention every value in the enum. 376 | /// 377 | /// ```dart 378 | /// Future _askedToLead() async { 379 | /// switch (await showDialog( 380 | /// context: context, 381 | /// builder: (BuildContext context) { 382 | /// return new SimpleDialog( 383 | /// title: const Text('Select assignment'), 384 | /// children: [ 385 | /// new SimpleDialogOption( 386 | /// onPressed: () { Navigator.pop(context, Department.treasury); }, 387 | /// child: const Text('Treasury department'), 388 | /// ), 389 | /// new SimpleDialogOption( 390 | /// onPressed: () { Navigator.pop(context, Department.state); }, 391 | /// child: const Text('State department'), 392 | /// ), 393 | /// ], 394 | /// ); 395 | /// } 396 | /// )) { 397 | /// case Department.treasury: 398 | /// // Let's go. 399 | /// // ... 400 | /// break; 401 | /// case Department.state: 402 | /// // ... 403 | /// break; 404 | /// } 405 | /// } 406 | /// ``` 407 | /// 408 | /// See also: 409 | /// 410 | /// * [SimpleDialogOption], which are options used in this type of dialog. 411 | /// * [AlertDialog], for dialogs that have a row of buttons below the body. 412 | /// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based. 413 | /// * [showDialog], which actually displays the dialog and returns its result. 414 | /// * 415 | class SimpleDialog extends StatelessWidget { 416 | /// Creates a simple dialog. 417 | /// 418 | /// Typically used in conjunction with [showDialog]. 419 | /// 420 | /// The [titlePadding] and [contentPadding] arguments must not be null. 421 | const SimpleDialog({ 422 | Key key, 423 | this.title, 424 | this.titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 0.0), 425 | this.children, 426 | this.contentPadding: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0), 427 | this.semanticLabel, 428 | }) : assert(titlePadding != null), 429 | assert(contentPadding != null), 430 | super(key: key); 431 | 432 | /// The (optional) title of the dialog is displayed in a large font at the top 433 | /// of the dialog. 434 | /// 435 | /// Typically a [Text] widget. 436 | final Widget title; 437 | 438 | /// Padding around the title. 439 | /// 440 | /// If there is no title, no padding will be provided. 441 | /// 442 | /// By default, this provides the recommend Material Design padding of 24 443 | /// pixels around the left, top, and right edges of the title. 444 | /// 445 | /// See [contentPadding] for the conventions regarding padding between the 446 | /// [title] and the [children]. 447 | final EdgeInsetsGeometry titlePadding; 448 | 449 | /// The (optional) content of the dialog is displayed in a 450 | /// [SingleChildScrollView] underneath the title. 451 | /// 452 | /// Typically a list of [SimpleDialogOption]s. 453 | final List children; 454 | 455 | /// Padding around the content. 456 | /// 457 | /// By default, this is 12 pixels on the top and 16 pixels on the bottom. This 458 | /// is intended to be combined with children that have 24 pixels of padding on 459 | /// the left and right, and 8 pixels of padding on the top and bottom, so that 460 | /// the content ends up being indented 20 pixels from the title, 24 pixels 461 | /// from the bottom, and 24 pixels from the sides. 462 | /// 463 | /// The [SimpleDialogOption] widget uses such padding. 464 | /// 465 | /// If there is no [title], the [contentPadding] should be adjusted so that 466 | /// the top padding ends up being 24 pixels. 467 | final EdgeInsetsGeometry contentPadding; 468 | 469 | /// The semantic label of the dialog used by accessibility frameworks to 470 | /// announce screen transitions when the dialog is opened and closed. 471 | /// 472 | /// If this label is not provided, a semantic label will be infered from the 473 | /// [title] if it is not null. If there is no title, the label will be taken 474 | /// from [MaterialLocalizations.dialogLabel]. 475 | /// 476 | /// See also: 477 | /// 478 | /// * [SemanticsConfiguration.isRouteName], for a description of how this 479 | /// value is used. 480 | final String semanticLabel; 481 | 482 | @override 483 | Widget build(BuildContext context) { 484 | final List body = []; 485 | String label = semanticLabel; 486 | 487 | if (title != null) { 488 | body.add(new Padding( 489 | padding: titlePadding, 490 | child: new DefaultTextStyle( 491 | style: Theme.of(context).textTheme.title, 492 | child: new Semantics(namesRoute: true, child: title), 493 | ))); 494 | } else { 495 | switch (defaultTargetPlatform) { 496 | case TargetPlatform.iOS: 497 | label = semanticLabel; 498 | break; 499 | case TargetPlatform.android: 500 | case TargetPlatform.fuchsia: 501 | label = 502 | semanticLabel ?? MaterialLocalizations.of(context)?.dialogLabel; 503 | } 504 | } 505 | 506 | if (children != null) { 507 | body.add(new Flexible( 508 | child: new SingleChildScrollView( 509 | padding: contentPadding, 510 | child: new ListBody(children: children), 511 | ))); 512 | } 513 | 514 | Widget dialogChild = new IntrinsicWidth( 515 | stepWidth: 56.0, 516 | child: new ConstrainedBox( 517 | constraints: const BoxConstraints(minWidth: 280.0), 518 | child: new Column( 519 | mainAxisSize: MainAxisSize.min, 520 | crossAxisAlignment: CrossAxisAlignment.stretch, 521 | children: body, 522 | ), 523 | ), 524 | ); 525 | 526 | if (label != null) 527 | dialogChild = new Semantics( 528 | namesRoute: true, 529 | label: label, 530 | child: dialogChild, 531 | ); 532 | return new Dialog(child: dialogChild); 533 | } 534 | } 535 | 536 | class _DialogRoute extends PopupRoute { 537 | _DialogRoute({ 538 | @required this.theme, 539 | bool barrierDismissible: true, 540 | this.barrierLabel, 541 | @required this.child, 542 | RouteSettings settings, 543 | }) : assert(barrierDismissible != null), 544 | _barrierDismissible = barrierDismissible, 545 | super(settings: settings); 546 | 547 | final Widget child; 548 | final ThemeData theme; 549 | 550 | @override 551 | Duration get transitionDuration => const Duration(milliseconds: 150); 552 | 553 | @override 554 | bool get barrierDismissible => _barrierDismissible; 555 | final bool _barrierDismissible; 556 | 557 | @override 558 | Color get barrierColor => Colors.black54; 559 | 560 | @override 561 | final String barrierLabel; 562 | 563 | @override 564 | Widget buildPage(BuildContext context, Animation animation, 565 | Animation secondaryAnimation) { 566 | return new SafeArea( 567 | child: new Builder(builder: (BuildContext context) { 568 | final Widget annotatedChild = new Semantics( 569 | child: child, 570 | scopesRoute: true, 571 | explicitChildNodes: true, 572 | ); 573 | return theme != null 574 | ? new Theme(data: theme, child: annotatedChild) 575 | : annotatedChild; 576 | }), 577 | ); 578 | } 579 | 580 | @override 581 | Widget buildTransitions(BuildContext context, Animation animation, 582 | Animation secondaryAnimation, Widget child) { 583 | return new FadeTransition( 584 | opacity: new CurvedAnimation(parent: animation, curve: Curves.easeOut), 585 | child: child); 586 | } 587 | } 588 | 589 | /// Displays a dialog above the current contents of the app. 590 | /// 591 | /// This function takes a `builder` which typically builds a [Dialog] widget. 592 | /// Content below the dialog is dimmed with a [ModalBarrier]. This widget does 593 | /// not share a context with the location that `showDialog` is originally 594 | /// called from. Use a [StatefulBuilder] or a custom [StatefulWidget] if the 595 | /// dialog needs to update dynamically. 596 | /// 597 | /// The `context` argument is used to look up the [Navigator] and [Theme] for 598 | /// the dialog. It is only used when the method is called. Its corresponding 599 | /// widget can be safely removed from the tree before the dialog is closed. 600 | /// 601 | /// The `child` argument is deprecated, and should be replaced with `builder`. 602 | /// 603 | /// Returns a [Future] that resolves to the value (if any) that was passed to 604 | /// [Navigator.pop] when the dialog was closed. 605 | /// 606 | /// The dialog route created by this method is pushed to the root navigator. 607 | /// If the application has multiple [Navigator] objects, it may be necessary to 608 | /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the 609 | /// dialog rather just 'Navigator.pop(context, result)`. 610 | /// 611 | /// See also: 612 | /// * [AlertDialog], for dialogs that have a row of buttons below a body. 613 | /// * [SimpleDialog], which handles the scrolling of the contents and does 614 | /// not show buttons below its body. 615 | /// * [Dialog], on which [SimpleDialog] and [AlertDialog] are based. 616 | /// * 617 | Future customShowDialog({ 618 | @required 619 | BuildContext context, 620 | bool barrierDismissible: true, 621 | @Deprecated( 622 | 'Instead of using the "child" argument, return the child from a closure ' 623 | 'provided to the "builder" argument. This will ensure that the BuildContext ' 624 | 'is appropriate for widgets built in the dialog.') 625 | Widget child, 626 | WidgetBuilder builder, 627 | }) { 628 | assert(child == null || builder == null); 629 | return Navigator.of(context, rootNavigator: true).push(new _DialogRoute( 630 | child: child ?? new Builder(builder: builder), 631 | theme: Theme.of(context, shadowThemeOnly: true), 632 | barrierDismissible: barrierDismissible, 633 | barrierLabel: 634 | MaterialLocalizations.of(context).modalBarrierDismissLabel, 635 | )); 636 | } --------------------------------------------------------------------------------