Flutter UI

Flutter UI

2019-12-14T23:46:37.892Z

Basics

Assets

Flutter uses the pubspec.yaml file, located at the root of your project, to identify assets required by an app.

pubspec.yaml
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:

pubspec.yaml
flutter:
  assets:
	- assets/

App icon

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 NewImage 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.

Splash screen

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.

Themes

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,
	),
);

Fonts

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:

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'),
);

Images

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',
);

Widgets

Drawer

Scaffold(
	drawer: Drawer(
		child: // populate
	)
);

Snackbar

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: () {
			// ...
		},
	),
);

Tabs

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),
						],
					),
				),
			),
		);
	}
}

Forms

1. Create a 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
		);
	}
}

2. Add a TextFormField with validation logic

Although 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;
	},
);

3. Create a button to validate and submit the 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),
			);
		},
	);
},

Handling a change event

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);
}

Gestures

Tappable widget

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'),
	),
);

Dismissible widget

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

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'),
		),
	],
);

Horizontal ListView

ListView(
	scrollDirection: Axis.horizontal,
	// ...
)

ListView with different types of items

To 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),
			);
		}
	},
);

Long 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]}'),
		);
	},
);

Grids with 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.

Padding with 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.

Rows and columns

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