mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 22:09:06 +00:00
move experimental/form_app to root of project (#623)
This allows us to reference this sample from other places. See https://github.com/flutter/flutter/pull/70321 for more context.
This commit is contained in:
94
form_app/lib/main.dart
Normal file
94
form_app/lib/main.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'src/autofill.dart';
|
||||
import 'src/form_widgets.dart';
|
||||
import 'src/http/mock_client.dart';
|
||||
import 'src/sign_in_http.dart';
|
||||
import 'src/validation.dart';
|
||||
|
||||
// Set up a mock HTTP client.
|
||||
final http.Client httpClient = MockClient();
|
||||
|
||||
void main() {
|
||||
runApp(FormApp());
|
||||
}
|
||||
|
||||
final demos = [
|
||||
Demo(
|
||||
name: 'Sign in with HTTP',
|
||||
route: '/signin_http',
|
||||
builder: (context) => SignInHttpDemo(
|
||||
httpClient: httpClient,
|
||||
),
|
||||
),
|
||||
Demo(
|
||||
name: 'Autofill',
|
||||
route: '/autofill',
|
||||
builder: (context) => AutofillDemo(),
|
||||
),
|
||||
Demo(
|
||||
name: 'Form widgets',
|
||||
route: '/form_widgets',
|
||||
builder: (context) => FormWidgetsDemo(),
|
||||
),
|
||||
Demo(
|
||||
name: 'Validation',
|
||||
route: '/validation',
|
||||
builder: (context) => FormValidationDemo(),
|
||||
),
|
||||
];
|
||||
|
||||
class FormApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Form Samples',
|
||||
theme: ThemeData(primarySwatch: Colors.teal),
|
||||
routes: Map.fromEntries(demos.map((d) => MapEntry(d.route, d.builder))),
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Form Samples'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [...demos.map((d) => DemoTile(d))],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DemoTile extends StatelessWidget {
|
||||
final Demo demo;
|
||||
|
||||
DemoTile(this.demo);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(demo.name),
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, demo.route);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Demo {
|
||||
final String name;
|
||||
final String route;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
const Demo({this.name, this.route, this.builder});
|
||||
}
|
||||
105
form_app/lib/src/autofill.dart
Normal file
105
form_app/lib/src/autofill.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Demonstrates how to use autofill hints. The full list of hints is here:
|
||||
// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
|
||||
class AutofillDemo extends StatefulWidget {
|
||||
@override
|
||||
_AutofillDemoState createState() => _AutofillDemoState();
|
||||
}
|
||||
|
||||
class _AutofillDemoState extends State<AutofillDemo> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Autofill'),
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: AutofillGroup(
|
||||
child: Column(
|
||||
children: [
|
||||
...[
|
||||
Text('This sample demonstrates autofill. '),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Jane',
|
||||
labelText: 'First Name',
|
||||
),
|
||||
autofillHints: [AutofillHints.givenName],
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Doe',
|
||||
labelText: 'Last Name',
|
||||
),
|
||||
autofillHints: [AutofillHints.familyName],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'foo@example.com',
|
||||
labelText: 'Email',
|
||||
),
|
||||
autofillHints: [AutofillHints.email],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '(123) 456-7890',
|
||||
labelText: 'Telephone',
|
||||
),
|
||||
autofillHints: <String>[AutofillHints.telephoneNumber],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '123 4th Ave',
|
||||
labelText: 'Street Address',
|
||||
),
|
||||
autofillHints: <String>[AutofillHints.streetAddressLine1],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '12345',
|
||||
labelText: 'Postal Code',
|
||||
),
|
||||
autofillHints: <String>[AutofillHints.postalCode],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'United States',
|
||||
labelText: 'Country',
|
||||
),
|
||||
autofillHints: <String>[AutofillHints.countryName],
|
||||
),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '1',
|
||||
labelText: 'Country Code',
|
||||
),
|
||||
autofillHints: <String>[AutofillHints.countryCode],
|
||||
),
|
||||
].expand(
|
||||
(widget) => [
|
||||
widget,
|
||||
SizedBox(
|
||||
height: 24,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
214
form_app/lib/src/form_widgets.dart
Normal file
214
form_app/lib/src/form_widgets.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
class FormWidgetsDemo extends StatefulWidget {
|
||||
@override
|
||||
_FormWidgetsDemoState createState() => _FormWidgetsDemoState();
|
||||
}
|
||||
|
||||
class _FormWidgetsDemoState extends State<FormWidgetsDemo> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String title = '';
|
||||
String description = '';
|
||||
DateTime date = DateTime.now();
|
||||
double maxValue = 0;
|
||||
bool brushedTeeth = false;
|
||||
bool enableFeature = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Form widgets'),
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Scrollbar(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Card(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...[
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: 'Enter a title...',
|
||||
labelText: 'Title',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
title = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
filled: true,
|
||||
hintText: 'Enter a description...',
|
||||
labelText: 'Description',
|
||||
),
|
||||
onChanged: (value) {
|
||||
description = value;
|
||||
},
|
||||
maxLines: 5,
|
||||
),
|
||||
_FormDatePicker(
|
||||
date: date,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
date = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Estimated value',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
intl.NumberFormat.currency(
|
||||
symbol: "\$", decimalDigits: 0)
|
||||
.format(maxValue),
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
Slider(
|
||||
min: 0,
|
||||
max: 500,
|
||||
divisions: 500,
|
||||
value: maxValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
maxValue = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: brushedTeeth,
|
||||
onChanged: (checked) {
|
||||
setState(() {
|
||||
brushedTeeth = checked;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text('Brushed Teeth',
|
||||
style: Theme.of(context).textTheme.subtitle1),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('Enable feature',
|
||||
style: Theme.of(context).textTheme.bodyText1),
|
||||
Switch(
|
||||
value: enableFeature,
|
||||
onChanged: (enabled) {
|
||||
setState(() {
|
||||
enableFeature = enabled;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
].expand(
|
||||
(widget) => [
|
||||
widget,
|
||||
SizedBox(
|
||||
height: 24,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FormDatePicker extends StatefulWidget {
|
||||
final DateTime date;
|
||||
final ValueChanged onChanged;
|
||||
|
||||
_FormDatePicker({
|
||||
this.date,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
_FormDatePickerState createState() => _FormDatePickerState();
|
||||
}
|
||||
|
||||
class _FormDatePickerState extends State<_FormDatePicker> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Text(
|
||||
'Date',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
Text(
|
||||
intl.DateFormat.yMd().format(widget.date),
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
],
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Edit'),
|
||||
onPressed: () async {
|
||||
var newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: widget.date,
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
|
||||
// Don't change the date if the date picker returns null.
|
||||
if (newDate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChanged(newDate);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
31
form_app/lib/src/http/mock_client.dart
Normal file
31
form_app/lib/src/http/mock_client.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
class MockClient extends Mock implements http.Client {
|
||||
MockClient() {
|
||||
when(post('https://example.com/signin', body: anyNamed('body')))
|
||||
.thenAnswer((answering) {
|
||||
var body = answering.namedArguments[Symbol('body')];
|
||||
|
||||
if (body != null && body is String) {
|
||||
var decodedJson = json.decode(body);
|
||||
|
||||
if (decodedJson['email'] == 'root' &&
|
||||
decodedJson['password'] == 'password') {
|
||||
return Future.value(http.Response('', 200));
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(http.Response('', 401));
|
||||
});
|
||||
|
||||
when(post('https://example.com/signout'))
|
||||
.thenAnswer((_) => Future.value(http.Response('', 401)));
|
||||
}
|
||||
}
|
||||
124
form_app/lib/src/sign_in_http.dart
Normal file
124
form_app/lib/src/sign_in_http.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'sign_in_http.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class FormData {
|
||||
String email;
|
||||
String password;
|
||||
|
||||
FormData({
|
||||
this.email,
|
||||
this.password,
|
||||
});
|
||||
|
||||
factory FormData.fromJson(Map<String, dynamic> json) =>
|
||||
_$FormDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$FormDataToJson(this);
|
||||
}
|
||||
|
||||
class SignInHttpDemo extends StatefulWidget {
|
||||
final http.Client httpClient;
|
||||
|
||||
SignInHttpDemo({
|
||||
this.httpClient,
|
||||
});
|
||||
|
||||
@override
|
||||
_SignInHttpDemoState createState() => _SignInHttpDemoState();
|
||||
}
|
||||
|
||||
class _SignInHttpDemoState extends State<SignInHttpDemo> {
|
||||
FormData formData = FormData();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Sign in Form'),
|
||||
),
|
||||
body: Form(
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
...[
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: 'Your email address',
|
||||
labelText: 'Email',
|
||||
),
|
||||
onChanged: (value) {
|
||||
formData.email = value;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
labelText: 'Password',
|
||||
),
|
||||
obscureText: true,
|
||||
onChanged: (value) {
|
||||
formData.password = value;
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Sign in'),
|
||||
onPressed: () async {
|
||||
// Use a JSON encoded string to send
|
||||
var result = await widget.httpClient.post(
|
||||
'https://example.com/signin',
|
||||
body: json.encode(formData.toJson()),
|
||||
headers: {'content-type': 'application/json'});
|
||||
|
||||
if (result.statusCode == 200) {
|
||||
_showDialog('Succesfully signed in.');
|
||||
} else if (result.statusCode == 401) {
|
||||
_showDialog('Unable to sign in.');
|
||||
} else {
|
||||
_showDialog('Something went wrong. Please try again.');
|
||||
}
|
||||
},
|
||||
),
|
||||
].expand(
|
||||
(widget) => [
|
||||
widget,
|
||||
SizedBox(
|
||||
height: 24,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDialog(String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
child: AlertDialog(
|
||||
title: Text(message),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('OK'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
form_app/lib/src/sign_in_http.g.dart
Normal file
23
form_app/lib/src/sign_in_http.g.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sign_in_http.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
FormData _$FormDataFromJson(Map<String, dynamic> json) {
|
||||
return FormData(
|
||||
email: json['email'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$FormDataToJson(FormData instance) => <String, dynamic>{
|
||||
'email': instance.email,
|
||||
'password': instance.password,
|
||||
};
|
||||
164
form_app/lib/src/validation.dart
Normal file
164
form_app/lib/src/validation.dart
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2020, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:english_words/english_words.dart' as english_words;
|
||||
|
||||
class FormValidationDemo extends StatefulWidget {
|
||||
@override
|
||||
_FormValidationDemoState createState() => _FormValidationDemoState();
|
||||
}
|
||||
|
||||
class _FormValidationDemoState extends State<FormValidationDemo> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String adjective;
|
||||
String noun;
|
||||
bool agreedToTerms = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('📖 Story Generator'),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: FlatButton(
|
||||
textColor: Colors.white,
|
||||
child: Text('Submit'),
|
||||
onPressed: () {
|
||||
// Validate the form by getting the FormState from the GlobalKey
|
||||
// and calling validate() on it.
|
||||
var valid = _formKey.currentState.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Your story'),
|
||||
content: Text('The $adjective developer saw a $noun'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Done'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// A text field that validates that the text is an adjective.
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please enter an adjective.';
|
||||
}
|
||||
if (english_words.adjectives.contains(value)) {
|
||||
return null;
|
||||
}
|
||||
return 'Not a valid adjective.';
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: 'e.g. quick, beautiful, interesting',
|
||||
labelText: 'Enter an adjective',
|
||||
),
|
||||
onChanged: (value) {
|
||||
adjective = value;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// A text field that validates that the text is a noun.
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please enter a noun.';
|
||||
}
|
||||
if (english_words.nouns.contains(value)) {
|
||||
return null;
|
||||
}
|
||||
return 'Not a valid noun.';
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: 'i.e. a person, place or thing',
|
||||
labelText: 'Enter a noun',
|
||||
),
|
||||
onChanged: (value) {
|
||||
noun = value;
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// A custom form field that requires the user to check a
|
||||
// checkbox.
|
||||
FormField(
|
||||
initialValue: false,
|
||||
validator: (value) {
|
||||
if (value == false) {
|
||||
return 'You must agree to the terms of service.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
builder: (FormFieldState formFieldState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: agreedToTerms,
|
||||
onChanged: (value) {
|
||||
// When the value of the checkbox changes,
|
||||
// update the FormFieldState so the form is
|
||||
// re-validated.
|
||||
formFieldState.didChange(value);
|
||||
setState(() {
|
||||
agreedToTerms = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'I agree to the terms of service.',
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!formFieldState.isValid)
|
||||
Text(
|
||||
formFieldState.errorText ?? "",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
.copyWith(color: Theme.of(context).errorColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user