Agent Skills by ALSEL
Anthropic Claudeその他⭐ リポ 0品質スコア 50/100

flutter

RiverpodによるState管理、Freezed、go_router、mocktailを活用したFlutterアプリ開発をサポートするスキルです。コード生成からテストまで、モダンなFlutter開発スタックを一貫してカバーします。

description の原文を見る

Flutter development with Riverpod state management, Freezed, go_router, and mocktail testing

SKILL.md 本文

Flutter Skill


プロジェクト構成

project/
├── lib/
│   ├── core/                           # コアユーティリティ
│   │   ├── constants/                  # アプリ定数
│   │   ├── extensions/                 # Dart拡張機能
│   │   ├── router/                     # go_router設定
│   │   │   └── app_router.dart
│   │   └── theme/                      # アプリテーマ
│   │       └── app_theme.dart
│   ├── data/                           # データレイヤー
│   │   ├── models/                     # Freezedデータモデル
│   │   ├── repositories/               # リポジトリ実装
│   │   └── services/                   # APIサービス
│   ├── domain/                         # ドメインレイヤー
│   │   ├── entities/                   # ビジネスエンティティ
│   │   └── repositories/               # リポジトリインターフェース
│   ├── presentation/                   # UIレイヤー
│   │   ├── common/                     # 共有ウィジェット
│   │   ├── features/                   # 機能モジュール
│   │   │   └── feature_name/
│   │   │       ├── providers/          # Riverpodプロバイダー
│   │   │       ├── widgets/            # 機能固有ウィジェット
│   │   │       └── feature_screen.dart
│   │   └── providers/                  # グローバルプロバイダー
│   ├── main.dart
│   └── app.dart
├── test/
│   ├── unit/                           # ユニットテスト
│   ├── widget/                         # ウィジェットテスト
│   └── integration/                    # 統合テスト
├── pubspec.yaml
├── analysis_options.yaml
└── CLAUDE.md

Riverpod状態管理

プロバイダータイプ

// シンプルな値プロバイダー
final appNameProvider = Provider<String>((ref) => 'My App');

// シンプルな可変状態用StateProvider
final counterProvider = StateProvider<int>((ref) => 0);

// 複雑な状態ロジック用NotifierProvider
final userProvider = NotifierProvider<UserNotifier, User?>(() => UserNotifier());

// 非同期操作用AsyncNotifierProvider
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
  () => UsersNotifier(),
);

// シンプルな非同期データ用FutureProvider
final configProvider = FutureProvider<Config>((ref) async {
  return await ref.watch(configServiceProvider).loadConfig();
});

// リアルタイムデータ用StreamProvider
final messagesProvider = StreamProvider<List<Message>>((ref) {
  return ref.watch(messageServiceProvider).watchMessages();
});

// パラメータ化されたデータ用Familyプロバイダー
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await ref.watch(userRepositoryProvider).getUser(userId);
});

Notifierパターン

@riverpod
class Users extends _$Users {
  @override
  Future<List<User>> build() async {
    return await _fetchUsers();
  }

  Future<List<User>> _fetchUsers() async {
    final repository = ref.read(userRepositoryProvider);
    return await repository.getUsers();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchUsers());
  }

  Future<void> addUser(User user) async {
    final repository = ref.read(userRepositoryProvider);
    await repository.addUser(user);
    ref.invalidateSelf();
  }
}

AsyncValue処理

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    return usersAsync.when(
      data: (users) => UsersList(users: users),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => ErrorDisplay(
        error: error,
        onRetry: () => ref.invalidate(usersProvider),
      ),
    );
  }
}

// パターンマッチング代替案
Widget build(BuildContext context, WidgetRef ref) {
  final usersAsync = ref.watch(usersProvider);

  return switch (usersAsync) {
    AsyncData(:final value) => UsersList(users: value),
    AsyncLoading() => const LoadingIndicator(),
    AsyncError(:final error) => ErrorDisplay(error: error),
  };
}

refメソッド

// watch - プロバイダーが変更されるとリビルド
final users = ref.watch(usersProvider);

// read - 1回限りの読み込み、リビルドなし
void onButtonPressed() {
  ref.read(counterProvider.notifier).state++;
}

// listen - リビルドなしで変更に反応
ref.listen(authProvider, (previous, next) {
  if (next == null) {
    context.go('/login');
  }
});

// invalidate - 強制的にリフレッシュ
ref.invalidate(usersProvider);

// keepAlive - 自動破棄を防ぐ
final link = ref.keepAlive();
// 後で: link.close() で破棄を許可

Freezedデータモデル

モデル定義

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
    @Default(false) bool isActive,
    DateTime? createdAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// 状態用ユニオン型
@freezed
sealed class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.authenticated(User user) = _Authenticated;
  const factory AuthState.unauthenticated() = _Unauthenticated;
  const factory AuthState.error(String message) = _Error;
}

Freezedユニオンの使用

Widget build(BuildContext context, WidgetRef ref) {
  final authState = ref.watch(authProvider);

  return authState.when(
    initial: () => const SplashScreen(),
    loading: () => const LoadingScreen(),
    authenticated: (user) => HomeScreen(user: user),
    unauthenticated: () => const LoginScreen(),
    error: (message) => ErrorScreen(message: message),
  );
}

go_routerナビゲーション

ルーター設定

final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authProvider);

  return GoRouter(
    initialLocation: '/',
    refreshListenable: authState,
    redirect: (context, state) {
      final isLoggedIn = authState.valueOrNull != null;
      final isLoggingIn = state.matchedLocation == '/login';

      if (!isLoggedIn && !isLoggingIn) return '/login';
      if (isLoggedIn && isLoggingIn) return '/';
      return null;
    },
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomeScreen(),
        routes: [
          GoRoute(
            path: 'user/:id',
            builder: (context, state) => UserScreen(
              userId: state.pathParameters['id']!,
            ),
          ),
        ],
      ),
      GoRoute(
        path: '/login',
        builder: (context, state) => const LoginScreen(),
      ),
    ],
    errorBuilder: (context, state) => ErrorScreen(error: state.error),
  );
});

ナビゲーション

// ルートへナビゲート
context.go('/user/123');

// スタックにプッシュ
context.push('/user/123');

// 現在のルートをポップ
context.pop();

// 現在のルートを置換
context.pushReplacement('/home');

// 名前付きルート
context.goNamed('user', pathParameters: {'id': '123'});

ウィジェットパターン

ConsumerWidget vs ConsumerStatefulWidget

// Riverpod対応ステートレス
class UserCard extends ConsumerWidget {
  const UserCard({super.key, required this.userId});

  final String userId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userByIdProvider(userId));
    return user.when(
      data: (user) => Card(child: Text(user.name)),
      loading: () => const CardSkeleton(),
      error: (e, _) => ErrorCard(error: e),
    );
  }
}

// Riverpod対応ステートフル
class SearchScreen extends ConsumerStatefulWidget {
  const SearchScreen({super.key});

  @override
  ConsumerState<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends ConsumerState<SearchScreen> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final results = ref.watch(searchProvider(_controller.text));
    return Column(
      children: [
        TextField(
          controller: _controller,
          onChanged: (_) => setState(() {}),
        ),
        Expanded(child: SearchResults(results: results)),
      ],
    );
  }
}

HookConsumerWidget (flutter_hooks使用)

class AnimatedCounter extends HookConsumerWidget {
  const AnimatedCounter({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = useAnimationController(duration: const Duration(milliseconds: 300));
    final count = ref.watch(counterProvider);

    useEffect(() {
      controller.forward(from: 0);
      return null;
    }, [count]);

    return ScaleTransition(
      scale: controller,
      child: Text('$count'),
    );
  }
}

Mocktailを使用したテスト

ユニットテスト

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpod/riverpod.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepository;
  late ProviderContainer container;

  setUp(() {
    mockRepository = MockUserRepository();
    container = ProviderContainer(
      overrides: [
        userRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  test('usersProvider returns list of users', () async {
    final users = [User(id: '1', name: 'John', email: 'john@example.com')];
    when(() => mockRepository.getUsers()).thenAnswer((_) async => users);

    final result = await container.read(usersProvider.future);

    expect(result, equals(users));
    verify(() => mockRepository.getUsers()).called(1);
  });
}

ウィジェットテスト

void main() {
  testWidgets('UserCard displays user name', (tester) async {
    final user = User(id: '1', name: 'John', email: 'john@example.com');

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userByIdProvider('1').overrideWith((_) => AsyncData(user)),
        ],
        child: const MaterialApp(home: UserCard(userId: '1')),
      ),
    );

    expect(find.text('John'), findsOneWidget);
  });

  testWidgets('UserCard shows loading indicator', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userByIdProvider('1').overrideWith((_) => const AsyncLoading()),
        ],
        child: const MaterialApp(home: UserCard(userId: '1')),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });
}

pubspec.yaml

name: my_app
description: A Flutter application
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.2.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # 状態管理
  flutter_riverpod: ^2.4.9
  riverpod_annotation: ^2.3.3

  # データモデル
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

  # ナビゲーション
  go_router: ^13.0.0

  # ネットワーク
  dio: ^5.4.0

  # ストレージ
  shared_preferences: ^2.2.2

  # ユーティリティ
  intl: ^0.19.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  # コード生成
  build_runner: ^2.4.8
  freezed: ^2.4.6
  json_serializable: ^6.7.1
  riverpod_generator: ^2.3.9

  # テスト
  mocktail: ^1.0.2

  # Lint
  flutter_lints: ^3.0.1

GitHub Actions

name: Flutter CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'
          cache: true

      - name: Install dependencies
        run: flutter pub get

      - name: Generate code
        run: dart run build_runner build --delete-conflicting-outputs

      - name: Analyze
        run: flutter analyze --fatal-infos

      - name: Run tests
        run: flutter test --coverage

      - name: Build APK
        run: flutter build apk --release

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

analyzer:
  exclude:
    - "**/*.g.dart"
    - "**/*.freezed.dart"
  errors:
    invalid_annotation_target: ignore
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true

linter:
  rules:
    - always_declare_return_types
    - avoid_dynamic_calls
    - avoid_print
    - avoid_type_to_string
    - cancel_subscriptions
    - close_sinks
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_final_locals
    - require_trailing_commas
    - unawaited_futures
    - use_super_parameters

Flutterアンチパターン

  • autoDisposeなしのプロバイダー - メモリリーク防止のために.autoDisposeを使用
  • コールバック内でwatch - onPressed/コールバック内ではref.watch()ではなくref.read()を使用
  • ウィジェット内のビジネスロジック - Notifiers/プロバイダーに移動
  • プロバイダー内の可変状態 - イミュータブルモデルにFreezedを使用
  • AsyncValueを使用しない - when()でロード/エラー状態を処理
  • Riverpodでsetstate - 共有状態にはプロバイダーを使用
  • 関数にrefを渡す - refの使用をウィジェット/プロバイダー内に留める
  • 深くネストされたConsumer - 代わりにConsumerWidgetを使用
  • パラメータにfamilyを使用しない - パラメータ化されたプロバイダーに.familyを使用
  • グローバルGoRouterインスタンス - リダイレクトロジック付きProviderを使用
  • 非同期を通じたBuildContext - contextではなく、awaitの前に値を保存
  • disposeを無視 - ConsumerStatefulWidgetでコントローラーをクリーンアップ

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
alinaqi
リポジトリ
alinaqi/claude-bootstrap
ライセンス
MIT
最終更新
不明

Source: https://github.com/alinaqi/claude-bootstrap / ライセンス: MIT

関連スキル

汎用その他⭐ リポ 1,982

superfluid

Superfluidプロトコルおよびそのエコシステムに関するナレッジベースです。Superfluidについて情報を検索する際は、ウェブ検索の前にこちらを参照してください。対応キーワード:Superfluid、CFA、GDA、Super App、Super Token、stream、flow rate、real-time balance、pool(member/distributor)、IDA、sentinels、liquidation、TOGA、@sfpro/sdk、semantic money、yellowpaper、whitepaper

by LeoYeAI
汎用その他⭐ リポ 100

civ-finish-quotes

実質的なタスクが真に完了した際に、文明風の儀式的な引用句を追加します。ユーザーやエージェントが機能追加、リファクタリング、分析、設計ドキュメント、プロセス改善、レポート、執筆タスクといった実際の成果物を完成させるときに、明示的な依頼がなくても使用します。短い返信や小さな修正、未完成の作業には適用しません。

by huxiuhan
汎用その他⭐ リポ 1,110

nookplot

Base(Ethereum L2)上のAIエージェント向け分散型調整ネットワークです。エージェントがオンチェーンアイデンティティを登録する、コンテンツを公開する、他のエージェントにメッセージを送る、マーケットプレイスで専門家を雇う、バウンティを投稿・請求する、レピュテーションを構築する、共有プロジェクトで協業する、リサーチチャレンジを解くことでNOOKをマイニングする、キュレーションされたナレッジを備えたスタンドアロンオンチェーンエージェントをデプロイする、またはアグリーメントとリワードで収益を得る場合に利用できます。エージェントネットワーク、エージェント調整、分散型エージェント、NOOKトークン、マイニングチャレンジ、ナレッジバンドル、エージェントレピュテーション、エージェントマーケットプレイス、ERC-2771メタトランザクション、Prepare-Sign-Relay、AgentFactory、またはNookplotが言及された場合にトリガーされます。

by BankrBot
汎用その他⭐ リポ 59

web3-polymarket

Polygon上でのPolymarket予測市場取引統合です。認証機能(L1 EIP-712、L2 HMAC-SHA256、ビルダーヘッダー)、注文発注(GTC/GTD/FOK/FAK、バッチ、ポストオンリー、ハートビート)、市場データ(Gamma API、Data API、オーダーブック、サブグラフ)、WebSocketストリーミング(市場・ユーザー・スポーツチャネル)、CTF操作(分割、統合、償却、ネガティブリスク)、ブリッジ機能(入金、出金、マルチチェーン)、およびガスレスリレイトランザクションに対応しています。AIエージェント、自動マーケットメーカー、予測市場UI、またはPolygraph上のPolymarketと統合するアプリケーション構築時に活用できます。

by elophanto
汎用その他⭐ リポ 52

ethskills

Ethereum、EVM、またはブロックチェーン関連のリクエストに対応します。スマートコントラクト、dApps、ウォレット、DeFiプロトコルの構築、監査、デプロイ、インタラクションに適用されます。Solidityの開発、コントラクトアドレス、トークン規格(ERC-20、ERC-721、ERC-4626など)、Layer 2ネットワーク(Base、Arbitrum、Optimism、zkSync、Polygon)、Uniswap、Aave、Curveなどのプロトコルとの統合をカバーします。ガスコスト、コントラクトのデシマル設定、オラクルセキュリティ、リエントランシー、MEV、ブリッジング、ウォレット管理、オンチェーンデータの取得、本番環境へのデプロイ、プロトコル進化(EIPライフサイクル、フォーク追跡、今後の変更予定)といったトピックを含みます。

by jiayaoqijia
汎用その他⭐ リポ 44

xxyy-trade

このスキルは、ユーザーが「トークン購入」「トークン売却」「トークンスワップ」「暗号資産取引」「取引ステータス確認」「トランザクション照会」「トークンスキャン」「フィード」「チェーン監視」「トークン照会」「トークン詳細」「トークン安全性確認」「ウォレット一覧表示」「マイウォレット」「AIスキャン」「自動スキャン」「ツイートスキャン」「オンボーディング」「IP確認」「IPホワイトリスト」「トークン発行」「自動売却」「損切り」「利益確定」「トレーリングストップ」「保有者」「トップホルダー」「KOLホルダー」などをリクエストした場合、またはSolana/ETH/BSC/BaseチェーンでXXYYを経由した取引について言及した場合に使用します。XXYY Open APIを通じてオンチェーン取引とデータ照会を実現します。

by Jimmy-Holiday
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: alinaqi · alinaqi/claude-bootstrap · ライセンス: MIT