Skip to content

10 Flutter - Navigation, Gestures,...

Screens and pages are called Routes

  • Android – Route is Activity
  • iOS – Route is ViewController
  • Flutter – route is just a widget

flutter

  • 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!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void main() {
  runApp(MaterialApp(
    title: 'Named Routes Demo',
    initialRoute: '/',
    routes: {
      '/': (context) => FirstScreen(),
      '/second': (context) => SecondScreen(),
    },
  ));
}
1
2
3
4
          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

  • TabBar
  • TabBarView

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

  • Lists are fundamental mobile apps development components (Facebook app is list, etc)
  • Flutter has ListView widget

  • ListTile gives every row some structure

  • Or create custom widget
 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'),
            ),

flutter

Card/ListTile

Properties

  • contentPadding, dense, enabled, isThreeLine, leading, onLongPress, onTap, selected, subtitle, title, trailing
1
2
3
4
5
6
7
8
9
          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),
              ),
            ),

flutter

Long lists

  • If lists are really long (or dynamical)
  • Use ListView.builder constructor
  • Generate list items in itemBuilder
1
2
3
4
5
6
7
8
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        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

  • GestureDetector
 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.
    • No access to ‘this’!
1
2
3
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
    • Loading
    • Success
    • Failure
 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