Flutter Notes
- Introduction
- Declarative UI
- Views
- Intents
- Project structure and resources
- Layouts
- Gesture detection and touch event handling
- ListView and adapters
- Text
- Databases and local storage
- Notifications
- Widget Lifecycles
- References
Introduction
This post is assuming that the reader has Android development background.
Declarative UI
Why
- Flutter lets the developer describe the current UI state and leaves the transitioning to the framework. This lightens the burden on developers from having to program how to transition between various UI states.
How
-
Example:
1 2 3 4 5
// Imperative style b.setColor(red) b.clearChildren() ViewC c3 = new ViewC(...) b.add(c3)
1 2 3 4 5
// Declarative style return ViewB( color: red, child: ViewC(...) )
-
In the declarative style, view configurations (such as Flutter’s Widgets) are immutable and only lightweight blueprints. To change the UI, a widget triggers a rebuild on itself and constructs a new Widget subtree. The framework manages many of the responsibilities behind the scenes.
Views
The equivalent of a view in Flutter
- In Flutter, the rough equivalent to a
View
is aWidget
. - The difference is that widgets are immutable and lightweight.
How to update widgets
-
Since widgets are immutable, we have to work with the state.
-
A
StatelessWidget
is a widget with no state info, similar to aImageView
with a logo, which doesn’t change during runtime. An example is theText
widget, which is a subclass ofStatelessWidget
:1 2 3 4
Text( 'I like Flutter!', style: TextStyle(fontWeight: FontWeight.bold), );
-
If we want the text to change dynamically, we wrap the
Text
widget in aStatefulWidget
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
import 'package:flutter/material.dart'; void main() { runApp(SampleApp()); } class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { // Default placeholder text String textToShow = "I Like Flutter"; void _updateText() { setState(() { // update the text textToShow = "Flutter is Awesome!"; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center(child: Text(textToShow)), floatingActionButton: FloatingActionButton( onPressed: _updateText, tooltip: 'Update Text', child: Icon(Icons.update), ), ); } }
Intents
The equivalent of an Intent in Flutter
-
Flutter doesn’t have intents. And Flutter doesn’t have a direct equivalent to activities and fragments; rather, we navigate between screens using a
Navigator
andRoute
s, all within the sameActivity
. -
A
Route
is an abstraction for a screen or page. And aNavigator
is a widget that manages routes. ARoute
roughly maps to anActivity
. -
A navigator can push and pop routes to move from screen to screen. It works like a stack on which you can
push()
new routes we want to navigate to andpop()
routes that we want to go back. -
We specify a
Map
of route names:1 2 3 4 5 6 7 8 9 10 11 12
void main() { runApp(MaterialApp( home: MyAppHome(), // becomes the route named '/' routes: <String, WidgetBuilder> { '/a': (BuildContext context) => MyPage(title: 'page A'), '/b': (BuildContext context) => MyPage(title: 'page B'), '/c': (BuildContext context) => MyPage(title: 'page C'), }, )); } Navigator.of(context).pushNamed('/b');
-
For calling a Camera or File picker, we need a native platform integration or use plugins.
How to handle incoming intents from external apps
-
Flutter handles incoming intents from Android by directly talking to the Android layer and requesting the data shared.
-
The basic flow is that we first handle the shared data in
Activity
and wait until Flutter requests with aMethodChannel
.
The equivalent of startActivityForResult()
-
It’s done by
await
ing on theFuture
returned bypush()
:1 2 3
Map coordinates = await Navigator.of(context).pushNamed('/location'); // Then in the location route Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
Project structure and resources
Image files
-
No predefined folder structure. We declare the assets (with location) in the
pubspec.yaml
file. -
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13
images/my_icon.png // Base: 1.0x image images/2.0x/my_icon.png // 2.0x image images/3.0x/my_icon.png // 3.0x image // declare these in pubspec.yaml assets: - images/my_icon.jpeg // Then access using AssetImage return AssetImage("images/my_icon.jpeg"); // Or in an Image widget @override Widget build(BuildContext context) { return Image.asset("images/my_image.png") }
Strings
-
No dedicated resources-like system. The best practice is:
1 2 3 4 5
class Strings { static String welcomeMessage = "Welcome to Flutter"; } // Access Text(Strings.welcomeMessage);
-
We’re encouraged to use the intl package for internationalization.
Layouts
Equivalent of a LinearLayout
-
The Row or Column widgets are the equivalent:
1 2 3 4 5 6 7 8 9 10 11 12
@override Widget build(BuildContext context) { return Row( // Or Column mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], ); }
-
Noteworthy properties:
mainAxisAlignment
mainAxisSize
crossAxisAlignment
: the cross axis forRow
is the vertical axis.
-
The alignment styles:
Flexible widget
-
The
Flexible
widget wraps a widget to make it resizable:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Row( children: [ BlueBox(), Flexible( fit: FlexFit.loose, // The widget's preferred size is used flex: 1, // Determines what fraction of the remaining space each widget gets: here is 1/2 child: BlueBox(), ), Flexible( fit: FlexFit.tight, // Forces the widget to fill all of its extra space flex: 1, child: BlueBox(), ), ], ); } }
Expanded widget
- It forces the widget to fill all the empty space:
Expanded(child: BlueBox())
.
SizedBox widget
-
It can be used in two ways:
- When it wraps a widget, it resizes the widget using
height
andweight
.
1 2 3 4
SizedBox( width: 100, // number of pixels child: BlueBox(), )
- When it doesn’t wrap a widget, it can create empty space:
SizedBox(width: 25)
- When it wraps a widget, it resizes the widget using
Spacer widget
- It creates empty space based on
flex
:Spacer(flex: 1)
Equivalent of a RelativeLayout
- We can achieve the same result by using a combination of Column, Row, and Stack widgets.
Equivalent of a ScrollView
-
THe equivalent is a ListView. A ListView in Flutter is both a ScrollView and an Android ListView.
1 2 3 4 5 6 7 8 9 10 11 12
@override Widget build(BuildContext context) { return ListView( // Or Column mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Row One'), Text('Row Two'), Text('Row Three'), Text('Row Four'), ], ); }
Gesture detection and touch event handling
Equivalent of onClick
-
If the widget supports event detection, pass a function to it and handle it in the function:
1 2 3 4 5 6 7 8
@override Widget build(BuildContext context) { return RaisedButton( onPressed: () { print("click"); }, child: Text("Button")); }
-
If the widget doesn’t support event detection, wrap it in a GestureDetector and pass a function to the
onTap
parameter:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
class SampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: GestureDetector( child: FlutterLogo( size: 200.0, ), onTap: () { print("tap"); }, ), )); } }
Other gestures
- Using GestureDetector, we can listen to gestures such as:
- Tap
- Double tap
- Long press
- Vertical drag
- Horizontal drag
ListView and adapters
Equivalent of ListView
-
The equivalent of ListView is ListView:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
import 'package:flutter/material.dart'; void main() { runApp(SampleApp()); } class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } List<Widget> _getListData() { List<Widget> widgets = []; for (int i = 0; i < 100; i++) { widgets.add(Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i"), )); } return widgets; } }
Which item is clicked
-
Use the touch handling provided by the passed-in widgets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
import 'package:flutter/material.dart'; void main() { runApp(SampleApp()); } class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView(children: _getListData()), ); } List<Widget> _getListData() { List<Widget> widgets = []; for (int i = 0; i < 100; i++) { widgets.add(GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i"), ), onTap: () { print('row tapped'); }, )); } return widgets; } }
Update ListView dynamically
-
Build a list with
ListView.Build
when we have a dynamic List or a List with very large amounts of data. It’s essentially equivalent to RecyclerView on Android, which automatically recycles list elements.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
import 'package:flutter/material.dart'; void main() { runApp(SampleApp()); } class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { List widgets = <Widget>[]; @override void initState() { super.initState(); for (int i = 0; i < 100; i++) { widgets.add(getRow(i)); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: ListView.builder( itemCount: widgets.length, itemBuilder: (BuildContext context, int position) { return getRow(position); })); } Widget getRow(int i) { return GestureDetector( child: Padding( padding: EdgeInsets.all(10.0), child: Text("Row $i"), ), onTap: () { setState(() { widgets.add(getRow(widgets.length + 1)); print('row $i'); }); }, ); } }
Text
Form input
Equivalent of a hint
-
The equivalent is
InputDecoration
:1 2 3 4 5
body: Center( child: TextField( decoration: InputDecoration(hintText: "This is a hint"), ) )
Show validation errors
-
Pass an
InputDecoration
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
import 'package:flutter/material.dart'; void main() { runApp(SampleApp()); } class SampleApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Sample App', theme: ThemeData( primarySwatch: Colors.blue, ), home: SampleAppPage(), ); } } class SampleAppPage extends StatefulWidget { SampleAppPage({Key key}) : super(key: key); @override _SampleAppPageState createState() => _SampleAppPageState(); } class _SampleAppPageState extends State<SampleAppPage> { String _errorText; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Sample App"), ), body: Center( child: TextField( onSubmitted: (String text) { setState(() { if (!isEmail(text)) { _errorText = 'Error: This is not an email'; } else { _errorText = null; } }); }, decoration: InputDecoration( hintText: "This is a hint", errorText: _getErrorText(), ), ), ), ); } _getErrorText() { return _errorText; } bool isEmail(String em) { String emailRegexp = r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'; RegExp regExp = RegExp(emailRegexp); return regExp.hasMatch(em); } }
Databases and local storage
Shared Preferences
-
Use the
Shared_Preferences
plugin, which wraps the functionality of both Shared Preferences and NSUserDefaults.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { runApp( MaterialApp( home: Scaffold( body: Center( child: RaisedButton( onPressed: _incrementCounter, child: Text('Increment Counter'), ), ), ), ), ); } _incrementCounter() async { SharedPreferences prefs = await SharedPreferences.getInstance(); int counter = (prefs.getInt('counter') ?? 0) + 1; print('Pressed $counter times.'); prefs.setInt('counter', counter); }
SQLite
- Use the
SQFlite
plugin.
Notifications
- Use the
firebase_messaging
plugin.
Widget Lifecycles
-
The lifecycles:
-
initState()
is the method that initializes any data needed before Flutter paints it to the screen. For example, we can format a string in it.