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で動作を確認しています。