Flutter UI
Flutter uses the pubspec.yaml file, located at the root of your project, to identify assets required by an app.
flutter:
assets:
- assets/my_icon.png
- assets/background.png
To include all assets under a directory, specify the directory name with the / character at the end:
flutter:
assets:
- assets/
Generate icons from image at https://appicon.co
In your Flutter project's root directory, navigate to .../android/app/src/main/res. The various bitmap resource folders such as mipmap-hdpi already contain placeholder images named ic_launcher.png. Replace them with your assets from https://appicon.co respecting the recommended icon size per screen density as indicated by the Android Developer Guide.
To remove the "squared circle" app icon effect, open project in Android Studio, right click on .../android/app/src/main/res and select New → Image asset and pick the original image (the one submitted to https://appicon.co). Resize the image in the wizard if necessary and Next and Finish.
To add a "splash screen" to your Flutter application, navigate to .../android/app/src/main/res. In res/drawable/launch_background.xml, use this layer list drawable XML to customize the look of your launch screen. The existing template provides an example of adding an image to the middle of a white splash screen in commented code. You can uncomment it or use other drawables to achieve the intended effect.
To share colors and font styles throughout an app, use themes. To share a theme across an entire app, provide a ThemeData to the MaterialApp constructor.
MaterialApp(
title: title,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.lightBlue[800],
accentColor: Colors.cyan[600],
fontFamily: 'Montserrat',
textTheme: TextTheme(
headline: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold),
title: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic),
body1: TextStyle(fontSize: 14.0, fontFamily: 'Hind'),
),
),
);
To override the app-wide theme in part of an application, wrap a section of the app in a Theme widget. There are two ways to approach this: creating a unique ThemeData, or extending the parent theme.
Theme(
data: ThemeData(accentColor: Colors.yellow),
child: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
);
// or
Theme(
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
);
Now that you have defined a theme, use it within the widget's build() methods by using the Theme.of(context) method.
Container(
color: Theme.of(context).accentColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.title,
),
);
It is common practice to put font files in a assets/fonts dir.
Get fonts at: https://fonts.google.com/
Add .ttf to project directory:
assets/
fonts/
RobotoMono-Regular.ttf
RobotoMono-Bold.ttf
Update pubspec.yaml:
flutter:
fonts:
- family: RobotoMono
fonts:
- asset: assets/fonts/RobotoMono-Regular.ttf
- asset: assets/fonts/RobotoMono-Bold.ttf
Set font in theme:
MaterialApp(
title: 'Custom Fonts',
theme: ThemeData(fontFamily: 'Roboto'),
home: MyHomePage(),
);
Set font in specific widget:
Text(
'Hello!',
style: TextStyle(fontFamily: 'RobotoMono'),
);
To work with images from a URL, use the Image.network() constructor.
Image.network(
'https://picsum.photos/250?image=9',
)
To fade in an image:
import 'package:transparent_image/transparent_image.dart';
FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: 'https://picsum.photos/250?image=9',
);
To cache an image:
CachedNetworkImage(
placeholder: (context, url) => CircularProgressIndicator(),
imageUrl: 'https://picsum.photos/250?image=9',
);
Scaffold(
drawer: Drawer(
child: // populate
)
);
Scaffold(
appBar: AppBar(
title: Text('SnackBar Demo'),
),
body: SnackBarPage(),
);
final snackBar = SnackBar(content: Text('Yay!'));
// find Scaffold in widget tree and use it to show SnackBar
Scaffold.of(context).showSnackBar(snackBar);
final snackBar = SnackBar(
content: Text('Yay! A SnackBar!'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
// ...
},
),
);
Create an enclosing TabController. Create a Tabbar containing a list of Tab instances, and a TabBarBiew containing the contents.
class TabBarDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
Icon(Icons.directions_car),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
),
),
),
);
}
}
Form with a GlobalKeyFirst, create a Form in a stateful widget. The Form widget acts as a container for grouping and validating multiple form fields.
When creating the form, provide a GlobalKey. This uniquely identifies the Form, and allows validation of the form.
class MyCustomForm extends StatefulWidget {
@override
MyCustomFormState createState() {
return MyCustomFormState();
}
}
class MyCustomFormState extends State<MyCustomForm> {
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: // to be completed
);
}
}
TextFormField with validation logicAlthough the Form is in place, it does not have a way for users to enter text. That is the job of a TextFormField.
Validate the input by providing a validator() function to the TextFormField. If the user's input is not valid, the validator function returns a String containing an error message. If there are no errors, the validator must return null.
TextFormField(
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
);
FormWhen the user attempts to submit the Form, check if the Form is valid. If it is, display a success message. If it is not (empty text field), display the error message.
RaisedButton(
onPressed: () {
if (_formKey.currentState.validate()) {
Scaffold
.of(context)
.showSnackBar(SnackBar(content: Text('Processing Data')));
}
},
child: Text('Submit'),
);
TextFieldRetrieving user input
Create a TextEditingController:
final myTextEditingController = TextEditingController();
Connect the TextEditingController to a text field:
TextField(
controller: myTextEditingController,
);
Display the value using the .text method:
onPressed: () {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text(myTextEditingController.text),
);
},
);
},
Supply an onChanged() callback:
TextField(
onChanged: (text) {
print("First text field: $text");
},
);
Alternatively, use a TextEditingController. To do so...
Create a TextEditingController:
final myTextEditingController = TextEditingController();
Connect the TextEditingController to a text field:
TextField(
controller: myTextEditingController,
);
Listen to the controller for changes and pass in a callback:
@override
void initState() {
super.initState();
myTextEditingController.addListener(_printLatestValue);
}
Use GestureDetector and an onTap callback.
GestureDetector(
onTap: () {
final snackBar = SnackBar(content: Text("Tap"));
Scaffold.of(context).showSnackBar(snackBar);
},
child: Container(
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Theme.of(context).buttonColor,
borderRadius: BorderRadius.circular(8.0),
),
child: Text('My Button'),
),
);
Create a List.
final items = List<String>.generate(20, (i) => "Item ${i + 1}");
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text('${items[index]}'));
},
);
Wrap the Widget returned from itemBuilder in a Dismissible.
Dismissible(
child: ListTile(title: Text('$item')),
key: Key(item),
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});
Scaffold
.of(context)
.showSnackBar(SnackBar(content: Text("$item dismissed")));
},
);
ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
),
ListTile(
leading: Icon(Icons.photo_album),
title: Text('Album'),
),
ListTile(
leading: Icon(Icons.phone),
title: Text('Phone'),
),
],
);
ListViewListView(
scrollDirection: Axis.horizontal,
// ...
)
ListView with different types of itemsTo represent different types of items in a list, define a class for each type of item.
abstract class ListItem {}
class HeadingItem implements ListItem {
final String heading;
HeadingItem(this.heading);
}
class MessageItem implements ListItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
}
Create a list of items to work with.
final items = List<ListItem>.generate(
1200,
(i) => i % 6 == 0
? HeadingItem("Heading $i")
: MessageItem("Sender $i", "Message body $i"),
);
Convert the list of items into a list of widgets.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is HeadingItem) {
return ListTile(
title: Text(
item.heading,
style: Theme.of(context).textTheme.headline,
),
);
} else if (item is MessageItem) {
return ListTile(
title: Text(item.sender),
subtitle: Text(item.body),
);
}
},
);
ListViewThe standard ListView constructor works well for small lists. To work with lists that contain a large number of items, it is best to use the ListView.builder constructor.
final items = List<String>.generate(10000, (i) => "Item $i");
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${items[index]}'),
);
},
);
GridViewIn some cases, you might want to display your items as a grid rather than a normal list of items that come one after the next. For this task, use the GridView widget.
SizedBoxFixed-sized padding with SizedBox widget.
BuildContextBuildContext is always available in stateful widgets, but it needs to be passed around in stateless widgets.
Row
To make a Row occupy only the space needed by its children: mainAxisSize: MainAxisSize.min
To make a Row occupy all horizontal space: mainAxisSize: MainAxisSize.max
To make a Row occupy all vertical space: crossAxisAlignment: CrossAxisAlignment.stretch
Column
To make a Column occupy only the space needed by its children: mainAxisSize: mainAxisSize.min
To make a Column occupy all vertical space: mainAxisSize: mainAxisSize.max
To make a Column occupy all horizontal space: crossAxisAlignment: CrossAxisAlignment.stretch