Interaksi Pengguna: Gesture & Forms
Pemrograman Mobile
Muhamad Saad Nurul Ishlah, M.Comp.
Dept. Sistem Informasi & Dept. Ilmu Komputer, Universitas Pakuan
Agenda Kuliah
- Intro
- User Interaction & Gestur
- Flutter forms
- Widget FormField
- Form UI & Focus Nodes
- Managing Form State with Form State
Interaksi Pengguna & Gestur
- Tugas utama aplikasi:
- membuat interaksi antara manusia dan data semudah mungkin
- Mengakses, Menambah dan Mengubah data
- Interaksi Pengguna dihandel 2 tipe widget:
- Input
- Gesture Detector
Interaksi Pengguna & Gestur
Widget GestureDetector
- Gestur: segala jenis event interaksi – taps, drags, pans, dll
- Widget Button – Membungkus pendeteksi gestur
- GestureDetector – widget user-interaction inti
- Bungkus widget ini di sekitar widget lain, sehingga widget di bawahnya dapat mendengarkan semua bentuk interaksi dari pengguna
- Konsep
- membuat widget menperhatikan interaksi pengguna,
- ketika interaksi terdeteksi, berikan sebuah callback untuk dieksekusi
- GestureDetector membutuhkan:
- Widget
- Sebuah callback (fungsi)
Widget GestureDetector
GestureDetector(
onTap: () => print("tapped!"),
child: Text("Tap Me"),
);
Child Widget
GestureDetector
Widget GestureDetector
- Walaupun hanya membutuhkan 1 callback, GestureDetector bisa memiliki callback yang berbeda, yang dapat merespon gestur yang dideteksinya.
- onTap
- onTapUp
- onTapDown
- onLongPress
- onDoubleTap
- onHorizontalDragStart
- onVerticalDragDown
- onPanDown
- onScaleStart
- ~30 gestur
- Memanggil atau mengembalikan detail lain
Contoh
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter GestureDetector Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key? key, required this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _lights = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
alignment: FractionalOffset.center,
color: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.lightbulb_outline,
color: _lights ? Colors.yellow.shade600 : Colors.black,
size: 160,
),
),
GestureDetector(
onTap: () {
setState(() {
_lights = !_lights;
});
},
child: Container(
color: Colors.yellow.shade600,
padding: const EdgeInsets.all(8),
child: const Text('Toggle Light'),
),
),
],
),
),
),
);
}
}
https://www.youtube.com/watch?v=WhVXkCFPmK4
Widget Dismissible
- Salah satu widget yang perlu disorot, lebih tricky
- Membutuhkan Key
Contoh
child: ListView.builder(
shrinkWrap: true,
itemCount: allAddedCities.length,
itemBuilder: (BuildContext context, int index) {
final city = allAddedCities[index];
return Dismissible(
onDismissed: (DismissDirection dir)
=> _handleDismiss(dir, city),
background: Container(
child: Icon(Icons.delete_forever),
decoration: BoxDecoration(color: Colors.red[700]),
),
key: ValueKey(city),
child: CheckboxListTile(
value: city.active,
title: Text(city.name),
onChanged: (bool b) => _handleCityActiveChange(b, city),
),
);
}
),
Kunci Widget Interaksi Pengguna
Ada beberapa widget dengan interaksi di dalamnya, dan Anda kemungkinan akan membuatnya sendiri. Mereka semua mengikuti aturan dasar yang sama:
- Mereka biasanya membungkus widget yang tidak interaktif, menambahkan fungsionalitas pendeteksi gestur.
- Mereka menyediakan callback yang meneruskan detail interaksi, yang memberi Anda kesempatan untuk menangani data sesuka Anda.
Forms di Flutter
Form di Flutter
- Menghandel input pengguna seringkali rumit
- local state, event, nilai inputan
- Proses dasar pembuatan form:
- Membuat UI – FieldForm
- Validasi data – field-by-field
- Ambil data – ketika submit
- Mengirimkan data
Widget Form
- Widget Form membungkus widget FormField – Opsional
- Widget Form mengelola state dari field yang ada dalam form, sehingga menghapus pengelolaan state dari masing-masing field secara individual
- Cara interaksi dengan form adalah dengan menyerahkan kunci (objek key - GlobalKey) dengan tipe FormState
- Bagian dari Form:
- Form
- Key - GlobalKey<FormState>
- Fields
Widget Form
Form(
key: GlobalKey<FormState>,
child: Column(
children: [
TextFormField(),
TextFormField(),
DropdownButtonFormField(),
FormField(child: Checkbox()),
// ...
Button(
child: Text("Submit"),
onPressed: FormState.save()
// ...
GlobalKey<FormState>
- Form key – GlobalKey<FormState>
- FormState
- menyediakan logic dan properties
- dapat diakses selama berinteraksi dengan form melalui key, termasuk di dalam widget di bawah form
- Beberapa metode yang disediakan FormState:
- FormState.save()
- FormState.reset()
- FormState.validate()
Contoh
/// Flutter code sample for Form
// This example shows a [Form] with one [TextFormField] to enter an email
// address and an [ElevatedButton] to submit the form. A [GlobalKey] is used here
// to identify the [Form] and validate input.
//
// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/form.png)
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
hintText: 'Enter your email',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate will return true if the form is valid, or false if
// the form is invalid.
if (_formKey.currentState!.validate()) {
// Process data.
}
},
child: const Text('Submit'),
),
),
],
),
);
}
}
Widget FormField
di Flutter
Widget FormField
- FormState memberikan kemudahan untuk mengelola Form dengan multi-input fields
- Untuk mengelola masing-masing input, harus dalam bentuk widget FormField
- Widget FormField dapat membungkus berbagai jenis widget input, tidak hanya widget input teks
- Secara umum, terdapat 3 widget FormField
- FormField – field standar, mengubah widget input menjadi form field
- TextFormField – form field khusus yang membungkus field teks
- DropdownButtonFormField – widget yang membungkus widget DropdownButton dalam sebuah form field
Widget FormField
Widget TextFormField
Kombinasi TextField dan FormField
- Metode spesial dari FormField. Panggil pada semua child FormField
- InputDecoration adalah properti yang hanya ada pada widget TextField
- Properti dr TextField, memastikan bagian pertama yang disorot
- Validasi di setiap interaksi
- Metode khusus dari FormField untuk memvalidasi input
Padding(
padding: const EdgeInsets.symetric(verical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
autoValidate: true,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Widget TextFormField
3 Properti penting:
-
validator
- membutuhkan fungsi callback
- kembalian akan ditambahkan sebagai teks error dalam field. Jika bernilai null, tidak akan ditampilkan
- dihandel FormState
-
autoValidate (deprecated)
- jika bernilai true akan memanggil fungsi callback di validator
- onSave – dieksekusi ketika FormState.save() dipanggil
Padding(
padding: const EdgeInsets.symetric(verical: 8.0),
child: TextFormField(
onSaved: (String val) {},
...
autoValidate: bool,
validator: (String val) {},
),
),
Widget DropdownFormButton
- Jenis widget lain yang mengekstensi widget FormField
- Data ditampilkan dalam bentuk dropdown ketika ditap
- menerima fungsi callback onSave dan validator
Widget DropdownFormButton
- Jenis widget lain yang mengekstensi widget FormField
- Data ditampilkan dalam bentuk dropdown ketika ditap
- menerima fungsi callback onSave dan validator
Form Fields Umum
- Input lain?
- checkbox, date picker, slider, dll.
- Bungkus dalam widget FormField
- Menambah fungsionalitas dari FormField ke widget input yang dibungkusnya
return FormField(
onSaved: (val) => _newCity.active = _isDefaultFlag,
enabled: _enabled,
builder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Default city?"),
Checkbox(
value: _isDefaultFlag,
onChanged: (val) {
setState(() => _isDefaultFlag = val);
/...
Contoh Form
Form UI & Focus Nodes
InputDecoration
- Semua input dan form field memiliki properti decoration, dapat diisi widget InputDecoration
-
InputDecoration dapat menerima banyak argumen untuk menambahkan style:
- background color
- mengubah warna berdasarkan focus
- mengubah bentuk field
- menambahkan style pada teks (input dan helper)
- dll
InputDecoration
- Semua input dan form field memiliki properti decoration, dapat diisi widget InputDecoration
- InputDecoration dapat menerima banyak argumen:
- background color
- mengubah warna berdasarkan focus
- mengubah bentuk field
- menambahkan style pada teks (input dan helper)
- dll
InputDecoration
Secara default sudah disediakan oleh Flutter, tidak perlu menambah library tambahan
- OutlineInputBorder, menambahkan style untuk border input
- helperText selalu diperlihatkan, kecuali jika ada error, akan diubah dengan pesan error
- labelText selalu diperlihatkan
- validator
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => print(val),
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Optional",
labelText: "State or Territory name",
),
validator: (String val) {
if (val.isEmpty) {
return "Field cannot be left blank";
}
return null;
},
),
),
Focus Node
- autofocus memastikan field langsung terpilih (fokus) ketika halaman dirender
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Focus Node
- autofocus memastikan field langsung terpilih (fokus) ketika halaman dirender
- FocusNode membuat fokus bergerak secara programatikal
- Contoh:
- blank field ketika submit
- Dikelola menggunakan objek State dalam lifecycle
- FocusNode didefinisikan di dalam metode initState()
- Digunakan di text field pada properti focusNode
class _AddNewCityPageState extends State<AddNewCityPage> {
City _newCity = City.fromUserInput();
bool _formChanged = false;
bool _isDefaultFlag = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
// clean up the focus node when this page is destroyed.
focusNode.dispose();
super.dispose();
}
...
//
...
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Focus Node
- Trigger fokus ketika tombol submit ditekan
- Validasi ketika submit
- FocusScope - widget yang mengelola pengalihan fokus ke node yang benar
// tombol submit
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
color: Colors.blue[400],
child: Text("Submit"),
onPressed: _formChanged
? () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_handleAddNewCity();
Navigator.pop(context);
} else {
FocusScope.of(context).requestFocus(focusNode);
}
}
: null,
),
)
Focus Node
- Jika Anda ingin memberikan fokus secara terprogram ke field teks apa pun, Anda harus menggunakan widget FocusNode dan meminta untuk fokus kepada field teks tersebut.
Submit
Nama
Focus Node
- Jika Anda ingin memberikan fokus secara terprogram ke field teks apa pun, Anda harus menggunakan widget FocusNode dan meminta untuk fokus kepada field teks tersebut.
Submit
Field tidak boleh kosong
Nama
Manajemen Form State
dengan FormState
Merespon Perubahan pada Form
- Gunakan metode yang disediakan oleh widget Form dan FormState
- Form.onChanged – dipanggil ketika field manapun pada form terjadi perubahan
- Form.onWillPop – dipanggil ketika pengguna akan meninggalkan halaman aktif, memunculkan popup
- FormState.save – memberitahu Form untuk menemukan semua field form dalam porsi widget tree yang aktif dan panggil metode onSaved untuk menyimpan perubahan state saat ini.
- _formKey.currentState.save()
Form.onChanged
class AddNewCityPage extends StatefulWidget {
final AppSettings settings;
const AddNewCityPage({Key key, this.settings}) : super(key: key);
@override
_AddNewCityPageState createState() => _AddNewCityPageState();
}
class _AddNewCityPageState extends State<AddNewCityPage> {
City _newCity = City.fromUserInput();
bool _formChanged = false;
bool _isDefaultFlag = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
// clean up the focus node when this page is destroyed.
focusNode.dispose();
super.dispose();
}
bool validateTextFields() {
return _formKey.currentState.validate();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Add City",
style: TextStyle(color: AppColor.textColorLight),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: _formKey,
onChanged: _onFormChange,
onWillPop: _onWillPop,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => print(val),
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Optional",
labelText: "State or Territory name",
),
validator: (String val) {
if (val.isEmpty) {
return "Field cannot be left blank";
}
return null;
},
),
),
CountryDropdownField(
country: _newCity.country,
onChanged: (newSelection) {
setState(() => _newCity.country = newSelection);
},
),
FormField(
onSaved: (val) => _newCity.active = _isDefaultFlag,
builder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Default city?"),
Checkbox(
value: _isDefaultFlag,
onChanged: (val) {
setState(() => _isDefaultFlag = val);
},
),
],
);
},
),
Divider(
height: 32.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
textColor: Colors.red[400],
child: Text("Cancel"),
onPressed: () async {
if (await _onWillPop()) {
Navigator.of(context).pop(false);
}
}),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
color: Colors.blue[400],
child: Text("Submit"),
onPressed: _formChanged
? () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_handleAddNewCity();
Navigator.pop(context);
} else {
FocusScope.of(context).requestFocus(focusNode);
}
}
: null,
),
)
],
),
],
),
),
),
);
}
void _onFormChange() {
if (_formChanged) return;
setState(() {
_formChanged = true;
});
}
void _handleAddNewCity() {
final city = City(
name: _newCity.name,
country: _newCity.country,
active: true,
);
allAddedCities.add(city);
}
Future<bool> _onWillPop() {
if (!_formChanged) return Future<bool>.value(true);
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("Are you sure you want to abandon the form? Any changes will be lost."),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () => Navigator.of(context).pop(false),
textColor: Colors.black,
),
FlatButton(
child: Text("Abandon"),
textColor: Colors.red,
onPressed: () => Navigator.pop(context, true),
),
],
) ??
false;
},
);
}
}
- Ketika tombol submit ditekan, metode onChanged akan dipanggil
- mengubah state dari properti _formChanged
Form.onChanged
class AddNewCityPage extends StatefulWidget {
final AppSettings settings;
const AddNewCityPage({Key key, this.settings}) : super(key: key);
@override
_AddNewCityPageState createState() => _AddNewCityPageState();
}
class _AddNewCityPageState extends State<AddNewCityPage> {
City _newCity = City.fromUserInput();
bool _formChanged = false;
bool _isDefaultFlag = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
// clean up the focus node when this page is destroyed.
focusNode.dispose();
super.dispose();
}
bool validateTextFields() {
return _formKey.currentState.validate();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Add City",
style: TextStyle(color: AppColor.textColorLight),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: _formKey,
onChanged: _onFormChange,
onWillPop: _onWillPop,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => print(val),
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Optional",
labelText: "State or Territory name",
),
validator: (String val) {
if (val.isEmpty) {
return "Field cannot be left blank";
}
return null;
},
),
),
CountryDropdownField(
country: _newCity.country,
onChanged: (newSelection) {
setState(() => _newCity.country = newSelection);
},
),
FormField(
onSaved: (val) => _newCity.active = _isDefaultFlag,
builder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Default city?"),
Checkbox(
value: _isDefaultFlag,
onChanged: (val) {
setState(() => _isDefaultFlag = val);
},
),
],
);
},
),
Divider(
height: 32.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
textColor: Colors.red[400],
child: Text("Cancel"),
onPressed: () async {
if (await _onWillPop()) {
Navigator.of(context).pop(false);
}
}),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
color: Colors.blue[400],
child: Text("Submit"),
onPressed: _formChanged
? () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_handleAddNewCity();
Navigator.pop(context);
} else {
FocusScope.of(context).requestFocus(focusNode);
}
}
: null,
),
)
],
),
],
),
),
),
);
}
void _onFormChange() {
if (_formChanged) return;
setState(() {
_formChanged = true;
});
}
void _handleAddNewCity() {
final city = City(
name: _newCity.name,
country: _newCity.country,
active: true,
);
allAddedCities.add(city);
}
Future<bool> _onWillPop() {
if (!_formChanged) return Future<bool>.value(true);
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("Are you sure you want to abandon the form? Any changes will be lost."),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () => Navigator.of(context).pop(false),
textColor: Colors.black,
),
FlatButton(
child: Text("Abandon"),
textColor: Colors.red,
onPressed: () => Navigator.pop(context, true),
),
],
) ??
false;
},
);
}
}
- Ketika tombol submit ditekan, metode onChanged akan dipanggil
- mengubah state dari properti _formChanged
FormState.save
class AddNewCityPage extends StatefulWidget {
final AppSettings settings;
const AddNewCityPage({Key key, this.settings}) : super(key: key);
@override
_AddNewCityPageState createState() => _AddNewCityPageState();
}
class _AddNewCityPageState extends State<AddNewCityPage> {
City _newCity = City.fromUserInput();
bool _formChanged = false;
bool _isDefaultFlag = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
// clean up the focus node when this page is destroyed.
focusNode.dispose();
super.dispose();
}
bool validateTextFields() {
return _formKey.currentState.validate();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Add City",
style: TextStyle(color: AppColor.textColorLight),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: _formKey,
onChanged: _onFormChange,
onWillPop: _onWillPop,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => print(val),
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Optional",
labelText: "State or Territory name",
),
validator: (String val) {
if (val.isEmpty) {
return "Field cannot be left blank";
}
return null;
},
),
),
CountryDropdownField(
country: _newCity.country,
onChanged: (newSelection) {
setState(() => _newCity.country = newSelection);
},
),
FormField(
onSaved: (val) => _newCity.active = _isDefaultFlag,
builder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Default city?"),
Checkbox(
value: _isDefaultFlag,
onChanged: (val) {
setState(() => _isDefaultFlag = val);
},
),
],
);
},
),
Divider(
height: 32.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
textColor: Colors.red[400],
child: Text("Cancel"),
onPressed: () async {
if (await _onWillPop()) {
Navigator.of(context).pop(false);
}
}),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
color: Colors.blue[400],
child: Text("Submit"),
onPressed: _formChanged
? () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_handleAddNewCity();
Navigator.pop(context);
} else {
FocusScope.of(context).requestFocus(focusNode);
}
}
: null,
),
)
],
),
],
),
),
),
);
}
void _onFormChange() {
if (_formChanged) return;
setState(() {
_formChanged = true;
});
}
void _handleAddNewCity() {
final city = City(
name: _newCity.name,
country: _newCity.country,
active: true,
);
allAddedCities.add(city);
}
Future<bool> _onWillPop() {
if (!_formChanged) return Future<bool>.value(true);
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("Are you sure you want to abandon the form? Any changes will be lost."),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () => Navigator.of(context).pop(false),
textColor: Colors.black,
),
FlatButton(
child: Text("Abandon"),
textColor: Colors.red,
onPressed: () => Navigator.pop(context, true),
),
],
) ??
false;
},
);
}
}
- Hal terpenting adalah menyimpan data
- Form membungkus proses ini dengan metode FormState.save
- _formKey.currentState.save()
- memberitahu Form untuk menemukan semua field form dalam porsi widget tree yang aktif dan panggil metode onSaved untuk menyimpan perubahan state saat ini.
Form.onWillPop
class AddNewCityPage extends StatefulWidget {
final AppSettings settings;
const AddNewCityPage({Key key, this.settings}) : super(key: key);
@override
_AddNewCityPageState createState() => _AddNewCityPageState();
}
class _AddNewCityPageState extends State<AddNewCityPage> {
City _newCity = City.fromUserInput();
bool _formChanged = false;
bool _isDefaultFlag = false;
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
FocusNode focusNode;
@override
void initState() {
super.initState();
focusNode = FocusNode();
}
@override
void dispose() {
// clean up the focus node when this page is destroyed.
focusNode.dispose();
super.dispose();
}
bool validateTextFields() {
return _formKey.currentState.validate();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Add City",
style: TextStyle(color: AppColor.textColorLight),
),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: _formKey,
onChanged: _onFormChange,
onWillPop: _onWillPop,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
onSaved: (String val) => _newCity.name = val,
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Required",
labelText: "City name",
),
autofocus: true,
// ignore: deprecated_member_use
autovalidate: _formChanged,
validator: (String val) {
if (val.isEmpty) return "Field cannot be left blank";
return null;
},
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextFormField(
focusNode: focusNode,
onSaved: (String val) => print(val),
decoration: InputDecoration(
border: OutlineInputBorder(),
helperText: "Optional",
labelText: "State or Territory name",
),
validator: (String val) {
if (val.isEmpty) {
return "Field cannot be left blank";
}
return null;
},
),
),
CountryDropdownField(
country: _newCity.country,
onChanged: (newSelection) {
setState(() => _newCity.country = newSelection);
},
),
FormField(
onSaved: (val) => _newCity.active = _isDefaultFlag,
builder: (context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text("Default city?"),
Checkbox(
value: _isDefaultFlag,
onChanged: (val) {
setState(() => _isDefaultFlag = val);
},
),
],
);
},
),
Divider(
height: 32.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
textColor: Colors.red[400],
child: Text("Cancel"),
onPressed: () async {
if (await _onWillPop()) {
Navigator.of(context).pop(false);
}
}),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
color: Colors.blue[400],
child: Text("Submit"),
onPressed: _formChanged
? () {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
_handleAddNewCity();
Navigator.pop(context);
} else {
FocusScope.of(context).requestFocus(focusNode);
}
}
: null,
),
)
],
),
],
),
),
),
);
}
void _onFormChange() {
if (_formChanged) return;
setState(() {
_formChanged = true;
});
}
void _handleAddNewCity() {
final city = City(
name: _newCity.name,
country: _newCity.country,
active: true,
);
allAddedCities.add(city);
}
Future<bool> _onWillPop() {
if (!_formChanged) return Future<bool>.value(true);
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("Are you sure you want to abandon the form? Any changes will be lost."),
actions: <Widget>[
FlatButton(
child: Text("Cancel"),
onPressed: () => Navigator.of(context).pop(false),
textColor: Colors.black,
),
FlatButton(
child: Text("Abandon"),
textColor: Colors.red,
onPressed: () => Navigator.pop(context, true),
),
],
) ??
false;
},
);
}
}
- Bagaimana jika pengguna keluar halaman tanpa mengisi form?
Form.onWillPop
Form Cookbook
- Menghandel perubahan di dalam TextField - https://docs.flutter.dev/cookbook/forms/text-field-changes
- Mengembalikan nilai dari Textfield - https://docs.flutter.dev/cookbook/forms/retrieve-input
Diskusi
Ada pertanyaan?
Referensi
- Introduction to Widgets. https://flutter.dev/docs/development/ui/widgets-intro, diakses 16 Maret 2021
- Layouts in Flutter. https://flutter.dev/docs/development/ui/layout, diakses 16 Maret 2021
- Understanding Constraints. https://flutter.dev/docs/development/ui/layout/constraints, diakses 16 Maret 2021
- Windmill, Eric. 2020. Flutter in Action. Manning Publications.
Terima Kasih
Interaksi Pengguna: Forms dan Gesture
By Muhamad Ishlah
Interaksi Pengguna: Forms dan Gesture
- 5,471