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

Made with Slides.com