[Flutter]コツまとめ


画面表示

背景を画像にする

背景を画像にするには以下のようにする。ポイントはContainerでScaffoldで囲むこと、ScaffoldのbackgroundColorを透明にするの二点。

child: Container(
        height: double.infinity,
        width: double.infinity,
        decoration: const BoxDecoration(
          image: DecorationImage(
            image: ExactAssetImage("画像url"),
            fit: BoxFit.cover,
          ),
        ),
        child: Scaffold(
            backgroundColor: Colors.transparent,

要素

要素の表示・非表示

// ignore: dead_code
appBar: boolTrue ? AppBar(...) : null

演算

bool変数のtrue, falseを簡単に変える

bool型の変数はture, falseの2つしかないので以下で変えられる

onPressed: () => setState(()=> isVisible = !isVisible),

Map

Mapをkeyやvalueによってソートする方法。

MapをSplayTreeMapでソートする方法。
ソートするマップは一番上。
keyでソートするのは2番目、mapがvalueとして保有するmapのvalueによってソートする方法が3番目。

Map<String, dynamic> scoreMap = {
      'One': {"Value" : 6000},
      'Two': {"Value" : 50},
      'Three': {"Value" : 1},
    };
scoreMap = SplayTreeMap.from(scoreMap, (a, b) => a.compareTo(b));
scoreMap = SplayTreeMap.from(scoreMap, (a, b) => scoreMap[a]["Value"]!.compareTo(scoreMap[b]["Value"]!));

Gridview

グリッドの大きさが縦、横に小さすぎる問題

Gridviewを作成した際にグリッドの大きさが縦に小さすぎる事がある。大体の原因がaspectratioの入れ間違えか、mainAxisSpacingをmainAxisExtentと入力し間違えているケース。


グラフ(syncfusion, SfCartesianChart)

x軸のタイトルに文字を使う場合

x軸に文字列タイトルを使用する際は下記のコードをSfCartesianChart下、seriesと同じレベルに記載する。
https://help.syncfusion.com/flutter/cartesian-charts/axis-types#inversed-numeric-axis

child: SfCartesianChart(
  primaryXAxis: CategoryAxis(),
  series: <ChartSeries<ChartData, String>>[

ページ遷移

ログインしていないユーザーをログイン画面へリダイレクト

ログインしていないユーザーを自動でログインしたい場合はgo routerのredirect機能が便利。
GoRouter( redirect :….
と書くことで新しいページに遷移した際にログインしていないとredirectしてくれる。
https://zenn.dev/flutteruniv_dev/articles/547dbbb7193ddf

Go routerでの現在ルートを取得する

GoRouter.of(context).routeInformationProvider.value.uriを使用する事で現在のページルートを取得出来る。
Go routerのredirect後に上記の関数を実行するとredirect前のルートが取得出来る。例えば/homeにアクセスするにはログイン後である必要があり、redirectで/loginにリダイレクトする。すると上記で取得したスタックは以下のようになる
/home 
なぜか/home/loginではないため、ログイン作業後にcontext.popをすると/homeにはアクセスできない。そのため上記で取得した/homeにcontext.pushする方法を使用している。

Go routerで一つ前のルートを取得する

GoRouter.of(context).routerDelegate.currentConfiguration.matches.map((e) => e.matchedLocation).toList();

パラメーターの数が分からないURLを扱う

Go routerを使用してurlを扱う際に事前に扱うパラメーターの数が毎度同じではないことがあります。特殊なケースを通知するなどのユースケースですね。そんな際はstate.uri.queryParametersが便利。遷移先のページではパラメーターの方をStringではなくString?にする、nullの可能性もあるから。

GoRoute(
        path: '/Login',
        builder: (BuildContext context, GoRouterState state) {
          return LogIn(
            group: state.uri.queryParameters['group'],
            next: state.uri.queryParameters['next']
          );
       }),

↓遷移先、LogIn
String? group;
  String? next;
  LogIn({super.key, this.group, this.next});

  @override
  // ignore: no_logic_in_create_state
  State<LogIn> createState() => _LogInState(group: group, next: next);
}

class _LogInState extends State<LogIn> {
  String? group;
  String? next;
  _LogInState({Key? key, this.group, this.next}) : super();



Stateful

setStateで更新されないときに確認するべきこと

setStateでの更新をしようとすると出来ないときがしばしば、当然みたいな理由で出来てないときもあるので確認する。

  1. そもそもsetStateになってない

    1. elevatedButtonのonPressedとかで変数の変更式は書いているのにsetStateで囲われていない。

  2. 変更したい変数の記載いち間違え

    1. これが一番多い、@override Widget build(BuildContext context)やScaffoldの中に変更したい変数を書いてしまうと変更されても再描画の際にまた最初の値が割り当てられるので意味がなくなってしまう。ちなみに記載する際は_を頭につける


デバイスに合わせる

パソコンとスマホで分ける際のサイズ

リンクを参考に3つに分ける。300px以下は何も表示させないようにすればエラーも起きない。
Small (300px to 640px)
Medium (641px to 1007px)
Large (1008px and larger)

テキストボックスサイズに合わせて文字サイズを自動で調整

スマホ用の画面など大きさが大きく異なる可能性のある場合はテキストの大きさを一定にしてしまうとデザインが崩れることが多い。
そのような場合にはFittedBoxを使用すると決められた範囲内に収まるように文字サイズを調整する事が可能

SizedBox(width : 50, 
    child : FittedBox( 
        child : Text("調整されています")
    )
)

Flutter Web

flutter web実行コマンド

vscodeの右上にある実行ボタンでデバッグを開始するより

flutter run -d chrome

コマンドをターミナルに打って実行する方が実行、リロードが早い。
リロードする際はファイルをセーブしてターミナルでrと入力
Corsエラーを避ける

flutter run -d chrome --web-browser-flag "--disable-web-security"

Firebase Storage Corsの処理

Flutter webで自身のFirebase Storageからデータを取得しようとしても表示されない。表示させるにはCorsの処理が必要。
参考1参考2
手順としては

  1. GCPコンソールにアクセス

  2. 画面上部の>_ボタンをクリックしcloud terminal sessionを起動

  3. sessionでcors.jsonというファイルを作成する

    1. 内容は[ { "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600 } ]

    2. 画面左にファイルの一覧があると思うが自分のアカウント名が記載されているフォルダ内に作ると分かりやすい気がする。

  4. 画面上部のツールバーからターミナルを起動

  5. ターミナルでcors認証を実行

    1. gcloud storage buckets update (Firebase Storageのフォルダパス)--cors-file=(3で作成したcors.jsonのパス)

  6. cors認証が出来てる確認が出来れば終了

    1. gsutil cors get (Firebase Storageのフォルダパス)

Flutter webアイコン変更

flutter webのアイコンを変更するには下記の手順を行う事で可能。この作業は下記のサイトを参考にしたものの多くの手順を端折っておりどのような影響があるのかは検証出来ていない。
https://qiita.com/hidea/items/4c003c6941a9b795c318#indexhtml-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA

  1. 下記の様なサイトを用いてウェブに適したアイコンを作成

    1. https://www.favicon-generator.org/

  2. 1で作成されたファイル内のfavicon~サイズ~.pngとflutterプロジェクト、webフォルダ内に入っているfavicon.pngを置き換える。作業時生成されたfaviconはサイズ16の物がflutterプロジェクトのものと一緒だった

  3. 1で作成されたフォルダの中身をflutterプロジェクトwebフォルダ下iconsフォルダの中身に追加。iconsフォルダがない場合は作成

  4. 1で作成されたフォルダ内にあるmanifest.jsonファイルのicons : [の中身をflutterプロジェクトwebフォルダのmanifest.jsonのicons : [の中に追加する

flutter webをfirebaseの複数サイトにアップロードする方法

https://firebase.google.com/docs/hosting/multisites?hl=ja
概ね上記のサイト通りに編集すればいいが一つ気を付けるべきはステップ 4でfirebase.jsonを編集する際にsiteが記載されている場合はtargetを追加し、siteを消す必要がある。
firebase initをするとfirebase.jsonに書き込んだ内容が初期化されています。これが正常ぽいので書き直す必要がある。あとpublicなどの他の内容も初期化させる可能性があるので現状の状態を記録しておくといいと思います。


Provider

ChangeNotifier

Providerで管理する変数はChnageNotifierを使用してclassとして作成します。下記が参考です。記載されている値は一つですが複数でも使用可能。
余談ですがクラスを管理するdartファイルを作成してそこに記載しておくのが便利。

class UserProvider extends ChangeNotifier {
  String userName;
  UserProvider({
    this.userName = "",
  });
  void changeUserName({
    required String newUserName,
  }) async {
    userName = newUserName;
    notifyListeners();
  }
}
  • UserProviderChangeNotifierとして作成

  • userNameという管理する(変更を適用、検知する)変数を作成

  • UserProvider({の中で初期値を定義

  • userNameを新しい値に変更する関する、changeUserNameを作成

  • notifyListenersで値が変更されたことを通知し変更する

MultiProvider

MultiProviderを使えば複数のProviderを追加して複数の状態を管理することができる。(参照
main.dartにて記載する。

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context)=>UserProvider())
      ],
      child: const MaterialApp(
        home: Pages(),
      ),
      );
  }
}
  • Widget build(BuildContext context)のreturnとしてMultiProviderを記載

  • providersキーのリスト内にProviderを記載していく

  • ChangeNotifierProviderは状態管理を行うことができます(参照)、詳しくわかっていなくってもこの方法だと基本このproviders内に書くだけなので大丈夫だと思う

  • UserProviderは先ほど作成したChangeNotifierのクラス、ChangeNotifierProvider内で定義する事で今後管理する事が可能

  • MaterialApp以降の通常のmain.dartの内容はMultiProviderのchildキーに記載する

値の取得と変更

管理している値の取得はwatch、変更はreadを使用して行います。

Text(context.watch<UserProvider>().userName,),

onPressed: () {context.read<UserProvider>().changeUserName(newUserName: 新しいuserNameの値);}
  • UserProviderは先ほど作成したChangeNotifierのクラス

  • context.watchでUserProvider内のuserName変数の値を取得している

  • context.readでUserProvider内のchangeUserName関数を使用しuserNameの値を変更している


Firebase

initStateでFirebaseから情報を取得する

  • 問題点としてinitState内で取得したデータを変数に渡す処理の前になぜか先にwidgetが描画されてしまうので反映されない(多分)。

  • initstate内にFuture(() async {})を記載し、{}内にfutureで作成した関数を記載する。そのあとfuture.delayedを記載した(意味あるかは知らないがあると気持ち的に安心する)。

  • futureの関数は戻り値なし、asyncとして記載し、firebaseからのgetをawaitし変数1に割り当てる。future関数(これ)の外の変数に取得した値をsetState(これ重要)で入れ込む。

以下の例ではSharedPreferenceからusernameというキーの値をinitstateで読み込み際の例。

late String userName;
void initState() {
    // TODO: implement initState
    super.initState();
    Future(() async {
      prefs = await SharedPreferences.getInstance();
      userName = prefs.getString('username')!;
    });

initstateでfirebaseからデータを読み込む

late Future<DocumentSnapshot> futureInfo;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    futureInfo = FirebaseFirestore.instance.collection(collection).doc(doc).get();
  }

FutureBuilder(
          future: futureInfo,
          builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const CircularProgressIndicator();
            }
            if (snapshot.hasError) {
              return Text("no");
            }
            if (snapshot.hasData) {
              var returnData = snapshot.data!.data();
              return Text(returnData.toString());
            } else {
              return Text("wow");
            }
          }),

Firebaseからデータを取得して情報を使用する場合futurebuilderを使うと思う。
しかし問題点としてsetstateなどを使用すると毎回再描画が行われるためfirebaseのリード数が何倍にもなる。
特に以前ユーザーが記載した内容を修正出来るシステムなんかを組む時にfutureに直接get()を指定したりするとread数はすごいことになる。
というわけでinitstate内でDocumentSnapshotを取得して変数futureInfoに入れ込む。futureInfoはDocumentSnapshotなので再描画されても新しいreadにはならない(はず、テストする方法を知りたいもんだ)。
参考動画

DocumentSnapShotからデータ取得

  • snapshotはAsyncSnapShot<DocumentSnapshot<Object>>(connectionState.active, Instance of 'JsonDocumentSnapshot', null, null)の様にデータだけでなくその状態も保持する。

  • snapshot.dataはinstance of 'jsondocumentsnapshot'、つまりはsnapshotの結果からJson….を引っ張り出してきただけ、まだ余分なものが同封されているらしい。

  • snapshot.data.data()でやっとMap<string dynamic>が、つまりはデータにアクセスできる。(ここまでリンク1)

  • snapshot.data.data()のMapから欲しいものだけを取り出すには(snapshot.data!.data() as Map<String, dynamic>)['Mapキー']で出来る。(これはリンク2)

QuerySnapshotからデータ取得

  • whereやorderbyを使ってデータを取得するとquerysnapshotで帰って来るためdata!の後の処理が変わる

  • data!.docs[0].data()となる

フィールドにあるデータでフィルタしてデータ取得

Firestoreからデータを取得する際にフィールドの値でソートする際は以下のように書く。

#条件が一つの場合はこれ
doc_ref = db.collection("testData").where("influencer", "==", "trader.aaa").get()
#条件が複数の場合はwhereの後ろに更にwhereを付け加える
doc_ref = db.collection("testData").where("influencer", "==", "trader.aaa").where("user", "==", "testUser").get()

Firebase上のリストにデータを追加

FieldValue.arrayUnionをupdateと使用

Firebase上のリストを取得

Firebaseからリストを取得しても中の値は登録している順番通りになる。ちなみに値を追加すると元の値が消えて、要素を追加した新しいリストが登録されるがその際は並べ替えられるらしい。


Firebase Hosting

Firebase CLI導入方法

参考ウェブサイトを基に進める。
作業環境はwindows、ideはvscode

  1. Firebase-CLIの導入

    1. VScodeのターミナルで以下を実行

    2. npm install -g firebase-tools

  2. firebaseにログイン

    1. vscodeのターミナルで以下を実行。firebase login

    2. エラーの場合はエラーセクションのfirebase loginを参照。

    3. Already logged in as….が出たら成功

  3. flutterfire_cliを導入

    1. 以下をVScodeのターミナルで実行。dart pub global activate flutterfire_cli

  4. Firebaseとの連携をセットアップ

    1. VScodeターミナルで実行。flutterfire configure

    2. 既に存在するプロジェクトにつなげることも、新規を作成する事も可能

  5. 必要なパッケージをインストール

    1. Vscodeターミナルでflutter pub add firebase_coreを実行

Firebase hostingへのデプロイ方法

  1. 初回はプロジェクトを初期化

    1. firebase init hosting

    2. 質問項目はこれを参考

  2. flutterアプリのビルド

    1. flutter build web

    2. エラーが発生したらエラーセクションへ

  3. firebase deploy --only hosting

    1. firebase : このシステムではスクリプトの実行が無効になっているためエラーが起きた場合はこれ参照

Firebase Storage

Firebase Storageに保存している画像を表示する

initStateで画像のURLを取得してFutureBuilderにパスする。

late Future<String> iconUrl;
@override
void initState() {
  super.initState();
  iconUrl = FirebaseStorage.instance.ref("url").getDownloadURL();
}

FutureBuilder(
  future: iconUrl,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return SizedBox(
        height: width * icon,
        child: SizedBox(width: width*icon,child: Center(child: const CircularProgressIndicator())),
      );
    } else if (snapshot.hasError) {
      return SizedBox(
        height: width * icon,
        child: Text(snapshot.error.toString()),
      );
    } else if (snapshot.hasData) {
      String iconRoot = snapshot.data!;
      return SizedBox(
        height: width * icon,
        child: CachedNetworkImage(imageUrl: iconRoot),
      );
    } else {
      return SizedBox(
        height: width * icon,
        child: const Text("error"),
      );
    }
  },
),

ListAllの結果表示

final storageRef = FirebaseStorage.instance.ref(フォルダー名);

storageRef.listAll()

var iconUrl = snapshot.data!.items;
iconUrl[0].fullPath

listAllでフォルダー内の物を取ってくる場合はrefの後に入れる。更に深い場合はref().child()でアクセス。

storageRef.listAll()は大体futurebuilderかinitState内で実行。
取得したデータはAsyncSnapshot<ListResult>なので使用する時はsnapshot(今回はcontext, AsyncSnapshot<ListResult> snapshot).data!に続き.items又は.prefixesを記載。

Firebase Storageの画像を早く読み込む方法

Firebase storageに保存している画像を読み込むのにはかなり時間がかかります。特にlistallとかでフォルダ内全部の画像を表示しようと思ったら数秒ユーザーを待たせることに。
原因はgetDownloadUrlなようでこれを使わず、Firestoreのデータベースに画像のtoken urlを保存しておき、そのリンクをimage.networkで読み込むと体感で数倍早くなります(30枚程度の画像の場合)。また余談ですがスマホに並べて表示する画像はそんなに画質が良くなくていいので一辺500pixel程度にしておくとimage.networkの結果は体感で数十倍速くなります。


パフォーマンス、SEO関連

パフォーマンス、SEOの測り方

Googleにおけるエンドユーザー側でのパフォーマンスを図る際は
Google search consoleとpagespeed insightを使用する

プロファイリングを行う

コマンドラインからflutter run --profileで実行する。

「最大コンテンツの描画」要素の削減

大きめの画像を表示する際に「最大コンテンツの描画」要素 124,970 ミリ秒かかっている。現在はFirebaseStorageに保存している画像をinitStateでurlを取得、FutureBuilderで表示するしている。画像フォーマットはpngです。
ビルドはflutter build web --web-renderer htmlを使用しています。

テスト1:precacheImage
precacheImageで先に画像をロードする。

テスト2:画像フォーマットをwebpにする


エラー

リリースしたらcachednetworkimage画像が表示されない

flutter webをリリースしたらcachednetworkimageの画像が表示されなくなってしまった際の対策。

確認1:アセット画像を使用していないか?
あくまでネットワークイメージの表示用なのでアセット画像を入れていると表示されない。

mapをSplayTreeMapでsortする際にMaximum call stack size exceeded

大きなマップをSplayTreeMapでsortしているとMaximum call stack size exceededが出たので解決法。
原因はsortしているmapと割り当てているmapが一緒だったから処理が終わらなかった。
map = SplayTreeMap.from(map ……
割り当てmapを新しいものにしたら解決

A Firebase App named "[DEFAULT]" already existsの対処

flutterアプリ開発中にA Firebase App named "[DEFAULT]" already existsのエラーが出た場合大体がgoogle-serviceの問題かプロジェクトの名前が被っているかの二通りだそう。
僕の場合は名前問題だったらしく
Firebase.initializeApp(
の後に
name: "dev project",
と付け加えたら解決した。参考

setState() called after dispose():

setStateが破棄された後に実行されているらしい。
setState分をif(mount){}で囲むと解決。

InternalError: Expression evaluation in async frames is not supported. No frame with index 28.

謎のエラーでsharedpreference、future、geolocatorとあらゆるinitstateのasync関連の関するに発生した。原因をいろいろ調べたり試したりしたが結局分からず、initstateを消し外で使用したら解決した。

TypeError: Cannot set properties of undefined (setting 'nativeCommunication'

flutter_inappwebview_webパッケージを追加、web/index.htmlに以下を追加
<script type="application/javascript" src="/assets/packages/flutter_inappwebview/assets/web/web_support.js" defer></script>

キーボードが表示されない・自動で閉じる

携帯などで入力をしようとするとキーボードが自動で閉じる、表示されない場合があります。解決には以下の方法が考えられます。

  • キーボードが表示されない

    • overflowエラーが発生していると思われます。singlechildscrollviewをScaffoldのbodyに充てる事で解決されます。

  • キーボードが自動で閉じる

    • キーボードは表示されるとリビルドされるみたいです。ちょうどsetStateと同じ挙動だと思われます。そのためFutureBuilderの中などで使用していると開いた瞬間にリビルドされ、キーボードを開いていなかった初期の状態に戻ります。そのため自動で閉じる挙動になります。FutureBuild外に外してやれば解決します。

Firebase loginでエラー

  • firebase : このシステムではスクリプトの実行が無効になっているため

    • 発生したらターミナルでGet-ExecutionPolicyを実行、Restrictedになっていたら一時的にexecutionpolicyを変更する。以下をターミナルで実行。Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

  • Firebase CLI v13.17.0 is incompatible with Node.js v16.15.0

    • Node.jsをアップデートします。この記事に従ってアップデート。

    • バージョン確認はnode --version

Firebase build webエラー

  • Target dart2js failed: Exception: Warning: The 'dart2js'

    • 発生した場合はflutter cleanとflutter pub getを実行し再チャレンジ

    • flutter upgradeとflutter pub upgrade --major-versionsですべてを最新にする。

    • 解決しない場合、/flutter/bin/cacheファイルを削除、flutter doctorをcmdで実行、flutter cleanとflutter pub getを実行し再チャレンジ。

スクリプト実行エラー

  • firebase deploy --only hostingなどの実行時にエラーが起きる場合は

    • Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

    • npm i -g firebase

いいなと思ったら応援しよう!