# Flutter Crash Note ![flutter dash](https://hackmd.io/_uploads/rkTzP2LqJx.jpg) # 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` ![image](https://hackmd.io/_uploads/SkHC_flh1l.png) ```javascript boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.5), spreadRadius: 1, blurRadius: 10, offset: Offset(0, 5), ) ] ``` ![image](https://hackmd.io/_uploads/H1XNJqzi1x.png) ```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 ![image](https://hackmd.io/_uploads/HJNfFKB21l.png) ```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)'); }); } } ```