flutter-dart-rules

star 1

Core Flutter & Dart coding standards for MediChain mobile (frontend-mobile/). Enforces widget composition, BLoC state management, Clean Architecture, and Dart language best practices. Use when writing any Dart code in the mobile app.

min2hi By min2hi schedule Updated 6/5/2026

name: flutter-dart-rules description: > Core Flutter & Dart coding standards for MediChain mobile (frontend-mobile/). Enforces widget composition, BLoC state management, Clean Architecture, and Dart language best practices. Use when writing any Dart code in the mobile app.

Flutter & Dart Development Rules — MediChain

Adapted from agentpedia.codes Flutter & Dart Development rule + MediChain-specific architectural constraints.


Core Principles

  • Everything is a Widget — Compose small, focused widgets
  • Composition over inheritance — Favor widget composition
  • Declarative UI — UI = f(state), never imperative mutations
  • State management is BLoC — No setState for business logic
  • Hot Reload friendly — Keep build() methods pure and fast

Dart Language

// ✅ Strong typing — ALWAYS type everything
List<AppointmentModel> appointments = [];   // NOT var
Map<String, dynamic> get toJson => {...};   // Return type explicit

// ✅ Null safety — handle nulls at boundaries
final String name = user?.name ?? 'Bệnh nhân'; // NOT user!.name

// ✅ const constructors — reduces rebuilds
const SizedBox(height: 16);
const EdgeInsets.symmetric(horizontal: 16, vertical: 8);

// ✅ Named parameters for readability
AppCard(
  padding: EdgeInsets.all(AppSpacing.md),
  onTap: _onCardTap,
  child: content,
)

// ✅ Extension methods for clean code
extension StringExtension on String {
  String get capitalizeFirst =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

// ❌ NEVER use dynamic unless absolutely necessary
dynamic data = response['data']; // ← Always parse to typed model

Widget Architecture

// ✅ StatelessWidget by default
// Use StatefulWidget ONLY when local ephemeral UI state needed
// (e.g., text field focus, animation controller)

// ✅ Extract widgets instead of nesting
// BAD:
Column(children: [
  Container(
    decoration: ...,
    child: Row(children: [
      Icon(...),
      Column(children: [
        Text(title),
        Text(subtitle),
      ]),
    ]),
  ),
])

// GOOD:
Column(children: [
  _DoctorCard(doctor: doctor),
])

class _DoctorCard extends StatelessWidget { ... }

// ✅ Private widgets (underscore prefix) for screen-specific widgets
// ✅ Public widgets in lib/presentation/widgets/ for reusable ones

// ✅ const keyword on widgets that don't depend on runtime data
const AppHeader(title: 'Lịch hẹn'),

BLoC Rules (MediChain specific)

// ✅ Event naming: verb + noun
class LoadAppointments extends AppointmentEvent {}
class BookAppointment extends AppointmentEvent {
  final String clinicId;
  final DateTime date;
  const BookAppointment({required this.clinicId, required this.date});
}

// ✅ State naming: noun + status
class AppointmentInitial extends AppointmentState {}
class AppointmentLoading extends AppointmentState {}
class AppointmentsLoaded extends AppointmentState {
  final List<AppointmentModel> appointments;
  const AppointmentsLoaded(this.appointments);
}
class AppointmentError extends AppointmentState {
  final String message;
  const AppointmentError(this.message);
}

// ✅ Bloc: thin — only orchestrates, NO business logic
on<LoadAppointments>((event, emit) async {
  emit(AppointmentLoading());
  final result = await _repository.getAppointments();
  result.fold(
    (error) => emit(AppointmentError(error.message)),
    (data) => emit(AppointmentsLoaded(data)),
  );
});

// ❌ NEVER call dio/http directly from Bloc
// ❌ NEVER put if/else business logic in Bloc — belongs in Repository/Service

Performance Rules

// ✅ Use ListView.builder for lists > 10 items
ListView.builder(
  itemCount: appointments.length,
  itemBuilder: (context, index) => AppointmentCard(
    appointment: appointments[index],
  ),
)

// ✅ RepaintBoundary for expensive sub-trees
RepaintBoundary(
  child: ComplexChart(...),
)

// ✅ Memoize expensive computations
late final sortedDoctors = doctors.sorted((a, b) => a.name.compareTo(b.name));

// ✅ Use cached_network_image for all remote images
CachedNetworkImage(
  imageUrl: doctor.avatarUrl,
  placeholder: (_, __) => CircleAvatar(child: Icon(Icons.person)),
  errorWidget: (_, __, ___) => CircleAvatar(child: Icon(Icons.person)),
)

// ❌ NEVER use Image.network directly (no caching)
// ❌ NEVER call setState in build() method
// ❌ NEVER create objects inside build() — use class variables or const

GoRouter Navigation Rules

// ✅ Use context.push / context.go — NEVER Navigator directly
context.push('/appointment/book');   // Push (back button exists)
context.go('/home');                  // Replace (no back)
context.pop();                        // Go back

// ✅ Pass data via extra or path params, NOT shared state
context.push('/doctor/$doctorId', extra: doctor);

// ✅ In receiving screen — read from GoRouterState
final doctor = state.extra as DoctorModel?;

// ❌ NEVER use Navigator.push — bypasses GoRouter guards
Navigator.push(context, ...); // ← breaks auth guards

Error Handling

// ✅ Always show user-friendly errors, log technical ones
on<LoadData>((event, emit) async {
  try {
    emit(Loading());
    final data = await _repository.fetchData();
    emit(Loaded(data));
  } on DioException catch (e) {
    debugPrint('[DataBloc] DioException: ${e.message}');
    emit(Error(e.response?.statusCode == 401
        ? 'Phiên đăng nhập hết hạn'
        : 'Không thể tải dữ liệu. Vui lòng thử lại.'));
  } catch (e, stack) {
    debugPrint('[DataBloc] Unexpected: $e\n$stack');
    emit(const Error('Đã xảy ra lỗi không mong đợi'));
  }
});

// ✅ Show SnackBar for transient errors, NOT dialog (unless critical)
ScaffoldMessenger.of(context)
  ..hideCurrentSnackBar()
  ..showSnackBar(SnackBar(
    content: Text(state.message),
    backgroundColor: AppTheme.kError,
    behavior: SnackBarBehavior.floating,
  ));

File Organization

lib/
├── core/
│   ├── constants/     ← app_constants.dart, api_endpoints.dart
│   ├── theme/         ← app_theme.dart (SINGLE SOURCE OF TRUTH)
│   ├── utils/         ← date_utils.dart, validators.dart
│   └── services/      ← secure_storage_service.dart, biometric_service.dart
├── data/
│   ├── models/        ← *.model.dart (KHÔNG logic trong model)
│   └── repositories/  ← *.repository.dart (API calls + parsing)
├── logic/             ← BLoC (events, states, bloc files)
└── presentation/
    ├── screens/       ← feature folder, mỗi screen 1 folder riêng
    │   └── <feature>/
    │       ├── <feature>_screen.dart   ← Orchestrator (BLoC wiring + layout)
    │       └── widgets/                ← Sub-widgets có thể reuse hoặc có concern riêng
    └── widgets/       ← Shared widgets dùng được toàn app

Khi nào tách widget ra file riêng? (SRP — Single Responsibility Principle)

// Tách khi ít nhất 1 trong các điều dưới đúng:
// 1. Widget có thể REUSE ở màn hình / feature khác
// 2. Widget có state / AnimationController riêng biệt
// 3. File chứa quá nhiều concern khác nhau (mix cả logic và UI)
// 4. Widget đủ phức tạp để viết unit test riêng

// Giữ trong cùng file khi:
// - Widget chỉ dùng ở 1 chỗ duy nhất, không có state riêng
// - Việc tách ra làm code KHÓ ĐỌC HƠN (over-engineering)
// - Tách ra không mang lại testability hơn

// Ví dụ đúng: Google’s cupertino/text_field.dart = 1600+ dòng, 1 class, 1 concern
// => Đó là đúng vì 1 lý do thay đổi duy nhất

Platform Considerations

// ✅ Platform-aware UI khi cần
import 'dart:io' show Platform;

// NHƯNG: chỉ dùng cho platform-specific behavior, KHÔNG cho design
// MediChain dùng Material 3 cho cả iOS và Android

// ✅ Biometric auth (HIPAA requirement)
// Dùng local_auth package — đã setup trong AppLockOverlay

// ✅ Admin dark mode — always dark
// AdminColors là constant, không theo system theme

// ✅ Patient portal — theo system theme
ThemeMode.system // (configured in MaterialApp)

Sources

Install via CLI
npx skills add https://github.com/min2hi/medi_chain --skill flutter-dart-rules
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator