
flutter-coding
by reown-com
Reown is the onchain UX platform that provides toolkits built on top of the WalletConnect Network that enable builders to create onchain user experiences that make digital ownership effortless, intuitive, and secure.
SKILL.md
name: flutter-coding description: Writes high-quality Flutter/Dart code following official conventions and reown-flutter project patterns. Use when writing, reviewing, or refactoring Flutter/Dart code in this codebase.
Flutter Coding Skill
Goal
Write production-quality Flutter/Dart code that follows official Flutter conventions and reown-flutter project-specific patterns.
When to use
- Writing new Flutter/Dart classes, functions, or modules
- Implementing features in Flutter codebase
- Refactoring existing Flutter/Dart code
- Code reviews for Flutter/Dart files
When not to use
- Non-Flutter/Dart codebases (Kotlin, Swift, etc.)
- Configuration files (yaml, json, xml)
- Documentation-only tasks
Project Context: reown-flutter
Stack:
- Dart 3.8.0+, Flutter 1.10.0+
- Multi-platform: Android, iOS, Web, macOS, Linux, Windows
- Code generation: build_runner, freezed, json_serializable
- State management: event package, ValueNotifier, ChangeNotifier
- Networking: http, web_socket_channel
- Storage: flutter_secure_storage, shared_preferences
- Cryptography: ed25519_edwards, x25519, pointycastle, pinenacl
- Testing: flutter_test, mockito, flutter_lints
Architecture: Layered Monorepo
reown_core (Foundation) → reown_sign (Protocol) → reown_walletkit/reown_appkit (Application)
Default Workflow
- Understand context - Read existing code in the module
- Follow existing patterns - Match the module's conventions
- Write minimal code - Only what's needed for the task
- Run code generation - Execute
generate_files.shif models changed - Add tests - Match existing test patterns
- Validate - Run through checklist below
Core Patterns
Interface-Based Design
// Public API via interface
abstract class ISignClient {
Future<void> connect(ConnectParams params);
Stream<SignClientEvent> get events;
}
// Internal implementation
class SignClient implements ISignClient {
// Implementation details
}
Immutable Models with Freezed
import 'package:freezed_annotation/freezed_annotation.dart';
part 'session.freezed.dart';
part 'session.g.dart';
@freezed
class Session with _$Session {
const factory Session({
required String topic,
required String pairingTopic,
required Map<String, Namespace> namespaces,
@JsonKey(name: 'expiry') required int expiry,
}) = _Session;
factory Session.fromJson(Map<String, dynamic> json) =>
_$SessionFromJson(json);
}
JSON Serialization
import 'package:json_annotation/json_annotation.dart';
part 'request.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class SessionRequest {
final int id;
final String topic;
final RequestParams params;
SessionRequest({
required this.id,
required this.topic,
required this.params,
});
factory SessionRequest.fromJson(Map<String, dynamic> json) =>
_$SessionRequestFromJson(json);
Map<String, dynamic> toJson() => _$SessionRequestToJson(this);
}
Event-Driven Architecture
import 'package:event/event.dart';
class SignClient {
final Event<SignClientEvent> _events = Event<SignClientEvent>();
Event<SignClientEvent> get events => _events;
void _emitEvent(SignClientEvent event) {
_events.broadcast(event);
}
}
// Usage
signClient.events.listen((event) {
if (event is SessionProposalEvent) {
// Handle proposal
}
});
Error Handling
// Custom exception hierarchy
class WalletConnectException implements Exception {
final String message;
final dynamic cause;
WalletConnectException(this.message, [this.cause]);
@override
String toString() => 'WalletConnectException: $message';
}
class InvalidSessionException extends WalletConnectException {
InvalidSessionException(String message) : super(message);
}
// Result pattern with try-catch
Future<Result<Session>> connect(ConnectParams params) async {
try {
final session = await _establishSession(params);
return Result.success(session);
} on InvalidSessionException catch (e) {
return Result.failure(e);
} catch (e, stackTrace) {
_logger.error('Connection failed', error: e, stackTrace: stackTrace);
return Result.failure(WalletConnectException('Connection failed', e));
}
}
Async/Await Best Practices
// Prefer async/await over Future.then
Future<Session> connect(ConnectParams params) async {
final pairing = await _createPairing(params);
final uri = await _generateUri(pairing);
return await _waitForApproval(pairing);
}
// Use Future.wait for parallel operations
Future<List<Balance>> fetchBalances(List<String> addresses) async {
final futures = addresses.map((addr) => _fetchBalance(addr));
return await Future.wait(futures);
}
// Handle timeouts
Future<Response> fetchWithTimeout(String url) async {
return await http.get(Uri.parse(url))
.timeout(const Duration(seconds: 10));
}
State Management
// ValueNotifier for simple state
class ConnectionState {
final ValueNotifier<bool> isConnected = ValueNotifier(false);
final ValueNotifier<String?> currentTopic = ValueNotifier(null);
}
// Usage in widget
ValueListenableBuilder<bool>(
valueListenable: connectionState.isConnected,
builder: (context, isConnected, child) {
return Text(isConnected ? 'Connected' : 'Disconnected');
},
);
// ChangeNotifier for complex state
class SessionManager extends ChangeNotifier {
List<Session> _sessions = [];
List<Session> get sessions => List.unmodifiable(_sessions);
void addSession(Session session) {
_sessions.add(session);
notifyListeners();
}
}
Storage Pattern
// Secure storage with fallback
class SecureStore {
final FlutterSecureStorage _secureStorage;
final SharedPreferences _fallback;
Future<String?> read(String key) async {
try {
return await _secureStorage.read(key: key);
} catch (e) {
// Fallback to shared preferences
return _fallback.getString(key);
}
}
Future<void> write(String key, String value) async {
try {
await _secureStorage.write(key: key, value: value);
} catch (e) {
// Fallback to shared preferences
await _fallback.setString(key, value);
}
}
}
Dependency Injection
// Constructor injection
class SignClient {
final IRelayClient relayClient;
final IStorage storage;
final Logger logger;
SignClient({
required this.relayClient,
required this.storage,
required this.logger,
});
}
// Factory pattern for complex initialization
class SignClientFactory {
static Future<SignClient> create({
required String projectId,
Logger? logger,
}) async {
final core = ReownCore(projectId: projectId);
final storage = await SecureStore.create();
return SignClient(
relayClient: core.relayClient,
storage: storage,
logger: logger ?? Logger(),
);
}
}
Logging
import 'package:logger/logger.dart';
class SignClient {
final Logger _logger;
SignClient({Logger? logger})
: _logger = logger ?? Logger(level: Level.info);
void _logInfo(String message) {
_logger.i(message);
}
void _logError(String message, {Object? error, StackTrace? stackTrace}) {
_logger.e(message, error: error, stackTrace: stackTrace);
}
}
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Classes | PascalCase | SignClient, SessionManager |
| Interfaces | I* prefix | ISignClient, IStorage |
| Variables/Functions | camelCase | connectSession, currentTopic |
| Constants | lowerCamelCase or SCREAMING_SNAKE_CASE | defaultRelayUrl, MAX_RETRIES |
| Files | snake_case.dart | sign_client.dart, session_manager.dart |
| Private members | _leadingUnderscore | _internalState, _processEvent() |
| Freezed models | * suffix for factory | Session, _Session (generated) |
Code Generation
Freezed Models
// Always run after modifying freezed models
dart run build_runner build --delete-conflicting-outputs
JSON Serialization
// Generate after adding @JsonSerializable
dart run build_runner build --delete-conflicting-outputs
Generate All
# From package root
sh generate_files.sh
Testing Pattern
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([IRelayClient, IStorage])
void main() {
late SignClient signClient;
late MockIRelayClient mockRelayClient;
late MockIStorage mockStorage;
setUp(() {
mockRelayClient = MockIRelayClient();
mockStorage = MockIStorage();
signClient = SignClient(
relayClient: mockRelayClient,
storage: mockStorage,
);
});
group('SignClient', () {
test('connect creates pairing and returns URI', () async {
// Given
when(mockRelayClient.createPairing(any))
.thenAnswer((_) async => Pairing(topic: 'test-topic'));
// When
final uri = await signClient.connect(ConnectParams());
// Then
expect(uri, isNotNull);
verify(mockRelayClient.createPairing(any)).called(1);
});
});
}
Validation Checklist
- Uses interfaces for public APIs (
I*prefix) - Models use
@freezedfor immutability - JSON models use
@JsonSerializablewithfieldRename: FieldRename.snake - All generated files are up-to-date (run
generate_files.sh) - Error handling with custom exceptions
- Async operations use
async/await(not.then()) - Logging uses
loggerpackage (notprint) - Storage uses secure storage with fallback
- Tests use
mockitowith@GenerateMocks - Code formatted with
dart format - No linter errors (
flutter analyze) - Line length ≤ 80 characters (preferred)
- Private members use
_prefix - Constants are properly scoped
- UI components use widgets, not functions (no
Widget _buildX()methods)
Flutter-Specific Patterns
Widget Composition
// Prefer composition over large widgets
class SessionList extends StatelessWidget {
final List<Session> sessions;
const SessionList({required this.sessions, super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: sessions.length,
itemBuilder: (context, index) => SessionTile(session: sessions[index]),
);
}
}
// Private widget for reusable UI
class _SessionTile extends StatelessWidget {
final Session session;
const _SessionTile({required this.session});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(session.topic),
subtitle: Text('Expires: ${session.expiry}'),
);
}
}
Avoid Functions for UI - Use Widgets Instead
IMPORTANT: Never use functions to return UI components. Always use StatelessWidget or StatefulWidget classes instead.
Functions for UI are problematic because:
- They don't benefit from Flutter's widget rebuild optimizations
- They can't be
constconstructed - They don't appear in widget inspector/devtools
- They're not reusable across files
- They don't support widget keys properly
// BAD - Don't use functions for UI
class MyPage extends StatelessWidget {
Widget _buildHeader() {
return Container(
child: Text('Header'),
);
}
Widget _buildContent() {
return Column(children: [...]);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildHeader(),
_buildContent(),
],
);
}
}
// GOOD - Use widget classes instead
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return const Column(
children: [
_Header(),
_Content(),
],
);
}
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return Container(
child: const Text('Header'),
);
}
}
class _Content extends StatelessWidget {
const _Content();
@override
Widget build(BuildContext context) {
return Column(children: [...]);
}
}
Const Constructors
// Use const where possible
const SessionTile({required this.session});
// In build methods
return const SizedBox(height: 16);
Platform-Specific Code
import 'dart:io' show Platform;
if (Platform.isAndroid) {
// Android-specific code
} else if (Platform.isIOS) {
// iOS-specific code
}
Examples
Example 1: New Model with Freezed
Task: Create a session proposal model
import 'package:freezed_annotation/freezed_annotation.dart';
part 'session_proposal.freezed.dart';
part 'session_proposal.g.dart';
@freezed
class SessionProposal with _$SessionProposal {
const factory SessionProposal({
required int id,
required ProposalParams params,
@JsonKey(name: 'expiry') required int expiry,
}) = _SessionProposal;
factory SessionProposal.fromJson(Map<String, dynamic> json) =>
_$SessionProposalFromJson(json);
}
@freezed
class ProposalParams with _$ProposalParams {
const factory ProposalParams({
required AppMetadata proposer,
required Map<String, Namespace> requiredNamespaces,
Map<String, Namespace>? optionalNamespaces,
}) = _ProposalParams;
factory ProposalParams.fromJson(Map<String, dynamic> json) =>
_$ProposalParamsFromJson(json);
}
Example 2: Service with Error Handling
Task: Create a service to fetch chain metadata
class ChainMetadataService {
final IHttpClient httpClient;
final Logger logger;
ChainMetadataService({
required this.httpClient,
Logger? logger,
}) : logger = logger ?? Logger();
Future<Result<ChainMetadata>> fetchMetadata(String chainId) async {
try {
final response = await httpClient.get(
Uri.parse('https://api.example.com/chains/$chainId'),
).timeout(const Duration(seconds: 10));
if (response.statusCode == 200) {
final metadata = ChainMetadata.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
return Result.success(metadata);
} else {
return Result.failure(
HttpException('Failed to fetch metadata: ${response.statusCode}'),
);
}
} on TimeoutException {
logger.e('Timeout fetching chain metadata');
return Result.failure(TimeoutException('Request timed out'));
} catch (e, stackTrace) {
logger.e('Error fetching chain metadata',
error: e,
stackTrace: stackTrace,
);
return Result.failure(
WalletConnectException('Failed to fetch metadata', e),
);
}
}
}
Example 3: Event-Driven Component
Task: Create a component that listens to session events
class SessionListener {
final ISignClient signClient;
final Event<SessionEvent> _sessionEvents = Event<SessionEvent>();
Event<SessionEvent> get sessionEvents => _sessionEvents;
SessionListener({required this.signClient}) {
_setupListeners();
}
void _setupListeners() {
signClient.events.listen((event) {
if (event is SessionProposalEvent) {
_sessionEvents.broadcast(SessionProposalReceived(event.proposal));
} else if (event is SessionApprovedEvent) {
_sessionEvents.broadcast(SessionApproved(event.session));
} else if (event is SessionDeletedEvent) {
_sessionEvents.broadcast(SessionDeleted(event.topic));
}
});
}
}
References
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です
