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 widget
Toggle Suggestion Details
Refactor
→ Center widget
Refactor
→ Wrap with...
Refactor
→ Extract widget
Open definition file
Peek definition
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(),
));
}
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);
}
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(),
]
);
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.