Flutter deep dive

(live coding)

Write your first app

Resources

Lists

A list is an indexable collection of objects with a length (an array).

The most common kinds of lists are:

  • Fixed-length list

  • Growable list

Some methods provided by a list

Lists in Flutter

There are various widgets in Flutter that accept a list of widgets as their children

A Column() code example

Column(
  children: <Widget>[
    Text('Deliver features faster'),
    Text('Craft beautiful UIs'),
    Expanded(
      child: FittedBox(
        fit: BoxFit.contain, // otherwise the logo will be tiny
        child: const FlutterLogo(),
      ),
    ),
  ],
)

Using ListView

ListView is a scrollable list of widgets arranged linearly.

It is the most commonly used scrolling widget. It displays its children one after another in the scroll direction. In the cross axis, the children are required to fill the ListView.

Using ListView's builder

Creates a scrollable, linear array of widgets that are created on demand.

This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.

A ListView code example

ListView.separated(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    var todo = todos[index];
    return Container(
      child: Text(todo),
      padding: EdgeInsets.all(10),
    );
  },
  separatorBuilder: (context, index) => Container(
    height: 1,
    color: Color.fromRGBO(240, 240, 240, 1),
  ),
),
List<String> todos = [
  'Do laundry',
  'Buy Christmas presents',
  'Vacuum the floor',
];

State management

The provider package is a mixture between dependency injection and state management, built with widgets for widgets.

Various types of providers

  • Provider - The most basic form of provider. It takes a value and exposes it, whatever the value is

 

  • ChangeNotifierProvider - A specification of ListenableProvider for ChangeNotifier. It will automatically call ChangeNotifier.dispose when needed

 

  • ProxyProvider - A provider that combines multiple values from other providers into a new object, and sends the result to Provider

 

  • MultiProvider - improves the readability of nested Providers

Types of providers - code

Provider<Foo>(create: (_) => Foo())
MultiProvider(
  providers: [
    Provider<Foo>(create: (_) => Foo()),
    Provider<Bar>(create: (_) => Bar()),
  ],
  child: someWidget,
)
ChangeNotifierProvider(create: (_) => Counter()),
ProxyProvider<Counter, Translations>(
  create: (_, counter, __) => Translations(counter.value),
),

Model example

import 'package:flutter/foundation.dart';

class TodoProvider with ChangeNotifier {
  List<String> _todos = [
    'Do laundry',
    'Buy Christmas presents',
    'Vacuum the floor',
  ];

  List<String> get todos {
    return _todos;
  }

  addTodo(data) {
    _todos.add(data);
    notifyListeners();
  }

  removeTodo(data) {
    _todos.removeWhere((todo) => todo == data);
    notifyListeners();
  }
}

Using the provider in a widget

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';

Widget build(BuildContext context) {
    var provider = Provider.of<TodoProvider>(context);
    var todos = provider.todos;

    return Center(...),
}

An alternative - using a consumer

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../providers/todo_provider.dart';

class ExampleConsumer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('')),
      body: Consumer<TodoProvider>(
        builder: (context, provider, child) {
          return ListView.separated(
            itemCount: provider.todos.length,
            itemBuilder: (context, index) => ...,
            separatorBuilder: ...,
          );
        },
      ),
    );
  }
}

Provider.of vs Consumer()

  • Connecting a widget to a Provider makes it refresh every time the listeners are notified
     
  • Using the Consumer lets you re-render only the part of the widget that is wrapped in the Consumer itself

Adding a provider to our app

Package management

The package information is located in the pubspec.yaml file. Just add the new dependency and your IDE should download it automatically.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  provider: 4.0.4
import 'package:flutter/foundation.dart';

class TodoProvider with ChangeNotifier {
  List<String> _todos = [
    'Do laundry',
    'Buy Christmas presents',
    'Vacuum the floor',
  ];

  List<String> get todos {
    return _todos;
  }

  addTodo(data) {
    _todos.add(data);
    notifyListeners();
  }

  removeTodo(data) {
    _todos.removeWhere((todo) => todo == data);
    notifyListeners();
  }
}

lib/providers/todo_provider.dart

Don't forget to notify the listeners when you want to react to a change in a provider

Initializing the provider in main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import './todo_provider.dart';

void main() {
  return runApp(
    ChangeNotifierProvider(child: MyApp(), create: (_) => TodoProvider()),
  );
}

Navigation

Mobile apps typically reveal their contents via full-screen elements called "screens" or "pages".

In Flutter these elements are called routes and they're managed by a
Navigator widget. The navigator manages a stack of Route objects and provides methods for managing the stack, like Navigator.push and Navigator.pop.

Pushing an unnamed route

Navigator.push(context, MaterialPageRoute<void>(
  builder: (BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('My Page')),
      body: Center(
        child: FlatButton(
          child: Text('POP'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  },
));

Registering named routes

void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/users': (BuildContext context) => MyPage(title: 'Users'),
      '/profile': (BuildContext context) => MyPage(title: 'Profile'),
    },
  ));
}

main.dart

Pushing a named route

Navigator.of(context).pushNamed('/todos');
Navigator.of(context).pushNamed('/todo', arguments: {'id': 1});

You can simply push a named route without arguments, for example /todos for Todo list

You can also provide any arguments for the component to use

class ExtractArgumentsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var args = ModalRoute.of(context).settings.arguments;

    return Scaffold(...);
  }
}

Creating a "Todos" route

import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
  List<String> todos = [...];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My todos'),
      ),
      body: ListView.separated(
        itemBuilder: (context, index) {
          var todo = todos[index];
          return Container(
            child: Text(todo),
            padding: EdgeInsets.all(10),
          );
        },
        separatorBuilder: (context, index) {
          return Container(
            height: 1,
            color: Color.fromRGBO(240, 240, 240, 1),
          );
        },
        itemCount: todos.length,
      ),
    );
  }
}

screens/todo_list_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import './providers/todo_provider.dart';
import './screens/todo_list.dart';

void main() => runApp(...);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todo app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: TodoList(),
    );
  }
}

main.dart

Connect the "Todo List" screen to the provider

Widget build(BuildContext context) {
  var provider = Provider.of<TodoProvider>(context);
  var todos = provider.todos;
    
  return Scaffold(...);
}

screens/todo_list_screen.dart

Create a new
"Add todo screen"

import 'package:flutter/material.dart';

class AddTodoScreen extends StatelessWidget {
  static const routeName = '/add';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('New todo'),
      ),
    );
  }
}

screens/add_todo_screen.dart

Register the "Add todo" screen

floatingActionButton: FloatingActionButton(
  child: Icon(Icons.add),
  onPressed: () {
    Navigator.of(context).pushNamed(AddTodoScreen.routeName);
  },
),

screens/todo_list_screen.dart

home: TodoList(),
routes: {
  AddTodoScreen.routeName: (_) => AddTodoScreen(),
},

main.dart

Forms in Flutter

TextField(
  decoration: InputDecoration(
    hintText: 'Add a todo',
  ),
  onChanged: (String value) {
    // set the value
  },
)

There are various ways to handle forms in flutter - using Form() widget, TextField() and TextFormField()

Handle input using a controller

import 'package:flutter/material.dart';

class AddTodoScreen extends StatelessWidget {
  static const routeName = '/add';
  var _todoController = TextEditingController(text: '');

  @override
  Widget build(BuildContext context) {
    return TextField(
      decoration: InputDecoration(
        hintText: 'Add a todo',
      ),
      controller: _todoController,
    );
  }
}

You can create a new TextEditingController and provide it as an argument for the TextField() - updating the value won't refresh the widget.

 

If you wish to monitor the changes, you can attach a listener to the controller.

Add a button to submit the form

FlatButton(
  onPressed: () => _handleSubmit(context),
  child: Text('Save todo'),
  color: Colors.blue,
),

Retrieving the controller's value

void _handleSubmit(BuildContext context) {
  var newTodo = _todoController.value.text;
  var provider = Provider.of<TodoProvider>(context, listen: false);

  provider.addTodo(newTodo);
  Navigator.of(context).pop();
}

Access the properties .value.text on your controller variable to get the current value inside it

Remove the current route from the route stack

GestureDetector

GestureDetector(
  onTap: _handleTap,
  child: Container(
    child: Text(todo),
    padding: EdgeInsets.all(10),
  ),
);

Whenever you want to react to user gestures, there's the GestureDetector widget, providing you:

  • on (double) tap action
  • on horizontal/vertical drag action
  • on long press action

InkWell makes it prettier

Material(
  child: InkWell(
    onTap: _handleTap,
    child: Container(
      child: Text(todo),
      padding: EdgeInsets.all(10),
    ),
  ),
);

Assuming InkWell is wrapped in a Material styled widget (Material, Container..), it provides user with a nice tap animation

Create a "Todo Detail" screen

import 'package:flutter/material.dart';

class TodoDetailScreen extends StatelessWidget {
  static const routeName = '/todo-detail';

  @override
  Widget build(BuildContext context) {
    var todo = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(title: Text(todo)),
      body: Center(
        child: Text(
          todo,
          style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }
}

Register it in main.js

routes: {
  AddTodoScreen.routeName: (_) => AddTodoScreen(),
  TodoDetailScreen.routeName: (_) => TodoDetailScreen(),
},

Redirect to "Todo Detail" on tap

itemBuilder: (context, index) {
  var todo = todos[index];
  return InkWell(
    child: Container(
      child: Text(todo),
      padding: EdgeInsets.all(10),
    ),
    onTap: () {
      Navigator.of(context).pushNamed(
        TodoDetailScreen.routeName,
        arguments: todo,
      );
    },
  );
},

screens/todo_list_screen.dart

Add delete functionality

floatingActionButton: FloatingActionButton(
  onPressed: () => _handleDelete(context, todo),
  child: Icon(Icons.delete),
  backgroundColor: Color.fromRGBO(240, 0, 50, 1),
),

screens/todo_detail_screen.dart

void _handleDelete(BuildContext context, String todo) {
  var provider = Provider.of<TodoProvider>(context, listen: false);
  provider.removeTodo(todo);

  Navigator.of(context).pop();
}

Intl

This package provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.

Parsing a date

new DateFormat.yMMMd().format(new DateTime.now())

Dio

A powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.

import 'package:dio/dio.dart';

void getHttp() async {
  try {
    Response response = await Dio().get("http://www.google.com");
    print(response);
  } catch (e) {
    print(e);
  }
}

View the final source code

Tune in after the presentation's finished to get the link!

Flutter deep dive

By Dana Janoskova

Flutter deep dive

Let's find out something about lists, state management, animations and navigation in Flutter!

  • 989