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 GlobalKey
First, 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;
},
);
Form
When 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'),
);
TextField
Retrieving 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'),
),
],
);
ListView
ListView(
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),
);
}
},
);
ListView
The 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]}'),
);
},
);
GridView
In 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.
SizedBox
Fixed-sized padding with SizedBox
widget.
BuildContext
BuildContext
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