Open15

【Flutter】メモ

WaterWoodWaterWood

【Flutter】下にスワイプで更新する(Swipe to Refresh)(RefreshIndicator)

TwitterやInstagramのタイムラインを更新する時の下スワイプ。

基本コード

RefreshIndicator(
    onRefresh: _refresh,
    child: ListView(),
)
Future<void> _refresh() {
    // スワイプした時のコード 
}

required

  • onRefresh
  • child(ListViewが一般的)

インディケーターの見た目に関するプロパティ

  • backgroundColor
  • color
  • strokeWidth (矢印の幅)

インディケーターの表示する位置

  • displacement(スワイプ後どこで表示されるか)
  • edgeOffset (スワイプしたら表示され始める位置がリストの始まりからどれだけ離れているか)

triggerMode。スワイプ開始時の状態。

  • anywhere(下にスクロールしている状態からリストの最上部を越えてスクロールするとそのまま更新)
  • onEdge(デフォルト。スクロールが一番上の状態からスワイプしないと更新されない)
WaterWoodWaterWood

【Flutter】RiverPodで二つの状態を同時に管理したい

リストとisLoading、など二つの状態を一緒に管理する場合、状態のクラスを別に作って管理するといい

状態を管理するクラス

class ItemListState {
  ItemListState({
    this.itemList = const [],
    this.isLoading = false,
  });
  final List<Item> itemList;
  final bool isLoading;

  ItemListState copyWith({
    List<Item>? itemList,
    bool? isLoading,
  }) {
    return ItemListState(
      itemList: itemList ?? this.itemList,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

Notifierクラスとプロバイダー

class ItemListNotifier extends StateNotifier<ItemListState> {
    ItemListNotifier() : super(ItemListState()) {fetchItemList();}

    Future<void> fetchItemList() async {
        state = state.copyWith(isLoading: true); // 状態はcopyWithを使って上書きする
        /*
             データフェッチ
        */
        state = state.copyWith(itemList: itemList, isLoading: false);
    }

    void addItem(Item newItem) {
        state = state.copyWith(itemList: [...state.itemList, newItem]);
    }
}

final itemListProvider =
    StateNotifierProvider<ItemListNotifier, ItemListState>(
        (ref) => ItemListNotifier());

呼び出し

class ItemListScreen extends ConsumerWidget {
  const ItemListScreen({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(itemListProvider);
    final isLoading = state.isLoading;
    final itemList = state.itemList;

    late Widget content;

    if (isLoading) {
      content = const Center(child: CircularProgressIndicator());
    } else if (itemList.isEmpty) {
      content = const Center(child: Text('登録データはありません'));
    } else {
      content = ListView.builder(
          itemCount: itemList.length,
          itemBuilder: (BuildContext context, int index) {
            final item = itemList[index];
            return ListTile(title: Text(item.name),
            );
          },
      );
    }
    return Scaffold(
      body: content,
    );
  }
}
WaterWoodWaterWood

【Flutter】Formの使い方。TextFieldとTextFormFieldの使い分け。controllerやvalidator、onSavedなどのプロパティの使いどころ。

Form

複数のユーザー入力をまとめてあつかうコンテナ。GlovalKey<FormKey>を設定することでフォーム全体の複数の入力をまとめてバリデーションしたり保存したりすることができる。Formウィジェットを使う場合はGlovalKeyをセットで使用するのが一般的。
GlovalKeyはアプリ全体でユニークなキーである。

final _formKey = GlobalKey<FormState>();
// ログイン
Form(
    key: _formKey,
    child: Column(
        children: [
            TextFormField(), // メールアドレス入力
            TextFormField(), // パスワード入力
            ElevatedButton(
                onPressed: _logIn,
                child: Text('Login'),
            ),
        ],
    ),
),

フォームキーを設定するメールアドレス、パスワードといった複数の入力をまとめて処理できる。

void _logIn() {
    // TextFormFieldに"validator"プロパティを設定することでまとめてヴァリデーション
    final isValid = _formKey.currentState!.validate(); 
    if (!isValid) {
      return;
    }
    // TextFormFieldに"onSaved"プロパティを設定することでまとめてセーブ
    _formKey.currentState!.save();
    // 以下ログイン処理が続く
  }
}

TextFormField

TextFieldはシンプルな入力フィールドで、ヴァリデーションやフォーム管理の必要のない場合に基本的に単独で使われる。複数の値を同時に処理したりヴァリデーションが必要な場合はTextFormFieldを使う。

var _enteredEmail = '';

TextFormField(
    keyboardType: TextInputType.emailAddress,
    decoration: const InputDecoration(
         hintText: 'E-Mail',
    ),
    // validatorやonSavedはFormが一括で処理をする
    validator: (value) {
        if (value == null || value.isEmpty || !value.contains('@')) {
            return '正しいE-Mailを入力してください';
        }
        return null; // 何も問題ない場合はnullを返す
    },
    onSaved: (value) {
        _enteredEmail = value!;
    },
);

Formウィジェットの中に入れることで、複数の値を同時にvalidatorやonSavedに書かれた処理を実行することができる。

TextEditingControllerを使うべきか?

TextFormFieldのプロパティに"controller"がある。TextEditingControllerを使ってcontrollerを設定するか否かはどう判断するのか?
TextEditingControllerの特徴は

  • リアルタイム更新: onChangedを使ってリアルタイム監視をすることも可能であるが、TextEditingControllerを使えばわざわざonChangedを設定する必要がない。
  • 初期値の設定: TextEditingController('初期値')で簡単に設定できる。
  • メモリリークしないよう、忘れずにcontroller.dispose()をする必要がある。

TextEditingControllerが便利なのは、リアルタイムで入力内容を更新・操作する必要がある時。
そうでない場合は変数を使った方が実装がシンプルだし管理がしやすい。メモリ管理も必要ないしヴァリデーションを一括で行うことができる。

WaterWoodWaterWood

【Flutter】Theme

https://docs.flutter.dev/cookbook/design/themes
https://zenn.dev/kai_oishi/articles/8c509581f77746
https://material-foundation.github.io/material-theme-builder/#/custom

アプリ全体のテーマを設定する。
メインはカラーとテキストスタイル。

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.purple,
      brightness: Brightness.dark,
    ),
    textTheme: TextTheme(
      displayLarge: const TextStyle(
        fontSize: 72,
        fontWeight: FontWeight.bold,
      ),
      titleLarge: GoogleFonts.oswald(
        fontSize: 30,
        fontStyle: FontStyle.italic,
      ),
      bodyMedium: GoogleFonts.merriweather(),
      displaySmall: GoogleFonts.pacifico(),
    ),
  ),
);

Themeを各ウィジェットに適応する時はTheme.of(context)

color: Theme.of(context).colorScheme.primary,
style: Theme.of(context).textTheme.bodyMedium,

ColorScheme

https://zenn.dev/gen_kk/articles/cc538ffa392922

fromSeed()

https://zenn.dev/issei_manabi/articles/8a75220670f484

TextTheme

https://zenn.dev/mukkun69n/articles/e654c706257722

Themeウィジェット

特定のコンポーネントで全く異なるテーマを設定したいとき。

Theme(
  data: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.pink,
    ),
  ),
  child: Text('違うテーマ'),
);

copyWith

テーマを継承しつつ特定の要素だけ設定を変更したいとき。

style: Theme.of(context).textTheme.titleLarge!
            .copyWith(color: Colors.blue),
WaterWoodWaterWood

【Flutter】url_launcher - WEBページや他のアプリへ飛ぶ

アプリの外部のページへ飛ぶ

Future<void> openWebSite(Uri url) async {
  if (await canLaunchUrl(url)) {
    await launchUrl(
      url,
      mode: LaunchMode.platformDefault,
    );
  } else {
    throw '開くことができません: $url';
  }
}

Twitterやインスタなどアプリをインストールしている場合はアプリへ、していない場合はWEBサイトへ移動する

Future<void> openAppOrWeb(Uri deepLinkUrl, Uri webUrl) async {
  if (await canLaunchUrl(deepLinkUrl)) {
    await launchUrl(
      deepLinkUrl,
      mode: LaunchMode.externalApplication,
    );
  } else if (await canLaunchUrl(webUrl)) {
    await launchUrl(
      webUrl,
      mode: LaunchMode.platformDefault,
    );
  } else {
    throw 'どちらのURLも開くことができません: $deepLinkUrl, $webUrl';
  }
}
WaterWoodWaterWood

【Flutter】logger

https://pub.dev/packages/logger
https://codezine.jp/article/detail/19645?p=3
今までprintでデバッグをやっていたが、より高度なロギングを導入したい。

基本用法

var logger = Logger();

logger.t("Trace log");
logger.d("Debug log");
logger.i("Info log");
logger.w("Warning log");
logger.e("Error log", error: 'Test Error');
logger.f("What a fatal log", error: error, stackTrace: stackTrace);

ログ表示がその重要度によって区分け、色分けされてて非常にわかりやすい。
まずは基本としてここまでを使いこなしたい。

しかし、どこまでをログにするか?
重要な制御をInfoにするとよさそうである。
TraceとDebugは開発段階のデバッグで使うだけが良さそう。

出力制御

ファイルなどへログを保存

void main() {
  var outputFile = File('log_output.txt');
  var logger = Logger(
    output: MultiOutput([ConsoleOutput(), FileOutput(outputFile)]),
  );
  logger.i('コンソールとファイルに出力');
}

開発段階にとどまらないloggerの利用
https://qiita.com/DiegoHonda/items/253e275c395bb068912e

WaterWoodWaterWood

非同期処理のエラーハンドリング try-catch

主な必要な場面

  • ネットワークエラー、
  • データベースエラー
  • ファイル操作エラー

主なエラーの種類

ネットワークエラー

  • タイムアウトエラー(Timeout Error)
    • 説明: サーバーからの応答が一定時間内に返ってこない場合に発生します。
    • 対処法: タイムアウトを設定し、再試行のオプションを提供します。
  • 接続エラー(Connection Error)
    • 説明: サーバーに接続できない場合に発生します。インターネット接続がない、サーバーがダウンしているなどの原因があります。
    • 対処法: 接続が失敗した場合にユーザーに通知し、インターネット接続を確認するよう促します。
  • HTTPエラー(HTTP Error)
    • 説明: サーバーからのHTTPステータスコードがエラーを示している場合に発生します(例:404 Not Found、500 Internal Server Error)。
    • 対処法: 各HTTPステータスコードに応じたエラーハンドリングを行います。例えば、404エラーの場合は「ページが見つかりません」のメッセージを表示します。
  • フォーマットエラー(Format Error)
    • 説明: サーバーからのレスポンスが期待される形式ではない場合に発生します。JSONパースエラーなどが含まれます。
    • 対処法: レスポンスの形式を検証し、パースエラーが発生した場合にエラーメッセージを表示します。

データベースエラー

  • 接続エラー (Connection Error)

    • 説明: データベースサーバーへの接続が確立できない場合に発生します。ネットワークの問題、データベースサーバーのダウン、誤った接続情報などが原因です。
    • 対処法: 接続情報(ホスト名、ポート、ユーザー名、パスワード)を確認し、ネットワーク接続をチェックします。
  • SQL構文エラー (Syntax Error)

    • 説明: SQLクエリに文法的な誤りがある場合に発生します。例えば、キーワードのスペルミスやクエリの不正な構文などが原因です。
    • 対処法: クエリを慎重にレビューし、SQL構文が正しいことを確認します。
  • データの一貫性エラー (Data Consistency Error)

    • 説明: データの整合性が保たれていない場合に発生します。例えば、一意制約違反や外部キー制約違反などです。
    • 対処法: データベース設計を見直し、制約を正しく設定する。データの入力時にバリデーションを行う。
  • タイムアウトエラー (Timeout Error)

    • 説明: データベース操作が指定された時間内に完了しない場合に発生します。大規模なクエリやサーバーの過負荷が原因です。
    • 対処法: クエリの最適化やインデックスの使用、タイムアウト設定の調整を行います。
  • デッドロック (Deadlock)

    • 説明: 複数のトランザクションが相互にロックを待っている状態で発生します。結果として、トランザクションが進行できなくなります。
    • 対処法: デッドロックを検出し、トランザクションのロックの順序を最適化します。トランザクションの再試行メカニズムを実装します。
  • データ型エラー (Data Type Error)

    • 説明: データベースのフィールドに対して不適切なデータ型の値が挿入される場合に発生します。
    • 対処法: データ型のバリデーションを行い、正しいデータ型を使用する。

ファイル操作エラー

  • ファイルが見つからないエラー (File Not Found Error)
    • 説明: 指定されたファイルが存在しない場合に発生します。ファイルパスが間違っている、ファイルが削除されたなどが原因です。
    • 対処法: ファイルパスを確認し、ファイルが存在することを確認します。必要に応じて、ファイルの存在をチェックするコードを追加します。
  • アクセス拒否エラー (Permission Denied Error)
    • 説明: ファイルへの読み書き権限がない場合に発生します。ファイルやディレクトリの権限が適切に設定されていないことが原因です。
    • 対処法: ファイルやディレクトリの権限を確認し、適切な権限を設定します。アプリケーションに必要な権限をリクエストするコードを追加します。
  • ディスク容量不足エラー (Disk Space Error)
    • 説明: ファイルを書き込む際にディスク容量が不足している場合に発生します。ディスクが満杯になっていることが原因です。
    • 対処法: ディスクの使用状況を確認し、不要なファイルを削除してディスク容量を確保します。
  • ファイルのロックエラー (File Lock Error)
    • 説明: 他のプロセスやアプリケーションがファイルをロックしているため、アクセスできない場合に発生します。
    • 対処法: ファイルがロックされている原因を特定し、他のプロセスがファイルを解放するのを待ちます。必要に応じて、ロック状態をチェックするコードを追加します。
  • ファイルフォーマットエラー (File Format Error)
    • 説明: ファイルのフォーマットが期待される形式と一致しない場合に発生します。たとえば、画像ファイルとして読み込むファイルが実際にはテキストファイルだった場合などです。
    • 対処法: ファイルの形式を検証し、正しい形式であることを確認します。

try-catchのコード構造

void functionC() {
  throw Exception('エラーが発生しました');
}

void functionB() {
  functionC();
}

void functionA() {
  functionB();
}

void main() {
  try {
    functionA();
    // 例外が発生すると以下のコードは実行されない
    /*
        コード
    */
  } catch (e) {
    print('キャッチされたエラー: $e');
  } finally {
    // finallyブロックはオプション。例外の有無にかかわらず時効される。
    print('finallyブロックは必ず実行されます');
  }
}

"catch(e)"の" e "はExceptionクラスのインスタンス。エラーの詳細、実態はその例外のクラスを見るとわかる。

コールスタック

例外が発生すると、try ブロックの残りの部分はスキップされ、コールスタックを巻き戻しながら適切な catch ブロックを探します。

コールスタック

呼び出しスタックとも呼ばれます。
エラーが発生した場合に、エラーをキャッチする適切な場所(catch ブロック)を探すために、実行中の関数呼び出しの履歴(コールスタック)を逆にたどることを指します。
関数やメソッドの呼び出し履歴を保持するデータ構造です。プログラムが実行される際、関数が呼び出されると、その情報がスタックに積み上げられます(プッシュされます)。関数の実行が完了すると、その情報はスタックから取り除かれます(ポップされます)

コールスタック: 
1. functionC() 
2. functionB() 
3. functionA() 
4. main()

データフローの視点

  1. 例外がスローされる: 例外がスローされると、現在の実行コンテキストが中断されます。
    • functionC() で例外がスローされる。
  2. スタックの巻き戻し: プログラムのコールスタックが巻き戻され、例外をキャッチするためのハンドラーが探されます。
    • コールスタックを巻き戻して functionB() に戻るが、ここには catch ブロックがない。
    • さらに巻き戻して functionA() に戻るが、ここにも catch ブロックがない。
    • 最終的に main() に戻り、ここで例外をキャッチする catch ブロックが見つかる。
  3. 例外のキャッチ: 適切な catch ブロックが見つかると、例外がそのブロックに渡され、エラーメッセージやリカバリロジックが実行されます。
    • catch ブロック内のコードが実行され、エラーメッセージが表示される。
WaterWoodWaterWood

assert とは

assertは、Dartで提供されるデバッグ用の機能で、条件が成立しない場合に例外を発生させることでプログラムの実行を停止し、問題を早期に発見するために使用されます。主に開発中のデバッグやテストで利用されますが、本番環境では無効化されることが一般的です。

assert(条件, メッセージ);

条件: assertがチェックするブール式です。この条件がfalseの場合、assertは例外を投げます。
メッセージ (任意): 例外が投げられたときに表示されるメッセージです。

例 assert

void main() {
  int x = 10;
  int y = 20;
  
  // xがyよりも小さいことを確認
  assert(x < y, 'x should be less than y');
  
  // xがyよりも大きい場合に例外を投げる
  // assert(x > y, 'x should be greater than y'); // これは失敗し、例外が発生します
}

使用場面

  1. 関数の引数の検証
    関数やメソッドが受け取る引数が期待通りの範囲内にあることを確認するために使用します。例えば、年齢を設定する関数で、年齢が正の整数であることを確認する場合です。
void setAge(int age) {
  assert(age > 0, 'Age must be positive');
  // 年齢を設定する処理
}
  1. コレクションの状態の検証
void processList(List<int> numbers) {
  assert(numbers.isNotEmpty, 'The list should not be empty');
  // リストを処理するコード
}
  1. コンストラクタの前提条件の検証
class User {
  final String name;
  final int age;

  User(this.name, this.age) {
    assert(name.isNotEmpty, 'Name must not be empty');
    assert(age > 0, 'Age must be positive');
  }
}
  1. 計算結果の確認
double divide(int a, int b) {
  assert(b != 0, 'Denominator must not be zero');
  return a / b;
}
  1. 計算結果の確認
class MyButton extends StatelessWidget {
  final bool isEnabled;

  MyButton({required this.isEnabled}) {
    assert(isEnabled != null, 'isEnabled must not be null');
  }

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isEnabled ? () => print('Button pressed') : null,
      child: Text('Press me'),
    );
  }
}

loggerとの比較

assertの役割

  • 目的: 前提条件が満たされていることを確認する。
  • 使用場所: 主に開発時やデバッグ時に使用され、本番環境では無効化される。
  • 動作: 指定した条件がfalseの場合に例外を投げる。メッセージを追加することも可能。
  • 利点: コードの前提条件を明示的に示し、誤った使用を早期に発見するのに役立つ。

loggerの役割

  • 目的: ログメッセージを記録し、コードの実行状況を把握する。
  • 使用場所: 開発、デバッグ、および本番環境で使用される。
  • 動作: 指定したメッセージをコンソールやログファイルに出力する。ログレベル(情報、警告、エラーなど)を設定することも可能。
  • 利点: 実行時の状態やエラーの追跡、パフォーマンスのモニタリングに役立つ。

使用シナリオの違い

  • assert:
    • 関数の引数が適切であることを確認する。
    • コレクションが空でないことを確認する。
    • コンストラクタの前提条件を確認する。
    • デバッグ時に特定の条件が満たされていることを確認する。
  • logger:
    • アプリケーションの開始や終了時にログを記録する。
    • エラーや例外が発生した際に詳細な情報を記録する。
    • パフォーマンスデータやメトリクスを記録する。
    • ユーザーアクションやイベントを記録する。
WaterWoodWaterWood

勘違いしていたawait

Future<void> fetchData() async {
  print('データの取得を開始します');
  await Future.delayed(Duration(seconds: 2)); // 2秒待機
  print('データの取得が完了しました');
}

void main() {
  fetchData();
  print('他の処理を実行します'); // この処理がfetchDate()の完了を待たずに進む
}

fetchData内のawaitにより処理が止まると思っていた。
しかし実際は呼び出し側であるmainの処理は進む。
mainでfetchData()の処理が終わるまで待つためには、呼び出し側にもawaitを付ける必要がある

void main() async {
  await fetchData(); // ここにawaitを付けないと進んじゃう
  print('fetchData関数が終わるのを待ちます');
}
WaterWoodWaterWood

定数クラス(ユーティリティクラス)

インスタンス化されることなく静的メンバ(定数やメソッド)を提供する。
データベースのテーブル名、URLのパスなどに使われる。
class.(); でプライベートコンストラクタにすることで外部から直接インスタンス化することを防ぐ。

class DatabaseTables {
  DatabaseTables._(); // プライベートコンストラクタ

  static const String users = 'users';
  static const String products = 'products';
  static const String orders = 'orders';
  static const String categories = 'categories';
  static const String reviews = 'reviews';
}

class ApiEndpoints {
  ApiEndpoints._(); // プライベートコンストラクタ

  static const baseUrl = 'https://api.example.com';
  static const getUsers = '/users';
  static const getPosts = '/posts';
  static const getComments = '/comments';
}

class AppColors {
  AppColors._(); // プライベートコンストラクタ

  static const Color primaryColor = Color(0xFF00FF00);  // 緑色
  static const Color secondaryColor = Color(0xFFFF0000);  // 赤色
  static const Color accentColor = Color(0xFF0000FF);  // 青色
}

https://jp-seemore.com/app/17321/

WaterWoodWaterWood

コンストラクタの{}と[]のちがい

{}

名前付き引数(Named Parameters)

class MyClass {
 MyClass({int? namedParam}) {
   // namedParam は名前付き引数で、省略可能
 }
}

void main() {
 MyClass(); // 引数を省略
 MyClass(namedParam: 5); // 名前付き引数を指定して渡す
}

[]

位置引数だがオプションにしたい場合

class MyClass {
 MyClass([int? optionalParam]) {
   // optionalParam は省略可能
 }
}

void main() {
 MyClass(); // 引数を省略
 MyClass(5); // 引数を渡す
}

WaterWoodWaterWood

RowのchildrenにTextFieldを置いたときのレンダリングエラー

Rowはchildrenのウィジェットたちを横に並べる。子のウィジェットに横幅が決まっていると問題ないのだが、横幅が決まってないウィジェットだとRowは必要な横幅が分らずエラーとなってしまう。TextFieldそれ自体は横幅が決まっておらず横幅 = 無限となっている。そのため幅を制限してやらないといけない。

Row(
  children: [
    Text('TextFieldだよ。'),
    TextField(),
  ],
);

これだとTextFieldが横方向に無限となってしまってレンダリングエラーになる。

// Expandedで画面幅に合わせていっぱいの幅を取る
Row(
  children: [
    Text('TextFieldだよ。'),
    Expanded(child: TextField()),
  ],
);

// SizedBoxやContainerの中にTextFieldを入れて幅を決める
Row(
  children: [
    Text('TextFieldだよ。'),
    SizedBox(width: 200, child: TextField()),
  ],
);

もちろんColumnの場合は縦方向で同様の挙動となる。
https://zenn.dev/masarufuruya/articles/flutter-row-textfield
https://qiita.com/kalupas226/items/5aa41ca409730606000f

WaterWoodWaterWood

BoxFit

cover

BoxFit.coverは画像のアスペクト比を変えずに範囲いっぱいに表示する。横幅が長い場合縦の高さに合わせるため、範囲から出る左右の部分は切り取られる。

Image.network(
  song.imageUrl,
  height: 140,
  width: 140,
  fit: BoxFit.cover,
),

contain

BoxFit.containは画像のアスペクト比を変えずに範囲に収まるように表示する。横幅が長い場合は横幅に合わせ、縦は短いままである。

fill

アスペクトを無視し範囲いっぱいに表示する。横幅が長い場合縦方向に引き延ばされる。

その他

none、なにもしない。
fitWidth、fitHeight、アスペクト比を変えずに既定の幅や高さに合わせる。