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
- add(E value) → void
- indexWhere(bool test(E element), [ int start = 0 ]) → int
- insert(int index, E element) → void
- removeAt(int index) → E
- removeWhere(bool test(E element)) → void
- sort([int compare(E a E b) ]) → void
- forEach(void f(E element)) → void
- join([String separator = "" ]) → String
- map<T>(T f(E e)) → Iterable<T>
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
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!
- 1,103