arsync_lints 0.1.1 copy "arsync_lints: ^0.1.1" to clipboard
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.

Dart Flutter License

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:

  1. Prevent spaghetti code - Clear boundaries between layers
  2. Enable testability - Each layer can be tested in isolation
  3. Improve maintainability - New developers understand the structure immediately
  4. 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.

0
likes
160
points
237
downloads

Publisher

unverified uploader

Weekly Downloads

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.

Homepage
Repository (GitHub)
View/report issues

Topics

#architecture #arsync #linter #static-analysis

Documentation

API reference

License

MIT (license)

Dependencies

analysis_server_plugin, analyzer, analyzer_plugin, path

More

Packages that depend on arsync_lints