This guide covers development practices, architecture decisions, and contribution guidelines.
-
IDE: VS Code or Android Studio
-
VS Code Extensions:
- Flutter
- Dart
- Flutter Riverpod Snippets
- Error Lens
- GitLens
-
Chrome DevTools: For debugging and performance profiling
Follow the official Dart Style Guide.
Key conventions:
- Use
lowerCamelCasefor variables and functions - Use
UpperCamelCasefor classes - Use
snake_casefor file names - Maximum line length: 80 characters
- Use trailing commas for better formatting
# Format all Dart files
flutter format .
# Check formatting
flutter format --set-exit-if-changed .# Analyze code
flutter analyze
# Fix auto-fixable issues
dart fix --applyWe use Riverpod for state management because:
- Type-safe
- Testable
- No BuildContext required
- Better performance than Provider
Key providers:
userProgressProvider: User stats and progresscurrentLevelProvider: Current level (computed)totalXPProvider: Total XP (computed)currentStreakProvider: Current streak (computed)
Hive is used for local storage:
- Fast and lightweight
- Type-safe with code generation
- No native dependencies
Boxes:
user_progress: User statisticslessons: Cached lesson datamodules: Module dataachievements: Achievement status
lib/
├── main.dart # Entry point
├── models/ # Data models
├── providers/ # Riverpod providers
├── screens/ # Full-screen views
├── widgets/ # Reusable UI components
├── services/ # Business logic
├── data/ # Static/generated content
└── utils/ # Helper functions
Files:
- Screens:
*_screen.dart - Widgets:
*_widget.dartor descriptive name - Services:
*_service.dart - Providers:
*_provider.dart - Models: descriptive name (e.g.,
user_progress.dart)
Classes:
- Screens:
*Screen - Widgets: Descriptive names
- Services:
*Service - Providers:
*Provideror*Notifier
- Create screen file in
lib/screens/
import 'package:flutter/material.dart';
class NewScreen extends StatelessWidget {
const NewScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('New Screen')),
body: Center(child: Text('New Screen')),
);
}
}- Add route in
main.dart
routes: {
'/new-screen': (context) => const NewScreen(),
}- Navigate to screen
Navigator.pushNamed(context, '/new-screen');- Create widget file in
lib/widgets/
import 'package:flutter/material.dart';
class CustomWidget extends StatelessWidget {
final String title;
final VoidCallback onTap;
const CustomWidget({
super.key,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Text(title),
);
}
}- Use widget in screens
CustomWidget(
title: 'Click me',
onTap: () => print('Tapped!'),
)- Create model in
lib/models/
import 'package:equatable/equatable.dart';
class NewModel extends Equatable {
final String id;
final String name;
const NewModel({
required this.id,
required this.name,
});
@override
List<Object?> get props => [id, name];
}- For Hive persistence, add annotations:
import 'package:hive/hive.dart';
part 'new_model.g.dart';
@HiveType(typeId: 1)
class NewModel extends HiveObject {
@HiveField(0)
String id;
@HiveField(1)
String name;
NewModel({required this.id, required this.name});
}- Generate code
flutter pub run build_runner build- Create provider in
lib/providers/
import 'package:flutter_riverpod/flutter_riverpod.dart';
class NewNotifier extends StateNotifier<int> {
NewNotifier() : super(0);
void increment() => state++;
}
final newProvider = StateNotifierProvider<NewNotifier, int>((ref) {
return NewNotifier();
});- Use in widget
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(newProvider);
return Text('Count: $count');
}
}- Use AIContentService
final aiService = AIContentService();
// Generate a lesson
final lesson = await aiService.generateLesson(
moduleId: 'my_module',
topic: 'Introduction to Python Lists',
difficulty: DifficultyLevel.beginner,
type: LessonType.tutorial,
);
// Generate questions
final questions = await aiService.generateQuestions(
topic: 'Python Lists',
difficulty: DifficultyLevel.beginner,
count: 5,
);- Save to storage
await StorageService.saveLesson(lesson.id, lesson.toJson());- Add to
learning_content.dart
Achievement(
id: 'my_achievement',
title: 'Achievement Title',
description: 'Achievement description',
type: AchievementType.lessons,
tier: AchievementTier.gold,
iconPath: 'assets/badges/my_badge.png',
requirement: 10,
xpReward: 100,
),- Check and unlock
final progress = ref.read(userProgressProvider);
if (progress!.completedLessonsCount >= 10) {
ref.read(userProgressProvider.notifier)
.unlockAchievement('my_achievement');
}// test/models/user_progress_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:learn_ai_data_science/models/user_progress.dart';
void main() {
group('UserProgress', () {
test('addXP increases total XP', () {
final progress = UserProgress(
userId: 'test',
lastActivityDate: DateTime.now(),
createdAt: DateTime.now(),
);
progress.addXP(50);
expect(progress.totalXP, 50);
});
});
}// test/widgets/stat_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:learn_ai_data_science/widgets/stat_card.dart';
void main() {
testWidgets('StatCard displays correct values', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StatCard(
icon: Icons.star,
iconColor: Colors.yellow,
label: 'Test',
value: '100',
),
),
),
);
expect(find.text('Test'), findsOneWidget);
expect(find.text('100'), findsOneWidget);
});
}# Run all tests
flutter test
# Run specific test file
flutter test test/models/user_progress_test.dart
# Run with coverage
flutter test --coverage
# View coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html- Use
constconstructors
const Text('Hello') // Good
Text('Hello') // Avoid- Minimize rebuilds with Riverpod
// Watch specific values
final level = ref.watch(currentLevelProvider);
// Instead of
final progress = ref.watch(userProgressProvider);
final level = progress?.currentLevel;- Use
ListView.builderfor long lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ItemWidget(items[index]),
)- Cache network images
CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => CircularProgressIndicator(),
)# Launch DevTools
flutter pub global activate devtools
flutter pub global run devtoolsimport 'dart:developer' as developer;
developer.log('Message', name: 'MyWidget');
debugPrint('Debug message');In VS Code:
- Click left gutter to set breakpoint
- Press F5 to start debugging
- Use Debug Console to inspect variables
feature/description- New featuresfix/description- Bug fixesrefactor/description- Code refactoringdocs/description- Documentation updates
Follow Conventional Commits:
feat: add new achievement system
fix: resolve streak calculation bug
docs: update setup guide
refactor: improve AI service performance
- Create feature branch
- Make changes and commit
- Push to remote
- Create pull request
- Wait for review
- Address feedback
- Merge when approved
# Update version in pubspec.yaml
flutter build ios --release
# Open in Xcode
open ios/Runner.xcworkspace
# Archive and upload to App StoreUpdate pubspec.yaml:
version: 1.1.0+2 # version+build_number-
Forgetting to run build_runner after model changes
- Solution:
flutter pub run build_runner build
- Solution:
-
Not disposing controllers
- Always dispose controllers in
dispose()method
- Always dispose controllers in
-
Using BuildContext across async gaps
- Check
mountedbefore using context after await
- Check
-
Not handling null safety properly
- Use
?.,??, and!appropriately
- Use
For questions, open an issue or contact the maintainers.