FlutterでiOSの独自ライセンスを自動取得/表示まで

## はじめに
とあるPJでFlutterを使用したアプリ開発中、セキュリティ診断等でiOSが独自で使用しているライブラリのライセンスの記載が不足しているとの診断結果を受け追加することに。
ですがライブラリが追加されるたびにライセンスファイルを都度追加するのは、メンテナンスコストがかかるため自動化しようということになりました。

ということで本記事では、iOSの独自ライセンスの取得方法とFlutter標準ライセンスに追加する方法までをご紹介します！

## 目次
1\. 使用ライブラリ / ツール紹介

2\. 手順
　　1. ライセンスファイルの取得
　　2. ライセンスファイルからライセンス本文を参照
　　3. Flutter標準ライセンス一覧追加

3\. まとめ

## 使用ライブラリ / ツール紹介
① Flutterの標準ライブラリのライセンスを表示する方法

https://api.flutter.dev/flutter/material/showLicensePage.html

② iOSの独自ライセンスファイルを取得するツール
こちらはCocoaPodsを使ってコマンドを実行することによって指定のフォルダにライセンスファイルを書き出すことができます

https://github.com/mono0926/LicensePlist

③ ①のFlutter標準ライセンスに②を追加する方法

https://api.flutter.dev/flutter/foundation/LicenseRegistry-class.html

## 手順
### 1. ライセンスファイルの取得
②のツールを使用してライセンスファイルを取得するには、
まずCocoapodsで`LicensePlist`をインストール
```dart
pod 'LicensePlist'
```
その後コマンドで指定のパスにライセンスファイルを取得します。
```terminal
license-plist --output-path="ライセンスファイル格納先のパス"
```
これらの一連の処理をiOSをビルドする際に自動化するため`Podfile`に以下を宣言をしましょう！
```dart
target 'Runner' do
    ... // Lintなどの色んなやつ
    pod 'LicensePlist'

post_install do |installer|
    ... 
    system('license-plist --output-path="ライセンスファイルのパス"')
```

`system`として記載すると`flutter ios build`の際にコマンドとして実行してくれます。
自分のPJの場合、外部参照ファイルは基本的に`assets`フォルダで管理をしているためライセンス格納先パスは上記の位置に設定しました。

### 2. ライセンスファイルからライセンス本文を参照
手順1で格納先のフォルダには複数のライセンスファイルが出力されます。
そこからフォルダ内のすべてのファイルにアクセスしてライセンスファイルのパスを取得する必要があります。
自分の場合は以下のように書きました。

```dart
Future<List<String>> _getIosLicenseFileAssetPath() async {
    final List<String> filePaths = [];
    try {
        final manifestContent = await rootBundle.loadString('AssetManifest.json');
        final Map<String, dynamic> manifestMap = json.decode(manifestContent);
        // manifestMapからファイルパスを抽出してfilePaths.addなどでList追加
    } catch {
        rethrow;
    }
    return filePaths;
}
```
`AssetManifest.json`はassetsのファイル名とパスをkey / valueでまとめたjsonファイルです。
そこからパス名'licenses/ios/com.mono0926.LicensePlist'以下のファイル名を抽出して返却する関数になっております！

上記関数で抽出したライセンスファイルを参照するとこのような形式になっています。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>PrefernceSpecifiers</key>
    <array>
        <dict>
            <key>FooterText</key>
            <string> 
                Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
            </string>
        </dict>
    </array>
  </dict>
</plist>
```

ここから本文を参照するためにはXMLドキュメントとして読み込む必要があるため以下のように書きました。
```dart
final fileData = await rootBundle.loadString('ライセンスファイルのパス');
final licenseElement = XmlDocument.parse(fileData).findAllElements('string').first;

// ライセンス本文は`licenseElement.text`で参照
```
ライセンスファイルのstring属性の一番最初をライセンス本文として扱っています。
もう少しいい方法がある気がしますが、自分のPJで追加した40ファイルほどはすべてこの形式でできたので問題ないかと思います。。。

### 3. Flutter標準ライセンス一覧追加
ここまででライセンスファイルの`作成 / 取得 / 本文抽出`まで進めてきたので、最後にこれをFlutter標準のライセンスに追加しましょう！

ライブラリ③を使用したライセンス追加方法は以下です。
```dart
final fileData = await rootBundle.loadString('ライセンスファイルのパス');
final licenseElement = XmlDocument.parse(fileData).findAllElements('string').first;

LicenseResistory.addLicense(
    () => Stream<LicenseEntry>.value(
        LicenseEntryWithLineBreaks(<String>['ファイル名'], licenseElement.text),
    )
);
```
`LicenseEntryWithLineBreaks`の第一引数にライセンス名を、第二引数にライセンス本文を指定することでFlutter標準のライセンスリストに追加することができます。

ということで手順2, 3を一連のコードで書くと次のようになります。

```dart
Future<List<String>> addIosLicense() async {
    try {
        final assetPathList = _getIosLicenseFileAssetPath();
        for (final assetPath in assetPathList) {
            // パスからライセンス名を生成
            final fileName = assetPath.split('/').last.split('.').first;
        
            // pathのライセンスファイルを取得
            final fileData = await rootBundle.loadString(assetPath);
            final licenseElement = XmlDocument.parse(fileData).findAllElements('string').first;
            
            // Flutterの標準ライセンスに追加
            LicenseResistory.addLicense(
                () => Stream<LicenseEntry>.value(
                    LicenseEntryWithLineBreaks(<String>[fileName], licenseElement.text),
                )
            );
        }
    } catch {
        // Logとか出す
    }
}

Future<List<String>> _getIosLicenseFileAssetPath() async {
    final List<String> filePaths = [];
    try {
        final manifestContent = await rootBundle.loadString('AssetManifest.json');
        final Map<String, dynamic> manifestMap = json.decode(manifestContent);
        // manifestMapからファイルパスを抽出してfilePaths.addなどでList追加
    } catch {
        rethrow;
    }
    return filePaths;
}
```

あとは良きタイミングで`showLicensePage`を使用して表示するだけ！

## まとめ
ライセンスや規約周りはアプリ審査やセキュリティ診断周りで大きく影響してきます。
OSに依存するものもありますので見直してみてください！

## 追記
一部PJで使用しているライセンスファイル格納先のパスやコードなどは伏せて記載をしています