1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Flutter でアプリを作ってみよう #4

Posted at

アプリ内WebViewの実装

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

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

見様見真似で書いていく。

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)),
        ],
      ),
    );
  }
}
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画像が表示されない不具合が起きた。
以下を追加することで改善された。

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にて用意した選択肢を選ぶshowModalBottomSheetのアクションを実装していく。
今回利用しているRssFeedは各地方、各都道府県毎にそれぞれRssが用意されていたので使っていく。
各地方はそれぞれ個別に書き、都道府県はswitch文でタップ時渡す値を条件分けしていく。

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;
//省略(実際は47都道府県分)
      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(),
                      ),
                    ],
                  )
                ],
              ),
              //以下省略
            ],
          ),
        ),
      ),
    );
  }
}
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
一応コーディングに関してはここでひと段落として、あとはテスト配信とリリースだ。
テスト配信の前か後かどちらになるか未定だが、あまり重たくない追加機能の検討、実装もしていくつもりなので、もしあれば引き続きこちらで記録していこうと思う。
今回はここまで。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?