10 Flutter - Navigation, Gestures,...
Navigation & Routing
Screens and pages are called Routes
- Android – Route is Activity
- iOS – Route is ViewController
- Flutter – route is just a widget

Navigator
- Navigator.push(context, route) – to move to next route
- Navigator.pop(context) – to move back to previous route
Route?
- Create with MaterialPageroute(builder: (context) => SecondRoute())
Push route
Navigate to next view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | class FirstRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: Center(
child: RaisedButton(
child: Text('Open route'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
),
);
}
}
|
Pop route
Navigate back to previous view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
|
Named routes
Ta avoid code duplication, when you need to navigate to same screen from several places
- Navigator.pushNamed
- Navigator.pop
- When defining initialRoute – don’t define home property!
| void main() {
runApp(MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/',
routes: {
'/': (context) => FirstScreen(),
'/second': (context) => SecondScreen(),
},
));
}
|
| onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
},
|
Passing data to/from route
Pass data the regular way – everything is just widget!
- To receive data
- Navigator.push returns Future, that completes after pop in new screen
- To send data back
- Navigator.pop(context, ’Option A was chosen!');
Async/Await pattern – same as in C# async
1
2
3
4
5
6
7
8
9
10
11
12
13 | _navigateAndDisplaySelection(BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectionScreen(
textToShow: 'Choose one!',
)),
);
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
|
Tabs
DefaultTabController
Order must match!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | 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),
],
),
),
),
);
}
}
|
Orientation
Use OrientationBuilder
1
2
3
4
5
6
7
8
9
10
11
12 | class OrientationController extends StatelessWidget {
@override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.landscape
? LandscapeLayout()
: PortraitLayout();
},
);
}
}
|
List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Basic List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
),
|

Card/ListTile
Properties
- contentPadding, dense, enabled, isThreeLine, leading, onLongPress, onTap, selected, subtitle, title, trailing
| children: <Widget>[
Card(
child: ListTile(
leading: FlutterLogo(size: 56.0),
title: Text('Two-line ListTile'),
subtitle: Text('Here is a second line'),
trailing: Icon(Icons.more_vert),
),
),
|

Long lists
- If lists are really long (or dynamical)
- Use ListView.builder constructor
- Generate list items in itemBuilder
| body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item: ${index}'),
);
},
),
|
Images
Image class, several constructors
- new Image, for obtaining an image from an ImageProvider.
- new Image.asset, for obtaining an image from an AssetBundle using a key.
- new Image.network, for obtaining an image from a URL.
- new Image.file, for obtaining an image from a File.
- new Image.memory, for obtaining an image from a Uint8List.
- Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg')
Image, fadein
Pubspec: transparent_image: ^2.0.0
| body: Stack(
children: <Widget>[
Center(child: CircularProgressIndicator()),
Center(
child: FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: 'https://picsum.photos/250?image=9',
),
),
],
),
|
SnackBar
- Create a Scaffold
- Ensures, that widgets don’t overlap
- Create a SnackBar
- Display SnackBar, using Scaffold
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | class SnackBarDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SnackBar Demo',
home: Scaffold(
appBar: AppBar(
title: Text('SnackBar Demo'),
),
body: SnackBarPage(),
),
);
}
}
class SnackBarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
onPressed: () {
final snackBar = SnackBar(
content: Text('Yay! A SnackBar!'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {},
),
);
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text('Show SnackBar'),
),
);
}
}
|
Gestures
Users need to interact with the app and sometimes built-in functionality is not enough
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final snackBar = SnackBar(content: Text("Tap"));
Scaffold.of(context).showSnackBar(snackBar);
},
// The custom button
child: Container(
padding: EdgeInsets.all(12.0),
decoration: BoxDecoration(
color: Theme.of(context).buttonColor,
borderRadius: BorderRadius.circular(8.0),
),
child: Text('My Button'),
),
);
}
}
|
GestureDetector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | onDoubleTap
onForcePressEnd
onForcePressPeak
onForcePressStart
onForcePressUpdate
onHorizontalDragCancel
onHorizontalDragDown
onHorizontalDragEnd
onHorizontalDragStart
onHorizontalDragUpdate
onLongPress
onLongPressEnd
onLongPressMoveUpdate
onLongPressStart
onLongPressUp
onPanCancel
onPanDown
onPanEnd
onPanStart
onPanUpdate
onScaleEnd
onScaleStart
onScaleUpdate
onSecondaryTapCancel
onSecondaryTapDown
onSecondaryTapUp
onTap
onTapCancel
onTapDown
onTapUp
onVerticalDragCancel
onVerticalDragDown
onVerticalDragEnd
onVerticalDragStart
onVerticalDragUpdate
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | GestureDetector(
onTap: () {
setState(() {
// Toggle light when tapped.
_lightIsOn = !_lightIsOn;
});
},
child: Container(
color: Colors.yellow.shade600,
padding: const EdgeInsets.all(8),
// Change button text when light changes state.
child: Text(_lightIsOn ? 'TURN LIGHT OFF' : 'TURN LIGHT ON'),
),
),
|
Fetch
Package: http
dependencies: http: ^0.13.5
import 'package:http/http.dart’ as http;
- Get() returns an Future containing Response (await/async pattern)
- Convert response to Dart object
- Factory constructors
- Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype.
| Future<http.Response> fetchInfo () {
return http.get('http://dad.akaver.com/api/SongTitles/SP';
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13 | class SongInfo {
final String artist;
final String title;
SongInfo({this.artist, this.title});
factory SongInfo.fromJson(Map<String, dynamic> json){
var songHistoryList = json['SongHistoryList'];
var song = songHistoryList[0];
return SongInfo(
artist: song['Artist'],
title: song['Title']
);
}
}
|
Fetch - json
Convert json to object
1
2
3
4
5
6
7
8
9
10
11
12 | Future<SongInfo> fetchInfo() async {
final response =
await http.get('http://dad.akaver.com/api/SongTitles/SP');
if (response.statusCode == 200) {
return SongInfo.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load data');
}
}
|
Fetch data
- Create future in initState
- Build method gets called often (UI redraw) – slowdown if requests where to happen every time
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<SongInfo> info;
@override
void initState() {
super.initState();
info = fetchInfo();
}
|
Fetch data / display
FutureBuilder<YourFutureDataClass>
- Builder to work with async data sources
- Future – async data source to work with
- Builder – what to render during
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | body: Center(
child: FutureBuilder<SongInfo>(
future: info,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
snapshot.data.artist + ' - ' +
snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return CircularProgressIndicator();
},
),
),
|
Timer
Periodic callback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | Timer _timer;
int _start = 10;
void startTimer() {
const oneSec = const Duration(seconds: 1);
_timer = new Timer.periodic(
oneSec,
(Timer timer) => setState(
() {
if (_start < 1) {
timer.cancel();
info = fetchInfo();
} else {
_start = _start - 1;
}
},
),
);
}
|
More topics
Still to come..., maybe...
- Persistence
- Networking
- Tokens/Auth, Json parsing in background, WebSockets/SignalR
- Working with SQLite
- Files, Key-Value data
- Cupertino vs Material
- Background services (Isolate), Media, notifications, push, sensors
- Plugins – interfacing with native API’s
- i18n, accessibility
- Animations, Assets