In this Flutter tutorial, we will show you how to create SQLite Offline CRUD iOS and Android Mobile Apps. Sometimes we need an Offline app, especially when the Internet connection unavailable. For this, we will use the common local database such as SQLite. It will be easier for someone who has to learn the standard query language (SQL).
This tutorial divided into several steps:
- Step #1: Preparation
- Step #2: Create a Flutter Application
- Step #3: Install SQLite Library
- Step #4: Display List of Data
- Step #5: Show Data Details
- Step #6: Add a New Data
- Step #7: Edit a Data
- Step #8: Run and Test Flutter Application to iOS and Android
The following tools, frameworks, and libraries are required for this tutorial:
- Flutter SDK
- SQLite
- Android SDK
- XCode
- Terminal (on Mac/Linux) or CMD (on Windows)
- IDE (Android Studio/IntelliJ/Visual Studio Code)
You can watch the tutorial on our YouTube channel too.
Let get started to the main steps!
Step #1: Preparation
Install Flutter SDK
To install flutter SDK, first, we have to download flutter_macos_v1.12.13+hotfix.8-stable.zip.
Extract the file to your desired location.
cd ~/development
unzip ~/Downloads/flutter_macos_v1.12.13+hotfix.8-stable.zip
Next, add flutter tools to the path.
export PATH="$PATH:~/development/flutter/bin"
Next, to make iOS and Android binaries can be downloaded ahead of time, type this Flutter command.
flutter precache
Check the Required Dependencies
To check the environment and displays a report to the terminal window to find dependencies that required to install, type this command.
flutter doctor
We have this summary in the Terminal.
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.12.13+hotfix.5, on Mac OS X 10.14.6 18G2022,
locale en-ID)
[!] Android toolchain - develop for Android devices (Android SDK version
29.0.0-rc1)
! Some Android licenses not accepted. To resolve this, run: flutter doctor
--android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 10.3)
[✓] Android Studio (version 3.4)
[✓] VS Code (version 1.37.1)
[!] Connected device
! No devices available
! Doctor found issues in 2 categories.
To fixes, the issues like this, just connect the Android device and update the Android license by type this command.
flutter doctor --android-licenses
Type `y` for every question that displayed in the terminal. Next, check the connected Android device by type this command.
adb devices
List of devices attached
FUZDON75YLOVVC5S device
Setup IDE
We need to set up the IDE to make it working with the Flutter app easily and compatibly. Start the Android Studio (we will use this IDE) then open Android Studio Menu -> Preference.
Choose plugins in the left pane.
Type Flutter in the plugins marketplace then presses Enter. Next, click the Install button on the Flutter. If there's a prompt to install Dart, click Yes. Restart the IDE when prompt to ask restart.
Now, the Flutter is ready to develop Android and iOS mobile apps.
Step #2: Create a Flutter Application
After the IDE setup complete, it will back to the Android starting dialog.
Choose `Start a New Flutter Project` then choose `Flutter Application`.
Click the Next Button then fill the required fields and choose the previously installed Flutter SDK path.
Click the next button then fill the package name with your own domain and leave the "Include Kotlin support for Android code" and "Include Swift support for iOS code" blank.
Click the Finish button and the Flutter application creation in progress. Next, run the Flutter application for the first time. In the Android Studio toolbar, choose the device and main.dart then click the play button.
Step #3: Install SQLite Library
We will use a standard way to install the library/module by add dependency to the pubspec.yaml file. So, add this line after cupertion_icons.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
sqflite: ^1.3.0
Next, click the "Packages get" button in the Flutter commands at the top of pubspec.yml content. That command will install the registered dependencies.
Next, create a folder and class or object `lib/models/trans.dart` that represent the SQLite table. This class is about earnings or expenses. So, the content of this class should be like this.
class Trans {
final int id;
final String transDate;
final String transName;
final String transType;
final int amount;
Trans({ this.id, this.transDate, this.transName, this.transType, this.amount });
Map<String, dynamic> toMap() {
return {
'id': id,
'date': transDate,
'name': transName,
'type': transType,
'amount': amount
};
}
@override
String toString() {
return 'Trans{id: $id, transName: $transName, amount: $amount}';
}
}
For SQLite database initialization, table creation, and CRUD operation, we will use a separate dart file. Create a new folder and dart file inside the lib folder.
mkdir lib/database
touch lib/database/dbconn.dart
Open that file then add these lines of dart codes.
import 'dart:async';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:flutter_offline/models/trans.dart';
class DbConn {
Database database;
Future initDB() async {
if (database != null) {
return database;
}
String databasesPath = await getDatabasesPath();
database = await openDatabase(
join(databasesPath, 'income.db'),
onCreate: (db, version) {
return db.execute(
"CREATE TABLE trans(id INTEGER PRIMARY KEY, date TEXT, name TEXT, type TEXT, amount INTEGER)",
);
},
version: 1,
);
return database;
}
Future<Trans> insertTrans(Trans trans) async {
final Database db = await database;
await db.insert(
'trans',
trans.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Future<List<Trans>> trans() async {
final Database db = await database;
final List<Map<String, dynamic>> maps = await db.query('trans');
return List.generate(maps.length, (i) {
return Trans(
id: maps[i]['id'],
transDate: maps[i]['date'],
transName: maps[i]['name'],
transType: maps[i]['type'],
amount: maps[i]['amount'],
);
});
}
Future<int> countTotal() async {
final Database db = await database;
final int sumEarning = Sqflite
.firstIntValue(await db.rawQuery('SELECT SUM(amount) FROM trans WHERE type = "earning"'));
final int sumExpense = Sqflite
.firstIntValue(await db.rawQuery('SELECT SUM(amount) FROM trans WHERE type = "expense"'));
return ((sumEarning == null? 0: sumEarning) - (sumExpense == null? 0: sumExpense));
}
Future<void> updateTrans(Trans trans) async {
final db = await database;
await db.update(
'trans',
trans.toMap(),
where: "id = ?",
whereArgs: [trans.id],
);
}
Future<void> deleteTrans(int id) async {
final db = await database;
await db.delete(
'trans',
where: "id = ?",
whereArgs: [id],
);
}
}
Now, the SQLite is ready to use with Flutter application.
Step #4: Display List of Data
We will display the list of data in a separate Dart file that will call from the main.dart home page body. So, the whole structure of widgets that display a list of data at least like this mockup.
Next, add a dart file for building the ListView widget.
touch lib/translist.dart
Open and edit that file then add these imports of Flutter material, model and detail page.
import 'package:flutter/material.dart';
import 'package:flutter_offline/models/trans.dart';
import 'detailwidget.dart';
Add TransList class after the imports that extends the StatelessWidget.
class TransList extends StatelessWidget {
}
On the first line of the class body, declare the variable of Trans object and the constructor of TransList class.
final List<Trans> trans;
TransList({Key key, this.trans}) : super(key: key);
The trans variable use to hold data from the list of SQLite that load from the main.dart. Next, add an overridden widget after the variable and constructor to build a ListView.
@override
Widget build(BuildContext context) {
return
ListView.builder(
itemCount: trans == null ? 0 : trans.length,
itemBuilder: (BuildContext context, int index) {
return
Card(
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailWidget(trans[index])),
);
},
child: ListTile(
leading: trans[index].transType == 'earning'? Icon(Icons.attach_money): Icon(Icons.money_off),
title: Text(trans[index].transName),
subtitle: Text(trans[index].amount.toString()),
),
)
);
});
}
That ListView builder contain the Card that has the child of InkWell that use to navigate to the DetailWidget using MaterialPageRoute. The child of the Card is ListTile that contains an Icon (leading), Text (title), and Text(subtitle).
The InkWell widget has an onTap event with an action to Navigate to the details page. Container, Column, Image, and Text have their own properties to adjust the style or layout.
Notes: Keep in mind, every widget that uses the child only has one widget as its child. If you need to put more than one widget to the parent widget, use children: <Widget> property.
Next, open and edit lib/main.dart then replace all Dart codes with these lines of codes to display the ListView in the main home page.
import 'package:flutter/material.dart';
import 'package:flutter_offline/adddatawidget.dart';
import 'dart:async';
import 'package:flutter_offline/models/trans.dart';
import 'package:flutter_offline/database/dbconn.dart';
import 'package:flutter_offline/translist.dart';
void main() async {
await DbConn;
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Transactions',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Transactions Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
DbConn dbconn = DbConn();
List<Trans> transList;
int totalCount = 0;
@override
Widget build(BuildContext context) {
if(transList == null) {
transList = List<Trans>();
}
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: new Container(
child: new Center(
child: new FutureBuilder(
future: loadList(),
builder: (context, snapshot) {
return transList.length > 0? new TransList(trans: transList):
new Center(child:
new Text('No data found, tap plus button to add!', style: Theme.of(context).textTheme.title));
},
)
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_navigateToAddScreen(context);
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
bottomNavigationBar: BottomAppBar(
child: new FutureBuilder(
future: loadTotal(),
builder: (context, snapshot) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Text('Total: $totalCount', style: Theme.of(context).textTheme.title),
);
},
),
color: Colors.cyanAccent,
),// This trailing comma makes auto-formatting nicer for build methods.
);
}
Future loadList() {
final Future futureDB = dbconn.initDB();
return futureDB.then((db) {
Future<List<Trans>> futureTrans = dbconn.trans();
futureTrans.then((transList) {
setState(() {
this.transList = transList;
});
});
});
}
Future loadTotal() {
final Future futureDB = dbconn.initDB();
return futureDB.then((db) {
Future<int> futureTotal = dbconn.countTotal();
futureTotal.then((ft) {
setState(() {
this.totalCount = ft;
});
});
});
}
_navigateToAddScreen (BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddDataWidget()),
);
}
}
We use the existing floating button as the add-data button and add a footer (BottomAppBar) that displays the sum of from the list which the sum of earnings subtracts by the sum of expenses.
Step #5: Show Data Details
We will display data details to another page that opened when tapping on a list item in the list page. For that, create a Dart file in the lib folder first.
touch lib/detailwidget.dart
We will use a scrollable Card widget to display a detail to prevent overflow if the Card content is longer. So, the layout structure of the widgets combination at least like this mockup.
Next, open and edit lib/detailwidget.dart then add these imports of Flutter material, database helper, editdatawidget, and trans object model.
import 'package:flutter/material.dart';
import 'database/dbconn.dart';
import 'editdatawidget.dart';
import 'models/trans.dart';
Add a DetailWidget class that extends StatefulWidget. This class has a constructor with an object field, a field of Trans object, and _DetailWidgetState that build the view for data detail.
class DetailWidget extends StatefulWidget {
DetailWidget(this.trans);
final Trans trans;
@override
_DetailWidgetState createState() => _DetailWidgetState();
}
Add a _DetailWidgetState class that implementing all required widgets to display data details.
class _DetailWidgetState extends State<DetailWidget> {
_DetailWidgetState();
DbConn dbconn = DbConn();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details'),
),
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(20.0),
child: Card(
child: Container(
padding: EdgeInsets.all(10.0),
width: 440,
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Name:', style: TextStyle(color: Colors.black.withOpacity(0.8))),
Text(widget.trans.transName, style: Theme.of(context).textTheme.title)
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Type:', style: TextStyle(color: Colors.black.withOpacity(0.8))),
Text(widget.trans.transType, style: Theme.of(context).textTheme.title)
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Amount:', style: TextStyle(color: Colors.black.withOpacity(0.8))),
Text(widget.trans.amount.toString(), style: Theme.of(context).textTheme.title)
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Date:', style: TextStyle(color: Colors.black.withOpacity(0.8))),
Text(widget.trans.transDate, style: Theme.of(context).textTheme.title)
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
RaisedButton(
splashColor: Colors.red,
onPressed: () {
_navigateToEditScreen(context, widget.trans);
},
child: Text('Edit', style: TextStyle(color: Colors.white)),
color: Colors.blue,
),
RaisedButton(
splashColor: Colors.red,
onPressed: () {
_confirmDialog();
},
child: Text('Delete', style: TextStyle(color: Colors.white)),
color: Colors.blue,
)
],
),
),
],
)
)
),
),
),
);
}
}
That codes build widgets combination of Container, Card, Column, Image, Text, and RaisedButton. The RaisedButtons has onPressed event that action to navigate to the EditDataWidget and trigger delete confirm dialog. Next, before the closing of _DetailWidgetState class body add this method or function to navigate to the EditDataWidget with trans object params.
_navigateToEditScreen (BuildContext context, Trans trans) async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => EditDataWidget(trans)),
);
}
To handle the delete button, we need to add method or function after the above method that shows an alert dialog to confirm if data will be deleted.
Future<void> _confirmDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text('Warning!'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text('Are you sure want delete this item?'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () {
final initDB = dbconn.initDB();
initDB.then((db) async {
await dbconn.deleteTrans(widget.trans.id);
});
Navigator.popUntil(context, ModalRoute.withName(Navigator.defaultRouteName));
},
),
FlatButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
Step #6: Add a New Data
We will create a new Dart file for the entry form to add new data. In the Form, there will be a TextFieldForm, RadioButton, and DateTimeField. DateTimeField is using the datetime_picker_formfield plugin or library. For that, open and edit pubspec.yaml then add this line after other dependencies.
dependencies:
...
datetime_picker_formfield: ^1.0.0
Next, run Package get by click on the Package get button in the Flutter commands toolbar. Now, the edit data widget is ready to build based on this mockup.
Next, open and edit lib/adddatawidget.dart then add these imports of datetime_picker_formfield, intl (Internationalization), Flutter Material, Database helper, and trans object model.
import 'package:intl/intl.dart';
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_offline/database/dbconn.dart';
import 'models/trans.dart';
The TextFormField will use DateTimeField and TextEditingController to binding the value. But for the Radio Button will binding it manually by first adding the enum variable after the imports.
enum TransType { earning, expense }
Next, create a class of AddDataWidget that extends StatefulWidget and has a constructor and _AddDataWidgetState initiation.
class AddDataWidget extends StatefulWidget {
AddDataWidget();
@override
_AddDataWidgetState createState() => _AddDataWidgetState();
}
Now, create a class that called in AddDataWidget class that will build the layout for the Add data.
class _AddDataWidgetState extends State<AddDataWidget> {
_AddDataWidgetState();
DbConn dbconn = DbConn();
final _addFormKey = GlobalKey<FormState>();
final format = DateFormat("dd-MM-yyyy");
final _transDateController = TextEditingController();
final _transNameController = TextEditingController();
String transType = 'earning';
final _amountController = TextEditingController();
TransType _transType = TransType.earning;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Data'),
),
body: Form(
key: _addFormKey,
child: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(20.0),
child: Card(
child: Container(
padding: EdgeInsets.all(10.0),
width: 440,
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Date'),
DateTimeField(
format: format,
controller: _transDateController,
onShowPicker: (context, currentValue) {
return showDatePicker(
context: context,
firstDate: DateTime(1900),
initialDate: currentValue ?? DateTime.now(),
lastDate: DateTime(2100));
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Name'),
TextFormField(
controller: _transNameController,
decoration: const InputDecoration(
hintText: 'Transaction Name',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter transaction name';
}
return null;
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Type'),
ListTile(
title: const Text('Earning'),
leading: Radio(
value: TransType.earning,
groupValue: _transType,
onChanged: (TransType value) {
setState(() {
_transType = value;
transType = 'earning';
});
},
),
),
ListTile(
title: const Text('Expense'),
leading: Radio(
value: TransType.expense,
groupValue: _transType,
onChanged: (TransType value) {
setState(() {
_transType = value;
transType = 'expense';
});
},
),
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Amount'),
TextFormField(
controller: _amountController,
decoration: const InputDecoration(
hintText: 'Amount',
),
keyboardType: TextInputType.number,
validator: (value) {
if (value.isEmpty) {
return 'Please enter amount';
}
return null;
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
RaisedButton(
splashColor: Colors.red,
onPressed: () {
if (_addFormKey.currentState.validate()) {
_addFormKey.currentState.save();
final initDB = dbconn.initDB();
initDB.then((db) async {
await dbconn.insertTrans(Trans(transDate: _transDateController.text, transName: _transNameController.text, transType: transType, amount: int.parse(_amountController.text)));
});
Navigator.pop(context) ;
}
},
child: Text('Save', style: TextStyle(color: Colors.white)),
color: Colors.blue,
)
],
),
),
],
)
)
),
),
),
),
);
}
}
That class declare all required variable that will save to the SQLite database, data binding, database helper initiation, and Form declaration. The form contains the DateTimeField, TextFormField, Radio Button, and Submit Button. The action on submit will save the data to the SQLite database then redirect back to the list view.
Step #7: Edit a Data
The layout for edit data is the same as the add data view with additional object params that get from the details page. This object will fill the default value of the DateTimeField, TextFormField, and Radio Button. On the submit it will update the data based on the ID then redirect to the list view. First, create a new dart file in the lib folder.
touch lib/editdatawidget.dart
Open and edit that file then add these lines of the dart codes to build the edit form and function to submit this form to the SQLite database.
import 'package:intl/intl.dart';
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';
import 'package:flutter/material.dart';
import 'package:flutter_offline/database/dbconn.dart';
import 'models/trans.dart';
enum TransType { earning, expense }
class EditDataWidget extends StatefulWidget {
EditDataWidget(this.trans);
final Trans trans;
@override
_EditDataWidgetState createState() => _EditDataWidgetState();
}
class _EditDataWidgetState extends State<EditDataWidget> {
_EditDataWidgetState();
DbConn dbconn = DbConn();
final _addFormKey = GlobalKey<FormState>();
int _id = null;
final format = DateFormat("dd-MM-yyyy");
final _transDateController = TextEditingController();
final _transNameController = TextEditingController();
String transType = '';
final _amountController = TextEditingController();
TransType _transType = TransType.earning;
@override
void initState() {
_id = widget.trans.id;
_transDateController.text = widget.trans.transDate;
_transNameController.text = widget.trans.transName;
_amountController.text = widget.trans.amount.toString();
transType = widget.trans.transType;
if(widget.trans.transType == 'earning') {
_transType = TransType.earning;
} else {
_transType = TransType.expense;
}
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Data'),
),
body: Form(
key: _addFormKey,
child: SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(20.0),
child: Card(
child: Container(
padding: EdgeInsets.all(10.0),
width: 440,
child: Column(
children: <Widget>[
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Date'),
DateTimeField(
format: format,
controller: _transDateController,
onShowPicker: (context, currentValue) {
return showDatePicker(
context: context,
firstDate: DateTime(1900),
initialDate: currentValue ?? DateTime.now(),
lastDate: DateTime(2100));
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Name'),
TextFormField(
controller: _transNameController,
decoration: const InputDecoration(
hintText: 'Transaction Name',
),
validator: (value) {
if (value.isEmpty) {
return 'Please enter transaction name';
}
return null;
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Transaction Type'),
ListTile(
title: const Text('Earning'),
leading: Radio(
value: TransType.earning,
groupValue: _transType,
onChanged: (TransType value) {
setState(() {
_transType = value;
transType = 'earning';
});
},
),
),
ListTile(
title: const Text('Expense'),
leading: Radio(
value: TransType.expense,
groupValue: _transType,
onChanged: (TransType value) {
setState(() {
_transType = value;
transType = 'expense';
});
},
),
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
Text('Amount'),
TextFormField(
controller: _amountController,
decoration: const InputDecoration(
hintText: 'Amount',
),
keyboardType: TextInputType.number,
validator: (value) {
if (value.isEmpty) {
return 'Please enter amount';
}
return null;
},
onChanged: (value) {},
),
],
),
),
Container(
margin: EdgeInsets.fromLTRB(0, 0, 0, 10),
child: Column(
children: <Widget>[
RaisedButton(
splashColor: Colors.red,
onPressed: () {
if (_addFormKey.currentState.validate()) {
_addFormKey.currentState.save();
final initDB = dbconn.initDB();
initDB.then((db) async {
await dbconn.updateTrans(Trans(id: _id, transDate: _transDateController.text, transName: _transNameController.text, transType: transType, amount: int.parse(_amountController.text)));
});
Navigator.popUntil(context, ModalRoute.withName(Navigator.defaultRouteName));
}
},
child: Text('Update', style: TextStyle(color: Colors.white)),
color: Colors.blue,
)
],
),
),
],
)
)
),
),
),
),
);
}
}
Step #8: Run and Test Flutter Application to iOS and Android
To run this Flutter apps to Android, simply click the play button in the Android Studio toolbar. Make sure your Android device is connected to your computer and appear in Android Studio Toolbar.
Here the working Flutter apps on Android device look like.
To run this Flutter apps to the iOS device, at least you must have Apple Developer personal account with your own domain (our example: com.djamware) as a bundle or package. Next, open the ios/Runner.xcworkspace in XCode then click Runner in the Project Navigator.
In Runner.xcodeproj click the Build Settings tab.
Scroll down and find Signing then choose Development Team to your Apple Developer personal account. Now, you can run the Flutter apps to iOS Device from Xcode or Android Studio. Here are the Flutter apps on iOS devices look like.
That just the basic. If you need more deep learning about Flutter, Dart, or related you can take the following cheap course:
- Learn Flutter From Scratch
- Flutter, Beginner to Intermediate
- Flutter in 7 Days
- Learn Flutter from scratch
- Dart and Flutter: The Complete Developer's Guide
That it's, the Flutter Tutorial: SQLite Offline CRUD iOS and Android Apps. You can get the full source code from our GitHub.