Flutter で作成した iOS アプリに kintone のレコードをリストで表示しよう!

著者名:Mamoru Fujinoki( Fuji Business International (External link)

目次

はじめに

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 を使用します。
詳細は 公式ドキュメント (External link) を参考にしてください。

インストールされていない場合は、以下のドキュメントを参照してインストールしてください。
Get started > Set up an editor (External link)

コードのエディタには Microsoft の Visual Studio Code を使用します(以下、VSCode)。

Flutter のインストールおよび設定は以下のドキュメントを参照してください。
Get started > Install (External link)

iOSアプリの作成

以下のドキュメントを参考に Flutter のプロジェクトファイルを作成します。
Get started > Test drive (External link)

任意のプロジェクトフォルダーを選び、適当なプロジェクトの名前を入力します。
今回は flutter_kintone_application としました。

また、シュミレータには「iOS Simulator」を選択します。
するとシュミレータ画面が表示されます。

ライブラリのインストール

iOS アプリから kintone API をたたくので、http リクエストを実行するためのパッケージ http をインストールします。
VSCode のコマンドラインで以下のコマンドを実行します。

1
flutter pub add http

詳細は以下のドキュメントを参照してください。
Fetch data from the internet (External link)

また、kintone の API トークンを秘匿するためのライブラリ「flutter_secure_storage」もインストールします。

1
flutter pub add flutter_secure_storage

詳細は以下のドキュメントを参照してください。
flutter_secure_storage (External link)

コードの作成

上記で作成したプロジェクトのファイルより「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 API トークン設定用のページを用意してください。

  • API トークンをそのままコード内に書き込まないでください。
    ただし、アプリを端末にインストールする際に、以下のコマンドでリリースバージョンを build すると、API トークンをコード内に書き込んだ場合も秘匿できます。

    1
    
    flutter build apk --obfuscate --split-debug-info=/<project-name>/<directory>

動作の確認

kintone の顧客リストアプリにレコードを追加します。
今回は、インストールの際にサンプルデータを含めました。

VSCode のステータスバーよりシュミレータのメニューを起動し、「iOS Simulator」を選択します。

画面上に以下のようなシュミレータが表示されます。

「実行」→「デバッグの開始」をクリックします。
または「F5」を押下します。

iOS シュミレータ画面に顧客リストのレコードがリストビュー表示されれば成功です。

まとめ

今回は iOS のアプリを Flutter で作成し、kintone のアプリと連携してレコードをリスト表示するサンプルを紹介しました。
Flutter では、iOS 以外にも Android 等のモバイルアプリを簡単に作成できます。
kintone のアプリと API でデータを連携すれば、kintone アプリをモバイルアプリに変換できます。

information

この Tips は、2023 年 9 月版 kintone で動作を確認しています。