Flutter
has Power
Kamil Rykowski
Prerequisites
- Programming skills
- Basic experience with command line tools
- Declarative paradigm
Environment
- Android Studio with latest SDK and Android emulator
- Optionally: macOS & xCode to build/run iOS app
Flutter
What is it?
Open source framework for building mobile cross-platform applications based on Dart.
Who's using it?
- 27 February 2018 - first beta release
- 4 December 2018 - stable 1.0 release
- Material Design / Cupertino
- Official support for Android, iOS, Web and Desktop
- Shared codebase
Highlights
- Custom UI rendering (no OEM widgets)
- Hot reloading
- High performance
- Simply works as expected
- OOP language (Dart)
Highlights
...more
- OOP
- Static typing
- Support for JIT & AOT compilation
- Developed by Google (just like Flutter)
- More popular than COBOL
Dart language
But why?
Alternative tech
do you event lift?
Why should I learn?
- Leverage your beloved OOP skills
- Reuse most of the code between different platforms
- Native like performance with 60 fps
- Open source
- Fuchsia
- Baked by Google
- Growing community and ready to use libraries
- Hot reloading
- Deploy to Android, iOS, Web* and Desktop*
* official support, technical preview
# Download zip from official site
wget <URL>
# Unpack
unzip -a <FILE> -d flutter/
# Add flutter to PATH env
echo export PATH='$PATH:<PATH>/bin' >>
~/.bash_profile
# Done, flutter is ready
flutter create <PROJECT_NAME>
Start the project
Install & create
Run the app
Start small
// main.dart
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Text('Flutter has power'),
);
}
}
Workshop project
FOOdy - mock my lunch
Workshop project
FOOdy - mock my lunch
External libs
Trust me - we'll use them
# pubspec.yaml
dependencies:
# ...
provider: ^3.0.0+1
badges: ^1.1.0
dio: ^2.1.13
# bash
flutter pub get
# or directly from Android Studio / VSCode
main.dart
Just configuration
import 'package:flutter/material.dart';
import 'ui/screens/home.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter has Power',
home: HomeScreen(),
);
}
}
models.dart
Not so clean architecture
Classes
as a data containers
import 'package:flutter/widgets.dart';
class Restaurant {
final int id;
final String name;
final String image;
Restaurant({
@required this.id,
@required this.name,
@required this.image,
});
}
Functions
as a data providers
List<Restaurant> _restaurants = [
Restaurant(
id: 1,
name: "McDonald",
image: "https://hmp.me/cn9k",
),
// ...
];
List<Restaurant> getRestaurants() {
return _restaurants;
}
home.dart
Screen is just a Widget
import 'package:flutter/material.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("Hello Home!"),
);
}
}
Scaffold
Fundamental layout builder
Widget build(BuildContext context) {
return Scaffold(
appBar: null,
body: Center(
child: Text("Hello Home!")
)
);
}
AppBar
Sticky header
appBar: AppBar(
backgroundColor: Colors.black,
title: Text("FOOdy"),
actions: [
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(Icons.shopping_basket),
),
],
)
ListView
Render some data
// 1
body: ListView(
padding: EdgeInsets.all(16),
children: [1, 2, 3].map((item) {
return Text("Item $item");
}).toList(),
)
// 2
body: ListView.builder(
padding: EdgeInsets.all(16),
itemCount: 3,
itemBuilder: (context, index) {
return Text("Item $index");
},
)
ListView
It's working!
body: ListView(
padding: EdgeInsets.all(16),
children: getRestaurants().map((restaurant) {
return Image.network(
restaurant.image,
height: 170,
fit: BoxFit.cover,
width: double.infinity,
);
}).toList(),
)
restaurant_item.dart
Widgets decomposition
class RestaurantItem extends StatelessWidget {
final bool isFavorite;
final Restaurant restaurant;
final Function onPressed;
RestaurantItem({
this.isFavorite,
this.restaurant,
this.onPressed,
});
@override
Widget build(BuildContext context) {
return Text("Single item");
}
}
Stack layout
Overlap the widgets (absolute)
Stack(
children: [
Container(
width: 250,
height: 100,
color: Colors.green,
),
Positioned(
left: 10,
bottom: 10,
child: Text("Restaurant"),
),
],
)
Clickable "things"
Click-click
IconButton(
onPressed: () => {},
icon: Icon(
Icons.favorite,
color: Colors.red,
size: 20,
),
)
FloatingActionButton(
onPressed: () => {},
backgroundColor: Colors.black,
child: Icon(Icons.favorite),
)
Clickable "THINGS"
Serious stuff
GestureDetector(
onTap: () {},
onDoubleTap: () {},
onLongPress: () {},
// ... and many more
child: Container(
color: Colors.red,
child: Text("Stop there!"),
),
)
State management
Local scope
class MyWidget extends StatefulWidget {
@override
MyState createState() => MyState();
}
class MyState extends State<MyWidget> {
int clickCount = 0;
@override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('counter: $clickCount'),
onPressed: () => setState(() {
clickCount++; }),
);
}
}
StatefulWidget
Manage your data
class HomeScreen extends StatelessWidget {
// ...
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() {
return _HomeScreenState();
}
}
class _HomeScreenState extends State<HomeScreen> {
// ...
}
Favorite restaurants
setState
List<Restaurant> favorites = [];
void toggleFavorite(Restaurant restaurant) {
setState(() {
favorites.contains(restaurant)
? favorites.remove(restaurant)
: favorites.add(restaurant);
});
}
API integration
https://my-json-server.typicode.com/vintage/flutter_has_power |
List<Restaurant> getRestaurants() {
return _restaurants;
}
Future<List<Restaurant>> getRestaurants() async {
var response = await Dio().get("URL");
return List<Restaurant>.from(response.data.map((data) {
return Restaurant(
id: data["id"],
name: data["name"],
image: data["image"],
);
});
}
FutureBuilder
complex widget building
FutureBuilder<List<Restaurant>>(
future: getRestaurants(),
builder: (context, snapshot) {
if (snapshot.connectionState !=
ConnectionState.done) {
return Text("Loading");
}
print(snapshot.data);
return Text("Got it!");
},
);
Lifecycle method(s)
avoid DDoSing your own API
Future<List<Restaurant>> restaurantsLoader;
@override
void initState() {
restaurantsLoader = getRestaurants();
super.initState();
}
FutureBuilder<List<Restaurant>>(
future: restaurantsLoader,
// ...
);
home.dart
Ready to l(a)unch
Sticky header
List of items
Stacked layout
Clickable widgets
Local state management
API integration
Handling async code in build
Lifecycle methods
detail.dart
Yet another screen
import 'package:flutter/material.dart';
import '../shared/header.dart';
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: Header(),
body: Center(
child: Text("Detail screen"),
),
);
}
}
header.dart
Bit tricky
class Header extends StatelessWidget implements
PreferredSizeWidget {
Size get preferredSize {
return new Size.fromHeight(kToolbarHeight);
}
@override
Widget build(BuildContext context) {
return AppBar(...);
}
}
Navigation
Configure the routes
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
routes: {
'/details': (context) => DetailScreen(),
},
);
}
}
Navigation
Screen transition
Navigator.of(context).pushNamed("/details");
Navigator.push(context, MaterialPageRoute(
builder: (context) => DetailScreen(),
));
.pop()
.popUntil()
.pushReplacement()
.popAndPushNamed()
...
Navigation
Different animation
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => DetailScreen(),
),
);
... or implement your own PageRoute transition!
Navigation
Passing arguments
class DetailScreenArguments {
final Restaurant restaurant;
DetailScreenArguments({this.restaurant});
}
Navigator.of(context).pushNamed(
"/details",
arguments: DetailScreenArguments(
restaurant: restaurant,
),
);
Navigation
Receiving arguments
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final DetailScreenArguments args =
ModalRoute.of(context).settings.arguments;
return Text(args.restaurant.name);
}
}
Flex layout
Row & Column
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("1"),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("2"),
Text("two"),
Text("double"),
],
),
Text("3"),
],
)
Flex layout
Expected behavior
Local assets
bundled static files
# pubspec.yaml
# ...
flutter:
uses-material-design: true
assets:
# - assets/menu_empty.jpg
- assets/
Local assets
access the assets
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/c.json');
}
Image.asset(
'assets/menu_empty.jpg',
height: 80,
width: 80,
fit: BoxFit.cover,
)
detail.dart
Ready to l(a)unch
-
Navigation between screens
-
Passing arguments during navigation
-
Grid layout with Row and Column
-
Adding static files (assets) to the app
-
Displaying image from local asset
cart.dart
Final screen
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text("Cart screen"),
),
);
}
}
Modal layout
Do It Yourself
Stack(
children: [
Center(child: Text("Main content")),
Positioned(
top: 10,
right: 10,
child: FloatingActionButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Icon(Icons.close),
),
)
],
)
Modal layout
Do It Yourself
State management #2
Global scope via provider
import 'package:flutter/material.dart';
import './models.dart';
class CartState extends ChangeNotifier {
Map<Menu, int> _items = {};
Map<Menu, int> get items => _items;
void addItem(Menu item) {
_items.putIfAbsent(item, () => 0);
_items[item] += 1;
notifyListeners();
}
int get count => _items.values.fold(0, (a, b) => a + b);
}
State management #2
Provide value down the tree
void main() {
return runApp(
ChangeNotifierProvider<CartState>.value(
value: CartState(),
child: App(),
),
);
}
State management #2
Many providers
void main() {
return runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(builder: (context) {
return CartState();
}),
ChangeNotifierProvider(builder: (context) {
return UserState();
}),
],
child: App(),
),
);
}
State management #2
Access stored data
Consumer<CartState>(
builder: (context, value, _) => Text(
value.count.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
)
Provider.of<CartState>(context).addItem(menu);
or
GridView
Real grid
GridView.count(
padding: EdgeInsets.all(8),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
crossAxisCount: 3,
scrollDirection: Axis.vertical,
children: items.keys.map((menu) {
return Text(menu.name);
}).toList(),
);
GridView
Easy & lovely
Expanded
Widget proportions
Column(
children: [
Expanded(
child: Container(
color: Colors.black,
),
),
Text("Bottom"),
],
);
Expanded
Relative positioning
cart.dart
Ready to l(a)unch
-
Hand crafted modal alike screen
-
Global state management with provider
-
Expanded widget
-
Real grid view
-
Finalize the app
Thank you
Flutter has Power
Kamil Rykowski
Flutter has power
By Kamil Rykowski
Flutter has power
- 362