Flutter Basics
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
Quick fix → Import widgetToggle Suggestion DetailsRefactor → Center widgetRefactor → Wrap with...Refactor → Extract widgetOpen definition filePeek definitionCall 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(),
    ));
}
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.
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);
}
PageViewAs 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(),
    ]
);
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,
                    );
                },
            );
        }
    },
);
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]),
                ),
            );
        },
        );
    },
);
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!'),
);
http package.http package.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.
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" },
    );
}
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.
Flutter apps can make use of the SQLite databases via the sqflite plugin.
Add sqflite and path packages:
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'),
);
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,
);
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],
    );
}
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.
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');
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.