# Flutter Crash Note

# Cheat Sheet
- `??` if value of the left is `null` than return the vlaue from the right
EX: `A ?? false` if A is null than return false
- `final` A value that can be assigned only once
# ListWheelScrollView.useDelegat
```dart
Container(
height: 100,
width: 100,
child: ListWheelScrollView.useDelegate(
physics: FixedExtentScrollPhysics(),
onSelectedItemChanged: (index) {
// set value here
},
itemExtent: 50,
childDelegate: ListWheelChildBuilderDelegate(
builder: (current, index) {
return index >= 0 && index <= 50
? Center(
child: Text("$index"),
)
: null;
}
)
)
)
```
# Animation
## Value base
Extends with `SingleTickerProviderStateMixin`
```dart
late AnimationController _controller;
late Animation<double> _animation;
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 500));
_animation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_controller.forward();
_controller.addListener(() {
setState(() {});
});
```
# JSON Class Converter
```js
class History {
int id;
int time;
int finishTime;
String name;
List<Exercise> data;
History({
required this.id,
required this.time,
required this.finishTime,
required this.name,
required this.data,
});
factory History.fromJson(Map<String, dynamic> json) => History(
id: json['id'],
time: json['time'],
finishTime: json['finishTime'],
name: json['name'],
data: (json['data'] as List)
.map((data) => Exercise.fromJson(data))
.toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'time': time,
'finishTime': finishTime,
'name': name,
'data': data.map((data) => data.toJson()).toList()
};
}
```
# Method Channel
## Android Platform
- Native Side setup
```js
const val channelName = "com.example.national_module3_prepare/channel"
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelName
).setMethodCallHandler { call, result ->
when (call.method) {
"write_workout" -> {
try {
// get data from flutter
val jsonString = call.arguments as String
result.success(null)
} catch(e:Exception) {
result.error("write_workout_error","error", e.message)
}
}
}
}
}
}
```
# Navigation Rails & Navigator
```js
class _EntryState extends State<Entry> {
List<Route> navItems = [
Route(route: "profile", label: "Profile", icon: Icons.account_circle_rounded),
Route(route: "workout", label: "Workout", icon: Icons.directions_run),
Route(route: "history", label: "History", icon: Icons.history)
];
int _currentIndex = 0;
final String initScreen = "profile";
final GlobalKey<NavigatorState> _navKey = GlobalKey<NavigatorState>();
List<String> navStack = [];
@override
void initState() {
super.initState();
navStack.add(initScreen);
}
Widget _buildPage(String route) {
switch (route) {
case "profile":
return ProfileScreen();
case "workout":
return WorkoutScreen();
default:
return Text("error");
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (navStack.length > 1) {
navStack.removeLast();
_navKey.currentState?.pop();
_currentIndex = navItems.indexWhere((r) => r.route == navStack.last);
setState(() {});
return false;
}
return true;
},
child: Scaffold(
body: Row(
children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
destinations: navItems
.map(
(item) => NavigationRailDestination(
icon: Icon(item.icon),
label: Text(item.label),
),
)
.toList(),
selectedIndex: _currentIndex,
onDestinationSelected: (int index) {
_currentIndex = index;
_navKey.currentState?.pushNamed(navItems[index].route);
navStack.add(navItems[index].route);
setState(() {});
},
),
const VerticalDivider(),
Expanded(
child: Navigator(
key: _navKey,
initialRoute: initScreen,
onGenerateRoute: (RouteSettings setting) => PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
_buildPage(setting.name!)),
),
)
],
),
),
);
}
}
```
# Share Data (ChangeNotifier)
- calling this will update all the widget that bind with `ListenableBuilder`
```js
class MyShareData extends ChangeNotifier {
static final MyShareData _instance = MyShareData._internal();
factory MyShareData() => _instance;
MyShareData._internal(){
// function to run when the class got called
}
List<ExerciseType> _shareData = [];
List<ExerciseType> get shareData => _shareData;
void update(){
notifyListeners();
}
}
```
- code snippets for `ListenableBuilder`
```js
ListenableBuilder(
listenable: MyShareData(),
builder: (context, child){
return Placeholder();
}
)
```
# Edge To Edge
```js
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark
)
);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
```
# List
- `List.filled( <how many length> , <value to fill in> )` to fill the length of value to the list
# StateFulBuilder
```js
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
bool isLightOn = false; // Local state for the switch
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isLightOn ? "Light is ON" : "Light is OFF",
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
isLightOn = !isLightOn; // Toggle the state
});
},
child: Text("Toggle Light"),
),
],
);
},
)
```
# Scroll list picker
```dart
Container(
color: Colors.greenAccent,
height: 100,
width: 100,
child: ListWheelScrollView.useDelegate(
itemExtent: 50,
perspective: 0.005,
diameterRatio: 1.5,
physics: FixedExtentScrollPhysics(),
onSelectedItemChanged: (index) {
setState(() {
// selectedMinutes = index;
});
},
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
if (index >= 0 && index <= 60) {
return Center(
child: Text(
index.toString().padLeft(2, '0'),
style: TextStyle(fontSize: 25),
),
);
}
return null;
},
childCount: 61,
),
)
)
```
# Timer
- Like `tick` in windows form, run a task base on a fixed time
## Basic Operation
### Start
Create a value for storing countdown time, a late init variable with type `Timer` for storing Timer and a variable for checking a timer is running
```dart
var _time = 60;
late Timer timer;
var timerStarted = false;
```
Start a Timer
```dart
void start() {
if (!timerStarted)
timerStarted = true;
else
return;
timer = Timer.periodic(Duration(seconds: 1), (time) {
setState(() {
if (_time > 0) {
_time--;
} else {
timer.cancel();
timerStarted = false;
}
});
});
}
```
Pause the timer
```dart
void pause(){
timer.cancel();
timerStarted = false;
}
```
# Painter
- All the drawing stuff in a class that extend `CustomPainter`
- `paint` function contain the main logic of drawing
- `shouldRepaint` function is for setting whether the paint should be update return `true` `false` or a logic
```javascript
class Artist extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red
..strokeWidth = 10
..style = PaintingStyle.stroke;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
```
## Text
```dart
final textSpan = TextSpan(text: "Hello World", style: TextStyle());
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout();
textPainter.paint(canvas,Offset(x,y));
```
## Circle
`(Center of the circle[Offset], Circle radius[double], Paint[Paint])`
```dart
canvas.drawCircle(center, 75.0, paint);
```
## Rectangle
https://api.flutter.dev/flutter/dart-ui/Rect/Rect.fromLTWH.html
### Flutter drawRect with Different Rect Constructors
Use `Canvas.drawRect` in a `CustomPainter` to draw rectangles with various `Rect` constructors:
```dart
// Enter point, width, height
canvas.drawRect(
Rect.fromCenter(center: Offset(100, 100), width: 100, height: 80),
Paint()
..color = Colors.red
..style = PaintingStyle.fill,
);
// Top-left and bottom-right points
canvas.drawRect(
Rect.fromPoints(Offset(50, 50), Offset(150, 130)),
Paint()
..color = Colors.green
..style = PaintingStyle.stroke
..strokeWidth = 2,
);
// Circle-shaped rect (square)
canvas.drawRect(
Rect.fromCircle(center: Offset(200, 200), radius: 50),
Paint()
..color = Colors.blue
..style = PaintingStyle.fill,
);
// Left, Top, Right, Bottom
canvas.drawRect(
Rect.fromLTRB(20, 20, 120, 100),
Paint()
..color = Colors.purple
..style = PaintingStyle.fill,
);
```
# Style
## Padding
```javascript
Padding(
padding: const EdgeInsets.only(left: 10, bottom: 10),
child: Placeholder()
)
```
## Border Radius
- With `BoxDecoration`
```javascript
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10.0),
),
child: Placeholder(),
)
```
- Custom Radius
```javascript
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(15.0),
bottomLeft: Radius.circular(10.0),
bottomRight: Radius.circular(5.0),
)
```
- For all single widget
```javascript
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image(
image: NetworkImage("https://eliaschen.dev/eliaschen.jpg"),
),
)
```
## Shadow
- With `BoxDecoration`

```javascript
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.5),
spreadRadius: 1,
blurRadius: 10,
offset: Offset(0, 5),
)
]
```

```javascript
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black12,
offset: Offset(0.5, 0.5),
blurRadius: 5,
spreadRadius: 0.5,
),
BoxShadow(
color: Colors.white,
offset: const Offset(0.0, 0.0),
blurRadius: 0.0,
spreadRadius: 0.0,
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Text("data"),
),
)
```
## Expanded
- Must be used in `Row,Flex`
# Widgets
## ElevatedButton
```js
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: WidgetStateProperty.all(Colors.grey),
foregroundColor: WidgetStateProperty.all(Colors.black),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
)
)
)
)
```
# Animation
## TweenAnimationBuilder
```dart
TweenAnimationBuilder(
tween: Tween(
begin: 1.0,
end: originalTime > 0
? currentTime / originalTime
: 0.0),
duration: Duration(seconds: 1),
builder: (BuildContext context, double value,
Widget? child) {
return CircularProgressIndicator(
value: value,
strokeWidth: 10,
);
},
)
```
# ReorderableList
```js
SizedBox(
width: double.infinity,
height: 500,
child: ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: data.map((item) {
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
key: ValueKey(item['id']),
title: Text(item['name']),
subtitle: Text(item['type'].toString().toUpperCase()),
leading: Text(TimeFormatter(item['time'])),
trailing: const Icon(Icons.drag_indicator),
);
}).toList(),
),
)
```
# DraggableScrollableSheet
:::warning
**Remember to put the `DraggableScrollableSheet` into a `Stack` widget to let it float on top other widget**
:::
```js
DraggableScrollableSheet(
initialChildSize: 0.15,
minChildSize: 0.15,
maxChildSize: 1,
builder: (context, scroller) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
),
child: SingleChildScrollView(
controller: scroller,
),
);
},
);
```
# Dropdown menu

```javascript
var workType = TextEditingController();
DropdownMenu(
label: Text("Chose the work type"),
width: 200,
initialSelection: 0,
controller: workType,
dropdownMenuEntries: [
DropdownMenuEntry(value: 0, label: "Rest"),
DropdownMenuEntry(value: 1, label: "Work"),
]
)
```
# TextField
use a textController to make the inputfield auto update the value in the input box when the value change
- create a text controller then use the `<controller-name>.text` to get the value
```js
var email = TextEditingController();
```
```js
TextField(
controller: password,
obscureText: hidePassword,
obscuringCharacter: '*',
decoration: InputDecoration(
labelText: "主密碼",
suffixIcon: IconButton(
icon: Icon(
hidePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
hidePassword = !hidePassword;
});
},
),
),
)
```
# PopupMenuButton
```js
PopupMenuButton(
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
const PopupMenuItem(
child: Text('Option1'),
),
const PopupMenuItem(
child: Text('Option2'),
),
const PopupMenuItem(
child: Text('Option3'),
),
],
)
```
# SnackBar
```js
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
duration: Duration(seconds: 2),
content: Text("登入成功")
)
);
```
# Dismissible
```js
Dismissible(
key: ValueKey(<a-string-that-is-static>),
onDissmissed: (direction){
if(direction == DismissDirection.endToStart){
// when swipe right to left
}else if(direction == DismissDirection.startToEnd){
// when swipe left to right
}else if(direction == DismissDirection.horizontal){
// when swipe both direction
}
}
)
```
# Copy to Clipboard
```js
Clipboard.setData(ClipboardData(text: password))
```
# Scaffold
```js
Scaffold(
appBar: AppBar(
title: Text('Playground'),
actions: [
// Right side
],
),
drawer: Drawer(
child: Placeholder(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
body: Placeholder(),
)
```
# PopupMenuItem
```js
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
onTap: () {},
child: Placeholder()
),
PopupMenuItem(
onTap: () {},
child: Placeholder()
),
]
)
```
# Dialog
> **All the dialog use pop back to dismiss**
## Dialog
<img src="https://hackmd.io/_uploads/r1ne8pcokg.png" width="400"/>
```javascript
ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (context) => Dialog(
child: Column(
children: [
Text("Hello Dialog"),
],
),
)),
child: Text("Show Dialog")
)
```
## Alert dialog
<img src="https://hackmd.io/_uploads/Bk3P8a9sye.png" width="400"/>
```javascript
ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("AlertDialog"),
content: Text(
"hello there",
),
actions: [
TextButton(
onPressed: () {}, child: Text("Close")),
TextButton(onPressed: () {}, child: Text("Ok"))
],
),
),
child: Text("Show AlertDialog")
),
```
## FullScreen dialog
<img src="https://hackmd.io/_uploads/BJT08Tcjkl.png" width="400"/>
```javascript
ElevatedButton(
onPressed: () => showDialog(
context: context,
builder: (BuildContext context) => Dialog.fullscreen(
child: Column(
children: [
Text("Hello FullScreen Dialog"),
],
),
)),
child: Text("Show FullScreen Dialog"),
)
```
# Navigator
- Nav to a widget **(make it async to handle task after popBack)**
```js
Navigator.push(context,
MaterialPageRoute(builder: (context) => YourDestinationScreen())
);
```
- Go back
```js
Navigator.pop(context);
```
# Delay
```js
await Future.delayed(const Duration(seconds: 2));
```
- `Duration` support unit `days` `horus` `minutes` `seconds` `milliseconds` `microseconds`
# Shaking
## Raw Shaking Detection
- Android Layer
In `MainActivity.kt` create a methodChannel to retrive event in flutter layer
```kotlin
package dev.eliaschen.re_flutter_skills_pre
import android.hardware.*
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlin.math.sqrt
class MainActivity : FlutterActivity(), SensorEventListener {
private lateinit var sensorManager: SensorManager
private var methodChannel: MethodChannel? = null
private var lastShakeTime = 0L
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "shake_detector")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
}
}
override fun onSensorChanged(event: SensorEvent?) {
event?.values?.let {
if (sqrt((it[0] * it[0] + it[1] * it[1] + it[2] * it[2]).toDouble()) / 9.81 > 2.7) {
val now = System.currentTimeMillis()
if (now - lastShakeTime > 500) {
lastShakeTime = now
methodChannel?.invokeMethod("onShake", null)
}
}
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
```
- Flutter Layer
use `ShakeDetector.startListening();` to excute a function when the phone shake
```javascript
import 'package:flutter/services.dart';
class ShakeDetector {
static const platform = MethodChannel('shake_detector');
static void startListening(Function onShake) {
platform.setMethodCallHandler((call) async {
if (call.method == "onShake") {
onShake();
}
});
}
}
```
Call the function when the phone shake
```javascript
@override
void initState() {
super.initState();
ShakeDetector.startListening(() {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("the phone shake")));
});
}
```
## DateTime
- `.now()` get the current time
- `.parse()` parsing from ISO 8601
- `.fromMillisecondsSinceEpoch()` parsing from UNIX TimeStamp
```Dart
DateTime fromISO = DateTime.parse(<iso-time-string>);
DateTime fromTimeStamp = DateTime.parse(<UNIX-TimeStamp>);
```
# Network Request
## GET
```Kotlin
Future<void> fetchData() async{
final url = Uri.parse('<API-endpoint>');
final httpClient = HttpClient();
try {
final request = await httpClient.getUrl(url);
final response = await request.close(); // Send the request
if (response.statusCode == HttpStatus.ok) {
final res = await response.transform(utf8.decoder).join();
final jsonData = jsondecode(res);
/* handle return */
}
} catch (e) {
/* handle error */
} finally {
httpClient.close();
}
}
```
- add haders
Just use `request.headers.add()` to add header to the request
```dart
request.headers.add("search","na");
```
## Headers as a list
Create a Map for storing headers
```dart
Map<String, dynamic> body = {
'title': 'Flutter POST Request',
'body': 'This is a test post',
'userId': 1,
};
```
Then add body to the request
```dart
request.add(utf8.encode(jsonEncode(body)));
```
## POST
basically same as the GET request, just replace `httpClient.getUrl(url)` to `httpClient.postUrl(url)`
# Json parsing
- the json the extract method is kind of like python, use `[ ]`instead of `.` in Javascript
- Ex: `assets/data.json`
```json
{
"result": {
"offset": 0,
"limit": 10000,
"count": 1,
"sort": "",
"results": [
{
"_id": 1,
"Station": "後山埤站",
"Destination": "頂埔站",
"UpdateTime": "2015-08-24T12:03:27.907"
},
{
"_id": 2,
"Station": "永寧站",
"Destination": "南港展覽館站",
"UpdateTime": "2015-08-24T12:03:27.907"
},
{
"_id": 3,
"Station": "台北車站",
"Destination": "南港展覽館站",
"UpdateTime": "2015-08-24T12:03:37.5"
}
]
}
}
```
- from `assets`
```kotlin
String jsonString = await rootBundle.loadString('assets/data.json');
final List<dynamic> jsonResponse = jsonDecode(jsonString)["results"][""];
```
- from `app dir`
```kotlin
final directory = await getApplicationDocumentsDirectory();
final filePath = '${directory.path}/$filename';
final file = File(filePath);
final jsonString = await file.readAsString();
final List<dynamic> jsonData = jsonDecode(jsonString);
```
## TypeSafe (with class & Future Builder)
Create class for data parsing & typesafe
```kotlin
class StationSchema {
final int id;
final String Station;
final String Destination;
final String UpdateTime;
StationSchema({
required this.id,
required this.Station,
required this.Destination,
required this.UpdateTime,
});
factory StationSchema.fromMap(Map<String, dynamic> map) {
return StationSchema(
id: map['_id'],
Station: map['Station'],
Destination: map['Destination'],
UpdateTime: map['UpdateTime'],
);
}
}
```
Use `fromMap` to convert Map format to the stander list in dart
```kotlin
Future<List<StationSchema>> parseJson() async {
String jsonString = await rootBundle.loadString('assets/data.json');
List<dynamic> jsonMap = jsonDecode(jsonString)[0]['result']['results'];
List<StationSchema> stations = jsonMap.map((json) => StationSchema.fromMap(json)).toList();
await Future.delayed(Duration(seconds: 2));
return stations;
}
```
Future Builder
- using `snapshot.data` to get the data of the list we do json parsing before
```kotlin
FutureBuilder<List<StationSchema>>(
key: ValueKey<int>(key),
future: parseJson(),
builder: (context, snapshot) {
// something like ListView in there
},
)
```
# Sorting Map & List
# Sqflite
## Dependencies
- `sqflite`
- `path_provider`
## Create a Schema
```js
class TodoSchema {
final String todo;
final int done;
final int? id;
TodoSchema({this.id, required this.todo, required this.done});
Map<String, dynamic> toMap() {
return {'id': id, 'todo': todo, 'done': done};
}
}
```
## Low level syntax
| `rawQuery` | `excute` |
| --- | --- |
| return `Future<List<Map<String, dynamic>>>` | Don’t have a retrun value |
| require `await` | require `await` |
| can be save to `List<Map<String, dynamic>>` | use `?` to set the value later in `[]` |
| `List<Map<String,dynamic>> data = await <db>.rawQuery('<sql-statement>', [ ])` | `await <db>.excute('<sql-statement>', [ ])` |
## CRUD operation example
```js
class Appdb {
static Database? db;
static Future<Database> getDbConnection() async {
db ??= await initDatabase(); // if db is null, initialize it
return db!; // return db if it is not null
}
static Future<List<TodoSchema>> getTodos() async {
final Database db = await getDbConnection();
final List<Map<String, dynamic>> maps = await db
.query('todos ORDER BY id DESC'); // get all todos in descending order
return List.generate(maps.length, (index) {
return TodoSchema(
id: maps[index]['id'],
todo: maps[index]['todo'],
done: maps[index]['done'],
);
});
}
static Future<void> addTodo(TodoSchema todo) async {
final Database db = await getDbConnection();
await db.insert(
'todos',
todo.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
static Future<void> deleteTodo(int id) async {
final Database db = await getDbConnection();
await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
}
static Future<void> deleteAllTodos() async {
final Database db = await getDbConnection();
await db.delete('todos');
}
static Future<void> updateTodo(int id, int done) async {
final Database db = await getDbConnection();
await db.update(
'todos',
{'done': done},
where: 'id = ?',
whereArgs: [id],
);
}
static Future<Database> initDatabase() async {
var databasesPath = await getApplicationDocumentsDirectory(); // IMPORTANT
String path = join(databasesPath.path, 'todoapp.db');
return await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE todos(id INTEGER PRIMARY KEY, todo TEXT, done INTEGER)');
});
}
}
```