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

Diskusi

Ada pertanyaan?

Referensi

Terima Kasih

Interaksi Pengguna: Forms dan Gesture

By Muhamad Ishlah

Interaksi Pengguna: Forms dan Gesture

  • 5,471