概要
トップページと同様にお問い合わせページをFlutterで再現していきます。
Contactコンポーネントを作成しバリデーションとAPI連携を実装していきます。
完成イメージ
Contact画面作成
ファイル作成
新しくmain.dart、top.dartと同様の階層に「contact.dart」を作成します。
入力フォーム作成(見た目だけ)
状態が変化するため、StatefulWidgetを利用します。
createStateメソッドをオーバーライドして、新しいウィジェットの状態クラスを作成します。
何も記入されていないcontact.dart上でstlと入力すると、statefull widgetの雛形が出てくるのでこちらを利用してもいいと思います。(nameの変更などしてください)
表示内容はContainer単位で見るとわかりやすいです。
import 'package:flutter/material.dart';
class ContactPage extends StatefulWidget {
const ContactPage({super.key});
@override
State<StatefulWidget> createState() => _ContactPageState();
}
class _ContactPageState extends State<ContactPage> {
@override
Widget build(BuildContext context) {
return Form(
child: Scaffold(
body: Center(
child: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(
top: 80,
left: 20,
right: 20,
bottom: 10,
),
child: const Text(
'お問い合わせ',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
top: 5,
bottom: 5,
),
child: const Align(
alignment: Alignment.centerLeft,
child: Text('お名前',
textAlign: TextAlign.left, style: TextStyle())),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextFormField(
decoration: const InputDecoration(
hintText: "お名前",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
)),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
top: 5,
bottom: 5,
),
child: const Align(
alignment: Alignment.centerLeft,
child: Text('Email',
textAlign: TextAlign.left, style: TextStyle())),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextFormField(
decoration: const InputDecoration(
hintText: "Email",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
)),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
top: 5,
bottom: 5,
),
child: const Align(
alignment: Alignment.centerLeft,
child: Text('お問合せ内容',
textAlign: TextAlign.left, style: TextStyle())),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 20,
),
child: TextFormField(
keyboardType: TextInputType.multiline,
maxLines: 4,
decoration: const InputDecoration(
hintText: "お問い合わせ内容",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
)),
ColoredBox(
color: const Color.fromARGB(255, 204, 204, 204),
child: SizedBox(
width: MediaQuery.of(context).size.width - 40,
child: TextButton(
onPressed: () {},
child: const Text(
"送信",
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)),
),
),
),
),
],
),
),
));
}
}
状態を確認したいので、mainを編集してcontactを表示させてみます。
import 'package:flutter/material.dart';
import 'top.dart';
+ import 'contact.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Website',
theme: ThemeData(
primarySwatch: Colors.blue,
),
+ home: const ContactPage(),
- home: const TopPage(),
);
}
}
バリデーションの追加
FormウィジェットとGlobalKeyを使用して、フォームの状態を管理していきます。
_formKeyはフォームの状態を管理するためのグローバルキーです。
validateメソッドを呼び出すことでフォームの検証を行えます。
import 'package:flutter/material.dart';
class ContactPage extends StatefulWidget {
const ContactPage({super.key});
@override
State<StatefulWidget> createState() => _ContactPageState();
}
class _ContactPageState extends State<ContactPage> {
+ final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
+ key: _formKey,
child: Scaffold(
お名前
お名前について編集を追加していきます。
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextFormField(
+ validator: (value) {
+ if (value == null || value.isEmpty) {
+ return "お名前は必須です。";
+ }
+ return null;
+ },
decoration: const InputDecoration(
hintText: "お名前",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "お名前は必須です。";
}
return null;
},
emailについて編集を追加していきます。
child: TextFormField(
+ validator: (value) {
+ if (value == null ||
+ value.isEmpty ||
+ !RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
+ .hasMatch(value)) {
+ return "emailは必須かつemailの形式で入力してください。";
+ }
+ return null;
+ },
decoration: const InputDecoration(
hintText: "Email",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
),
validator: (value) {
if (value == null ||
value.isEmpty ||
!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return "emailは必須かつemailの形式で入力してください。";
}
return null;
},
お問い合わせ内容
問い合わせ内容についても同様に追加していきます。
child: TextFormField(
+ validator: (value) {
+ if (value == null || value.isEmpty || value.length > 10) {
+ return "お問合せ内容は必須かつ1文字以上10文字以下で入力してください。";
+ }
+ return null;
+ },
keyboardType: TextInputType.multiline,
maxLines: 4,
decoration: const InputDecoration(
hintText: "お問い合わせ内容",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240),
),
),
),
validator: (value) {
if (value == null || value.isEmpty || value.length > 10) {
return "お問合せ内容は必須かつ1文字以上10文字以下で入力してください。";
}
return null;
},
ボタンをクリックしたときの動作を設定
FocusScope.of(context).unfocus();
はキーボードを閉じる動作を追加しています。
child: TextButton(
onPressed: () {
+ FocusScope.of(context).unfocus();
+ if (_formKey.currentState!.validate()) {
+ //Data valid
+ } else {
+ //Data invalid
+ }
},
child: const Text(
"送信",
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)),
),
),
FocusScope.of(context).unfocus();
if (_formKey.currentState!.validate()) {
//Data valid
} else {
//Data invalid
}
確認すると、エラーが出る状態で送信ボタンをクリックするとエラーメッセージが見えます。
API連携
送信ボタンを押したときの動作を設定していきます。
値のハンドリング
TextEditingControllerを使って、それぞれの値の状態を管理できるようにします。
また、値の破棄の記述もしておきます。
class _ContactPageState extends State<ContactPage> {
final _formKey = GlobalKey<FormState>();
+ final nameController = TextEditingController();
+ final emailController = TextEditingController();
+ final contentController = TextEditingController();
+ @override
+ void dispose() {
+ nameController.dispose();
+ super.dispose();
+ }
final nameController = TextEditingController();
final emailController = TextEditingController();
final contentController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
それぞれのフォームにcontrollerを追加します。
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return "お名前は必須です。";
}
return null;
},
+ controller: nameController,
decoration: const InputDecoration(
hintText: "お名前",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextFormField(
validator: (value) {
if (value == null ||
value.isEmpty ||
!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return "emailは必須かつemailの形式で入力してください。";
}
return null;
},
+ controller: emailController,
decoration: const InputDecoration(
hintText: "Email",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240)),
),
),
Container(
margin: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 20,
),
child: TextFormField(
validator: (value) {
if (value == null || value.isEmpty || value.length > 10) {
return "お問合せ内容は必須かつ1文字以上10文字以下で入力してください。";
}
return null;
},
+ controller: contentController,
keyboardType: TextInputType.multiline,
maxLines: 4,
decoration: const InputDecoration(
hintText: "お問い合わせ内容",
filled: true,
border: InputBorder.none,
fillColor: Color.fromARGB(255, 240, 240, 240),
),
),
),
APIを叩く
httpパッケージ追加
http通信できるようにするためにパッケージを追加します。
pubspec.yamlを更新します。
dependencies:
+ http: any
flutter:
sdk: flutter
http.clientをcontact.dartでインポート
http.clientをインポートします。
import 'package:flutter/material.dart';
+ import 'package:http/http.dart' as http;
+ import 'dart:convert';
送信ボタンをクリックしたらAPIを叩くようにしていきます。
返ってきたメッセージを一旦コンソールに表示させてみます。
Flutter内でGoogleAppsScriptを叩くと必ずリダイレクトが発生します。(GASの仕様)
今回のAPIはGASで実装をしているため、ステータスコード302が返って来るのでリダイレクト先でレスポンスを取得する処理が必要となっていますが、GAS以外で実装したAPIの場合はリダイレクトの処理は不要となる場合があります。
ColoredBox(
color: const Color.fromARGB(255, 204, 204, 204),
child: SizedBox(
width: MediaQuery.of(context).size.width - 40,
child: TextButton(
- onPressed: () {
+ onPressed: () async {
FocusScope.of(context).unfocus();
if (_formKey.currentState!.validate()) {
+ String url ="API_URL";
+ final client = http.Client();
+
+ http.Response response = await client
+ .post(Uri.parse(url), headers: <String, String>{
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }, body: {
+ "name": nameController.text,
+ "email": emailController.text,
+ "body": contentController.text
+ });
+ if (response.statusCode == 302) {
+ String redirecturl = response.headers['location']!;
+ var res = await client.get(Uri.parse(redirecturl));
+ var data = jsonDecode(res.body);
+ print(data["message"]);
+ client.close();
}
} else {
//Data invalid
}
},
child: const Text(
"送信",
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)),
),
),
),
),
onPressed: () async {
FocusScope.of(context).unfocus();
if (_formKey.currentState!.validate()) {
String url =
"https://script.google.com/macros/s/{デプロイID}/exec";
final client = http.Client();
http.Response response = await client
.post(Uri.parse(url), headers: <String, String>{
'Content-Type': 'application/x-www-form-urlencoded'
}, body: {
"name": nameController.text,
"email": emailController.text,
"body": contentController.text
});
if (response.statusCode == 302) {
String redirecturl = response.headers['location']!;
var res = await client.get(Uri.parse(redirecturl));
var data = jsonDecode(res.body);
print(data["message"]);
client.close();
}
} else {
//Data invalid
}
},
確認してみます。
デバッグコンソールに「success!」と返ってきているのでAPIを叩いて戻り値も取得できているようです。
ダイアログ作成
ダイアログで戻り値を表示できるようにしていきます。
showAlertDialogの作成
表示したいダイアログを作成して、ダイアログを表示する機能を作成します。
contact.dartの一番下に追加します。
showAlertDialog(BuildContext context, String title, String message) {
//ダイアログのボタンの設定
Widget okButton = TextButton(
child: const Text("OK"),
onPressed: () {
Navigator.of(context).pop();
},
);
// ダイアログ設定
AlertDialog alert = AlertDialog(
title: Text(title),
content: Text(message),
actions: [
okButton,
],
);
// ダイアログ表示
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
設定
printで表示していた部分をShowAlertDialogに変更します。
if (response.statusCode == 302) {
String redirecturl = response.headers['location']!;
var res = await client.get(Uri.parse(redirecturl));
var data = jsonDecode(res.body);
- print(data["message"]);
+ showAlertDialog(context, "", data["message"]);
client.close();
}
確認
確認するとこのようにレスポンスが返ってきているのが確認できます。
Androidでビルドする場合
Androidでビルドする場合は「AndroidManifest.xml」に下記を追加します。
android > app > src > profile > AndroidManifest.xmlです。
<uses-permission android:name="android.permission.INTERNET" />