flutter

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars1,609
mentions57
071142213284Oct 4Oct 18Nov 2
UTC days · repositories flutter owns13 repositories

Package downloads

No package download history available yet.

Top repositories

flutter’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside flutter’s account where flutter opened or reviewed pull requests in the past 365 days.

Who noticed flutter

Everyone who starred or forked flutter’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

58posts
Nov 31 post
  1. Flutterで箱入り娘を作成する4 概要 Flutterで箱入り娘を作成し、iOS と Android のアプリとして販売する このシリーズでは、作り始めから販売までを順番に書いていく - 第3回:Flutterで箱入り娘を作成する3 第4回では、次のことをする 遊んだ記録を端末に残す 一覧の行に、クリアの印と自分の手数を出す 途中でやめた盤面から続けられるようにする 実行環境 Flutter $ flutter --version Flutter 3.47.9 • channel stable • https://github.com/flutter/flutter.git Framework • revision c197ca5199 (4 days ago) • 2026-10-29 10:10:20 -0400 Engine • hash 9e025265bef30b838c0c1b68cad19decf97048f2 (revision 89c39edfdf) (6 days ago) • 2026-10-27 19:51:20.000Z Tools • Dart 3.13.7 • DevTools 2.60.0 手順 パッケージを入れる flutter pub add shared_preferences flutter pub add dev:shared_preferences_platform_interface 2つ目はテストだけで使う(後述)。 ファイルの構成 lib/ main.dart … 起動のときに記録を読み込む model/ progress.dart … 遊んだ記録 screen/ stage_list_screen.dart … ステージの一覧(前回から) game_screen.dart … 遊ぶ画面(前回から) test/ memory_store.dart … テスト用の、メモリの中だけの置き場 model/progress_test.dart screen/progress_screens_test.dart 以下のコードは主要な部分の抜粋(... は省略)。 何を残すか 残すのは、次の3つ。 | 残すもの | 使うところ | |---|---| | 配置ごとの、クリアしたうちでいちばん少ない手数 | 一覧の印と「自分 N 手」 | | 配置ごとの、途中の盤面と手数 | 途中から続ける | | 最後に開いた配置 | 一覧のいちばん上の「続きから」 | 一覧の印は4通りにする。 | 印 | いつ | |---|---| | 王冠 | 最短手数でクリアした | | ✓ | クリアした(最短より多い手数) | | 途中 | クリアしたことがなく、途中の盤面がある | | なし | まだ遊んでいない | - 行を押すと、途中の盤面があればそこから続ける。遊ぶ画面の「はじめから」で、途中の盤面を捨てる - 一度クリアした配置は、また途中でやめても、クリアの印のままにする(クリアしたことは消えないため) - 「続きから」は、最後に開いた配置に途中の盤面があるときだけ出す。クリアしたら消える 保存のパッケージを選ぶ shared_preferences を使う。 Flutter のチームが作っていて、iOS・Android のほか Web でも動く。 説明には、次のように書いてある。 > there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data. 書いたものが必ず端末に残るとは限らないので、大事なデータには使わない。 残すのは、消えても遊び直せば戻るものだけにする(今回の3つはどれもそう)。 - pub.dev 使い方は3通りある。 古い SharedPreferences は、いずれ使わなくなる予定なので、新しく書くコードでは SharedPreferencesWithCache か SharedPreferencesAsync を使う。 SharedPreferencesWithCache は、起動のときに一度だけ端末から読み込み、メモリに写しを持つ。 そのあとの読み出しは await なしで写しから読めるので、画面を作る build の中でそのまま使える。 記録のクラス 画面からは shared_preferences を直接触らず、Progress のクラスを通す。 - 「どの文字の名前で、どんな形の値を置くか」を1か所にまとめる - テストでは、置き場をメモリの中のものに差しかえられる(後述) /// 遊んだ記録。端末に残す。 class Progress { Progress._(this._prefs); /// 端末から記録を読み込む。 static Future<Progress> load() async { ... await SharedPreferencesWithCache.create( cacheOptions: SharedPreferencesWithCacheOptions(allowList: _keys), ), ... } final SharedPreferencesWithCache? _prefs; // 配置は名前で覚える(並び順を変えても記録がずれないように) static String _bestKey(NamedLayout layout) => 'best:${layout.name}'; static String _unfinishedKey(NamedLayout layout) => 'unfinished:${layout.name}'; static const _lastKey = 'last'; static final _keys = { _lastKey, for (final layout in namedLayouts) ...[ _bestKey(layout), _unfinishedKey(layout), ], }; ... } - 配置は、リストの番号ではなく名前で覚える。あとで配置を足したり並べ直したりしても、記録が別の配置にずれない - allowList には、使う名前を全部渡す。ほかのパッケージが同じ置き場に書いた値を、写しに読み込まない 読み出し /// クリアしたうちで、いちばん少ない手数。クリアしていなければ null。 int? best(NamedLayout layout) { final value = _prefs?.get(_bestKey(layout)); // 最短より少ない手数は、ありえないので読めない値とみなす return value is int && value >= layout.moves ? value : null; } 置き場の値は、アプリの外から書きかえられることもある。 型と中身を確かめて、おかしな値は「記録なし」として扱う。 途中の盤面を文字にする 途中の盤面は、1つの文字にして置く。 手数;駒,駒,… (例:3;s00,s10,d20,h01,…) 駒は、種類の頭文字(d = 娘、v = 縦長、h = 横長、s = 小さい駒)と、左上の x と y。 static String _encode(Board board, int moves) => '$moves;${board.pieces.map((p) => '${_letters[p.type]}${p.x}${p.y}').join(',')}'; /// 読めない値・[layout] と駒の数がちがう盤面・クリアした盤面は null。 static Unfinished? _decode(NamedLayout layout, String text) { final match = RegExp(r'^([1-9]\d{0,8});((?:[dvhs]\d\d,)*[dvhs]\d\d)$') .firstMatch(text); if (match == null) return null; ... if (!_sameKinds(pieces, layout.board.pieces)) return null; final Board board; try { board = Board(pieces); } on ArgumentError { return null; } if (board.isSolved) return null; return Unfinished(board, int.parse(match[1]!)); } - 駒の並び順は、盤面のままにする。読み戻したときに、遊ぶ画面が「何番目の駒をなぞったか」で動かせるように - 読み戻すときは、文字の形 → 駒の種類ごとの数 → 盤からはみ出さないか・重ならないか(第1回の Board が確かめる)→ クリアした盤面でないか、の順に確かめる クリアを書く /// [moves] 手でクリアしたことを覚え、途中の盤面を忘れる。前より少ない手数のときだけ、いちばん少ない手数を書きかえる。 /// 読み出しに使う写しへは、待たずにすぐ入る(端末へ書き終わるのはあと)。 Future<void> recordClear(NamedLayout layout, int moves) { final best = this.best(layout); return Future.wait([ if (_prefs != null && (best == null || moves < best)) _prefs.setInt(_bestKey(layout), moves), clearUnfinished(layout), ]); } SharedPreferencesWithCache の setInt などは、呼んだときにメモリの写しを書きかえ、端末へはそのあと書く。 なので、書いた直後に読み出すと、もう新しい値になっている。 画面につなぐ 起動のときに読み込む Future<void> main() async { // 記録を読み込むには、アプリの画面より先に端末とのつなぎを用意しておく WidgetsFlutterBinding.ensureInitialized(); runApp(KlotskiApp(progress: await Progress.load())); } runApp より前に端末の機能を使うときは、先に WidgetsFlutterBinding.ensureInitialized() を呼ぶ。 遊ぶ画面で、1手ごとに書く @override void initState() { super.initState(); final unfinished = _progress.unfinished(_layout); _board = unfinished?.board ?? _layout.board; _moves = unfinished?.moves ?? 0; // 書き終わるのは待たない(画面は先に出す)。以下の書き込みも同じ unawaited(_progress.open(_layout)); } void _move(int index, Piece to) { ... unawaited( _board.isSolved ? _progress.recordClear(_layout, _moves) : _progress.saveUnfinished(_layout, _board, _moves), ); } - 書き込みは待たない。指を離すたびに書き終わるのを待つと、次の手が遅れるため - 待たないことをはっきりさせるため、unawaited(dart:async)で包む 一覧で、印と「続きから」を出す final best = _progress.best(layout); // 印:クリアしたことがあればクリアの印(最短なら王冠)。無ければ、途中の盤面があるとき途中の印 final (IconData, String)? mark = best == layout.moves ? (Icons.emoji_events, '最短でクリア') : best != null ? (Icons.check_circle, 'クリア') : _progress.unfinished(layout) != null ? (Icons.timelapse, '途中') : null; ... subtitle: Row( children: [ if (mark != null) ...[ Icon( mark.$1, size: 16, color: colors.primary, semanticLabel: mark.$2, ), const SizedBox(width: 4), ], Flexible( child: Text( best == null ? '最短 ${layout.moves} 手' : '最短 ${layout.moves} 手 自分 $best 手', ... - 印と読み上げの言葉を、レコード (IconData, String) でまとめて持つ - 印は形だけで分かるように、王冠・✓・時計の形を変える(色だけで分けない) - semanticLabel で、画面の読み上げ機能でも「最短でクリア」などと読ませる 遊ぶ画面から戻ったら、一覧を新しくする 一覧の画面は StatefulWidget にして、遊ぶ画面を閉じたら setState で作り直す。 /// 遊ぶ画面を開き、戻ってきたら記録の印を出し直す。 Future<void> _open(NamedLayout layout) async { await Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) => GameScreen(layout: layout, progress: _progress), ), ); if (mounted) setState(() {}); } - Navigator.push の Future は、その画面を閉じたときに終わる。await のあとが「戻ってきたとき」になる - await のあいだに一覧の画面が消えていることもあるので、mounted を確かめてから setState する 記録が壊れたとき 端末によっては、記録のファイルが壊れていると、SharedPreferencesWithCache.create がエラーになる(壊れ方によって FormatException も TypeError も出る)。 main で読み込みを待ってから runApp しているので、そのままだと画面が出ないまま止まる。 読み込みでエラーが出たら、記録なしで始める。 static Future<Progress> load() async { try { return Progress._( await SharedPreferencesWithCache.create( cacheOptions: SharedPreferencesWithCacheOptions(allowList: _keys), ), ); } catch (e, stack) { // 壊れ方によって、FormatException も TypeError(Exception でない)も出るので、全部受け止める debugPrint('記録を読み込めなかったので、記録なしで始める: $e\n$stack'); return Progress._(null); } } - on Exception だと、Exception でない TypeError を受け止められない - 置き場が null のときは、読み出しは全部「記録なし」、書き込みは何もしない(_prefs?.get(...) など) - 遊べないよりは、記録なしで遊べる方がよい。壊れた記録は、どのみち読めない テスト 置き場をメモリの中のものにする テストでは、端末の置き場を InMemorySharedPreferencesAsync に差しかえる。 shared_preferences 自身のテストでも使われているもので、shared_preferences_platform_interface に入っている。 /// 端末の置き場を、メモリの中だけのものにする([data] は始めから入っている値)。 void useMemoryStore([Map<String, Object> data = const {}]) { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({...data}); } /// 端末の置き場を、読み込むと [error] を投げるもの(記録のファイルが壊れたとき)にする。 void useBrokenStore([Object error = const FormatException('壊れた記録')]) { SharedPreferencesAsyncPlatform.instance = _BrokenStore(error); } base class _BrokenStore extends InMemorySharedPreferencesAsync { ... @override Future<Map<String, Object>> getPreferences( GetPreferencesParameters parameters, SharedPreferencesOptions options, ) => throw error; } withData で始めから値を入れておけるので、「壊れた値が入っていたら」も書ける。 for (final (why, value) in [ ('空', ''), ('手数が 0', good.replaceFirst('1;', '0;')), ('知らない駒', good.replaceFirst('d', 'x')), ('駒が重なる', good.replaceFirst(RegExp(r'd\d\d'), 's00')), ('盤からはみ出す', good.replaceFirst(RegExp(r'd\d\d'), 'd33')), ('クリアした盤面', solvedCode), ... ]) { test(why, () async { useMemoryStore({key: value}); expect((await Progress.load()).unfinished(first), isNull); }); } - 「読める見本」のテストも1つ置く。見本が読めないと、下の「読めない」のテストが全部、別の理由で通ってしまうため 起動のテスト 置き場を壊れたものにして、本物の main を呼ぶ。 testWidgets('記録のファイルが壊れて読み込めなくても、アプリは立ち上がって一覧を出す', (tester) async { useBrokenStore(); await app.main(); await tester.pump(); expect(find.text('1. 前呼後擁'), findsOneWidget); }); flutter test アプリの実行 アプリを起動 flutter run 実行結果 !klotski4.gif 次回 詰まったときに、今の盤面からの解き方を見せる。

  2. Flutterで箱入り娘を作成する3 概要 Flutterで箱入り娘を作成し、iOS と Android のアプリとして販売する このシリーズでは、作り始めから販売までを順番に書いていく - 第2回:Flutterで箱入り娘を作成する2 - 第4回:Flutterで箱入り娘を作成する4 第3回では、次のことをする 昔から伝わる、名前の付いた配置を44個入れ、ソルバーで手数を確かめて、手数の少ない順に並べる ステージを選ぶ一覧の画面を作る 遊ぶ画面に、ステージの番号と名前を出す 実行環境 Flutter $ flutter --version Flutter 3.47.9 • channel stable • https://github.com/flutter/flutter.git Framework • revision c197ca5199 (4 days ago) • 2026-10-29 10:10:20 -0400 Engine • hash 9e025265bef30b838c0c1b68cad19decf97048f2 (revision 89c39edfdf) (5 days ago) • 2026-10-27 19:51:20.000Z Tools • Dart 3.13.7 • DevTools 2.60.0 手順 ファイルの構成 lib/ model/ named_layouts.dart … 名前の付いた配置44個 name_notes.dart … 名前の読みと意味 screen/ stage_list_screen.dart … ステージの一覧 game_screen.dart … 遊ぶ画面(前回から) widget/ board_thumbnail.dart … 盤の小さな絵 board_view.dart … 盤(前回から) test/ model/named_layouts_test.dart screen/ 以下のコードは主要な部分の抜粋(... は省略)。 名前の付いた配置 箱入り娘は、中国では「華容道」と呼ばれる。 華容道には、配置ごとに名前が付いている。1943年に梁青が名前を付けたとされる。 定番の配置の名前は「横刀立馬」。 - zh.wikipedia.org) 名前の付いた配置のうち、解けて、ほかと同じ盤面にならない44個をステージにする。 - 名前は日本の漢字に直す(峰迴路轉 → 峰回路転 など) - 同じ名前の配置が2つあるものは、番号を付ける(挿翅難飛1、挿翅難飛2) - 手数は、前回のソルバーで出した数を使う 配置をクラスにする 名前・手数・始めの盤面をまとめて、NamedLayout にする。 /// 名前の付いた配置。 class NamedLayout { const NamedLayout(this.name, this.moves, this.board, {this.alias}); /// 日本の漢字で書いた名前。 final String name; /// 日本の漢字で書いた別名(1987 年の本《独立钻石和华容道》での名前)。付けていなければ null。 final String? alias; /// 最短手数(同じ駒なら曲がっても1手)。テストでソルバーと照らし合わせる。 final int moves; /// 始めの盤面。 final Board board; } 手数はアプリの中で毎回解かずに、数として持つ。 一覧を開くたびに44個を解くと、138手の配置などで時間がかかるため。 数が正しいかは、テストでソルバーと照らし合わせる(後述)。 配置をコードに書く 44個は、手数の少ない順にリストに書く。 駒の座標だけだと形が分からないので、盤の絵をコメントに添える。 /// 昔から伝わる、名前の付いた配置(中国の「華容道」)44 個。手数の少ない順。 /// /// 盤の絵は D = 娘、V = 縦長、H = 横長、S = 小さい駒、. = 空き。 final namedLayouts = [ // S S D D // H H D D // H H H H // H H H H // . . S S NamedLayout( '前呼後擁', 22, Board(const [ Piece(PieceType.small, 0, 0), Piece(PieceType.small, 1, 0), Piece(PieceType.daughter, 2, 0), Piece(PieceType.horizontal, 0, 1), ... ]), ), ... ]; - 駒のリストは const にする。同じ値のリストを何度も作らずに済む - Board は const にできない(前回、hashCode を late final にしたため)。final のリストにする データの正しさをテストで守る 手で入力したデータは、打ち間違いが起きる。 44個を1つずつ目で確かめるのではなく、決まりごとをテストにする。 test('手数の少ない順に並んでいる', () { for (var i = 1; i < namedLayouts.length; i++) { expect( namedLayouts[i].moves, greaterThanOrEqualTo(namedLayouts[i - 1].moves), reason: namedLayouts[i].name, ); } ... }); test('左右を反転しても、同じ盤面が2つない', () { final seen = <Board>{}; for (final layout in namedLayouts) { expect(seen, isNot(contains(layout.board)), reason: layout.name); seen ..add(layout.board) ..add(mirror(layout.board)); } }); group('最短手数がソルバーと同じ', () { for (final layout in namedLayouts) { test(layout.name, () { expect(layout.board.isSolved, isFalse); expect(solve(layout.board).path!.length - 1, layout.moves); }); } }); - reason に名前を入れる。落ちたときに、どの配置か分かる - group の中で配置ごとに test を作る。1つ落ちても、ほかの配置の結果も見られる - 左右を反転した盤面は、遊ぶ人から見れば同じ配置なので、重なりとして数える - 前回、Board の == を「同じ種類の駒が同じマスにあれば同じ」にしたので、Set で重なりを調べられる 手数は22手から138手。定番の横刀立馬(81手)は31番目になった。 44個のうち、いくつかを挙げる。 | 名前 | 手数 | 意味 | |---|---:|---| | 1. 前呼後擁<br>ぜんここうよう | 22 | お供をたくさん連れた、にぎやかな行列のようす | | 14. 四面楚歌<br>しめんそか | 56 | まわりが敵ばかりで、味方がいないこと | | 31. 横刀立馬<br>おうとうりつば | 81 | 刀を横に構え、馬を止めて立つこと(定番の配置) | | 43. 層層設防2<br>そうそうせつぼう | 120 | 何重にも守りを固めること | | 44. 峰回路転<br>ほうかいろてん | 138 | 行き詰まった物事に、思いがけず道が開けること | 定番の81手は、どれくらい難しいのか。 定番の駒のセット(縦長4・横長1・小さい駒4)だけで作れる盤面を、ゴールから逆向きにたどって全部数えた。 - 作れる盤面は65,880個 - そのうち、横刀立馬から動かしてたどり着ける盤面は25,955個 - その中で、ゴールからいちばん遠い盤面は93手 定番の81手は、いちばん難しい盤面ではない。 ただ、横刀立馬はどう1手動かしても、ゴールからそれより遠くならない盤面になっている。 ステージの一覧 一覧の行 1行に、盤の小さな絵・番号と名前・最短手数・i ボタンを並べる。 ListTile を使うと、左の絵・名前・下の行・右のボタンの場所が決まっていて、押したときの反応も付く。 // 挿翅難飛1 と 2 を聞き分けられるよう、末尾の番号は読みにも付ける final number = RegExp(r'\d+$').stringMatch(layout.name) ?? ''; return ListTile( leading: BoardThumbnail(board: layout.board), title: Text( '${index + 1}. ${layout.name}', // 読み上げは漢字を読みまちがえうるので、かなで読ませる semanticsLabel: '${index + 1}. ${noteOf(layout.name).reading}$number', ), subtitle: Text( '最短 ${layout.moves} 手', style: TextStyle(color: colors.onSurfaceVariant), ), trailing: IconButton( tooltip: '名前の読みと意味', icon: const Icon(Icons.info_outline), onPressed: () => _showNotes(context, index), ), onTap: () => Navigator.of(context).push( MaterialPageRoute<void>( builder: (context) => GameScreen(layout: layout), ), ), onLongPress: () => _showNotes(context, index), ); - semanticsLabel は、画面の読み上げ機能(iPhone の VoiceOver など)で読む文字。画面に出す文字は変えずに、読み方だけをかなにできる - 読みは行に出さず、i ボタンの中に入れる。行には遊ぶのに要るもの(名前と手数)だけを残す - 長押しでも同じ説明を出す。ただし、長押しだけだと気づきにくく、iPhone の読み上げ機能からは使えないので、i ボタンも置く 盤の小さな絵 絵は触らないので、駒ごとに Widget を作らず、CustomPainter で1枚の絵として描く。 色は遊ぶ画面の盤とそろえる。 class _ThumbnailPainter extends CustomPainter { _ThumbnailPainter(this.board); final Board board; @override void paint(Canvas canvas, Size size) { // 枠の太さをマスの 1/4 にして、縦横どちらにも収まるマスの大きさにする final cell = math.min( size.width / (Board.width + 0.5), size.height / (Board.height + 0.5), ); ... for (final piece in board.pieces) { ... canvas.drawRRect( RRect.fromRectAndRadius(rect, Radius.circular(cell * 0.12)), Paint() ..color = piece.type == PieceType.daughter ? Colors.red.shade400 : Colors.orange.shade200, ); } } @override bool shouldRepaint(_ThumbnailPainter oldDelegate) => !identical(oldDelegate.board, board); } shouldRepaint は、描き直すかどうかを決める。 盤面は変わらないので、別の盤面になったときだけ描き直す。 i ボタンで、名前の読みと意味を出す 名前の読みと意味は、i ボタンを押すか行を長押しすると、下から出す。 遊ぶのには要らないので、遊ぶ画面には出さない。 showModalBottomSheet<void>( context: context, showDragHandle: true, // つまみが上の時計などの下に入らないようにする useSafeArea: true, // 文字が大きいときは、画面の半分より高くなってもいいようにする isScrollControlled: true, builder: (context) { ... }, ); - showDragHandle で、上の端につまみを出す。下へなぞると閉じられることが見て分かる(標準は出さない) - isScrollControlled が false だと、高さは画面の半分までになる。文字を大きくしたときに入りきるよう true にする 遊ぶ画面 バーに番号と名前 バーに「1. 前呼後擁」のように番号と名前を出す。 何番目のステージを遊んでいるかが分かる。 appBar: AppBar( // 文字が大きくて入りきらないときは、切らずに小さくして全部見せる title: FittedBox(fit: BoxFit.scaleDown, child: Text(_title)), ... FittedBox に BoxFit.scaleDown を付けると、入るときはそのまま、入らないときだけ小さくする。 文字の大きさは2倍まで 文字を大きくすると、盤の上の案内と手数が高くなり、その分だけ盤が小さくなる。 遊ぶ画面では、文字の大きさを2倍で止める。 // 文字は 2 倍で止める。Apple の「大きな文字」の目安(200%)は満たし、それより大きくして盤を削らない return MediaQuery.withClampedTextScaling( maxScaleFactor: 2, child: _scaffold(textTheme, colors), ); - MediaQuery.withClampedTextScaling は、中の Widget の文字の倍率に上限を付ける - App Store には「大きな文字」に対応しているかを示す項目があり、目安は200%以上 - developer.apple.com テスト testWidgets('文字は 2 倍で止まり、320x568・時計の帯ありで 3 倍以上にしても、はみ出さず盤も 2 倍のときと同じ', ( tester, ) async { ... for (final scale in const [2.5, 3.1, 3.5]) { await pumpGame(tester, first, size: const Size(320, 568), scale: scale); expect(tester.takeException(), isNull, reason: '$scale'); expect(board(), atTwo, reason: '$scale'); ... } }); tester.platformDispatcher.textScaleFactorTestValue で、端末の文字の大きさの設定をテストの中で変えられる。 flutter test アプリの実行 アプリを起動 flutter run 実行結果 !klotski3.gif 次回 どこまで遊んだかを覚えておき、途中から続けられるようにする。

  3. Reddit · · r/BestGitHubRepos · u/amootiranian · flutter/agent-plugins

    Flutter Agent Plugins: Agent plugins for Flutter, maintained by the Flutter team (3.1k stars) What it does All text below is quoted directly from the project README. >Agent plugins for Flutter, maintained by the Flutter team. Available Skills: - flutter-add-integration-test: Configures Flutter Driver for app interaction and converts MCP actions into permanent integration tests. - flutter-add-widget-preview: Adds interactive widget previews to the project using the previews.dart system. - flutter-add-widget-test: Implement a component-level test using WidgetTester to verify UI rendering and user interactions (tapping, scrolling, entering text). - flutter-apply-architecture-best-practices: Architects a Flutter application using the recommended layered approach (UI, Logic, Data). - flutter-fix-layout-issues: Fixes Flutter layout errors (overflows, unbounded constraints) using Dart and Flutter MCP tools. - flutter-setup-declarative-routing: Configure MaterialApp.router using a package like go_router for advanced URL-based navigation. >You can also install the Dart skills for Dart tasks. Repository flutter/agent-plugins Repository stats |Metric|Value| |:-|:-| |Stars|3,123| |Forks|190| |Last release|dart_skills_lint-v0.4.0 (2026-06-18)| |Languages|Dart 98.2%, Python 1.8%| |Contributors|21| |AI contributor accounts (Claude / Codex / Cursor / Copilot)|None| |AI co-authored commits|None| Main contributor |Field|Value| |:-|:-| |Name|Reid Baker| |GitHub|@reidbaker| |Commits to this repo|183 (top contributor)| |Company / Location|Google / Atlanta GA| |Followers|91| ────────────────────────────── 📌 How many great GitHub repos have you discovered and completely forgotten about? Don't let another useful tool disappear into your Reddit feed. Get the community's best repos, organized by category, plus key takeaways from real developer discussions, delivered to your inbox twice a week. 📬 Never lose track of great repos again. Subscribe free.

    2 points
  4. It’s graduation day for the Firebase MCP Server: Web Architecture & Implementation Guide Original: codingcat.dev If you have ever watched an AI coding agent hallucinate deprecated Firebase v8 syntax or deploy wide-open Firestore rules, you know that raw LLM prompts are not enough for production backends. Following the announcement of It’s graduation day for the Firebase MCP Server by Kara Yu on the Official Firebase Blog (2025-10-08), here is our hands-on CodingCat.dev breakdown of what actually shipped, how it works under the hood, and how we wire it into production TypeScript, Next.js, Astro, SvelteKit, React, and Angular applications. Update: Gemini CLI has transitioned to Antigravity CLI. The Mental Model: Why It’s graduation day for the Firebase MCP Server Matters for Web Devs As a Google Developer Expert (GDE) for Firebase and Senior Solutions Engineer at CodingCat.dev, I evaluate every Firebase update through one lens: does it reduce boilerplate while keeping our runtime security and cloud bills predictable? Instead of relying on stale training weights, wiring up the Model Context Protocol (MCP) and structured Firebase agent skills gives your IDE or CLI direct, schema-aware tools to provision resources, inspect security rules, and run deterministic CLI deployments. > AJ Pro-Tip from Alex: Never let an autonomous coding agent push Firestore or Data Connect rules straight to production without a gate. Always require the agent to run your test suite against the Firebase Local Emulator Suite first, and audit every generated rule for strict request.auth.uid ownership checks. How It Works: Technical Breakdown & Implementation If you previously installed the Firebase extension for Gemini CLI, you can migrate it to Antigravity CLI using the instructions in Migrate the Firebase extension from Gemini CLI to Antigravity CLI. For more details about the sunset, see the transition announcement. At Google I/O in May, we launched an experimental Firebase Model Context Protocol (MCP) server to help AI agents understand and manage your Firebase project, resources, and data all through natural language prompts that you provide, for example, “show items in the cart collection” even “delete a document from my Firestore collection”. The Model Context Protocol (MCP) extends the capabilities of AI tools used by millions of developers every day. By connecting to MCP servers, coding agents can directly access data and take action with integrated services. The Firebase CLI includes an MCP server to connect to your favorite IDEs and AI tools. The Firebase team made improvements to the Firebase MCP server since launch, including: - New MCP tools leveraging Gemini in Firebase (like dataconnect_generate_schema to generate a schema for Firebase Data Connect). - New MCP tools covering many more Firebase products (like Cloud Functions for Firebase and Crashlytics) - More consolidated, streamlined MCP tools (like a single validate_security_rules tool that can be used across Firestore, Storage, and Realtime Database). - New deploy MCP prompt to deploy your web app to a Firebase hosting product. Thanks to your feedback, The Firebase team improved the quality and stability of the Firebase MCP server, and In this update, the Firebase team announced that the Firebase MCP server is graduating from its experimental phase. Introducing the new Firebase extension for Gemini CLI Firebase is also integrating with the Gemini CLI, Google’s open-source AI agent that brings the power of Gemini models directly into your terminal. Starting today, Firebase has an official extension in the Gemini CLI gallery, alongside many other new helpful extensions from Google (like the Flutter extension). This new extension bundles our graduated MCP server together with additional context files (custom instructions for LLMs), giving you a powerful, integrated way to work with your Firebase project, resources, and data. To get started, just type the following into your command line: the Firebase team's goal is to reduce friction in your common development tasks, whether it’s adding database support, deploying your app, or even adding genAI features to your app! - /firebase:init This command provides you options like setting up backend services for your app and implementing genAI features that access the Gemini API. **With a single command, you can set up Firebase for your app. This command helps set up Firestore as your database and Firebase Authentication as a way to secure your app and your user’s data. It also helps you to deploy your app in one go! If you’re building an AI feature in your app, this command can give you a head start. It sets up Firebase AI Logic and writes the code to easily and securely access the Gemini API directly from your mobile and web apps. Learn more here. - /firebase:deploy This command **deploys your application to a Firebase hosting service regardless of whether it’s a static app or a full-stack app. The prompt instructs the AI to analyze your code and choose the correct Firebase hosting service based on the app’s needs. No more decision-making or misconfigured services. - /firebase:consult This command accesses detailed up-to-date documentation for the Firebase platform. Just ask a question and the model will find an answer grounded in the latest Firebase documentation. All these commands work with existing Firebase projects. But if you’re new to Firebase, don’t worry! All these commands can also help you set up Firebase in your app from scratch! Why use Gemini CLI to build your Firebase app? You spend a large portion of your time in the terminal. This new Firebase extension for Gemini CLI is a step towards giving your Gemini CLI environment a practical, working understanding of Firebase. The exciting part is that we’ve really improved how well your coding agent handles complex, long-running tasks. Things like setting up a new database or deploying an app have always been tricky because they take time and often need your approval at certain steps. The new commands in the Gemini CLI extension are designed to manage that entire process, from creating resources to managing them, and even cleaning them up if needed. the Firebase team's goal is to make this a helpful, low-friction way to bring AI-assisted development into your daily flow. It’s not going to solve every problem, but we think it can make these specific setup and deployment tasks a lot smoother. Show Me the Code: TypeScript & Web SDK Setup Here is the production baseline pattern we use at CodingCat.dev when wiring It’s graduation day for the Firebase MCP Server into a modern TypeScript web application: import { initializeApp } from "firebase/app"; import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai"; import { initializeAppCheck, ReCaptchaEnterpriseProvider } from "firebase/app-check"; const app = initializeApp({ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, }); // Protect AI endpoints in web apps with Firebase App Check if (typeof window !== "undefined") { initializeAppCheck(app, { provider: new ReCaptchaEnterpriseProvider(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!), isTokenAutoRefreshEnabled: true, }); } const ai = getAI(app, { backend: new GoogleAIBackend() }); export const geminiModel = getGenerativeModel(ai, { model: "gemini-2.5-flash" }); Production Web Architecture & Security Checklist Whether we are deploying on Firebase App Hosting, Cloudflare Workers, or Vercel, shipping AI features to production requires defense-in-depth across three layers: - Enforce Firebase App Check & Attestation: **Protect your web endpoints, Cloud Functions, Data Connect GraphQL resolvers, and Firebase AI Logic calls against unauthorized replay or billing abuse using Firebase App Check with reCAPTCHA Enterprise**. - **Validate Locally with the Firebase Emulator Suite: **Run deterministic integration tests against local Firestore, Auth, Functions, and Data Connect PostgreSQL emulators before promoting schema or rule changes to production. - **Keep Server & Client Boundaries Type-Safe: **Use the modular Firebase Web SDK and generated TypeScript bindings so SSR frameworks like Next.js, Astro, SvelteKit, and Angular serialize state cleanly without hydration flickers. Conclusion Accelerate your full-stack web and AI development by pairing It’s graduation day for the Firebase MCP Server with strict Security Rules, Firebase App Check, and local emulator testing. Come tell us on X (@CodingCatDev) and LinkedIn (CodingCat.dev) how you are using Firebase in your stack, and subscribe to the CodingCat.dev YouTube Channel so you don't miss what we build next! Watch Our Related Firebase Video Tutorial on YouTube Want to see how we architect and debug Firebase web applications live? Watch our related CodingCat.dev session Firebase SQL with Data Connect below, and make sure to Subscribe to CodingCat.dev on YouTube for weekly full-stack web and AI deep dives: {% youtube N25khadfUag %} Resources, Official Docs & Related CodingCat.dev Guides - Primary Release Source: **It’s graduation day for the Firebase MCP Server on the Official Firebase Blog (published 2025-10-08 by Kara Yu) - **Official Firebase Documentation: **Firebase Developer Docs & Web SDK Reference - **Related on CodingCat.dev: Add AI features to your app using Gemini CLI and Firebase AI Logic - Related on CodingCat.dev: Smart photo organization through privacy-first, on-device AI - Related on CodingCat.dev: Firebase SQL with Data Connect - Related on CodingCat.dev: Build a Custom No-Code App in 90 Minutes Frequently Asked Questions (FAQ) What are the key capabilities introduced in It’s graduation day for the Firebase MCP Server? It’s graduation day for the Firebase MCP Server (announced 2025-10-08 by the Firebase team) enhances the platform for Firebase, Google Cloud, Web Dev, AI, MCP workloads. Update: Gemini CLI has transitioned to Antigravity CLI. Developers can review the primary announcement on the Firebase Blog alongside our implementation patterns and production checklist above. How do web developers use It’s graduation day for the Firebase MCP Server with Next.js, SvelteKit, React, or Angular? Web developers can integrate these features using the modular Firebase JavaScript/TypeScript SDK, Firebase Data Connect web bindings, and Firebase App Hosting. Always pair client-side SDK initialization with Firebase App Check and strict Security Rules, and test locally with the Firebase Local Emulator Suite before deploying. Where can I watch video tutorials on Firebase Web and AI development? You can watch our hands-on Firebase video walkthroughs on the CodingCat.dev YouTube Channel, including Firebase SQL with Data Connect, and follow us on X (@CodingCatDev) for weekly full-stack tutorials.

  5. Flutterはむしろ継承だらけだし、Widgetの元ネタはReactのVirtualDOMだけどな〜 > Sky widgets are built using a functional-reactive framework, which takes inspiration from React. github.com/flutter/flutter/blob/1b440cc68baaa167d…

    2 likes · 1 repost · 277 views
  6. Flutterで箱入り娘を作成する2 概要 Flutterで箱入り娘を作成し、iOS と Android のアプリとして販売する このシリーズでは、作り始めから販売までを順番に書いていく - 第1回:Flutterで箱入り娘を作成する1 - 第3回:Flutterで箱入り娘を作成する3 第2回では、次のことをする 1手の数え方を調べる 最短手数を探すプログラムを作り、定番の配置が81手になるか確かめる 駒をなぞる間に、角を曲がれるようにする 実行環境 Flutter $ flutter --version Flutter 3.47.8 • channel stable • https://github.com/flutter/flutter.git Framework • revision 737ba8f885 (8 days ago) • 2026-10-22 21:37:31 -0700 Engine • hash dfd2e6cb7d319db05e3bf1503f3e2ac60f6288a2 (revision cf636b1dfb) (7 days ago) • 2026-10-23 02:13:38.000Z Tools • Dart 3.13.7 • DevTools 2.60.0 手順 ファイルの構成 lib/ model/ … ルール(前回から) solver/ solver.dart … 最短手数を探す screen/ widget/ tool/ solve_classic.dart … 定番の配置を解いて、手数を表示する test/ solver/solver_test.dart ソルバーは前回の model(Board と Piece)をそのまま使う。 model は Flutter を import していないので、ソルバーも画面なしで動かせる。 以下のコードは主要な部分の抜粋(... は省略)。 1手の数え方 箱入り娘の手数は、数え方が何通りかある。 | 数え方 | 1手 | |---|---| | 1マスずつ | 1マス動かすごとに1手 | | まっすぐ | 1つの駒をまっすぐ何マス動かしても1手 | | 曲がってもいい | 1つの駒を、ほかの駒を動かさずに行ける所なら、曲がっても1手 | よく知られている「定番の配置は81手」は、3つめの数え方。 指で駒に触って、動かして、離すまでが1手になる。 - en.wikipedia.org 3通りとも数えられるように、数え方を enum にする。 /// 何を1手と数えるか。 enum MoveRule { /// 1マス動かすごとに1手。 step, /// 1つの駒をまっすぐ何マス動かしても1手。 straight, /// 1つの駒を、ほかの駒を動かさずに行ける所なら、曲がっても1手。 piece, } 最短手数を探す 幅優先探索 最初の盤面から、1手で行ける盤面、2手で行ける盤面…と順に調べる。 近いものから順に調べるので、クリアした盤面が最初に見つかったときの手数が最短になる。 - 調べる順番は Queue(先に入れたものから取り出す) - 見つけた盤面は Map に入れる。値は1手前の盤面 - 同じ盤面を2回調べない - クリアしたら、1手前を順にたどれば手順になる /// 幅優先探索で、[start] から娘を出口に出すまでの最短の手順を探す。 /// /// [path] は最初の盤面からクリアした盤面までの並び(手数は `path.length - 1`)。 /// 解けなければ null。[visited] は調べた盤面の数。 ({List<Board>? path, int visited}) solve( Board start, { MoveRule rule = MoveRule.piece, }) { // 見つけた盤面と、その1手前の盤面。見つけたかどうかの記録も兼ねる final previous = <Board, Board?>{start: null}; final queue = Queue<Board>()..add(start); while (queue.isNotEmpty) { final board = queue.removeFirst(); if (board.isSolved) { return (path: _path(previous, board), visited: previous.length); } for (final next in nextBoards(board, rule)) { if (previous.containsKey(next)) continue; previous[next] = board; queue.add(next); } } return (path: null, visited: previous.length); } 戻り値はレコード ({List<Board>? path, int visited})。 手順と、調べた盤面の数を、クラスを作らずに名前付きで返せる。 /// [goal] から1手前をたどって、最初の盤面からの並びにする。 List<Board> _path(Map<Board, Board?> previous, Board goal) { final path = <Board>[]; Board? board = goal; while (board != null) { path.add(board); board = previous[board]; } return path.reversed.toList(); } 前回「調べた盤面を Set に入れて覚える」と書いたが、1手前も覚えるために Map にした。 1手で行ける盤面 数え方ごとに、駒の行き先を switch 式で切りかえる。 sync* と yield で、見つけた盤面を1つずつ返す(リストを作らずに済む)。 /// [board] から1手で行ける盤面。 Iterable<Board> nextBoards(Board board, MoveRule rule) sync* { for (var i = 0; i < board.pieces.length; i++) { final destinations = switch (rule) { MoveRule.step => _steps(board, i), MoveRule.straight => _straights(board, i), MoveRule.piece => _reachables(board, i), }; for (final piece in destinations) { yield board.place(i, piece); } } } 「曲がってもいい」は、1つの駒だけを1マスずつ動かして行ける所を、同じく幅優先で集める。 /// [index] 番目の駒だけを何マスか動かして行ける位置(曲がってもいい)。今の位置は含まない。 Iterable<Piece> _reachables(Board board, int index) { final start = board.pieces[index]; final found = <Piece>{start}; final queue = Queue<Piece>()..add(start); while (queue.isNotEmpty) { final piece = queue.removeFirst(); for (final d in Direction.values) { final moved = piece.moved(d); if (board.canPlace(index, moved) && found.add(moved)) queue.add(moved); } } return found.skip(1); } Set.add は、新しく入ったときだけ true を返す。調べたかどうかの確認と追加が1回で済む。 canPlace と place は Board に足した。 /// [index] 番目の駒を [piece] の位置に置けるか(ほかの駒はそのまま)。 bool canPlace(int index, Piece piece) { if (!_isInside(piece)) return false; for (var i = 0; i < pieces.length; i++) { if (i != index && pieces[i].overlaps(piece)) return false; } return true; } /// [index] 番目の駒を [piece] に置き換えた盤面。 Board place(int index, Piece piece) { final next = [...pieces]; next[index] = piece; return Board(next); } 盤面を Map のキーにする Map のキーにするので、Board にも == と hashCode を書く。 ここで「同じ盤面」をどう決めるかで、調べる量が大きく変わる。 定番の配置には、縦長の駒が4つ、小さい駒が4つある。 同じ種類の駒どうしを入れ替えても、遊ぶ人から見れば同じ盤面になる。 V D D V V D D V V D D V V D D V V H H V と V H H V は同じ盤面 V S S V V S S V S . . S S . . S (左の V が駒A、右の V が駒B)(左右の V を入れ替えた) そこで、駒の並び順も、同じ種類の駒の入れ替えも区別しないことにする。 /// 同じ種類の駒が同じマスにあれば、同じ盤面とみなす。 /// 駒の並び順や、同じ種類の駒どうしが入れ替わっているかは区別しない。 @override bool operator ==(Object other) => other is Board && other.pieces.length == pieces.length && _pieceSet.containsAll(other.pieces); @override late final int hashCode = Object.hashAllUnordered(pieces); late final Set<Piece> _pieceSet = pieces.toSet(); - Piece の == は「種類と位置」で比べる(前回)。なので、同じ種類で同じ位置の駒は、どの駒でも同じになる - Object.hashAllUnordered は、並び順を変えても同じ値を返す - Board は変更できないので、hashCode と Set は late final で1回だけ作る(Map の中で何度も使われる) 区別する場合としない場合で、定番の配置を解いて比べた(時間は手元の PC で測ったもの)。 | 同じ種類の駒の入れ替え | 手数 | 調べた盤面 | 時間 | |---|---|---|---| | 区別する | 81手 | 10,534,300 | 約123秒 | | 区別しない | 81手 | 23,960 | 約0.3秒 | 手数は同じで、調べる盤面は約440分の1になる。 == を変えたときに気を付けること == で「並び順の違う盤面」も同じになったので、「盤面が替わったか」を == で調べると、並び順だけ違う盤面を見落とす。 画面の BoardView は、なぞっている途中で盤面が替わったら(はじめからボタンなど)、掴んでいた駒を離す。 ここは「別のオブジェクトになったか」を知りたいので、identical で比べる。 @override void didUpdateWidget(BoardView oldWidget) { super.didUpdateWidget(oldWidget); // なぞっている途中で盤面が替わったら(はじめからなど)、掴んでいた駒は離す // Board の == は駒の並び順を見ないので、別の盤面かどうかは identical で比べる if (!identical(oldWidget.board, widget.board) || !widget.enabled) { _release(); } } 定番の配置を解く 3通りの数え方で解いて表示する。 void main() { for (final rule in MoveRule.values) { final watch = Stopwatch()..start(); final result = solve(classicLayout, rule: rule); watch.stop(); final moves = result.path == null ? '解けない' : '${result.path!.length - 1} 手'; print( '${rule.name}: $moves' '(調べた盤面 ${result.visited}、${watch.elapsedMilliseconds} ms)', ); } } $ dart run tool/solve_classic.dart step: 116 手(調べた盤面 24098、287 ms) straight: 90 手(調べた盤面 23946、228 ms) piece: 81 手(調べた盤面 23960、276 ms) 「曲がってもいい」数え方で81手になった。 アプリでも、見つけた手順どおりに動かすと、81手でクリアになった。 駒をなぞる間に、角を曲がれるようにする 前回の画面は、まっすぐ動かしたときだけ1手だった。 81手と同じ数え方にするため、1回なぞる間に角を曲がれるようにする。 1. 指がとなりのマスの半分を越えたら、駒をそのマスへ進める(L字になぞれば角を曲がる) 2. 指を離すまでが1手(曲がっても、何マス動かしても1手) 3. 元のマスにいるときだけ、マスの4割なぞれば動く(前回と同じく、少しなぞるだけで動くように) /// なぞっている間の、掴んだ駒のマス。指について角を曲がると変わる。 Piece? _at; ... /// 指が [_at] からマスの [threshold] 倍より多くずれていて、その向きの隣が空いていれば、その向き。 Direction? _nextStep(double cell, double threshold) { for (final (direction, amount) in _split(_rest(cell))) { if (amount > cell * threshold && widget.board.canPlace(_grabbed!, _at!.moved(direction))) { return direction; } } return null; } /// 指が隣のマスの半分を越えたら、駒をそのマスへ進める。L 字になぞれば角を曲がる。 void _followFinger(double cell) { for (var d = _nextStep(cell, 0.5); d != null; d = _nextStep(cell, 0.5)) { _at = _at!.moved(d); } } _split は、指のずれを「横の向きと量」「縦の向きと量」に分けて、量の大きい順に並べる。 大きいほうの向きがふさがっていれば、もう一方の向きを試す。 離したときの行き先は _target で決める。 /// 今指を離したら、駒が収まるマス。元のマスに戻るなら null(動かさない)。 /// まだ元のマスにいるときだけ、マスの 4 割より多くなぞれば隣へ動かす(少しなぞるだけで動くように)。 /// 進んだあとは [_at] のまま(4 割で戻すと、来た向きへ引き返してしまうため)。 Piece? _target(double cell) { if (_grabbed == null) return null; final start = widget.board.pieces[_grabbed!]; if (_at != start) return _at; final d = _nextStep(cell, 0.4); return d == null ? null : start.moved(d); } 進む判定を「半分」にしているのは、行ったり来たりしないため。 4割で進むようにすると、1つ進んだ直後に「戻る向きに6割ずれている」ことになり、すぐ戻ってしまう。 画面に知らせる内容は「何番目の駒を、どの位置へ」にした。 GameScreen は place で置いて、1手増やすだけになる。 /// 指を離したときに呼ばれる。触ってから離すまでを、曲がっても 1 手と数える。 void _move(int index, Piece to) { if (_board.isSolved) return; setState(() { _board = _board.place(index, to); _moves++; }); } テスト test('定番の配置は、同じ駒なら曲がっても1手と数えて 81 手', () { expect(moves(classicLayout, MoveRule.piece), 81); }); test('まっすぐなら1手と数えると 90 手、1マスずつなら 116 手', () { expect(moves(classicLayout, MoveRule.straight), 90); ... testWidgets('L 字になぞると、駒は角を曲がって 1 回で動く', (tester) async { ... final gesture = await tester.startGesture(center(tester, 1, 4)); await gesture.moveBy(const Offset(cell, 0)); // 右へ await tester.pump(); await gesture.moveBy(const Offset(0, -cell)); // 上へ await tester.pump(); ... await gesture.up(); expect(moves, [(5, const Piece(PieceType.small, 2, 3))]); }); flutter test アプリの実行 アプリを起動 flutter run 実行結果 !klotski2.gif 次回 いろいろな配置(ステージ)を作って、難しさの順に並べる。

  7. Reddit · · r/flutterhelp · u/PriorError2799 · flutter/flutter

    white screen during Android debugging on a physical device I am trying to run Android on a physical mobile device because my computer cannot launch the emulator. However, when I use the "Run" button in Android Studio, the screen just turns white, and the `main` function never actually executes. Conversely, when I use the command `flutter run --no-track-widget-creation` in the terminal, the app opens normally, but the `print` statements do not execute. Flutter 3.47.8 • channel stable • flutter/flutter Framework • revision 737ba8f885 (7 days ago) • 2026-10-22 21:37:31 -0700 Engine • hash dfd2e6cb7d319db05e3bf1503f3e2ac60f6288a2 (revision cf636b1dfb) (7 days ago) • 2026-10-23 02:13:38.000Z Tools • Dart 3.13.7 • DevTools 2.60.0 Log: Launching lib\main.dart on moto e22 in debug mode... Running Gradle task 'assembleDebug'... √ Built build\app\outputs\flutter-apk\app-debug.apk Installing build\app\outputs\flutter-apk\app-debug.apk...

    3 points · 14 comments
  8. Fix Google Sign-In [28444] in Flutter with google_sign_in 7 --- title: Fix Google Sign-In [28444] in Flutter with google_sign_in 7 published: true date: 2026-10-29 00:00:00 UTC tags: flutter,dart,android,firebase canonical_url: saymyframe.com --- Since version 7, google_sign_in signs users in on Android through Credential Manager, and a broken setup shows up as one of a few messages. The best known is [28444] Developer console is not set up correctly, and the checklists for it usually start with the SHA-1. I reproduced each case on an emulator, and with current Google Play services a missing SHA-1 doesn't give the app this error at all. The app gets a canceled with Account reauth failed instead. 28444 showed up when the app sent the wrong client ID. Everything below was checked on an Android 17 emulator with Google Play services 26.41, google_sign_in 7.2.0 and google_sign_in_android 7.2.18, against a Firebase project. Before version 7, the plugin reported a broken setup as ApiException: 10, the DEVELOPER_ERROR of Google Play services. What each Google Sign-In error means GoogleSignIn.instance.authenticate() throws a GoogleSignInException. The code and the description tell the cases apart: | Description | Code | What is wrong | | --- | --- | --- | | [28444] Developer console is not set up correctly. | unknownError | Google doesn't take the serverClientId as the web client of the app: it is the ID of the Android client, or a client that doesn't exist | | serverClientId must be provided on Android | clientConfigurationError | The app has no web client ID at all: google-services.json has none, or the release build dropped it | | [16] Account reauth failed. | canceled | No Android OAuth client with the package name of the app and the SHA-1 of the key that signed it, or no connection to Google | | [16] Cancelled by user. | canceled | The user closed the sheet, or backed out of adding a Google account | The two cases of canceled share a code. The README of the plugin warns about this: it can't tell some setup errors from a user who cancelled. attemptLightweightAuthentication() hides a missing SHA-1 completely and just returns null. logcat prints GetCredentialResponse error returned from framework for every failure, a plain cancel included, so that line doesn't point to the cause. And on a device or an emulator without a Google account, the sign-in never gets far enough to fail: Credential Manager opens the screen that adds an account. Add one before you test the setup. See the real cause in logcat Google Play services logs what its server answered. Filter logcat to its two tags while you try to sign in: adb logcat -s Auth:W Auth.Api.Credentials:W For a missing SHA-1, the first lines are: W Auth : [GetTokenResponseHandler] Server returned error: This android application is not registered to use OAuth2.0, please confirm the package name and SHA-1 certificate fingerprint match what you registered in Google Developer Console. W Auth.Api.Credentials: cwsp: [8] Unknown error [status=UNREGISTERED_ON_API_CONSOLE]. W Auth.Api.Credentials: cwsp: [16] Account reauth failed. For a wrong client ID, the same tags show 28444: Developer console is not set up correctly. [28444] Developer console is not set up correctly: the wrong client ID To give the app an ID token, Google needs the client ID of a web application OAuth client, which google_sign_in sends as serverClientId. When Google doesn't accept that ID, the app gets: GoogleSignInException(code GoogleSignInExceptionCode.unknownError, [28444] Developer console is not set up correctly., null) I got it in two ways. The first was passing the ID of the Android client to initialize(): a Firebase project has both kinds, and they look alike, <number>-<letters>.apps.googleusercontent.com. The second was an ID that doesn't exist, as with a typo or a client deleted in the Google Cloud console. In google-services.json, the web client is the one with "client_type": 3, and the Android clients have "client_type": 1. To print the web client ID: grep -B1 '"client_type": 3' android/app/google-services.json | grep -m1 client_id If you pass the ID yourself, pass that one: await GoogleSignIn.instance.initialize( serverClientId: '1234567890-abc123.apps.googleusercontent.com', ); With a google-services.json that has a web client, you don't need to pass anything: the plugin reads the ID from the resources that the Google services Gradle plugin generates from that file. The checklists aren't wrong to mention the SHA-1, though. Inside Google Play services, a missing SHA-1 is 28444 too: logcat shows cwsp: [28444] when attemptLightweightAuthentication() runs into one, even though the app only gets null. I can't rule out that other versions of Play services pass it on to the app. If the client ID is right, check the SHA-1 next. Account reauth failed: the SHA-1 of the signing key is missing Google recognizes an Android app by its package name and the SHA-1 of the certificate that signed it. With Firebase, adding a SHA fingerprint to the Android app creates the Android OAuth client that Google looks for. When there is none, the user picks an account and the app gets: GoogleSignInException(code GoogleSignInExceptionCode.canceled, [16] Account reauth failed., null) The same message shows up in two other cases. When the device can't reach Google (on the emulator I turned off Wi-Fi and mobile data), logcat shows [7] Network error. right before it. And in flutter/flutter#190296, an app with a correct setup got it on every sign-in after the first one. The reporter fixed it by calling GoogleSignIn.instance.signOut() on sign-out. I tried that on the same emulator: eight sign-ins in a row without signOut(), some right after the app's own sign-out, and all of them worked. So it may depend on something that report had and my setup didn't, such as a physical device, an older plugin or an app installed from Google Play. logcat tells the cases apart: a missing SHA-1 shows not registered to use OAuth2.0, and no network shows Network error. Find the SHA-1 of the build that fails Gradle prints the key that signs each build type, and it is not always the one you expect: cd android && ./gradlew signingReport Variant: release Config: debug Store: /Users/you/.android/debug.keystore Alias: AndroidDebugKey SHA1: 60:45:3E:2E:7F:82:01:EC:F4:84:F8:43:A2:44:70:F9:4D:F9:7B:F5 A new Flutter app signs its release build with the debug key too (signingConfig = signingConfigs.getByName("debug") in android/app/build.gradle.kts), until you give it a key of its own. From then on, the release needs that key's SHA-1. For an APK you already have, read the certificate from the file with apksigner from the Android SDK build tools: $ANDROID_HOME/build-tools/<version>/apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apk keytool -printcert -jarfile app.apk from Google's client authentication guide prints Not a signed jar file for a Flutter APK. Flutter sets minSdk to 24, and from that level on, the APK has only a v2 signature, which keytool doesn't read. Some keys are easy to forget. Every machine has its own ~/.android/debug.keystore, so a teammate's debug build has a different SHA-1, and so does a CI runner that creates a fresh key. Google Play adds two more: your upload key, and the app signing key. With Play App Signing, users install a build that Google signed with its own key, and its SHA-1 is in the Play Console, under Test and release > App integrity > App signing. Add the SHA-1 to Firebase In the Firebase console, open Project settings, pick the Android app under Your apps and click Add fingerprint. Add the SHA-256 too. The Firebase CLI does the same, with the app ID from firebase apps:list ANDROID: firebase apps:android:sha:create <appId> <sha1> You don't need to download google-services.json again for this: the app never reads the Android clients from it. In my test, the same build signed in about 40 seconds after I added the SHA-1, with the old file. Every SHA you add creates another Android OAuth client in the project, and Google caps the number of them. Removing a SHA from Firebase doesn't remove its client. Once the project is full, apps:android:sha:create fails with Failed to create SHA certificate hash, and only --debug shows the answer of the API: 429 Number of OAuth clients created exceeded limit. I ran into that because the CI of SMF registered the fresh debug key of every runner. Give CI one debug keystore that doesn't change, and register its SHA-1 once. serverClientId must be provided on Android: no web client in google-services.json When you don't pass serverClientId, google_sign_in_android reads the string resource default_web_client_id. The Google services Gradle plugin generates it from the web client in android/app/google-services.json. Without it, authenticate() fails at once, before any Google screen: GoogleSignInException(code GoogleSignInExceptionCode.clientConfigurationError, serverClientId must be provided on Android, null) The usual cause is a google-services.json downloaded before Google sign-in was enabled in Firebase Authentication. That file has "oauth_client": [], and the build gets no default_web_client_id. Check your file: grep -c '"client_type": 3' android/app/google-services.json 0 means no web client. Enable Google in the Firebase console, under Authentication > Sign-in method, then download the file again, from Project settings or with the Firebase CLI: firebase apps:sdkconfig ANDROID <appId> --out android/app/google-services.json Also check that android/app/build.gradle.kts applies the com.google.gms.google-services plugin, which FlutterFire CLI adds when it configures the app. Without it, nothing generates the resource. Works in debug, fails in release When Google Sign-In works in debug builds and fails in the release, the message tells you which of two things happened. If the release fails with [16] Account reauth failed., Firebase doesn't know the key of the release: your release key, or, for a build from Google Play, the app signing key of Google Play. Add its SHA-1 as above. If it fails with serverClientId must be provided on Android, the release build lost default_web_client_id. Flutter shrinks the resources of a release build by default, and the plugin looks the string up by its name at run time, which the shrinker doesn't see. Up to google_sign_in_android 7.2.15, the shrinker removed it from every app (flutter/flutter#188363). Version 7.2.16 added a keep rule in res/raw/keep.xml of the plugin (flutter/packages#12075). That rule has a catch (flutter/flutter#194943, open as I write this, with two pull requests that rename the plugin's file under review). If your app has its own android/app/src/main/res/raw/keep.xml, for example to keep a notification icon, Gradle merges the resources by file name, and the app's file replaces the plugin's. With 7.2.18, the release build then fails with serverClientId must be provided on Android. The shrinker says why: grep default_web_client_id build/app/outputs/mapping/release/resources.txt string:default_web_client_id:2131689529 is not reachable. Rename your file, for example to res/raw/app_keep.xml. The Android guide to keeping resources asks for unique names for exactly this reason, and the shrinker reads every keep file in res/raw. After the rename, the same line says reachable from keep xml file, and sign-in works. You can also add @string/default_web_client_id to the tools:keep of your own file. Either way, check that the app has 7.2.16 or later: flutter pub deps | grep google_sign_in_android A CI that builds only debug never sees either case. The same thing happened with the shared_preferences release build crash, where R8 broke a plugin only in release builds, so start a release build on an emulator before your users do. How SMF sets up Google Sign-In with Firebase I went through all of this while building the google_sign_in module of SMF, a free, open-source Flutter project generator. In an app with the firebase_auth module, SMF generates a script, tool/enable_firebase_sign_in.dart, and runs it after flutterfire configure once you agree. The script enables Google in the Firebase project, registers the SHA-1 and SHA-256 of the debug key of your machine, downloads google-services.json again with the web client in it, and writes the URL scheme of the iOS app. So a debug build signs in with Google from the first run, and a teammate runs the script once on their own machine. The keys of a release stay a step you do by hand, with the commands in the README of the app.

  9. Flutter やめました。電話アプリを Native にした理由 はじめに こんにちは。株式会社IVRyで Engineering Managerをしています。tsutou です。 アイブリーのモバイルアプリのユースケースは、電話の転送を受けたり、着信を折り返して発信したりすることがメインです。 体験としては、アイブリーの対話型音声AIで、一次対応を自動化しながら、忙しい最中でも人が出ないといけない電話に応答する体験が、そのままエンドユーザーの体験になります。 note.com そんな中で、このアプリで進めていた Flutter Exit というプロジェクトが今月に完了しました。 Flutterで構築していたアプリを、iOSは Swift / SwiftUI 、Android は Kotlin / Jetpack Compose によるフル Native 構成へ移行し、2026年10月に両OSとも全ユーザーへの展開を終えています。 この記事では、なぜ Flutter をやめることにしたのか について経緯や意思決定を含めて書きたいと思っています。 apps.apple.com play.google.com Flutter 導入時の判断 Flutter導入当時は採用戦略の一部と、「クロスプラットフォームで UI を共有し、少人数で iOS/Android を同時に開発する」という合理的な判断でした。 導入当時は LLM も今ほど隆盛を極めておらず、限られたエンジニアリングリソースで出来るだけ早く iOS と Android の両方にアプリを提供するという課題がありました。 当時の採用戦略も踏まえれば、 Dart で書いたコードを両OSで共有でき、機能や仕様を揃えやすく、 Hot Reload でUIの変更をすぐ確かめられる Flutter は、当時はとても合理的な選択肢でした。 また、OS 標準の通話機能や利用する 通話SDK との連携には、Native側の実装が必要だったため、内部のSDK として通話部分は切り離され、Native な内製 SDK で作られておりました。外部公開も見据えてこの設計になっていたのは当時の判断としてとても助かりました。 現在の前提変化 今回の Flutter Exit には、大きく2つの目的がありました。 ひとつは、アイブリーの中核機能である音声通話の品質・安定性を高める こと。もうひとつは、アーキテクチャをシンプルにする ことで、今後の開発生産性と保守性を高めることです。 Flutterを導入した当時は、少人数で2つのプラットフォームを開発できることに大きな合理性がありました。しかし、プロダクトの成長に伴ってOS固有の機能を扱う重要性が増し、Flutter と Native の境界を維持することによる複雑性も無視できなくなっていました。 一方で、AIコーディングツールの進化によって、Swift / Kotlin それぞれで実装・保守することのコストも変わり始めています。 移行を決意した背景 こうした変化を踏まえ、私たちは Flutter Exit を決断しました。その理由を3つの観点から紹介します。 通話とOSの距離 Flutter 時代と Native 時代のアーキテクチャ比較 中核機能である音声通話処理のほぼ全ては Kotlin/Swift で実装されており、Flutter は UI (WebView) のラッパーとして機能していました。 そんな中で、アイブリーアプリはほぼ WebView で構成されているため、Flutterで UI Components を共通化するメリットも十分に享受できておらず、Flutterの各種ライブラリ、特に WebViewを中心に音声通話にも影響を及ぼす事象がいくつか発生してしまっていました。 voice-quickstart-android#641 flutter#175099 Native と Flutter のオーバーヘッド Flutter 時代と Native 時代の着信イベントフロー比較 Flutter と Native の間には、Platform Channels による呼び出しやコールバックが合計 50弱 ありました。着信イベントはOS・Native・Flutterの間を行き来し、問題が起きると、どの層に原因があるのかを切り分ける必要がありました。 特にAndroidでは、アプリが終了した状態で着信を受ける際に Headless な Flutter Engine を1つのコードで通常の Flutter Engine とは別で起動・管理する構成があり、OSのライフサイクルや相互の Flutter Engine の状態と合わせた細かな制御が求められました。 zenn.dev 移行前のPoCでは、Android cold-start 時の「アプリ起動〜初期画面表示までの時間」の中央値が 192.5ms から 49.6ms (約74%の短縮、約3.9倍の高速化) になりました。 アイブリーアプリは起動速度が、着信応答の速度にそのまま繋がるため、開発効率だけでなくユーザー体験にも改善をもたらすインサイトが見えました。その他にも着信通知受信 → 着信UI表示までの時間など、通話における色々なパスで改善のインサイトが見られました。 ただし、これはPoCでの比較であり、もちろん差のすべてを Platform Channels だけに帰するものではありません。それでも、通話体験に関わる経路をシンプルにする意義を確かめる材料になりました。 AI コーディングツールの進化 これまでは、SwiftとKotlinでそれぞれ実装する開発コストが大きな懸念でした。しかし、Claude Code や Devin など、AIコーディングツールの進化によって、定型的な UI や API 連携の実装コストを下げられるようになりました。 Anthropicも、既存コードを仕様として参照でき、テストによって移行結果を検証しやすい大規模なコードマイグレーションは、AIエージェントと特に相性のよい領域だと紹介しています。 claude.com 実際に PoC では、既存のネイティブ通話コードには手を加えず、 Dart -> Swift / Kotlin へのコード移行に限れば わずか2〜3日で、iOS / Android ともに、電話をかけ、受けて転送といったアイブリーアプリの基本的な体験が成立するところまで到達 しました。もちろん、これは移行全体が数日で終わったという意味ではありません。品質検証やリリースには別の時間が必要です。 また、Nativeに寄せることで、人間の開発者だけでなく、AIコーディングツールにとっても扱いやすいアーキテクチャになると考えました。 Swift / Kotlin は公式ドキュメントやWeb上の技術情報が豊富であり、OS標準の開発ツールを直接利用できます。加えて、Platform Channels や Flutter Engine をまたぐ複雑な状態・イベント処理が不要になり、追うべきコードや依存関係も減らせます。 AIによって実装コストが下がることに加えて、AIが理解・変更する対象そのものをシンプルにできる点も、今回の意思決定を後押ししました。 expo.dev もちろんいいことばかりではなく、 CodingはAIで圧縮できますが、リリース作業や運用だったり OS ごとのルール設計など純増してしまうコストは一定存在します。デザインなども今後は OS ごとに最適なデザインを考えていく必要があるでしょう。 最後に 今回のFlutter Exitは、Flutter という技術そのものを否定するものではありません。導入時には合理的だった選択も、プロダクトの中核機能に及ぼす作用について考え続け、技術環境や前提条件の変化が時代と共に変われば再考する必要があります。 近年では、Notion がWebView中心の構成から Native へ移行してモバイル体験の改善に取り組み、ShopifyもAIコーディングツールの進化を背景に React Native から Native への回帰を発表しています。 notion.com shopify.engineering 技術選定においては、その選択を支えていた前提が、今も成り立っているかを問い続けること が大切だと感じます。 今回の開発や検証をを具体的にどう進めていったかについては、また別のメンバーの記事でご紹介したいと思っております。 また、Android側の進め方は、DroidKaigi 2026のポスターセッション 「FlutterからAndroidネイティブへ: Agentic Codingを活用したフルリライトの戦略と実践」 でも発表しています。 We are hiring! IVRyでは「イベントや最新ニュース、募集ポジションの情報を受け取りたい」「会社について詳しく話を聞いてみたい」といった方に向けて、キャリア登録やカジュアル面談の機会をご用意しています。ご興味をお持ちいただけた方は、ぜひ以下のページよりご登録・お申し込みください。 herp.careers ivry-jp.notion.site

  10. iOSでJPEGをデコードするのに標準のImageIOを使わずにlibjpeg-turboとかいうサードパーティライブラリを使ったほうが速くなることがある?flutterは過去にImageIOに移行しようとしたがパフォーマンスに問題があったためやめた? flutter#177328

Trending awards, every completed day and week
Gold
41st place finishes
Silver
122nd place finishes
Bronze
93rd place finishes
Awarded repositories
1Owned by flutter now

Repositories

Ordered by gold, then silver, then bronze.

  1. flutterBest: 1st by stars, 2,483 stars4129

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.