Привет! Меня зовут Никита Грибков, я Flutter-разработчик в AGIMA. Хочу в очередной раз поднять важную тему — CodeStyle. Думаю, что все понимают преимущества единообразного, понятного, красивого кода. Но к сожалению, оформить единые правила для всей команды — это большая задача, и выделить на нее время получается не всегда. Мы решили эту ситуацию изменить.
Недавно я осознал, как сильно раздражает разбираться с долгосрочными проектами, которые мы развиваем годами. За это время команда неизбежно меняется, и каждый разработчик привносит свой уникальный стиль. Как результат, понять, что хотел сделать предыдущий автор, бывает настоящим испытанием. Именно поэтому мы с коллегами решили внедрить единый стандарт разработки, которым я теперь делюсь с читателями Хабра.
Надеюсь, собранные здесь правила помогут вам сэкономить пару-тройку рабочих часов, но главное — сберегут нервы.
Зачем нам вообще CodeStyle
Но для начала давайте освежим в памяти, что такое CodeStyle и зачем он вообще нужен. Если совсем просто, то CodeStyle — это свод правил, которые помогают разным разработчикам писать код в одном стиле. Благодаря этому код выглядит одинаково и понятен другим.
В целом же можно выделить такие задачи CodeStyle:
-
Улучшение читаемости. Чистый, структурированный и единообразный код легко читать и понимать даже разработчикам, которые впервые работают с проектом. Это снижает порог вхождения в команду и помогает быстро находить ошибки.
-
Стандартизация. Придерживаясь единых правил, разработчики могут писать код, который выглядит так, будто его написал один человек, даже если проектом занимается большая команда. Это особенно важно для масштабируемых приложений.
-
Поддерживаемость. Код с четкими стандартами легче поддерживать и рефакторить. Это сокращает время, необходимое для внесения изменений, и снижает вероятность появления ошибок.
Почему при написании Flutter-кода дисциплина особенно важна
Flutter присуща гибкость, которую одновременно можно считать его силой и слабостью. Если нет строгих правил, на проекте может начаться хаос:
-
Разрозненная структура файлов и директорий затрудняет поиск нужных компонентов.
-
Разные подходы к оформлению кода приводят к несоответствиям, которые усложняют понимание и ревью.
-
Непоследовательное использование инструментов и библиотек вызывает путаницу и дублирование функциональности.
Дисциплина помогает преодолеть эти вызовы:
-
Создает прозрачную и предсказуемую структуру проекта.
-
Обеспечивает простоту интеграции новых разработчиков.
-
Упрощает работу с внешними зависимостями, управление состоянием и обработку данных.
В итоге соблюдение CodeStyle мы воспринимаем не только как инструмент эстетики, но и как важный компонент разработки в целом.
Как эффективно внедрить CodeStyle
-
Обучение. Стоит проводить мастер-классы, воркшопы или небольшие лекции, на которых можно показывать преимущества единого CodeStyle. При этом лучше использовать примеры из реальных проектов, чтобы подчеркнуть, как это помогает ускорить работу и улучшить качество кода.
-
Автоматизация. Настройте линтеры и хуки, чтобы разработчики сразу видели, когда их код не соответствует стандартам. Внедрите форматирование кода (например, flutter format) как обязательное требование.
-
Код-ревью. Хорошая идея — сделать ревью обязательным этапом каждого Pull Request. Это не только позволит следить за соблюдением CodeStyle, но и улучшит общий уровень кода в проекте. Подключите документацию CodeStyle как справочник, чтобы ревьюеры могли ссылаться на правила.
-
Командное соглашение. Включите CodeStyle в культуру команды: создайте общее соглашение о том, что стандарты — это не ограничения, а инструмент, который упрощает жизнь каждому разработчику.
Инструменты автоматизации
Автоматизация процессов — ключ к поддержанию стандарта кода. Линтеры и другие инструменты помогают команде соблюдать CodeStyle без лишних усилий.
Чтобы настроить линтеры, нужно добавить flutter_lints в проект для автоматической проверки базовых правил:
dev_dependencies: flutter_lints: ^2.0.0
Это обеспечит соблюдение стандартов форматирования и поможет избежать распространенных ошибок.
Pre-commit хуки — еще один важный элемент автоматизации. Интегрировать их можно с помощью инструментов вроде Husky или Lefthook, чтобы автоматически проверять и форматировать код перед каждым коммитом.
Базовые правила нашего CodeStyle
Директорий и файлы
Именуем файлы и директории только по шаблону name_case
. Те и другие обязательно должны соответствовать архитектуре папок DDD. Названия файлов должны соответствовать первому и главному используемому классу внутри файла.
example_package/ └─ file_example.dart └─ slider_menu_example.dart
Форматирование
Расставляются все возможные запятые, в соответствие с правилами линтера (Trailing commas).
Trailing commas (запятые в конце последнего элемента) — это небольшая, но важная деталь, которая помогает улучшить читаемость сложных структур кода и упрощает автоматическое форматирование.
Выносим части кода, которые можем выделить, в отдельные функции/методы. Это повышает читаемость кода и сокращает размер основного Widget. |
ListView.separated( shrinkWrap: true, itemCount: recentSearches.length, separatorBuilder: _separatorBuilder, itemBuilder: ( BuildContext context, int index, ) { final queryItem = recentSearches[index]; return _LastQueryCard( title: queryItem.query, onTap: () => context.read<GlobalSearchBloc>().add( RemoveRecentSearchEvent( query: queryItem, ), ), ); }, ), Widget _separatorBuilder( BuildContext context, int _, ) { final themeExtension = AppTheme.themeExtension(context); final s16Value = themeExtension.sizes.s16Value; return SizedBox( height: s16Value, ); }
Переменные и функции
Именование переменных: примеры хорошей практики
Именование переменных — важный аспект читаемости кода. Хорошо подобранные имена помогают разработчикам быстрее понять назначение переменной и улучшить сопровождение проекта.
Основные правила:
-
Использовать camelCase для локальных переменных и свойств класса. Это стандарт в Dart.
final userAge = 25; final isActive = true;
-
Для констант и глобальных переменных рекомендуется использовать
SCREAMING_SNAKE_CASE
:
const MAX_CONNECTIONS = 10; const API_URL = 'https://api.example.com';
-
Имена должны быть понятными и отражать смысл переменной:
// Плохо int x = 25; // Хорошо int userAge = 25;
Приватные методы и геттеры: минимизация публичных функций
В архитектуре приложений важно скрывать внутренние детали реализации и оставлять публичными только те методы и свойства, которые нужны внешнему коду. Приведу примеры.
Приватный метод:
class UserManager { // Приватный метод используется только внутри класса String _hashPassword(String password) { return 'hashed_$password'; } void saveUser(String username, String password) { final hashed = _hashPassword(password); // Сохранение данных... } }
Приватный геттер:
class Config { String get _apiKey => 'super_secret_key'; // только для внутреннего использования }
Рекомендации:
-
Оставлять публичными только те методы, которые реально необходимы для работы внешнего кода.
-
Упрощать интерфейс классов, скрывая сложные детали.
Классы и архитектура
Объявление конструкторов: выбор между именованными и фабричными
Конструкторы в Dart позволяют гибко управлять процессом создания объектов.
-
Именованные конструкторы часто используют для разных способов инициализации объекта:
class User { final String name; final int age; User(this.name, this.age); // Именованный конструктор User.fromJson(Map<String, dynamic> json) : name = json['name'], age = json['age']; }
-
Фабричные конструкторы удобны, когда требуется управлять созданием объектов или возвращать кэшированные экземпляры:
class Singleton { static final Singleton _instance = Singleton._internal(); factory Singleton() => _instance; Singleton._internal(); }
Примеры декомпозиции виджетов и рекомендаций по Container:
-
Громоздкие Build-методы делают код нечитаемым. Вместо этого стоит разделять логику на мелкие виджеты:
class UserProfile extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ ProfilePicture(), UserName(), ], ); } } class ProfilePicture extends StatelessWidget { @override Widget build(BuildContext context) { return CircleAvatar(radius: 40, child: Text('A')); } } class UserName extends StatelessWidget { @override Widget build(BuildContext context) { return Text('Alex Johnson'); } }
-
Также Container стоит использовать, только если нужны стили (паддинги, отступы, декорации). Для других целей лучше выбирать специализированные виджеты:
// Плохо Container( child: Text('Hello!'), ); // Хорошо Padding( padding: const EdgeInsets.all(8.0), child: Text('Hello!'), );
Управление состоянием
Организация папок в BloC
Четкая структура папок помогает держать код в порядке. Рекомендуемая структура для Bloc:
lib/ application/ auth/ auth_bloc.dart auth_event.dart auth_state.dart settings/ settings_bloc.dart settings_event.dart settings_state.dart
Оптимальное решение — использовать реализацию BloC от flutter_bloc + Equtable.
BloC-виджеты
Для виджетов BlocListener
, BlocBuilder
, BlocConsumer
стоит всегда указывать параметры ограничения обновления состояния: listenWhen
и buildWhen
.
BlocBuilder<ExampleBloc, ExampleState>( buildWhen: ( ExampleState previous, ExampleState current, ) => previous != current, builder: ( BuildContext context, ExampleState state, ) { ... }, );
Также необходимо использовать явно BlocSelector для определения обновления состояния конкретной части виджета.
BloC–bloc
В данном случае такой подход дает нам удобство в использовании самих ивентов и их реализаций.
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> { final ExampleInteractor _interactor; ExampleBloc( required ExampleInteractor interactor ) : _interactor = interactor, super( const ExampleLoadingState(), ) { on<_PutDate>(_putDate); } Future<void> _putDate( _PutDate event, Emitter<ExampleState> emit, ) async { final putDateOrFailure = await interactor.putDate(date: date); putDateOrFailure.fold( (failure) => emit( ExampleFailureState( error: failure.error, ), ), (unit) => emit( ExampleFatchedState(), ), ); } }
BloC–state
State может быть реализован в виде общего класса с данными PageState, как изложено в документации [[BloC]], но есть вариант, который соответствует более узкой логике.
Наш state выглядит в архитектуре всё так же. У нас есть ExampleLoadingState, ExampleFailureState, ExampleFetchedState. Каждый из state принимает в себя те параметры, которые нужны под данную реализацию.
ExampleLoadingState
Не имеет параметров => его задача индексировать state, на основе которого будет отрисован loader
.
final class ExampleLoadingState extends GlobalSearchState { const ExampleLoadingState(); }
ExampleFailureState
Имеет точную реализацию для отображения текста ошибки.
final class ExampleLFailureState extends GlobalSearchState { final String error; const ExampleLFailureState({required this.error}); @override List<Object> get props => [error]; }
ExampleFatchedState
В реализацию данного state
передаются данные, которые нужны для отображения основного контента страницы.
final class ExampleLFatchedState extends GlobalSearchState { final List<ExampleClass> result; const ExampleLFatchedState({required this.result}); @override List<Object> get props => [result]; }
State не должны знать друг о друге. Они должны отвечать за одну задачу, принцип единой ответственности S.O.L.I.D#S.
Работа с данными и HTTP
Добавление интерцепторов позволяет управлять запросами на уровне Dio. Пример настройки:
dio.interceptors.addAll([ InterceptorsWrapper( onRequest: (options, handler) { /// Через метод [onRequest], можем прослушивать наши запросы, /// и обрабатывать их в консоль. return handler.next(options); }, onResponse: (response, handler) { /// Обработка [response] return handler.next(response); }, onError: (error, handler) async { /// Обработка [error] if (error.type == DioExceptionType.connectionError) { /// Прослушиваем тип ошибки и обрабатываем интернет соединение } var code = error.response?.statusCode ?? 0; if ((code >= 300) && (code < 400)) { /// Обработка ошибок code >=300 && code <400 /// /// ```example /// final response = error.copyWith( /// error: ServerResponse( /// err: error.toString(), /// code: (error.response?.statusCode ?? '').toString(), /// body: error.response?.data, /// ), /// ); /// ``` /// return handler.reject(response); } else if ((code >= 400) && (code < 500)) { /// Обработка ошибок code >=400 && code <500 } else if (code > 500) { /// Обработка ошибок code >500 } }, ), // adding logger ])
Примеры антипаттернов
Чтобы избежать дублирования кода, следует выносить повторяющиеся элементы в отдельные виджеты:
class CustomButton extends StatelessWidget { final String label; const CustomButton({required this.label}); @override Widget build(BuildContext context) { return ElevatedButton(onPressed: () {}, child: Text(label)); } }
Итого
Единый CodeStyle стал для нашей команды решением, которое не только упорядочило работу, но и кардинально изменило подход к разработке. Мы избавились от хаоса в структуре проектов, стандартизировали управление состоянием, форматирование и работу с данными. В итоге код стал понятным, поддерживаемым и легким для масштабирования.
А это, в свою очередь, позволило ускорить код-ревью, упростить интеграцию новых разработчиков и обеспечить высокое качество продукта. Теперь, вместо того чтобы тратить время на разбор несогласованных решений, мы фокусируемся на создании новых функций.
Я даже могу утверждать, что CodeStyle стал не просто инструментом, но и основой нашего успеха.
Если у вас остались вопросы, буду рад ответить в комментариях. Рассказывайте, как вы внедряли CodeStyle на своих проектах и насколько легко это было. В общем, давайте обмениваться опытом. А если вам интересен Flutter, подписывайтесь на телеграм-канал нашего Head of Mobile Саши Ворожищева.
Что еще почитать
ссылка на оригинал статьи https://habr.com/ru/articles/873576/
Добавить комментарий