flutterの基本を試してみた

# flutterでテストアプリを作ってみる

自分用の備忘録

## とりあえずプロジェクト作成

今回はWindows（SurfaceGo2）で試す。
SDカード側に入れたからか、平行でWindowsアップデート中だからか分からないが遅い・・・
一応AndroidStudioも入れたが、こっちも環境作成に時間がかかるので、まずはWeb版からがいいかも。

最終的にはスマホ機能（電話関係とか）を試した。

createでプロジェクト作成

```
flutter create test
```
で出来たものを利用する。

## VSCodeのプラグイン

公式っぽいやつは入れておいた。

他にもあるらしい。

[https://zenn.dev/hagakun_dev/articles/2f2eb65b892bea](https://zenn.dev/hagakun_dev/articles/2f2eb65b892bea)

## サンプル作成

* とりあえずトップ画面にアイコンを配置して、別の画面を開くようにしてみる
* アイコンから適当なURLに飛ぶ（ブラウザが開く）
* アイコンから電話アプリが起動する
* アイコンからローカルのファイルを開く

くらいが出来るようにしてみたい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2081290/b5218b69-c999-2568-0f1c-f5654779803b.png)

見た目はこんな感じにとりあえず作った。

IconButtonというウィジェットがあるので、アイコンはこれを配置する。
アイコンそのものにはアイコンウィジェットというものを利用するのが楽。
手元の画像を使う方法もある。
[https://qiita.com/ikemura23/items/5671bba76136d940b618](https://qiita.com/ikemura23/items/5671bba76136d940b618)とか
[https://zerokara-app.com/930/](https://zerokara-app.com/930/)

レイアウトは[https://future-architect.github.io/articles/20210513b/](https://future-architect.github.io/articles/20210513b/)
をまずは参考に。

基本的にはヘッダ、ボディ、フッタのエリアがあって、ボディ部分にrow,columnでテーブルみたいに並べる、
というようにする。

[https://www.wenyanet.com/opensource/ja/5fedcb7d9c1222717e361102.html](https://www.wenyanet.com/opensource/ja/5fedcb7d9c1222717e361102.html)のようにフレキシブルなグリッドレイアウトもあるっぽい。

### パッケージのインストール

インストールは書くモジュールのページに説明があるので略

### 引数付きでonPressedを実行する

何も考えずに書くと
`Error: This expression has type 'void' and can't be used`
になった。

```dart
Ink(
    color: Colors.blue, 
    child:IconButton(
        icon: Icon(Icons.android),
        onPressed: () => _linkPage(
            Uri(
            scheme:'https',
            host:'www.city.kagoshima.lg.jp',
            path:'/',
            queryParameters: {"lang":"ja"},
            )
        ),
    )
)
```

こんな感じで`() => function(x,y)`とする。

### 一旦作成

とりあえず基本機能を盛って試してみたmain

```dart
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:test/page1.dart';
import 'dart:io';
import 'dart:convert';
import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'sapmle',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'MainPage'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // JSON分解のテスト用
  static JsonMap parseJsonMap(String input) => json.decode(input) as JsonMap;
  
  // ネットから取得して保存されるテキストデータの代わりに固定文字のJSONで試す
  JsonMap psjson = parseJsonMap('{ "urls":[{"icon":"1.png","url":"www.google.com"},{"icon":"2.png","url":"www.yahoo.co.jp"},{"icon":"3.png","url":"www.excite.co.jp/"},{"icon":"4.png","url":"www.pref.kagoshima.jp/"}]}');

  void _changePage() {
    setState(() {
      Navigator.push(
                context, 
                MaterialPageRoute(builder: (context) => Page1(title: 'SubPage'))
              );
    });
  }

 // PDFファイルのダウンロードテスト
  void _pdfDownload() async {
    String filePath = (await getApplicationDocumentsDirectory()).path;
    var _httpClient = http.Client();
    final http.Response res = await _httpClient.get(Uri.parse("https://www.pref.kyoto.jp/kamogawa/documents/1200618028055.pdf"));
    final file = File('${filePath}/sample.pdf');
    await file.create();
    await file.writeAsBytes(res.bodyBytes);
  }

  // 外部ブラウザで開く
  void _linkPage(uri) async  {
    try{
      setState(() async {
        await launchUrl(
          uri,
          mode: LaunchMode.externalApplication,
        );
      });
    }
    catch(e){
      throw 'Could not Launch $uri';
    }
  }

  // アプリ起動時に一度実行するメソッド。非同期を待つことは出来ない模様
  @override
  void initState(){
    _pdfDownload();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            // 固定配置分
            Container(
              height:150,
              child: GridView.extent(
                maxCrossAxisExtent: 150,
                padding: const EdgeInsets.all(4),
                mainAxisSpacing: 4,
                crossAxisSpacing: 4,
              children: <Widget>[
                Ink(
                  height: 150,
                  width: 150,
                  color: Colors.blue, 
                  child:IconButton(
                    icon: const Icon(Icons.pages),
                    onPressed: _changePage,
                  )
                ),
                Ink(
                  height: 150,
                  width: 150,
                  color: Colors.orange, 
                  child:IconButton(
                    icon: const Icon(Icons.link),
                    onPressed: () => _linkPage(
                      Uri(
                        scheme:'https',
                        host:'www.city.kagoshima.lg.jp',
                        path:'/',
                        queryParameters: {"lang":"ja"},
                      )
                    ),
                  )
                ),
                Ink(
                  height: 150,
                  width: 150,
                  color: Colors.pink, 
                  child:IconButton(
                    icon: const Icon(Icons.phone),
                    onPressed: () => _linkPage(
                      Uri.parse('tel:0120444444')
                    ),
                  )
                )
              ],
            )
            ),
            // Mapから動的に作成する分
            Expanded(
              child: GridView.extent(
                maxCrossAxisExtent: 150,
                padding: const EdgeInsets.all(4),
                mainAxisSpacing: 4,
                crossAxisSpacing: 4,
                children: psjson["urls"].map((e) =>  Ink(
                  height: 150,
                  width: 150,
                  color: Colors.amber,
                  child:TextButton(
                  child: Text(e["icon"]),
                  onPressed: () => _linkPage(
                      Uri(
                        scheme:'https',
                        host:e["url"],
                        path:'/',
                        queryParameters: {"lang":"ja"},
                      )
                    ),
                  )
                 )
                ).toList().cast<Widget>(),
              ),
            )
          ],
        ),
      ),
    );
  }
}

```

あと、PDFファイルをShareする場合はこんな感じ

```dart
  void _pdfOpen(filename) async {
    setState(() async{
      String filePath = (await getApplicationDocumentsDirectory()).path;
      await Share.shareFiles(['${filePath}/${filename}']);
    });
  }


 onPressed: () => _pdfOpen("sample.pdf")
```

正解なのかは分からないが、とりあえず動いた。

###　注意点
iOSのシミュレーターでは電話アプリが起動しないだとか、実機でないと出来ない事も
ちょいちょいあるので注意が必要。（色々時間を浪費した）

## 画面を非同期で対応する

さて、ここでインターネットからダウンロードしたJSONファイルからメニューを作成したい
というような要望がある場合に先のコードでは上手くいかない。
Dartのasync付きの関数は`Future<String>`のような、非同期だけど後からString型を返すよ、
みたいな感じの型を設定してておく必要がある。
この場合は先のようなWigetの書き方ではエラーになってしまう。
`initState()`で実行させてList型を作る用にしておいたところで、
非同期の完了をこのメソッドは待ってくれないので非同期処理が終わる前に
画面表示が走ってしまうと上手く表示してくれない・・・

という時に役に立つのが
FutureBuilderかStreamBuilder、らしい。

[https://gakogako.com/fluter_futurebuilder/](https://gakogako.com/fluter_futurebuilder/)

これは次回試してみておきたい。。。