flutter-architecture — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited flutter-architecture (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Design Flutter applications to scale by strictly adhering to the following principles:
into distinct layers (UI, Logic, Data) and further separate by feature within those layers.
is the only component authorized to mutate its respective data.
upwards from the UI layer to the Data layer.
the underlying state changes.
Rule: Do not useChangeNotifierfor application state. Use RiverpodAsyncNotifier/Notifierproviders or BLoCCubitclasses.
Rule: Never model async state with nullable optional fields. Use AsyncValue<T> (Riverpod) or sealed state classes (BLoC):// BLoC sealed state
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState { final User user; UserLoaded(this.user); }
class UserError extends UserState { final String message; UserError(this.message); }
// Riverpod
final userProvider = AsyncNotifierProvider<UserNotifier, User>(UserNotifier.new);
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() => ref.watch(userRepositoryProvider).fetchUser();
}
// In widget: ref.watch(userProvider) returns AsyncValue<User>
// Use .when(data: ..., loading: ..., error: ...)Separate the application into 2 to 3 distinct layers depending on complexity. Restrict communication so that a layer only interacts with the layer directly adjacent to it.
#### 1. UI Layer (Presentation)
Restrict widget logic to UI-specific concerns (e.g., animations, routing, layout constraints).
them into presentation-friendly formats. Expose state to the Views and handle user interaction events. Implement as Riverpod AsyncNotifier/Notifier providers or BLoC Cubit classes: not ChangeNotifier.
#### 2. Logic Layer (Domain) - Conditional
Interactors. Use this layer to orchestrate interactions between multiple repositories before passing data to the UI layer.
Repositories.
#### 3. Data Layer (Model)
processing, and data synchronization.
#### Services
#### Repositories
ViewModels/providers.
Follow this sequential workflow when adding a new feature to the application.
Task Progress:
feature.
Domain Models.
AsyncNotifier/Notifier providers or BLoC Cubitclasses that consume the Repositories. Expose immutable state and define methods (commands) for user actions. Do not use ChangeNotifier.
interaction.
tests for Views.
#### Data Layer: Service and Repository
// 1. Service (Stateless API Wrapper)
class UserApiService {
final HttpClient _client;
UserApiService(this._client);
Future<Map<String, dynamic>> fetchUserRaw(String userId) async {
final response = await _client.get('/users/$userId');
return response.data;
}
}
// 2. Domain Model (Immutable, generated by freezed — the demonstrated default)
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// 3. Repository (SSOT & Data Transformer)
// The cache snapshot is repository-internal and replaced atomically, never
// exposed mutably. Ad-hoc mutable fields scattered through the data layer are
// the shared-mutable-state anti-pattern: keep any cache private and immutable.
class UserRepository {
final UserApiService _apiService;
User? _cachedUser;
UserRepository(this._apiService);
Future<User> getUser(String userId) async {
final cached = _cachedUser;
if (cached != null && cached.id == userId) {
return cached;
}
final rawData = await _apiService.fetchUserRaw(userId);
final user = User(id: rawData['id'], name: rawData['name']);
_cachedUser = user; // Replace the cached snapshot atomically
return user;
}
}#### UI Layer: Riverpod AsyncNotifier and View
// 4a. Riverpod provider (preferred)
// State is AsyncValue<User> — no nullable fields for loading/error
final userProvider = AsyncNotifierProvider<UserNotifier, User>(UserNotifier.new);
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() =>
ref.watch(userRepositoryProvider).getUser('current');
Future<void> reload(String userId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(
() => ref.read(userRepositoryProvider).getUser(userId),
);
}
}
// 5a. View consuming Riverpod provider
class UserProfileView extends ConsumerWidget {
const UserProfileView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
data: (user) => Text('Hello, ${user.name}'),
);
}
}#### UI Layer: BLoC Cubit and View
// 4b. BLoC Cubit (use for complex event-driven features)
// State is a sealed class — no nullable fields for loading/error
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}
class UserCubit extends Cubit<UserState> {
UserCubit(this._repository) : super(UserInitial());
final UserRepository _repository;
Future<void> loadUser(String userId) async {
emit(UserLoading());
try {
final user = await _repository.getUser(userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
// 5b. View consuming BLoC Cubit
class UserProfileView extends StatelessWidget {
const UserProfileView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<UserCubit, UserState>(
builder: (context, state) => switch (state) {
UserInitial() => const SizedBox.shrink(),
UserLoading() => const CircularProgressIndicator(),
UserLoaded(:final user) => Text('Hello, ${user.name}'),
UserError(:final message) => Text('Error: $message'),
},
);
}
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.