.claude/skills/flutter-coding/SKILL.md
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.
npx skillsauth add reown-com/reown_flutter flutter-codingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write production-quality Flutter/Dart code that follows official Flutter conventions and reown-flutter project-specific patterns.
Stack:
Architecture: Layered Monorepo
reown_core (Foundation) → reown_sign (Protocol) → reown_walletkit/reown_appkit (Application)
generate_files.sh if models changed// Public API via interface
abstract class ISignClient {
Future<void> connect(ConnectParams params);
Stream<SignClientEvent> get events;
}
// Internal implementation
class SignClient implements ISignClient {
// Implementation details
}
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);
}
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);
}
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
}
});
// 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));
}
}
// 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));
}
// 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();
}
}
// 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);
}
}
}
// 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(),
);
}
}
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);
}
}
| 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) |
// Always run after modifying freezed models
dart run build_runner build --delete-conflicting-outputs
// Generate after adding @JsonSerializable
dart run build_runner build --delete-conflicting-outputs
# From package root
sh generate_files.sh
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);
});
});
}
I* prefix)@freezed for immutability@JsonSerializable with fieldRename: FieldRename.snakegenerate_files.sh)async/await (not .then())logger package (not print)mockito with @GenerateMocksdart formatflutter analyze)_ prefixWidget _buildX() methods)// 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}'),
);
}
}
IMPORTANT: Never use functions to return UI components. Always use StatelessWidget or StatefulWidget classes instead.
Functions for UI are problematic because:
const constructed// 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: [...]);
}
}
// Use const where possible
const SessionTile({required this.session});
// In build methods
return const SizedBox(height: 16);
import 'dart:io' show Platform;
if (Platform.isAndroid) {
// Android-specific code
} else if (Platform.isIOS) {
// iOS-specific code
}
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);
}
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),
);
}
}
}
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));
}
});
}
}
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.