Flutter Basics

Flutter Basics

2019-11-14T23:46:37.821Z

Commands

To test installation:

flutter doctor

To create project:

flutter create myapp

Or Flutter: New Project in VScode.

To run project:

flutter run

(At project root dir, with emulator active or device plugged in.)

To hot reload:

r

Or [F5] in VScode.

To hot restart:

R

Or [stop] [start] in VScode.

Assets and dependencies for a Flutter app are managed by pubspec.yaml. To get packages listed in pubspec.yaml:

flutter pub get

Or Flutter: Get Packages in VScode.

Performing Packages get also auto-generates a pubspec.lock file with a list of all packages pulled into the project and their version numbers.

To compile to release mode:

flutter run --release

VSCode Tips

Extensions

  • Dart extension
  • Awesome Flutter snippets extension
  • Android iOS emulator extension

Hotkeys

  • Select + right click → Quick fixImport widget
  • Ctrl + Space: Toggle Suggestion Details
  • Right click on widget → RefactorCenter widget
  • Right click on widget → RefactorWrap with...
  • Right click on widget → RefactorExtract widget
  • Select widget → F12: Open definition file
  • Select widget → Alt + F12: Peek definition

App template

Call main() to return a call to runApp() taking in the widget instantiated by a standard widget class or by your custom widget class MyApp(), as long as it extends StatelessWidget and overrides its build() method.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            home: MyHomePage(title: 'Hello World!'),
        );
    }
}

Many Material Design widgets need to be inside of a MaterialApp to display properly, in order to inherit theme data. Therefore, run the application with a MaterialApp.

void main() {
    runApp(MaterialApp(
        title: 'My app', // used by the OS task switcher
        home: MyScaffold(),
    ));
}

Nameless routes

To navigate forward, use Navigator.push() with MaterialPageRoute:

onPressed: () {
    Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondScreen()),
        // `SecondScreen` is a custom widget
    );
}

To navigate back, use Navigator.pop():

onPressed: () {
    Navigator.pop(context);
}

When pushing routes to Navigator, any new widgets instantiated become direct descendants of MaterialApp, not of the screenwhich you navigated from, so if the destination screen needs access to Provider, Provider will have to be above MaterialApp.

Named routes

To define routes, enter a Map in the routes argument of MaterialApp:

MaterialApp(
    initialRoute: '/',
    routes: {
        '/': (context) => FirstScreen(),
        '/second': (context) => SecondScreen(),
    },
);

// ...

onPressed: () {
    Navigator.pushNamed(context, '/second');
}

onPressed: () {
    Navigator.pop(context);
}

PageView

As an alternative to nameless and named routes, you may use a PageView widget for navigation, which makes swipeable screens.

Final pageView = PageView(
    controller: controller,
    scrollDirection: vertical,
    children: [
        MyPage1(),
        MyPage2(),
        MyPage3(),
    ]
);

Passing arguments to a named route

Use the arguments parameter of the Navigator.pushNamed() method. To do so...

Define the arguments you need to pass:

class ScreenArguments {
    final String title;
    final String message;

    ScreenArguments(this.title, this.message);
}

Create a widget that extracts the arguments:

class ExtractArgumentsScreen extends StatelessWidget {
    static const routeName = '/extractArguments';

    @override
    Widget build(BuildContext context) {

        // get ModalRoute settings, extract arguments, cast as ScreenArguments
        final ScreenArguments args = ModalRoute.of(context).settings.arguments;

        return Scaffold(
            appBar: AppBar(
                title: Text(args.title),
            ),
            body: Center(
                child: Text(args.message),
            ),
        );
    }
}

Register the widget in the routes table:

MaterialApp(
    routes: {
        ExtractArgumentsScreen.routeName: (context) => ExtractArgumentsScreen(),
    },
);

Navigate to the widget:

onPressed: () {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => ExtractArgumentsScreen(),
            settings: RouteSettings(
                arguments: ScreenArguments(
                    'Extract Arguments Screen',
                    'This message is extracted in the build method.',
                ),
            ),
        ),
    ),
}

Alternatively, use onGenerateRoute():

MaterialApp(
    onGenerateRoute: (settings) {
        if (settings.name == PassArgumentsScreen.routeName) {
            final ScreenArguments args = settings.arguments;
            return MaterialPageRoute(
                builder: (context) {
                return PassArgumentsScreen(
                    title: args.title,
                    message: args.message,
                    );
                },
            );
        }
    },
);

Sending data to a screen

ListView.builder(
    itemCount: todos.length,
    itemBuilder: (context, index) {
        return ListTile(
        title: Text(todos[index].title),
        onTap: () {
            // create DetailScreen and pass in current Todo
            Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) => DetailScreen(todo: todos[index]),
                ),
            );
        },
        );
    },
);

Returning data from a screen

  1. Define a home screen
  2. Add to it a button that launches a second screen
  3. Show a second screen with two buttons
  4. When one of them is tapped, close the second screen
  5. Show a snackbar on the home screen with the selected value.
class SelectionButton extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return RaisedButton(
        onPressed: () {
            _navigateAndDisplaySelection(context);
        },
        child: Text('Pick an option, any option!'),
        );
    }

    _navigateAndDisplaySelection(BuildContext context) async {

        // launch SelectionScreen while awaiting result
        final result = await Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => SelectionScreen()),
        );

        // after result returned, hide previous snackbars
        // and show new result
        Scaffold.of(context)
            ..removeCurrentSnackBar()
            ..showSnackBar(SnackBar(content: Text("$result")));
    }
}

RaisedButton(
    onPressed: () {
        Navigator.pop(context, 'Yep!'); // returns with argument
    },
    child: Text('Yep!'),
);

Networking

Simple HTTP request

  1. Add and import the http package.
  2. Make a network request using the http package.
  3. Convert the response into a custom Dart object.

Add http to pubspec.yaml:

dependencies:
  http: <latest_version>

Import http:

import 'package:http/http.dart' as http;

Create class:

class Post {
    final int userId;
    final int id;
    final String title;
    final String body;

    Post({this.userId, this.id, this.title, this.body});

    factory Post.fromJson(Map<String, dynamic> json) {
        return Post(
            userId: json['userId'],
            id: json['id'],
            title: json['title'],
            body: json['body'],
        );
    }
}

Convert HTTP response (Future) to class instance:

Future<Post> fetchPost() async {
    final response =
        await http.get('https://jsonplaceholder.typicode.com/posts/1');

    if (response.statusCode == 200) {
        return Post.fromJson(json.decode(response.body));
    } else {
        throw Exception('Failed to load post');
    }
}

Fetch and display:

class _MyAppState extends State<MyApp> {
    Future<Post> post;

    @override
    void initState() {
        super.initState();
        post = fetchPost(); // called inside initState()
}

Why is fetchPost() called in initState()? Flutter calls the build() method every time it wants to change anything in the view, and this happens surprisingly often. If you leave the fetch call in your build() method, you'll flood the API with unnecessary calls and slow down your app.

Authenticated HTTP request

The http package provides a convenient way to add headers to your requests. Alternatively, use the HttpHeaders class from the dart:io library.

Future<http.Response> fetchPost() {
    return http.get(
        'https://jsonplaceholder.typicode.com/posts/1',
        headers: { HttpHeaders.authorizationHeader: "Basic your_api_token_here" },
    );
}

Parsing JSON in an isolate

  1. Add the http package. (omitted here)
  2. Make a network request using the http package. (omitted here)
  3. Convert the response into a list of photos. (omitted here)
  4. Move this work to a separate isolate.

You can remove the jank by moving the parsing and conversion to a background isolate using the compute() function provided by Flutter. The compute() function runs expensive functions in a background isolate and returns the result. In this case, run the parsePhotos() function in the background.

Future<List<Photo>> fetchPhotos(http.Client client) async {
    final response =
        await client.get('https://jsonplaceholder.typicode.com/photos');
    return compute(parsePhotos, response.body);
}

Isolates communicate by passing messages back and forth. These messages can be primitive values, such as null, num, bool, double, or String, or simple objects such as the List<Photo> in this example. You might experience errors if you try to pass more complex objects, such as a Future or http.Response between isolates.

Persistence

  • Local storage
  • Relational db
  • NoSQL db
  • User settings

SQLite

Flutter apps can make use of the SQLite databases via the sqflite plugin.

Connection

Add sqflite and path packages:

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  sqflite:
  path:

Import them:

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

Define a data model:

class Dog {
    final int id;
    final String name;
    final int age;

    Dog({this.id, this.name, this.age});
}

Open a connection to the db with openDatabase():

final Future<Database> database = openDatabase(
    join(await sqflite.getDatabasesPath(), 'dogs.db'),
);

Table creation

Create a table with onCreate:

final Future<Database> database = openDatabase(
    join(await getDatabasesPath(), 'doggie_database.db'),
    onCreate: (db, version) {
        return db.execute(
            "CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
        );
    },
    version: 1,
);

Row insertion

Convert the instance to a map:

// highlight-range{7-14}
class Dog {
    final int id;
    final String name;
    final int age;

    Dog({this.id, this.name, this.age});

    Map<String, dynamic> toMap() {
        return {
            'id': id,
            'name': name,
            'age': age,
        };
    }
}

Storing the map in the db with db.insert():

// highlight-range{1-8}
Future<void> storeDog(Dog dog) async {
    final Database db = await database;
    await db.insert(
        'dogs',
        dog.toMap(),
        conflictAlgorithm: ConflictAlgorithm.replace,
    );
}

final fido = Dog(
    id: 0,
    name: 'Fido',
    age: 35,
);

await storeDog(fido);

Retrieve all records with db.query():

Future<List<Dog>> dogs() async {
    final Database db = await database;

    final List<Map<String, dynamic>> maps = await db.query('dogs');

    // Convert the List<Map<String, dynamic> into a List<Dog>.
    return List.generate(maps.length, (i) {
        return Dog(
            id: maps[i]['id'],
            name: maps[i]['name'],
            age: maps[i]['age'],
        );
    });
}

print(await dogs());

Update a record with db.update():

Future<void> updateDog(Dog dog) async {

    final db = await database;

    await db.update(
        'dogs',
        dog.toMap(),
        where: "id = ?",
        whereArgs: [dog.id], // `id` as `whereArg` to prevent SQL injection
    );
}

Delete a record with db.delete():

Future<void> deleteDog(int id) async {
    final db = await database;

    await db.delete(
        'dogs',
        where: "id = ?",
        whereArgs: [id],
    );
}

Shared preferences

Key-value storage is easy and convenient, but has limitations: (1) Only primitive types can be used: int, double, bool, string, and stringList. (2) It's not designed to store a lot of data.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: "<newest version>"

To persist data, use the setter methods provided by the SharedPreferences class. Setter methods are available for various primitive types, such as setInt, setBool, and setString.

Persist data:

// obtain shared preferences
final prefs = await SharedPreferences.getInstance();

// set value
prefs.setInt('counter', counter);

Read data:

final prefs = await SharedPreferences.getInstance();

// Try reading data from the counter key. If it doesn't exist, return 0.
final counter = prefs.getInt('counter') ?? 0;

Remove data:

final prefs = await SharedPreferences.getInstance();

prefs.remove('counter');

Release

Source

Go to android/app/src/main/AndroidManifest.xml and check if android:label="MyAppName" is correct.

Go to android/app/build.gradle and check the applicationId.

Add a custom launcher icon using the flutter_launcher_icon library (Source). Add it as a dev dependency and save an icon file to the assets dir.

Sign the app with keytool or in Android Studio. Go to the android dir and create a key.properties file and enter your values. Then go to android/app/build.gradle and to specify where your key.properties file is located. This lets Flutter build and sign the release every time you want to release a new version of your app.

Run flutter build apk. Go to build/outputs/apk/release and upload app-release.apk to Google Play Store.

Sign up for Google Play Store (USD 25), create an application, and get four green checkmarks.

If you are using Google Sign-In with Firebase, update your SHA-1 certificate. Go to the App Signing page of the Google Play Store, get the values and paste them in Firebase console.