alinaqi/maggy1 file

Flutter

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

Specification
Skill ID
alinaqi/maggy/flutter
Publisher
alinaqi
Repository
maggy
Installs
0
Files
1
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

alinaqi/maggy/flutterInstalls these files
  • SKILL.md

What this skill tells the agent

Flutter Skill


Project Structure

project/
├── lib/
│   ├── core/                           # Core utilities
│   │   ├── constants/                  # App constants
│   │   ├── extensions/                 # Dart extensions
│   │   ├── router/                     # go_router configuration
│   │   │   └── app_router.dart
│   │   └── theme/                      # App theme
│   │       └── app_theme.dart
│   ├── data/                           # Data layer
│   │   ├── models/                     # Freezed data models
│   │   ├── repositories/               # Repository implementations
│   │   └── services/                   # API services
│   ├── domain/                         # Domain layer
│   │   ├── entities/                   # Business entities
│   │   └── repositories/               # Repository interfaces
│   ├── presentation/                   # UI layer
│   │   ├── common/                     # Shared widgets
│   │   ├── features/                   # Feature modules
│   │   │   └── feature_name/
│   │   │       ├── providers/          # Riverpod providers
│   │   │       ├── widgets/            # Feature-specific widgets
│   │   │       └── feature_screen.dart
│   │   └── providers/                  # Global providers
│   ├── main.dart
│   └── app.dart
├── test/
│   ├── unit/                           # Unit tests
│   ├── widget/                         # Widget tests
│   └── integration/                    # Integration tests
├── pubspec.yaml
├── analysis_options.yaml
└── CLAUDE.md

Riverpod State Management

Provider Types

// Simple value provider
final appNameProvider = Provider<String>((ref) => 'My App');

// StateProvider for simple mutable state
final counterProvider = StateProvider<int>((ref) => 0);

// NotifierProvider for complex state logic
final userProvider = NotifierProvider<UserNotifier, User?>(() => UserNotifier());

// AsyncNotifierProvider for async operations
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
  () => UsersNotifier(),
);

// FutureProvider for simple async data
final configProvider = FutureProvider<Config>((ref) async {
  return await ref.watch(configServiceProvider).loadConfig();
});

// StreamProvider for real-time data
final messagesProvider = StreamProvider<List<Message>>((ref) {
  return ref.watch(messageServiceProvider).watchMessages();
});

// Family providers for parameterized data
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await ref.watch(userRepositoryProvider).getUser(userId);
});

Notifier Pattern

@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 Handling

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),
      ),
    );
  }
}

// Pattern matching alternative
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 Methods

// watch - rebuilds when provider changes
final users = ref.watch(usersProvider);

// read - one-time read, no rebuild
void onButtonPressed() {
  ref.read(counterProvider.notifier).state++;
}

// listen - react to changes without rebuild
ref.listen(authProvider, (previous, next) {
  if (next == null) {
    context.go('/login');
  }
});

// invalidate - force refresh
ref.invalidate(usersProvider);

// keepAlive - prevent auto-dispose
final link = ref.keepAlive();
// Later: link.close() to allow disposal

Freezed Data Models

Model Definition

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);
}

// Union types for states
@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;
}

Using Freezed Unions

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 Navigation

Router Configuration

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),
  );
});

Navigation

// Navigate to route
context.go('/user/123');

// Push onto stack
context.push('/user/123');

// Pop current route
context.pop();

// Replace current route
context.pushReplacement('/home');

// Named routes
context.goNamed('user', pathParameters: {'id': '123'});

Widget Patterns

ConsumerWidget vs ConsumerStatefulWidget

// Stateless with 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),
    );
  }
}

// Stateful with Riverpod
class SearchScreen extends ConsumerStatefulWidget {
  const SearchScreen({super.key});

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

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

  @override

Use this skill in a project

Open any RiverX project, search the Skills panel for alinaqi/maggy/flutter, and install it in one click.

Use this skill