FlutterはiOSやAndroidアプリの作成が可能なUI Frameworkです。
今回のサンプルでは、kintoneアプリに記録されているレコード情報をFlutterで開発したモバイルアプリにリストビュー表示してみたいと思います。
kintoneの顧客リストアプリを利用して実現してみます。
kintone APIをたたくためのAPIトークンを秘匿にする方法も紹介します。
顧客リストアプリの作成
固定リンクがコピーされました
kintoneアプリストアより顧客リストアプリを選択します。
表示されていない場合、「アプリストア検索」欄に「顧客リスト」と入力して検索してください。
「サンプルデータを含める」をチェックし、「このアプリを追加」をクリックしてアプリを作成します。
アプリ欄に顧客リストアプリが追加されていればアプリの作成は完了です。
サンプルデータがすでに登録されていることを確認します。
「アプリの設定」で各フィールドの「フィールドコード」を確認します。
デフォルトでは以下のような設定になっています。
フィールドの種類 |
フィールド名 |
フィールドコード |
備考 |
文字列(1行) |
会社名 |
会社名 |
|
文字列(1行) |
部署名 |
部署名 |
|
文字列(1行) |
担当者名 |
担当者名 |
|
文字列(1行) |
郵便番号(数字のみ) |
郵便番号 |
|
文字列(1行) |
TEL(数字のみ) |
TEL |
|
文字列(1行) |
FAX(数字のみ) |
FAX |
|
文字列(1行) |
住所 |
住所 |
|
ドロップダウン |
顧客ランク |
顧客ランク |
選択肢:A, B, C |
文字列(1行) |
メールアドレス |
メールアドレス |
|
添付ファイル |
会社ロゴ |
会社ロゴ |
|
文字列(複数行) |
備考 |
備考 |
|
レコード番号 |
レコード番号 |
レコード番号 |
|
APIトークンの生成
固定リンクがコピーされました
アプリの設定においてAPIトークンを生成し、「レコード閲覧」をチェックします。
APIトークンの値をメモしておきます。
カスタムコードの作成
固定リンクがコピーされました
今回のiOSアプリの作成にはFlutterを使用します。
詳細は
公式ドキュメント
を参考にしてください。
インストールされていない場合は、以下のドキュメントを参照してインストールしてください。
Get started > Set up an editor
コードのエディタにはMicrosoftのVisual Studio Codeを使用します(以下、VSCode)。
Flutterのインストールおよび設定は以下のドキュメントを参照してください。
Get started > Install
以下のドキュメントを参考にFlutterのプロジェクトファイルを作成します。
Get started > Test drive
任意のプロジェクトフォルダーを選び、適当なプロジェクトの名前を入力します。
今回はflutter_kintone_application
としました。
また、シュミレータには「iOS Simulator」を選択します。
するとシュミレータ画面が表示されます。
ライブラリのインストール
固定リンクがコピーされました
iOSアプリからkintone APIをたたくので、httpリクエストを実行するためのパッケージhttp
をインストールします。
VSCodeのコマンドラインで以下のコマンドを実行します。
詳細は以下のドキュメントを参照してください。
Fetch data from the internet
また、kintoneのAPIトークンを秘匿するためのライブラリ「flutter_secure_storage」もインストールします。
1
|
flutter pub add flutter_secure_storage
|
詳細は以下のドキュメントを参照してください。
flutter_secure_storage
上記で作成したプロジェクトのファイルより「main.dart」を開きます。
こちらを参考にコードを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
/*
* Flutter sample program
* Copyright (c) 2023 Cybozu
*
* Licensed under the MIT License
*/
import 'package:flutter/foundation.dart'; // low-level Utilityクラスのライブラリ
import 'package:flutter/material.dart'; // Material Designのウィジェットのライブラリ
import 'dart:async'; // 非同期プログラムをサポートするライブラリ
import 'dart:convert'; // JSONデータの変換用ライブラリ
import 'package:http/http.dart' as http; // Http Request用のライブラリ
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // Secure Storage用のライブラリ
Future<List<Customer>> fetchCustomers(http.Client client) async {
const param = {"app": '{kintoneアプリID}'}; // kintoneアプリのIDを設定
final uri = Uri.https('{kintoneのURI}', '/k/v1/records.json',
param); // kintoneアプリのレコードを取得するAPIのURIを設定
// kintoneのAPIトークンをiOSのKeychainに格納して秘匿にします。
SecureStorage().deleteSecureData('kintoneAPI'); // Keychainに格納されているAPIトークンを削除
SecureStorage().writeSecureData(
// Keychainにkintone APIトークンの書込み
'kintoneAPI',
'{kintone APIトークン}');
final token = await SecureStorage()
.readSecureData('kintoneAPI'); // Keychainに格納されているkintone APIトークンの読み込み
final response = await client.get(
// http GET requestの実行
uri,
// 承認用ヘッダのバックエンドへの送信
headers: {
'X-Cybozu-API-Token': token.toString(),
},
);
if (response.statusCode == 200) {
// サーバからのレスポンスコードが200なら、JSONデータに変換します。
return compute(parseCustomers, response.body);
} else {
// サーバのレスポンスコードが200でない場合、例外処理をします。
throw Exception('Failed to load customers');
}
}
// レスポンスの内容をList<Customer>に変換する関数です。
List<Customer> parseCustomers(String responseBody) {
final parsed =
jsonDecode(responseBody)['records'].cast<Map<String, dynamic>>();
return parsed.map<Customer>((json) => Customer.fromJson(json)).toList();
}
class Customer {
// Customerクラス
final String company;
final String department;
final String name;
final String postalCode;
final String phone;
final String fax;
final String address;
final String rank;
final String email;
final String note;
const Customer({
required this.company,
required this.department,
required this.name,
required this.postalCode,
required this.phone,
required this.fax,
required this.address,
required this.rank,
required this.email,
required this.note,
});
factory Customer.fromJson(Map<String, dynamic> json) {
return Customer(
company: json['会社名']['value'],
department: json['部署名']['value'],
name: json['担当者名']['value'],
postalCode: json['郵便番号']['value'],
phone: json['TEL']['value'],
fax: json['FAX']['value'],
address: json['住所']['value'],
rank: json['顧客ランク']['value'],
email: json['メールアドレス']['value'],
note: json['備考']['value'],
);
}
}
void main() => runApp(const MyApp()); // Mainブロック
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const appTitle = '顧客リストデモ';
return const MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: FutureBuilder<List<Customer>>(
future: fetchCustomers(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
return CustomersList(customers: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class CustomersList extends StatelessWidget {
const CustomersList({super.key, required this.customers});
final List<Customer> customers;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: customers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('会社名:${customers[index].company}'),
subtitle: Text('担当者:${customers[index].name}'),
);
},
);
}
}
class SecureStorage {
// 秘匿情報をKeychainに格納するクラス
final storage = const FlutterSecureStorage();
Future<String?> readSecureData(String key) async {
return await storage.read(key: key);
}
writeSecureData(String key, String value) async {
await storage.write(key: key, value: value);
}
deleteSecureData(String key) async {
await storage.delete(key: key);
}
}
|
今回のカスタマイズで使用するライブラリを読み込んでいます。
使用しているライブラリは以下のとおりです。
7
8
9
10
11
12
13
14
|
import 'package:flutter/foundation.dart'; // low-level Utilityクラスのライブラリ
import 'package:flutter/material.dart'; // Material Designのウィジェットのライブラリ
import 'dart:async'; // 非同期プログラムをサポートするライブラリ
import 'dart:convert'; // JSONデータの変換用ライブラリ
import 'package:http/http.dart' as http; // Http Request用のライブラリ
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; // Secure Storage用のライブラリ
|
main関数です。
MyApp
クラスで定義されたアプリを実行します。
95
|
void main() => runApp(const MyApp()); // Mainブロック
|
アプリ全般の設定をします。
アプリ名および、ホームページのウィジェットの設定をしています。
97
98
99
100
101
102
103
104
105
106
107
108
109
|
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const appTitle = '顧客リストデモ';
return const MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
|
MyHomePage
ウィジェットを設定します。
アプリのトップバーにタイトルを表示し、ボディにkintoneの顧客リストから所得したレコードをリストビュー表示します。
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: FutureBuilder<List<Customer>>(
future: fetchCustomers(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
return CustomersList(customers: snapshot.data!);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
|
kintone顧客アプリからレコードを取得する関数です。
Customer
クラスのリストを返します。
{kintoneアプリID}
には、上記で作成したアプリのIDを設定してください。
また、{kintoneのURI}
には、お使いのkintoneのURLを指定してください。
URLを指定する際はhttps://
を除き、mykintoneuri.cybozu.com
の形式で指定してください。
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
Future<List<Customer>> fetchCustomers(http.Client client) async {
const param = {"app": '{kintoneアプリID}'}; // kintoneアプリのIDを設定
final uri = Uri.https('{kintoneのURI}', '/k/v1/records.json',
param); // kintoneアプリのレコードを取得するAPIのURIを設定
// kintoneのAPIトークンをiOSのKeychainに格納して秘匿にします。
SecureStorage().deleteSecureData('kintoneAPI'); // Keychainに格納されているAPIトークンを削除
SecureStorage().writeSecureData(
// Keychainにkintone APIトークンの書込み
'kintoneAPI',
'{kintone APIトークン}');
final token = await SecureStorage()
.readSecureData('kintoneAPI'); // Keychainに格納されているkintone APIトークンの読み込み
final response = await client.get(
// http GET requestの実行
uri,
// 承認用ヘッダのバックエンドへの送信
headers: {
'X-Cybozu-API-Token': token.toString(),
},
);
if (response.statusCode == 200) {
// サーバからのレスポンスコードが200なら、JSONデータに変換します。
return compute(parseCustomers, response.body);
} else {
// サーバのレスポンスコードが200でない場合、例外処理をします。
throw Exception('Failed to load customers');
}
}
|
kintoneの顧客アプリへのAPIトークンを秘匿するため、iOSのKeychain
に格納します。
{kintone APIトークン}
には、上記で設定したAPIトークンの値を設定します。
20
21
22
23
24
25
|
// kintoneのAPIトークンをiOSのKeychainに格納して秘匿にします。
SecureStorage().deleteSecureData('kintoneAPI'); // Keychainに格納されているAPIトークンを削除
SecureStorage().writeSecureData(
// Keychainにkintone APIトークンの書込み
'kintoneAPI',
'{kintone APIトークン}');
|
今回はデモのため同じコード内にて削除、書き込み、読み込みを行っています。
セキュリティ保護の観点から、
注意事項を確認してください。
Keychain
からkintone APIトークンを取得して、アプリからレコードを取得します。
レスポンスのコードが200ならデータ取得成功ですので、結果をJSONに変換します。
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
final token = await SecureStorage()
.readSecureData('kintoneAPI'); // Keychainに格納されているkintone APIトークンの読み込み
final response = await client.get(
// http GET requestの実行
uri,
// 承認用ヘッダのバックエンドへの送信
headers: {
'X-Cybozu-API-Token': token.toString(),
},
);
if (response.statusCode == 200) {
// サーバからのレスポンスコードが200なら、JSONデータに変換します。
return compute(parseCustomers, response.body);
} else {
// サーバのレスポンスコードが200でない場合、例外処理をします。
throw Exception('Failed to load customers');
}
|
parseCustomers
は、HTTPリクエストで取得したkintone APIからのレスポンスをJSON形式に変換する関数です。
また、Customer
クラスを定義しています。
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
// レスポンスの内容をList<Customer>に変換する関数です。
List<Customer> parseCustomers(String responseBody) {
final parsed =
jsonDecode(responseBody)['records'].cast<Map<String, dynamic>>();
return parsed.map<Customer>((json) => Customer.fromJson(json)).toList();
}
class Customer {
// Customerクラス
final String company;
final String department;
final String name;
final String postalCode;
final String phone;
final String fax;
final String address;
final String rank;
final String email;
final String note;
const Customer({
required this.company,
required this.department,
required this.name,
required this.postalCode,
required this.phone,
required this.fax,
required this.address,
required this.rank,
required this.email,
required this.note,
});
factory Customer.fromJson(Map<String, dynamic> json) {
return Customer(
company: json['会社名']['value'],
department: json['部署名']['value'],
name: json['担当者名']['value'],
postalCode: json['郵便番号']['value'],
phone: json['TEL']['value'],
fax: json['FAX']['value'],
address: json['住所']['value'],
rank: json['顧客ランク']['value'],
email: json['メールアドレス']['value'],
note: json['備考']['value'],
);
}
}
|
リストビューのウィジェットを設定しています。
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
class CustomersList extends StatelessWidget {
const CustomersList({super.key, required this.customers});
final List<Customer> customers;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: customers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('会社名:${customers[index].company}'),
subtitle: Text('担当者:${customers[index].name}'),
);
},
);
}
}
|
Keychain
に秘匿情報を保存、読込、削除するクラスを定義しています。
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
class SecureStorage {
// 秘匿情報をKeychainに格納するクラス
final storage = const FlutterSecureStorage();
Future<String?> readSecureData(String key) async {
return await storage.read(key: key);
}
writeSecureData(String key, String value) async {
await storage.write(key: key, value: value);
}
deleteSecureData(String key) async {
await storage.delete(key: key);
}
}
|
セキュリティリスクを軽減するためにAPIトークンは秘匿してください。
実運用の際は次の注意事項を踏まえ、各自判断の上、設定してください。
kintoneの顧客リストアプリにレコードを追加します。
今回は、インストールの際にサンプルデータを含めました。
VSCodeのステータスバーよりシュミレータのメニューを起動し、「iOS Simulator」を選択します。
画面上に以下のようなシュミレータが表示されます。
「実行」→「デバッグの開始」をクリックします。
または「F5」を押下します。
iOSシュミレータ画面に顧客リストのレコードがリストビュー表示されれば成功です。
今回はiOSのアプリをFlutterで作成し、kintoneのアプリと連携してレコードをリスト表示するサンプルを紹介しました。
Flutterでは、iOS以外にもAndroid等のモバイルアプリを簡単に作成できます。
kintoneのアプリとAPIでデータを連携すれば、kintoneアプリをモバイルアプリに変換できます。