CodeStyle на Flutter-проектах: базовые принципы и правила — шаблон на все случаи жизни

Привет! Меня зовут Никита Грибков, я Flutter-разработчик в AGIMA. Хочу в очередной раз поднять важную тему — CodeStyle. Думаю, что все понимают преимущества единообразного, понятного, красивого кода. Но к сожалению, оформить единые правила для всей команды — это большая задача, и выделить на нее время получается не всегда. Мы решили эту ситуацию изменить.

Недавно я осознал, как сильно раздражает разбираться с долгосрочными проектами, которые мы развиваем годами. За это время команда неизбежно меняется, и каждый разработчик привносит свой уникальный стиль. Как результат, понять, что хотел сделать предыдущий автор, бывает настоящим испытанием. Именно поэтому мы с коллегами решили внедрить единый стандарт разработки, которым я теперь делюсь с читателями Хабра.

Надеюсь, собранные здесь правила помогут вам сэкономить пару-тройку рабочих часов, но главное — сберегут нервы.

22b66de79410e8b91305c5d680ce4634.png

Зачем нам вообще CodeStyle

Но для начала давайте освежим в памяти, что такое CodeStyle и зачем он вообще нужен. Если совсем просто, то CodeStyle — это свод правил, которые помогают разным разработчикам писать код в одном стиле. Благодаря этому код выглядит одинаково и понятен другим.

В целом же можно выделить такие задачи CodeStyle:

  1. Улучшение читаемости. Чистый, структурированный и единообразный код легко читать и понимать даже разработчикам, которые впервые работают с проектом. Это снижает порог вхождения в команду и помогает быстро находить ошибки.

  2. Стандартизация. Придерживаясь единых правил, разработчики могут писать код, который выглядит так, будто его написал один человек, даже если проектом занимается большая команда. Это особенно важно для масштабируемых приложений.

  3. Поддерживаемость. Код с четкими стандартами легче поддерживать и рефакторить. Это сокращает время, необходимое для внесения изменений, и снижает вероятность появления ошибок.

Почему при написании Flutter-кода дисциплина особенно важна

Flutter присуща гибкость, которую одновременно можно считать его силой и слабостью. Если нет строгих правил, на проекте может начаться хаос:

  • Разрозненная структура файлов и директорий затрудняет поиск нужных компонентов.

  • Разные подходы к оформлению кода приводят к несоответствиям, которые усложняют понимание и ревью.

  • Непоследовательное использование инструментов и библиотек вызывает путаницу и дублирование функциональности.

Дисциплина помогает преодолеть эти вызовы:

  • Создает прозрачную и предсказуемую структуру проекта.

  • Обеспечивает простоту интеграции новых разработчиков.

  • Упрощает работу с внешними зависимостями, управление состоянием и обработку данных.

В итоге соблюдение CodeStyle мы воспринимаем не только как инструмент эстетики, но и как важный компонент разработки в целом.

Как эффективно внедрить CodeStyle

  1. Обучение. Стоит проводить мастер-классы, воркшопы или небольшие лекции, на которых можно показывать преимущества единого CodeStyle. При этом лучше использовать примеры из реальных проектов, чтобы подчеркнуть, как это помогает ускорить работу и улучшить качество кода.

  2. Автоматизация. Настройте линтеры и хуки, чтобы разработчики сразу видели, когда их код не соответствует стандартам. Внедрите форматирование кода (например, flutter format) как обязательное требование.

  3. Код-ревью. Хорошая идея — сделать ревью обязательным этапом каждого Pull Request. Это не только позволит следить за соблюдением CodeStyle, но и улучшит общий уровень кода в проекте. Подключите документацию CodeStyle как справочник, чтобы ревьюеры могли ссылаться на правила.

  4. Командное соглашение. Включите 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().add(
            RemoveRecentSearchEvent(
              query: queryItem,
            ),
          ),
    );
  },
),


Widget _separatorBuilder(
	BuildContext context,
	int _,
) {
	final themeExtension = AppTheme.themeExtension(context);
	final s16Value = themeExtension.sizes.s16Value;

	return SizedBox(
		height: s16Value,
	);
}

Переменные и функции

Именование переменных: примеры хорошей практики

Именование переменных — важный аспект читаемости кода. Хорошо подобранные имена помогают разработчикам быстрее понять назначение переменной и улучшить сопровождение проекта.

Основные правила:

  1. Использовать camelCase для локальных переменных и свойств класса. Это стандарт в Dart.

final userAge = 25;
final isActive = true;
  1. Для констант и глобальных переменных рекомендуется использовать SCREAMING_SNAKE_CASE:

const MAX_CONNECTIONS = 10;
const API_URL = 'https://api.example.com';
  1. Имена должны быть понятными и отражать смысл переменной:

// Плохо
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 json)
      : name = json['name'],
        age = json['age'];
}
  • Фабричные конструкторы удобны, когда требуется управлять созданием объектов или возвращать кэшированные экземпляры:

class Singleton {
  static final Singleton _instance = Singleton._internal();
  factory Singleton() => _instance;
  Singleton._internal();
}

Примеры декомпозиции виджетов и рекомендаций по Container:

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(
  buildWhen: (
    ExampleState previous,
    ExampleState current,
  ) => previous != current,
  builder: (
    BuildContext context,
    ExampleState state,
  ) {
  
   ...
   
  },
);

Также необходимо использовать явно BlocSelector для определения обновления состояния конкретной части виджета.

BloC–bloc

В данном случае такой подход дает нам удобство в использовании самих ивентов и их реализаций.

class ExampleBloc extends Bloc {
  final ExampleInteractor _interactor;

  ExampleBloc(
	  required ExampleInteractor interactor
	  ) : _interactor = interactor,
	   super(
	      const ExampleLoadingState(),
	    ) {
     on<_PutDate>(_putDate);
  }

  Future _putDate(
    _PutDate event,
    Emitter 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 get props => [error];
}

ExampleFatchedState

В реализацию данного state передаются данные, которые нужны для отображения основного контента страницы. 

final class ExampleLFatchedState extends GlobalSearchState {
  final List result;

  const ExampleLFatchedState({required this.result});

  @override
  List 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 Саши Ворожищева.

Что еще почитать

© Habrahabr.ru