【Flutter】AndroidでWebのPDFをDLフォルダに保存せずに閲覧しつつ、必要な時は保存する

## きっかけ

まだ flutter 初心者の域を脱していない（と思う[^1]）んですが、有難いことに簡単な Android アプリの製作依頼[^2]を頂きまして、その中で下記のような要件が出てきました。

* Web上のPDFを開きたい
* 閲覧しかしないときは都度DLフォルダに保存しないでほしい
* 一方でファイルが必要になる場合もある（メール添付とか）ので、その際はDLフォルダに保存もできるようにしてほしい

iOSやデスクトップ版なら普通に `url_launcher` とかでデフォルトのブラウザでPDF開けばいいだけ（内蔵のPDFビューアでいずれの要件もカバーできる）なのでそれ以外に特段何かする必要はないんですが
Android だとデフォルトのブラウザである Chrome に内蔵ビューアがなく[^3] 開こうとすると都度DLフォルダに保存してしまうので、それを回避するために行ったことの記録になります。

結論としては極めて当たり前のことしかやってない/書いてないのですが、目先に集中していると意外とこういうことってすぐに思いつかなかったりするので[^4]、同様の要件に直面した方のコロンブスの卵?になることももしかしたらあるかもしれないと思い一応記事にしておきました。

## 環境

```console:$ flutter doctor
[√] Flutter (Channel stable, 3.24.4, on Microsoft Windows [Version 10.0.19045.5371], locale ja-JP)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.3.6)
[!] Android Studio (version 2021.3)
    X Unable to determine bundled Java version.
[√] Android Studio (version 2024.1)
[√] VS Code (version 1.96.3)
[√] VS Code, 64-bit edition (version 1.64.2)
[√] Connected device (3 available)
[√] Network resources
```

## やったこと

をコードと共に列記すると、

① URLが `.pdf` で終わる、もしくは `.pdf?` を含むかでPDFか否かを判定[^5]
② `platform` が Android 且つURLがPDFだった場合PDF表示画面に遷移する
③ 上記以外の場合はURLを `url_launcher` で開く

```dart
import 'package:url_launcher/url_launcher.dart';

final url = "https://url.for.webpage.or.pdffile";
final isPdf = url.endsWith('.pdf') || url.contains('.pdf?');  // <= 1
if (Platform.isAndroid && isPdf) {
  Navigator.push(context, MaterialPageRoute(
    builder: (context) => PdfScreen(url: url)
  ));  // <= 2
} else if (await canLaunchUrl(Uri.parse(url))) {
  launchUrl(Uri.parse(url));  // <= 3
} else {
  // エラー処理・表示
}
```

④ PDF表示画面はPDFビューアパッケージのウィジェットによるPDF表示と、DLボタンを設けたAppBarで構成
⑤ DLボタンが押された際はそのPDFを **改めて** `url_launcher` **で開く**

```dart:pdf_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_cached_pdfview/flutter_cached_pdfview.dart';
import 'package:url_launcher/url_launcher.dart';

class PdfScreen extends StatelessWidget {
  const PdfScreen({super.key, required this.url});

  final String url;

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text('PDF表示'),  // <= 何でもOK、可変にしたければメンバ変数とコンストラクタ引数を追加して外から与える
      actions: [
        IconButton(  // アイコンの種類・色はお好みで
          icon: Icon(Icons.file_download_outlined, color: Colors.white),
          onPressed: () async => await launchUrl(Uri.parse(url))  // <= 5
        )
      ]
    ),
    body: PDF(
      backgroundColor: Colors.grey  // PDF表示の設定もお好みで
    ).fromUrl(url)
  );
}
```

ミソは **⑤** で、問答無用にDLフォルダへ保存したくないから `url_launcher` でのオープンを一度は回避したわけですが、でもやっぱり保存したいのなら改めてそれで開けばいいじゃん、と。
DL（指定）フォルダへの保存が要件といわれるとPDFファイルをバイト列でGETするようhttpリクエストを送り、それをファイルに保存するコードを書いて・・・とか考えてしまいがちですが[^4]
昨今の Android ではダウンロードフォルダにアプリからアクセスするのも一筋縄ではいかない[^6]ようなので、それを完璧にやってくれる別のアプリ＝Chrome があるならそれに任せるのが話が早かったです。（すぐには思い至りませんでしたorz）

あと補足があるとすれば、PDFを表示する flutter パッケージはいくつもありましたが
今回はWeb上のPDFを直接表示できる上に使用方法もURLを与えるだけと簡単な `flutter_cached_pdfview` を用いました。

https://note.com/blue_69/n/n5077e0503460

他のビューアを用いる場合は `body` のところを各々の表示コードに置き換えてください。
遷移元が前述1～3のコードのみである場合、Android でしかこの画面には到達せずプラットフォーム依存の縛りも小さいので、Android で実績のあるPDFビューアならどれでも適応できるんじゃないかとは（未確認ですが）思います。

## ちなみに

Android + Chorme でPDFをDLせず表示するには Googleドライブで開く：具体的にはPDFのURLの前に `https://docs.google.com/viewer?url=` を付してアクセスする――という方法もあり、上手く開ければドライブの表示ページからDL・保存もできるのですが
Googleアカウントでのログインが必要なのと[^7]、何故か表示失敗（グレー一色になる：エラー表示等は特になし）が多発したので断念しました。
コンソール出力にもこれに関係しそうな内容は見当たらず、原因は突き止められていないので知見をお持ちの方がいらしたら補足頂けますと幸いです。


[^1]: Riverpod とか Firebase とか使いこなせるようになれば脱せるんでしょうか。今のところ未履修なので勉強して使えるようになりたいです
[^2]: Android だけなら Kotlin とかでよいのでは…と言われそうですが、将来的なiOS展開も含み持たせておきたいという話があるのと、そもそも私が flutter しか使えない等々の事情で flutter で作ってます
[^3]: ビューアを組み込まないのは Chrome のポリシーの問題らしいので（情報源失念）、今後も変わることはないものと思われます
[^4]: ソースは私
[^5]: ガバガバ判定な気もしなくはないですが、今のところこれで特に問題にはなってないのでﾖｼ!!（現場猫AAry）としてます
[^6]: アプリ専用フォルダなら簡単（`getApplicationDocumentsDirectory()`）ですが、ファイルビューアから開きたい時（メール添付とか）に階層が深く探しづらい
[^7]: Androidを使っていればアカウントがないということはないと思いますが、ドライブを普段使用していない場合別途ログインを求められたりはしました