【Flutter】LicenseRegistryを使って自前でライセンスページを表示する

2024/03/07に公開

1. はじめに

Flutterでライセンスページを用意したい場合、SDK標準で簡単な方法が用意されています。

https://docs.flutter.dev/resources/faq#how-can-i-determine-the-licenses-my-flutter-application-needs-to-show

There’s an API to find the list of licenses you need to show:

  • If your application has a Drawer, add an AboutListTile.
  • If your application doesn’t have a Drawer but does use the Material Components library, call either showAboutDialog or showLicensePage.
  • For a more custom approach, you can get the raw licenses from the LicenseRegistry.

確かに、AboutListTileshowLicensePageを利用すれば非常に簡単です。
例えばこれだけで済みます。

ElevatedButton(
  child: const Text('showLicensePage'),
  onPressed: () => showLicensePage(context: context),
)

でもこれ、カスタマイズできないんですよね。Powered by Flutterが必ず表示されますし。
そこで、ライセンスページを自前で用意してみようと思います。

2. 例

色などは適当ですが、このように表示できました。

3. コード

  • For a more custom approach, you can get the raw licenses from the LicenseRegistry.

ドキュメントにこのように記載されていますので、LicenseRegistryを使用します。

パッケージ名を入手するのは簡単そうなのですが、paragraphをどのように処理すれば良いのか悩みました。インデントも考慮しないと上記のような見た目にはなりません。
そこで、showLicensePageの中のコードを読んでいくと、about.dart のこの辺り(_PackageLicensePage)が参考になりそうだということで、その周辺を参考にしつつ以下のように実装しました。

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'main.g.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    return const MaterialApp(home: Home());
  }
}

class Home extends StatelessWidget {
  const Home({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('ライセンス')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: const Text('showLicensePage'),
              onPressed: () => showLicensePage(context: context),
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              child: const Text('カスタムライセンス表示'),
              onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => const CustomLicense())),
            ),
          ],
        ),
      ),
    );
  }
}

class CustomLicense extends ConsumerWidget {
  const CustomLicense({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('カスタムライセンス')),
      body: switch (ref.watch(licenseNotifierProvider)) {
        AsyncData(:final value) => _License(entries: value),
        _ => const Center(child: CircularProgressIndicator()),
      },
    );
  }
}

class _License extends StatelessWidget {
  final List<LicenseEntry> entries;

  const _License({required this.entries});

  
  Widget build(BuildContext context) {
    final licenseMap = {};
    for (final entry in entries) {
      for (final package in entry.packages) {
        if (licenseMap.containsKey(package)) {
          licenseMap[package].add(entry);
        } else {
          licenseMap[package] = [entry];
        }
      }
    }

    final licenses = licenseMap.entries
        .map((entry) {
      final List<Widget> list = <Widget>[];

      list.add(
        Text(
          entry.key,
          style: const TextStyle(
            color: Colors.red,
            fontSize: 24,
          ),
        ),
      );

      for (final licenseEntry in entry.value) {
        for (final LicenseParagraph paragraph in licenseEntry.paragraphs) {
          if (paragraph.indent == LicenseParagraph.centeredIndent) {
            list.add(
              Padding(
                padding: const EdgeInsets.only(top: 16.0),
                child: Text(
                  paragraph.text,
                  style: const TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.blue,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            );
          } else {
            list.add(
              Padding(
                padding: EdgeInsetsDirectional.only(
                    top: 8.0, start: 16.0 * paragraph.indent),
                child: Text(paragraph.text),
              ),
            );
          }
        }
        list.add(const SizedBox(height: 16));
      }

      list.add(const Divider(height: 15, color: Colors.red));

      return list;
    })
        .flattened
        .toList();

    return ListView.builder(
      itemCount: licenses.length,
      padding: const EdgeInsets.all(16),
      itemBuilder: (_, index) => licenses[index],
    );
  }
}


class LicenseNotifier extends _$LicenseNotifier {
  
  Future<List<LicenseEntry>> build() async {
    final entries = <LicenseEntry>[];
    await for (final license in LicenseRegistry.licenses) {
      entries.add(license);
    }
    return entries;
  }
}

ここではRiverpodを利用していますが、StatefulWidgetでLicenseRegistry.licenses.listen((event) { })で処理しても良いと思います。

パッケージごとにライセンスをズラッと表示したかったため、元のList<LicenseEntry>からパッケージ名をkey、該当するList<LicenseEntry>をvalueというMapを用意し、あとはPackageLicensePageと同じような処理をしただけです。
paragraphはインデントの判定ができるようで、Apacheライセンスのように中央表示するようなケースはif (paragraph.indent == LicenseParagraph.centeredIndent)のように判定できるようでした。

4. おわりに

ライセンスページなんて誰も見ませんよね。(でも実は私はたまに見ています。あ、このアプリこのパッケージ使ってる!!とか)

アプリのユーザは誰もほぼ気にしないところですから、SDK標準のAPIを利用するのが簡単で良いですね。それでもカスタマイズしたいという場合は、このような方法になりそうです。

5. 追記

LicenseEntryWithLineBreaksに問題がありましたので、以下のissueを起票しています。
2024/3/22現在、P2にてラベリングされています。LicenseEntryWithLineBreaksは、個別でライセンスをロードさせたいときに使用することになると思います。

https://github.com/flutter/flutter/issues/145453

GitHubで編集を提案

Discussion