arsync_lints 0.1.1
arsync_lints: ^0.1.1 copied to clipboard
A custom lint package for Flutter/Dart that enforces the Arsync 4-layer architecture with strict separation of concerns, Riverpod best practices, and clean code standards.
arsync_lints #
A powerful lint package for Flutter/Dart that enforces the Arsync 4-Layer Architecture with strict separation of concerns, Riverpod best practices, and clean code standards.
Requirements #
- Dart SDK: 3.10.0 or higher
- Flutter SDK: 3.38.0 or higher
Overview #
arsync_lints treats architectural violations as build errors, not warnings. This ensures your codebase maintains clean architecture from day one and prevents "spaghetti code" from creeping into your project.
Installation #
1. Add to your pubspec.yaml #
dev_dependencies:
arsync_lints: ^0.1.1
2. Enable the plugin in analysis_options.yaml #
# Dart 3.10+ native plugin system
plugins:
arsync_lints:
analyzer:
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
Note: The plugins: section is a top-level key, not nested under analyzer:.
3. Restart your IDE #
After adding the plugin, restart your IDE (VS Code, Android Studio, IntelliJ) to activate the lints. The diagnostics will appear automatically in your editor.
4. Run analysis #
# Analyze your project
dart analyze
Project Structure #
For arsync_lints to work correctly, organize your project like this:
lib/
├── main.dart
├── screens/ # UI pages (can use Scaffold)
│ ├── home/
│ │ └── home_screen.dart
│ └── auth/
│ └── login_screen.dart
├── widgets/ # Reusable UI components (no Scaffold, no providers)
│ ├── buttons/
│ │ └── primary_button.dart
│ └── cards/
│ └── user_card.dart
├── providers/ # State management (Riverpod Notifiers)
│ ├── core/ # Infrastructure providers (dioProvider, etc.)
│ │ └── dio_provider.dart
│ ├── auth_provider.dart
│ └── user_provider.dart
├── models/ # Data classes (Freezed)
│ ├── user.dart
│ └── auth_state.dart
├── repositories/ # Data access layer
│ ├── auth_repository.dart
│ └── user_repository.dart
└── utils/
├── constants.dart # k-prefixed constants and functions
└── images.dart # Asset path constants
Rules Overview #
| # | Rule Name | Description |
|---|---|---|
| 1 | presentation_layer_isolation |
Prevents screens/widgets from importing repositories or data layer packages |
| 2 | shared_widget_purity |
Ensures widgets don't import providers; one public widget per file |
| 3 | model_purity |
Models must use @freezed with fromJson; no provider imports |
| 4 | repository_isolation |
Repositories cannot import screens or UI-specific packages |
| 5 | provider_autodispose_enforcement |
Requires autoDispose on all providers except core/ infrastructure |
| 6 | provider_file_naming |
Provider files must end with _provider.dart |
| 7 | provider_state_class |
Provider state classes must use proper naming conventions |
| 8 | provider_declaration_syntax |
Enforces correct Riverpod provider declaration syntax |
| 9 | provider_class_restriction |
Only Notifier classes allowed in provider files |
| 10 | provider_single_per_file |
One provider per file for maintainability |
| 11 | viewmodel_naming_convention |
ViewModels must follow naming conventions |
| 12 | no_context_in_providers |
BuildContext not allowed in provider classes |
| 13 | async_viewmodel_safety |
Async providers must handle loading/error states properly |
| 14 | repository_provider_declaration |
Repository providers must use correct declaration pattern |
| 15 | repository_dependency_injection |
Repositories must receive dependencies via constructor |
| 16 | repository_class_restriction |
Only Repository classes allowed in repository files |
| 17 | repository_no_try_catch |
Repositories should not catch exceptions; let them propagate |
| 18 | repository_async_return |
Repository async methods must return proper types |
| 19 | complexity_limits |
Enforces max nesting depth and method length limits |
| 20 | global_variable_restriction |
No global variables except k-prefixed constants |
| 21 | print_ban |
Bans print() statements; use proper logging |
| 22 | barrel_file_restriction |
Discourages barrel files that re-export everything |
| 23 | ignore_file_ban |
Bans // ignore_for_file: comments |
| 24 | hook_safety_enforcement |
Hooks must be called unconditionally in build() |
| 25 | scaffold_location |
Scaffold only allowed in screens, not shared widgets |
| 26 | asset_safety |
Asset paths must use constants, not hardcoded strings |
| 27 | file_class_match |
Public class name must match file name |
| 28 | avoid_consecutive_sliver_to_box_adapter |
Merge consecutive SliverToBoxAdapters into one |
| 29 | avoid_hardcoded_color |
Use theme colors instead of hardcoded Color values |
| 30 | avoid_shrink_wrap_in_list_view |
Avoid shrinkWrap in ListView for performance |
| 31 | avoid_single_child |
Avoid Row/Column with single child; use simpler widget |
| 32 | prefer_dedicated_media_query_methods |
Use MediaQuery.sizeOf() instead of MediaQuery.of().size |
| 33 | prefer_space_between_elements |
Use SizedBox for spacing instead of Padding |
| 34 | prefer_to_include_sliver_in_name |
Sliver widgets should have "Sliver" in their name |
| 35 | unsafe_null_assertion |
Warns against unsafe ! null assertions |
| 36 | avoid_unnecessary_padding_widget |
Avoid wrapping widgets with zero padding |
| 37 | unnecessary_hook_widget |
Use StatelessWidget if no hooks are used |
| 38 | remove_listener |
Listeners added in initState must be removed in dispose |
| 39 | dispose_notifier |
Notifiers with resources must implement dispose |
| 40 | unnecessary_container |
Use simpler widgets instead of Container when possible |
Rules Reference #
Category A: Architectural Layer Isolation #
These rules prevent layers from "leaking" into each other.
| Rule | Target | Description |
|---|---|---|
presentation_layer_isolation |
screens/, widgets/ |
Cannot import repositories, cloud_firestore, http, or dio. Use Dart records instead of parameter classes. |
shared_widget_purity |
widgets/ |
Cannot import providers or Riverpod packages. Each file must have only ONE public widget. |
model_purity |
models/ |
Must use @freezed and have fromJson factory; no provider imports |
repository_isolation |
repositories/ |
Cannot import screens or UI-specific Riverpod (flutter_riverpod, hooks_riverpod) |
Example: presentation_layer_isolation
// NOT RECOMMENDED - lib/screens/home_screen.dart
import 'package:my_app/repositories/auth_repository.dart'; // ERROR!
import 'package:dio/dio.dart'; // ERROR!
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final repo = AuthRepository(); // Direct repo access!
return Container();
}
}
// RECOMMENDED - lib/screens/home_screen.dart
import 'package:my_app/providers/auth_provider.dart';
class HomeScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final authState = ref.watch(authProvider); // Watch provider instead
return Container();
}
}
Example: shared_widget_purity (Single Widget Per File)
// NOT RECOMMENDED - lib/widgets/buttons.dart
class PrimaryButton extends StatelessWidget {} // ALLOWED - first public widget
class SecondaryButton extends StatelessWidget {} // ERROR! Multiple public widgets
// RECOMMENDED - lib/widgets/primary_button.dart
class PrimaryButton extends StatelessWidget {}
class _ButtonContent extends StatelessWidget {} // ALLOWED - private helper
Example: Use Records Instead of Parameter Classes
// NOT RECOMMENDED - lib/screens/profile_screen.dart
class UpdateProfileParams {
final String userId;
final String name;
const UpdateProfileParams({required this.userId, required this.name});
}
// RECOMMENDED - Use Dart records
typedef UpdateProfileParams = ({
String userId,
String name,
String? phone,
});
// Usage
void updateProfile(UpdateProfileParams params) {
print(params.userId);
}
Example: model_purity
// NOT RECOMMENDED - lib/models/user.dart
import 'package:riverpod/riverpod.dart'; // ERROR: No provider imports in models!
class User {
final String name;
User(this.name);
}
// RECOMMENDED - lib/models/user.dart
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
Example: repository_isolation
// NOT RECOMMENDED - lib/repositories/user_repository.dart
import 'package:flutter_riverpod/flutter_riverpod.dart'; // ERROR: UI Riverpod!
import 'package:my_app/screens/home_screen.dart'; // ERROR: Screen import!
// RECOMMENDED - lib/repositories/user_repository.dart
import 'package:riverpod/riverpod.dart'; // ALLOWED: Core Riverpod only
import 'package:dio/dio.dart';
class UserRepository {
final Dio _dio;
UserRepository(this._dio);
}
Category B: Riverpod & State Management #
These rules enforce the "Arsync Riverpod Pattern".
| Rule | Target | Description |
|---|---|---|
provider_autodispose_enforcement |
providers/ |
Providers must use .autoDispose or call ref.keepAlive(). |
provider_file_naming |
providers/ |
Files must end with _provider.dart and contain a matching Notifier class |
provider_state_class |
providers/ |
State classes must be @freezed and defined in the same file |
provider_declaration_syntax |
providers/ |
Must use .new constructor syntax (e.g., AuthNotifier.new) |
provider_class_restriction |
providers/ |
Only Notifier classes and @freezed state classes allowed |
provider_single_per_file |
providers/ |
Each file can only have ONE NotifierProvider matching the file name |
viewmodel_naming_convention |
providers/ |
Notifier classes must end with "Notifier" |
no_context_in_providers |
providers/ |
BuildContext cannot be used as a parameter |
async_viewmodel_safety |
providers/ |
Async methods in Notifiers must have try/catch |
Example: provider_file_naming
// File: lib/providers/auth_provider.dart
// RECOMMENDED
class AuthNotifier extends Notifier<AuthState> { ... } // Matches file name prefix
// NOT RECOMMENDED - lib/providers/auth.dart (missing _provider suffix)
// NOT RECOMMENDED - lib/providers/auth_provider.dart with class UserNotifier (prefix mismatch)
Example: provider_declaration_syntax
// NOT RECOMMENDED - Explicit generics and closure
final authProvider = NotifierProvider.autoDispose<AuthNotifier, AuthState>(
() => AuthNotifier(),
);
// RECOMMENDED - Clean .new syntax
final authProvider = NotifierProvider.autoDispose(AuthNotifier.new);
Example: provider_autodispose_enforcement
// NOT RECOMMENDED - Memory leak potential
final authProvider = NotifierProvider<AuthNotifier, AuthState>(() {
return AuthNotifier();
}); // ERROR: Missing autoDispose
// RECOMMENDED - Option 1: Use autoDispose
final authProvider = NotifierProvider.autoDispose(AuthNotifier.new);
// RECOMMENDED - Option 2: Use keepAlive for persistent state
final authProvider = NotifierProvider.autoDispose(AuthNotifier.new);
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() {
ref.keepAlive(); // Explicitly opt-in to persistence
return AuthState();
}
}
Example: provider_state_class
// NOT RECOMMENDED - State class not @freezed
class AuthState {
final bool isLoggedIn;
AuthState(this.isLoggedIn);
}
// RECOMMENDED - Immutable state with @freezed
@freezed
class AuthState with _$AuthState {
const factory AuthState({
@Default(false) bool isLoggedIn,
User? user,
}) = _AuthState;
}
Example: provider_single_per_file
// NOT RECOMMENDED - lib/providers/auth_provider.dart
final authProvider = NotifierProvider.autoDispose(AuthNotifier.new);
final userProvider = NotifierProvider.autoDispose(UserNotifier.new); // ERROR!
// RECOMMENDED - One provider per file
// lib/providers/auth_provider.dart -> authProvider
// lib/providers/user_provider.dart -> userProvider
Example: async_viewmodel_safety
// NOT RECOMMENDED - Unhandled async errors
class UserNotifier extends AsyncNotifier<User> {
Future<void> fetchUser() async {
await userRepository.getUser(); // ERROR: No try/catch
}
}
// RECOMMENDED - Proper error handling
class UserNotifier extends AsyncNotifier<User> {
Future<void> fetchUser() async {
try {
await userRepository.getUser();
} catch (e) {
ref.showExceptionSheet(e);
}
}
}
Example: viewmodel_naming_convention
// NOT RECOMMENDED - lib/providers/auth_provider.dart
class AuthViewModel extends Notifier<AuthState> {} // ERROR: Must end with "Notifier"
class Auth extends Notifier<AuthState> {} // ERROR: Must end with "Notifier"
// RECOMMENDED
class AuthNotifier extends Notifier<AuthState> {} // Correct naming
class UserAuthNotifier extends Notifier<AuthState> {} // Also correct
Example: no_context_in_providers
// NOT RECOMMENDED - BuildContext in provider method
class AuthNotifier extends Notifier<AuthState> {
void showError(BuildContext context) { // ERROR: No BuildContext!
ScaffoldMessenger.of(context).showSnackBar(...);
}
}
// RECOMMENDED - UI-agnostic, let the UI handle presentation
class AuthNotifier extends Notifier<AuthState> {
void setError(String message) {
state = state.copyWith(errorMessage: message);
}
}
Example: provider_class_restriction
// NOT RECOMMENDED - lib/providers/auth_provider.dart
class AuthHelper {} // ERROR: Only Notifier classes allowed
class AuthUtils {} // ERROR: Move to utils/
@freezed
class AuthState with _$AuthState {} // ALLOWED: @freezed state class
// RECOMMENDED - Only Notifier and @freezed state classes
class AuthNotifier extends Notifier<AuthState> {}
@freezed
class AuthState with _$AuthState {
const factory AuthState({...}) = _AuthState;
}
Category C: Repository & Data Integrity #
| Rule | Target | Description |
|---|---|---|
repository_provider_declaration |
repositories/ |
Must define a Provider ending with RepoProvider |
repository_dependency_injection |
repositories/ |
Dependencies must be injected via constructor; Ref parameter banned |
repository_class_restriction |
repositories/ |
Only classes with "Repository" in name; files must end with _repository.dart |
repository_no_try_catch |
repositories/ |
Repositories must throw errors, not catch them |
repository_async_return |
repositories/ |
Public methods must return Future<T> or Stream<T> |
Example: repository_provider_declaration
// lib/repositories/auth_repository.dart
// RECOMMENDED - Provider at top ending with RepoProvider
final authRepoProvider = Provider<AuthRepository>((ref) {
final dio = ref.watch(dioProvider);
return AuthRepository(dio);
});
class AuthRepository {
final Dio _dio;
AuthRepository(this._dio);
}
Example: repository_dependency_injection
// NOT RECOMMENDED - Direct instantiation
class AuthRepository {
final Dio _dio = Dio(); // ERROR: Create objects directly
}
// NOT RECOMMENDED - Ref parameter
class AuthRepository {
final Ref ref; // ERROR: Ref not allowed
AuthRepository(this.ref);
}
// RECOMMENDED - Constructor injection
class AuthRepository {
final Dio _dio;
AuthRepository(this._dio); // Injected via provider
}
Example: repository_no_try_catch
// NOT RECOMMENDED - Swallowing errors
class UserRepository {
Future<User?> getUser(String id) async {
try {
return await api.fetchUser(id);
} catch (e) {
return null; // ERROR: Hiding the error!
}
}
}
// RECOMMENDED - Let errors bubble up
class UserRepository {
Future<User> getUser(String id) async {
return await api.fetchUser(id); // Throws on error
}
}
Example: repository_class_restriction
// NOT RECOMMENDED - lib/repositories/user_helper.dart
class UserHelper {} // ERROR: File must end with _repository.dart
// NOT RECOMMENDED - lib/repositories/user_repository.dart
class UserService {} // ERROR: Class must contain "Repository"
// RECOMMENDED - lib/repositories/user_repository.dart
class UserRepository {
Future<User> getUser(String id) async { ... }
}
// RECOMMENDED - Private helper classes are allowed
class _UserCacheHelper {} // ALLOWED: Private class
Example: repository_async_return
// NOT RECOMMENDED - Synchronous public methods
class UserRepository {
User getUser(String id) { ... } // ERROR: Must return Future<T>
List<User> getAllUsers() { ... } // ERROR: Must return Future<T> or Stream<T>
}
// RECOMMENDED - Async public methods
class UserRepository {
Future<User> getUser(String id) async { ... }
Stream<List<User>> watchUsers() { ... }
// Private methods can be sync
User _parseUser(Map<String, dynamic> json) { ... } // ALLOWED: Private
}
Category D: Code Quality & Complexity #
| Rule | Description |
|---|---|
complexity_limits |
Max 4 parameters, max 3 nesting levels, max 60 lines per method, max 120 lines in build(), no nested ternary |
global_variable_restriction |
Top-level variables must be private (_), constants (k prefix), or Providers. Top-level functions must be private, k-prefixed (in constants.dart), or main() |
print_ban |
print() and debugPrint() are banned; use custom logging using log() extension method on Object |
barrel_file_restriction |
No index.dart barrel files in screens/, features/, widgets/, or providers/ |
ignore_file_ban |
// ignore_for_file: comments are banned |
Example: complexity_limits
// NOT RECOMMENDED - Nested ternary
Widget build(BuildContext context) {
return isLoading
? LoadingWidget()
: hasError
? ErrorWidget() // ERROR: Nested ternary!
: ContentWidget();
}
// RECOMMENDED - Use if/else or switch
Widget build(BuildContext context) {
if (isLoading) return LoadingWidget();
if (hasError) return ErrorWidget();
return ContentWidget();
}
// NOT RECOMMENDED - Too many parameters (max 4)
void updateUser(
String id,
String name,
String email,
String phone,
String address, // ERROR: More than 4 parameters
) {}
// RECOMMENDED - Use a parameter object
void updateUser(UpdateUserParams params) {}
// NOT RECOMMENDED - Method exceeds 60 lines
void processData() {
// ... 61+ lines of code ... // ERROR!
}
// RECOMMENDED - Extract into smaller methods
void processData() {
_validateInput();
_transformData();
_saveResult();
}
Example: global_variable_restriction
// NOT RECOMMENDED - lib/utils/helpers.dart
String globalApiUrl = 'https://api.example.com'; // ERROR!
void helperFunction() {} // ERROR: Top-level function
// RECOMMENDED - lib/utils/constants.dart
const kApiUrl = 'https://api.example.com'; // ALLOWED: k prefix in constants.dart
void kFormatDate() {} // ALLOWED: k prefix in constants.dart
// RECOMMENDED - lib/providers/config_provider.dart
final configProvider = Provider((ref) => Config()); // ALLOWED: Provider variable
// RECOMMENDED - Private functions anywhere
void _internalHelper() {} // ALLOWED: Private function
Example: print_ban
// NOT RECOMMENDED - Using print statements
void doSomething() {
print('Debug info'); // ERROR: Use logging instead
debugPrint('More debug'); // ERROR: Also banned
}
// RECOMMENDED - Use structured logging
void doSomething() {
'Debug info'.log(); // Custom log extension
logger.info('Structured log'); // Logging framework
}
Example: barrel_file_restriction
// NOT RECOMMENDED - lib/screens/index.dart (barrel file)
export 'home_screen.dart';
export 'profile_screen.dart';
// ERROR: No barrel files in screens/, widgets/, providers/
// NOT RECOMMENDED - lib/widgets/index.dart
export 'button.dart';
export 'card.dart';
// ERROR: Barrel files are banned
// RECOMMENDED - Import directly
import 'package:my_app/screens/home_screen.dart';
import 'package:my_app/widgets/button.dart';
Example: ignore_file_ban
// NOT RECOMMENDED - File-level ignore
// ignore_for_file: print_ban
// ERROR: File-level ignores are banned!
print('This would be ignored'); // But we don't allow this
// RECOMMENDED - Line-specific ignore for rare exceptions
// ignore: print_ban
print('Debug only - remove before commit');
Category E: UI Safety & Consistency #
| Rule | Target | Description |
|---|---|---|
hook_safety_enforcement |
build() methods |
Controllers must use hooks; GlobalKey<FormState>() banned in HookWidgets |
scaffold_location |
widgets/ |
Scaffold is not allowed in widgets folder |
asset_safety |
All files | Image.asset() must use constants, not string literals |
file_class_match |
All files | Class name must match file name (snake_case to PascalCase) |
Example: hook_safety_enforcement
// NOT RECOMMENDED - Memory leak in build
class MyWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = TextEditingController(); // ERROR: Leaks memory!
return TextField(controller: controller);
}
}
// RECOMMENDED - Use hooks
class MyWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final controller = useTextEditingController();
return TextField(controller: controller);
}
}
// NOT RECOMMENDED - GlobalKey<FormState> resets on keyboard open/orientation change
class MyFormWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = GlobalKey<FormState>(); // ERROR: Resets unexpectedly!
return Form(key: formKey, child: ...);
}
}
// RECOMMENDED - Use GlobalObjectKey with context for stable identity
class MyFormWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = GlobalObjectKey<FormState>(context); // Stable across rebuilds
return Form(key: formKey, child: ...);
}
}
Example: scaffold_location
// NOT RECOMMENDED - lib/widgets/user_card.dart
class UserCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold( // ERROR: Scaffold in widgets/ folder
body: Text('User info'),
);
}
}
// RECOMMENDED - lib/screens/user_screen.dart
class UserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold( // ALLOWED: Scaffold in screens/ folder
body: UserCard(),
);
}
}
// RECOMMENDED - lib/widgets/user_card.dart
class UserCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card( // ALLOWED: No Scaffold
child: Text('User info'),
);
}
}
Example: asset_safety
// NOT RECOMMENDED - Typo-prone string literal
Image.asset('assets/images/logo.png'); // ERROR!
// RECOMMENDED - Use constants
// lib/utils/images.dart
class Images {
static const logo = 'assets/images/logo.png';
}
// Usage
Image.asset(Images.logo);
Example: file_class_match
// File: lib/screens/user_profile_screen.dart
// NOT RECOMMENDED
class ProfilePage {} // ERROR: Should be UserProfileScreen
// RECOMMENDED
class UserProfileScreen {} // Matches file name
Category F: Flutter Best Practices #
| Rule | Description |
|---|---|
avoid_consecutive_sliver_to_box_adapter |
Use SliverList.list() instead of consecutive SliverToBoxAdapter widgets |
avoid_hardcoded_color |
Use Theme.of(context).colorScheme instead of hardcoded colors |
avoid_shrink_wrap_in_list_view |
Avoid shrinkWrap: true in ListView; use SliverList instead |
avoid_single_child |
Don't use Column/Row/Stack with single child; use appropriate widget |
prefer_dedicated_media_query_methods |
Use MediaQuery.sizeOf() instead of MediaQuery.of().size |
prefer_space_between_elements |
Require blank lines between class members |
prefer_to_include_sliver_in_name |
Widgets returning Slivers should have "Sliver" in name |
unsafe_null_assertion |
Avoid force null assertion (!); use ?? or null-aware operators |
avoid_unnecessary_padding_widget |
Don't wrap Container with Padding; use Container's margin/padding |
unnecessary_hook_widget |
Use StatelessWidget instead of HookWidget when no hooks are used |
unnecessary_container |
Remove Container when it doesn't use any Container-specific properties |
Example: avoid_consecutive_sliver_to_box_adapter
// NOT RECOMMENDED - Inefficient consecutive SliverToBoxAdapter
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Text('Item 1')),
SliverToBoxAdapter(child: Text('Item 2')),
SliverToBoxAdapter(child: Text('Item 3')),
],
)
// RECOMMENDED - Use SliverList.list
CustomScrollView(
slivers: [
SliverList.list(
children: [
Text('Item 1'),
Text('Item 2'),
Text('Item 3'),
],
),
],
)
Example: avoid_hardcoded_color
// NOT RECOMMENDED - Hardcoded colors don't adapt to themes
Container(color: Color(0xFF00FF00))
Container(color: Colors.red)
// RECOMMENDED - Use theme colors
Container(color: Theme.of(context).colorScheme.primary)
Container(color: Theme.of(context).colorScheme.surface)
// ALLOWED - Transparent is allowed
Container(color: Colors.transparent)
Example: avoid_shrink_wrap_in_list_view
// NOT RECOMMENDED - shrinkWrap causes performance issues
ListView(
shrinkWrap: true, // ERROR: Avoid shrinkWrap
children: items,
)
// RECOMMENDED - Use SliverList in CustomScrollView
CustomScrollView(
slivers: [
SliverList.builder(
itemCount: items.length,
itemBuilder: (context, index) => items[index],
),
],
)
Example: avoid_single_child
// NOT RECOMMENDED - Multi-child widget with single child
Column(
children: [Text('Hello')], // ERROR: Use single-child widget
)
Row(
children: [Icon(Icons.star)], // ERROR: Unnecessary Row
)
// RECOMMENDED - Use appropriate single-child widgets
Center(child: Text('Hello'))
Align(alignment: Alignment.centerLeft, child: Icon(Icons.star))
Example: prefer_dedicated_media_query_methods
// NOT RECOMMENDED - Causes unnecessary rebuilds
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; // ERROR!
final padding = MediaQuery.of(context).padding; // ERROR!
return Container(width: size.width);
}
// RECOMMENDED - Use dedicated methods (more efficient)
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context);
return Container(width: size.width);
}
Example: prefer_space_between_elements
// NOT RECOMMENDED - No spacing between members
class MyClass {
final String name;
final int age;
void greet() {}
void farewell() {} // ERROR: Missing blank line before method
}
// RECOMMENDED - Blank lines between members
class MyClass {
final String name;
final int age;
void greet() {}
void farewell() {}
}
Example: prefer_to_include_sliver_in_name
// NOT RECOMMENDED - Returns sliver but name doesn't indicate it
class MyHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter( // ERROR: Name should include "Sliver"
child: Text('Header'),
);
}
}
// RECOMMENDED - Name indicates it returns a sliver
class MySliverHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Text('Header'),
);
}
}
Example: avoid_unnecessary_padding_widget
// NOT RECOMMENDED - Padding wrapping Container
Padding(
padding: EdgeInsets.all(16),
child: Container( // ERROR: Use Container's margin instead
color: Colors.blue,
child: Text('Hello'),
),
)
// RECOMMENDED - Use Container's margin property
Container(
margin: EdgeInsets.all(16),
color: Colors.blue,
child: Text('Hello'),
)
Example: unsafe_null_assertion
// NOT RECOMMENDED - Force null assertion can crash
String getValue(String? name) => name!;
// RECOMMENDED - Use null coalescing
String getValue(String? name) => name ?? 'default';
// RECOMMENDED - Use null-aware access
String? getUserName(User? user) => user?.name;
Example: unnecessary_hook_widget
// NOT RECOMMENDED - HookWidget without any hooks
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) => Text('Hello');
}
// RECOMMENDED - Use StatelessWidget when no hooks needed
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => Text('Hello');
}
// RECOMMENDED - HookWidget with hooks
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useTextEditingController();
return TextField(controller: controller);
}
}
Example: unnecessary_container
// NOT RECOMMENDED - Container with only child
Container(
child: Text('Hello'), // ERROR: Container adds no value
)
Container(
key: Key('myKey'),
child: Text('Hello'), // ERROR: Only key and child, Container is useless
)
// RECOMMENDED - Just use the widget directly
Text('Hello')
// RECOMMENDED - Container with meaningful properties
Container(
padding: EdgeInsets.all(8),
child: Text('Hello'),
)
Container(
color: Theme.of(context).colorScheme.surface,
child: Text('Hello'),
)
Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: Text('Hello'),
)
Category G: Resource Management #
| Rule | Target | Description |
|---|---|---|
remove_listener |
State classes | Listeners added via addListener must be removed in dispose() |
dispose_notifier |
State classes | ChangeNotifier instances (controllers) must be disposed |
Example: remove_listener
// NOT RECOMMENDED - Listener never removed (memory leak)
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
widget.controller.addListener(_onChanged); // ERROR!
}
void _onChanged() {}
@override
Widget build(BuildContext context) => Container();
}
// RECOMMENDED - Properly remove listener
class _MyWidgetState extends State<MyWidget> {
@override
void initState() {
super.initState();
widget.controller.addListener(_onChanged);
}
@override
void dispose() {
widget.controller.removeListener(_onChanged);
super.dispose();
}
void _onChanged() {}
@override
Widget build(BuildContext context) => Container();
}
Example: dispose_notifier
// NOT RECOMMENDED - Controller never disposed (memory leak)
class _MyWidgetState extends State<MyWidget> {
final _controller = TextEditingController(); // ERROR!
@override
Widget build(BuildContext context) => TextField(controller: _controller);
}
// RECOMMENDED - Properly dispose controller
class _MyWidgetState extends State<MyWidget> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => TextField(controller: _controller);
}
Suppressing Rules #
While // ignore_for_file: is banned, you can still use line-specific ignores for rare exceptions:
// ignore: print_ban
print('Debug only - remove before commit');
CI/CD Integration #
Add to your CI pipeline to enforce architecture:
# GitHub Actions example
- name: Run Analysis
run: dart analyze --fatal-infos --fatal-warnings
Philosophy #
"Architecture is about intent. These rules make your intent explicit and your boundaries clear."
The Arsync architecture is designed to:
- Prevent spaghetti code - Clear boundaries between layers
- Enable testability - Each layer can be tested in isolation
- Improve maintainability - New developers understand the structure immediately
- Catch issues early - Violations are build errors, not runtime surprises
Contributing #
Contributions are welcome! Please feel free to submit issues and pull requests.
License #
MIT License - see LICENSE for details.