Flutter でアプリを作ってみよう　＃４

### アプリ内WebViewの実装
前回にてRssFeedで引っ張ってきた要素をリストで表示することができた。
そのそれぞれのリストタイルをタップすることで、RssItemのリンクURLサイトをアプリ内webviewにて表示させていく。
webviewを実装していくにあたり、使えるパッケージが２点あった。
・`webview_flutter`
・`flutter_inappwebview`
`webview_flutter`はシンプルなもの、`flutter_inappwebview`はより複雑なものを作れる、といったような違いだそう。
詳しい違いについては下記サイトにて紹介されている。

https://zenn.dev/taku_zenn/articles/a7ef926c73ec3d

今回は`webview_flutter`で充分そうなのでこちらを使う。
コーディング前に使い方を調べていると、割と最近かなり大きなバージョンのアップデートがされていたようで、バージョン4.0以上は使用感がガラッと変わる。
以下サイトにてわかりやすく解説されているので大いに参考にする。

https://www.fuwamaki.com/article/438

見様見真似で書いていく。
```dart: web_view_page.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewPage extends StatefulWidget {
  const WebViewPage({Key? key, required this.rssUrl}) : super(key: key);
  final String rssUrl;
  @override
  State<WebViewPage> createState() => _WebViewPageState();
}

class _WebViewPageState extends State<WebViewPage> {
  late final WebViewController _controller;
  bool _isLoading = false;
  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
    ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (String url) {
            setState(() {
              _isLoading = true;
            });
          },
          onPageFinished: (String url) {
            setState(() {
              _isLoading = false;
            });
          },
        ),
    )
    ..loadRequest(Uri.parse(widget.rssUrl));
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        //閉じるボタン
        leading: IconButton(
          onPressed: (){ Navigator.pop(context);} ,
          icon: const Icon(Icons.clear)
        ),
        title: const Text('イベント詳細'),
        actions: [
        　　　　//戻るボタン
          IconButton(
            onPressed: () async {
              _controller.goBack();
            },
            icon: const Icon(
              Icons.arrow_back,
            ),
          ),
          //進むボタン
          IconButton(
            onPressed: () async {
              _controller.goForward();
            },
            icon: const Icon(
              Icons.arrow_forward,
            ),
          )
        ],
      ),
      body: Column(
        children: [
          if (_isLoading) const Center(
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 4),
              child: CircularProgressIndicator(),
            ),
          ),
          Expanded(child: WebViewWidget(controller: _controller)),
        ],
      ),
    );
  }
}
```
```dart:main.dart
import 'package:eve_search/web_view_page.dart';

onTap:(){
  setState(() {
    Navigator.push(context, MaterialPageRoute(builder: (context) 
        => WebViewPage(rssUrl: onlyHiphopList[index].linkUrl)));
  });
},
```
遷移やAppbarの各ボタンは問題なく動いたが、サムネイルにも利用したOGP画像が表示されない不具合が起きた。
以下を追加することで改善された。
```diff_dart: web_view_page.dart
@override
  void initState() {
    super.initState();
    _controller = WebViewController()
+   ..setJavaScriptMode(JavaScriptMode.unrestricted)
    ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (String url) {
            setState(() {
              _isLoading = true;
            });
          },
          onPageFinished: (String url) {
            setState(() {
              _isLoading = false;
            });
          },
        ),
    )
    ..loadRequest(Uri.parse(widget.rssUrl));
  }
```
### エリア毎の絞り込み機能の実装
[＃2](https://qiita.com/shanbentaye10w/items/dc3216f14d8edc92fcd5 "Flutterでアプリを作ってみよう　＃2")にて用意した選択肢を選ぶshowModalBottomSheetのアクションを実装していく。
今回利用しているRssFeedは各地方、各都道府県毎にそれぞれRssが用意されていたので使っていく。
各地方はそれぞれ個別に書き、都道府県はswitch文でタップ時渡す値を条件分けしていく。
```dart:prefectures_window.dart
import 'package:flutter/material.dart';
import 'const.dart';

class PrefecturesWindow extends StatefulWidget {
  const PrefecturesWindow({super.key});

  @override
  State<PrefecturesWindow> createState() => _PrefecturesWindowState();
}
class _PrefecturesWindowState extends State<PrefecturesWindow> {
  void selectPrefecture(String prefecture){
    switch(prefecture){
      case '青森県':
        Navigator.pop(context, [' ＞ 東北 ＞ $prefecture', 'https://iflyer.tv/rss/events/tohoku/aomori/']);
        break;
      case '岩手県':
        Navigator.pop(context, [' ＞ 東北 ＞ $prefecture', 'https://iflyer.tv/rss/events/tohoku/iwate/']);
        break;
//省略(実際は４７都道府県分)
      case '鹿児島県':
        Navigator.pop(context, [' ＞ 九州 ＞ $prefecture', 'https://iflyer.tv/rss/events/kyushu/kagoshima/']);
        break;
      case '沖縄県':
        Navigator.pop(context, [' ＞ 九州 ＞ $prefecture', 'https://iflyer.tv/rss/events/kyushu/okinawa/']);
        break;
      default:
        errorDialog();
        break;
    }
  }
  void errorDialog(){
    showDialog<void>(
      context: context,
      builder: (_){
        return AlertDialog(
          title: Column(
            children: [
              const Text('エラーが発生しました'),
              const Text('もう一度お試しください'),
              const SizedBox(height: 8,),
              ElevatedButton(
                child: const Text('OK'),
                onPressed: (){
                  Navigator.pop(context,);
                },
              )
            ],
          ),
        );
      }
    );
  }
@override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width*0.95,
      height: MediaQuery.of(context).size.height*0.95,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: SingleChildScrollView(
          child: Column(
            children: [
              const SizedBox(height: 16,),
              ListTile(
                title: const Text('全国'),
                trailing: const Icon(
                  Icons.keyboard_arrow_right,
                ),
                onTap: () {
                  setState(() {
                    Navigator.pop(context, ['', 'https://iflyer.tv/rss/events/']);
                  });
                },
              ),
              ListTile(
                title: const Text('北海道'),
                trailing: const Icon(
                  Icons.keyboard_arrow_right,
                ),
                onTap: () {
                  setState(() {
                    Navigator.pop(context, [' ＞ 北海道', 'https://iflyer.tv/rss/events/hokkaido/']);
                  });
                },
              ),
              ExpansionTile(
                title: const Text('東北'),
                children: [
                  Column(
                    children: [
                      const Divider(
                        height: 1,
                      ),
                      Padding(
                        padding: const EdgeInsets.only(left: 16.0),
                        child: ListTile(
                          title: const Text('東北全域'),
                          trailing: const Icon(
                            Icons.keyboard_arrow_right,
                          ),
                          onTap: (){
                            setState(() {
                              Navigator.pop(context, [' ＞ 東北全域', 'https://iflyer.tv/rss/events/tohoku/']);
                            });
                          },
                        ),
                      ),
                      Column(
                        children: 
                          touhokuList.map((e) => Column(
                            children: [
                              const Divider(
                                height: 1,
                              ),
                              Padding(
                                padding: const EdgeInsets.only(left: 16.0),
                                child: ListTile(
                                  title: Text(e),
                                  trailing: const Icon(
                                    Icons.keyboard_arrow_right,
                                  ),
                                  onTap: (){
                                    setState(() {
                                      selectPrefecture(e);
                                    });
                                  },
                                ),
                              ),
                            ],
                          )).toList(),
                      ),
                    ],
                  )
                ],
              ),
              //以下省略
            ],
          ),
        ),
      ),
    );
  }
}
```
```diff_dart:main.dart
class _MyHomePageState extends State<MyHomePage> {
  List<String> _selectArea = ['', 'https://iflyer.tv/rss/events'];
  List<Rss> onlyHiphopList = [];
  String areaUrl = '';
  Future<List<Rss>> fetchFeed() async {
    final response = await http
      .get(Uri.parse(_selectArea[1]));
    if (response.statusCode != 200) {
      throw Exception('Failed to fetch');
    }

//省略

ElevatedButton(
  onPressed: () async{ 
-   String selectArea = await showModalBottomSheet(
+   List<String> selectArea = await showModalBottomSheet(
      context: context,
      isScrollControlled: true, 
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(10)),
      ),
      builder: (BuildContext context) {
        return const PrefecturesWindow();
      },
    );
    setState(() {
      _selectArea = selectArea;
    });
  },
  child: const Text('エリアを選択する',),
),
```
これにて最低限実装したい機能は問題なく積み込めた。
最後に全体的にレイアウトをいい感じにしたのがこちら↓
![画面収録-2023-06-26-20.01.29.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3461823/3d15f8f1-e82d-c791-aa9b-69f19c44578d.gif)
一応コーディングに関してはここでひと段落として、あとはテスト配信とリリースだ。
テスト配信の前か後かどちらになるか未定だが、あまり重たくない追加機能の検討、実装もしていくつもりなので、もしあれば引き続きこちらで記録していこうと思う。
今回はここまで。