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 is unavailable. For this, we will use a common local database such as SQLite. It will be easier for someone who has to learn the standard query language (SQL).
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 that can be downloaded ahead of time, type this Flutter command.
flutter precache
Check the Required Dependencies
To check the environment and display a report to the terminal window to find dependencies that require installation, 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 fix the issues like this, just connect the Android device and update the Android license by typing this command.
flutter doctor --android-licenses
Type `y` for every question that is displayed in the terminal. Next, check the connected Android device by typing this command.
adb devices
List of devices attached
FUZDON75YLOVVC5S device
Setup IDE
We need to set up the IDE to make it work with the Flutter app easily and compatibly. Start Android Studio (we will use this IDE), then open Android Studio Menu -> Preferences.
Choose plugins in the left pane.
Type Flutter in the plugins marketplace, then press Enter. Next, click the Install button on Flutter. If there's a prompt to install Dart, click Yes. Restart the IDE when prompted to restart.
Now, Flutter is ready to develop Android and iOS mobile apps.
Step #2: Create a Flutter Application
After the IDE setup is complete, it will go 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 adding the 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 represents 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 operations, 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 code.
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, SQLite is ready to use with a Flutter application.
Step #4: Display List of Data
We will display the list of data in a separate Dart file that will be called 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, mode,l and detail page.
import 'package:flutter/material.dart';
import 'package:flutter_offline/models/trans.dart';
import 'detailwidget.dart';
Add the 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 the Trans object and the constructor of the TransList class.
final List<Trans> trans;
TransList({Key key, this.trans}) : super(key: key);
The trans variable is used to hold data from the list of SQLite that is loaded 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 contains the Card that has the child of InkWell that is used 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 code 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 the list, which is the sum of earnings subtracts by the sum of expenses.
Step #5: Show Data Details
We will display data details on another page that opens 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 is 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 builds the view for data detail.
class DetailWidget extends StatefulWidget {
DetailWidget(this.trans);
final Trans trans;
@override
_DetailWidgetState createState() => _DetailWidgetState();
}
Add a _DetailWidgetState class that implements 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 a widget combination of Container, Card, Column, Image, Text, and RaisedButton. The RaisedButtons has an onPressed event that action to navigates to the EditDataWidget and triggers a 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 a method or function after the above method that shows an alert dialog to confirm if the 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, a RadioButton, and a 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 clicking 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 bind the value. But for the Radio Button will bind 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 called AddDataWidget 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 declares all required variables that will be saved 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 the Data
The layout for editing data is the same as the add data view, with additional object params that are obtained from the details page. This object will fill the default value of the DateTimeField, TextFormField, and Radio Button. On submission, 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 Dart code 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 on iOS and Android
To run this Flutter app on Android, simply click the play button in the Android Studio toolbar. Make sure your Android device is connected to your computer and appears in the Android Studio Toolbar.
Here the working Flutter apps on an Android device look like.
To run this Flutter app on an 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 for your Apple Developer personal account. Now, you can run the Flutter apps on iOS devices from Xcode or Android Studio. Here is what the Flutter apps on iOS devices look like.
That's just the basics. 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's the Flutter Tutorial: SQLite Offline CRUD iOS and Android Apps. You can get the full source code from our GitHub.