Урок 24 из 25 · Месяц 6. Чат и публикация приложений

Мок-интервью и code review

Содержание урока

Техническое собеседование и code review — два навыка, без которых не обходится ни одна работа в команде. На собеседовании вас проверяют, понимаете ли вы, почему код работает, а на code review — умеете ли вы писать код, который удобно читать другим. В этом уроке разберём, как проходит собеседование, частые вопросы на junior и middle Flutter-разработчика с краткими ответами, а также как устроен code review и по какому чек-листу проверять код.

Как проходит собеседование

Обычно техническое собеседование на Flutter-разработчика состоит из нескольких частей:

  1. Знакомство (5–10 минут) — расскажите о себе и проектах. Подготовьте рассказ на 2 минуты: что делали, на каком стеке, какую задачу решили сами.
  2. Теория (20–30 минут) — вопросы по Dart, Flutter, управлению состоянием, архитектуре.
  3. Практика (20–40 минут) — live-coding (написать функцию или виджет), разбор чужого кода или обсуждение вашего тестового задания.
  4. Ваши вопросы — о команде, проекте, процессах. Задайте хотя бы один: это показывает интерес.

Мок-интервью (mock — «пробный») — репетиция собеседования: ментор или одногруппник играет роль интервьюера. Это лучший способ перестать волноваться и найти пробелы в знаниях.

Вопросы по Dart

Чем final отличается от const? final — значение задаётся один раз, но может вычисляться во время работы (final now = DateTime.now()). const — значение известно на этапе компиляции, а одинаковые const-объекты — это один объект в памяти. Поэтому const-виджеты не пересоздаются при перерисовке.

Что такое null safety? Тип String не может быть null, а String? — может. Компилятор заставляет проверить null до использования. Оператор ! говорит «я уверен, что тут не null» — если ошиблись, будет ошибка во время работы.

Чем Future отличается от Stream? Future — одно значение в будущем, Stream — последовательность значений (см. урок о чате и потоках).

Как Dart выполняет асинхронный код, если он однопоточный? Через event loop: есть очередь микрозадач (microtask) и очередь событий. await не блокирует поток — функция «встаёт на паузу», а цикл событий выполняет другие задачи. Для тяжёлых вычислений (парсинг огромного JSON) используют Isolate — отдельный поток со своей памятью, например через Isolate.run.

final result = await Isolate.run(() => heavyParse(bigJsonString));

Что такое mixin и extension? mixin — набор методов, который можно «подмешать» в класс через with, без наследования. extension — добавляет методы к существующему типу, не меняя его ('text'.capitalize()).

Что такое sealed class и зачем он? Класс, у которого все наследники известны в пределах файла. Компилятор проверяет, что switch обработал все варианты, — удобно для событий и состояний Bloc.

Вопросы по Flutter

Что такое три дерева во Flutter? Widget — неизменяемое описание («чертёж»). Element — живой экземпляр в дереве, связывает виджет и его место. RenderObject — отвечает за размеры, расположение и отрисовку. Виджеты пересоздаются часто и дёшево, а Element и RenderObject по возможности переиспользуются.

Чем StatelessWidget отличается от StatefulWidget? У Stateless нет изменяемого состояния — он зависит только от параметров. У Stateful есть объект State, который живёт дольше самого виджета и может вызвать setState, чтобы перерисоваться.

Назовите жизненный цикл State. createState → initState (один раз, подписки и контроллеры) → didChangeDependencies → build (много раз) → didUpdateWidget (родитель передал новые параметры) → dispose (освобождаем ресурсы).

Что такое BuildContext? Это ссылка на Element — место виджета в дереве. Через него ищут предков: Theme.of(context), context.read<Bloc>().

Зачем нужны Key? Чтобы Flutter правильно сопоставлял виджеты при перестановке однотипных элементов. Без ключей при удалении элемента из списка состояние (например, текст в поле) может «переехать» к соседу. Для элементов списка используют ValueKey(item.id).

Почему не стоит делать тяжёлую работу в build? build вызывается очень часто — при каждом setState, смене темы, анимации. Запросы и создание потоков в build приводят к лишним запросам и подтормаживаниям.

Что такое hot reload и hot restart? Hot reload подгружает изменённый код и сохраняет состояние (работает благодаря JIT). Hot restart перезапускает приложение с нуля, состояние сбрасывается. Подробнее — в уроке о JIT и AOT.

Вопросы по состоянию и архитектуре

Чем Bloc отличается от Cubit? В Cubit вызываете методы (cubit.increment()), в Bloc — отправляете события (bloc.add(Increment())). Bloc многословнее, но даёт историю событий и трансформеры (debounce, отмена предыдущих запросов). Cubit проще для простой логики.

BlocBuilder, BlocListener, BlocConsumer, BlocSelector — в чём разница? Builder перерисовывает UI, Listener выполняет одноразовые действия (навигация, SnackBar), Consumer — оба сразу, Selector перерисовывает только при изменении выбранной части состояния (урок).

Зачем Equatable в состояниях? Bloc не выдаёт новое состояние, если оно == текущему. Без Equatable сравниваются ссылки, и одинаковые по содержанию состояния считаются разными.

Расскажите о Clean Architecture. Три слоя: presentation (UI и Bloc), domain (сущности, UseCase, абстрактные репозитории — чистый Dart без Flutter), data (модели, источники данных, реализации репозиториев). Зависимости направлены внутрь, к domain. Плюсы: тестируемость, замена источников данных без переписывания UI.

Что такое DI и зачем GetIt? Dependency Injection — объект получает зависимости снаружи, а не создаёт сам. GetIt — контейнер (service locator), где регистрируются зависимости: registerLazySingleton, registerFactory. Это позволяет подменять реализации в тестах.

Чем Riverpod отличается от Provider? Riverpod не зависит от BuildContext, ошибки находятся на этапе компиляции, провайдеры можно комбинировать и переопределять в тестах.

Вопросы по сети и данным

  • Как обработать ошибки Dio? Поймать DioException и посмотреть type (таймаут, нет соединения, плохой ответ) и response?.statusCode; превратить в понятную доменную ошибку.
  • Где хранить токен? В flutter_secure_storage (Keychain/Keystore), не в SharedPreferences.
  • Как обновить истёкший токен? Интерцептор Dio: на 401 обновить токен и повторить запрос.
  • Как сделать пагинацию? Хранить номер страницы и флаг hasReachedMax, догружать при прокрутке к концу, не запускать второй запрос, пока идёт первый.

Как проходит code review

Code review — проверка кода коллегами перед тем, как он попадёт в основную ветку. Процесс обычно такой:

  1. Вы создаёте ветку (feature/cart-badge), делаете изменения и открываете Pull Request (PR, в GitLab — Merge Request).
  2. В описании PR пишете: что сделано, зачем, как проверить, скриншоты для UI.
  3. Ревьюер читает изменения и оставляет комментарии к строкам.
  4. Вы исправляете или объясняете своё решение, ревьюер одобряет (Approve), код вливается.

Правила хорошего тона:

  • PR маленький — до 300–400 строк. Огромный PR невозможно нормально проверить.
  • Критикуют код, а не человека: «здесь может быть утечка подписки», а не «ты опять забыл».
  • Объясняйте, почему: не «переделай», а «вынеси в UseCase — так проще тестировать».
  • Разделяйте важное и вкусовое: префикс nit: (nitpick, мелочь) означает «можно не исправлять».

Пример ревью

Посмотрим на код, который пришёл на ревью:

class ProductsPage extends StatefulWidget {
  @override
  _ProductsPageState createState() => _ProductsPageState();
}

class _ProductsPageState extends State<ProductsPage> {
  var products = [];

  @override
  Widget build(BuildContext context) {
    Dio().get('https://api.shop.com/products').then((r) {
      setState(() => products = r.data);
    });
    return ListView(
      children: products.map((p) => Text(p['title'])).toList(),
    );
  }
}

Комментарии ревьюера:

  1. Запрос в build — setState вызывает build, а build снова делает запрос: бесконечный цикл запросов. Загрузку нужно делать в initState или, лучше, в Bloc.
  2. Нет обработки ошибок и загрузки — при плохой сети пользователь видит пустой экран.
  3. var products = [] — это List<dynamic>, а p['title'] без типов. Нужна модель Product.fromJson.
  4. Новый Dio() в виджете — нет общих настроек и интерцепторов; Dio получаем через DI.
  5. ListView(children:) для списка с сервера — создаёт все элементы сразу; нужен ListView.builder.
  6. nit: нет const и super.key в конструкторе.

Исправленный вариант в духе курса:

class ProductsPage extends StatelessWidget {
  const ProductsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => getIt<ProductsBloc>()..add(ProductsRequested()),
      child: BlocBuilder<ProductsBloc, ProductsState>(
        builder: (context, state) => switch (state) {
          ProductsLoading() => const Center(child: CircularProgressIndicator()),
          ProductsFailure(:final message) => Center(child: Text(message)),
          ProductsSuccess(:final products) => ListView.builder(
              itemCount: products.length,
              itemBuilder: (_, i) => ListTile(title: Text(products[i].title)),
            ),
        },
      ),
    );
  }
}

Здесь ProductsState — sealed-класс, а ProductsFailure(:final message) — паттерн Dart 3, который сразу достаёт поле из состояния.

Чек-лист для code review

Область Что проверить
Корректность Код делает то, что заявлено; учтены пустой список, null, ошибка сети
Архитектура Логика не в виджетах; слои не перепутаны; зависимости через DI
Ресурсы Контроллеры, подписки, таймеры закрыты в dispose/close
Производительность Нет запросов в build; const где можно; ListView.builder для длинных списков
Типы Нет лишних dynamic и !; модели вместо Map
Ошибки try/catch там, где может упасть; пользователь видит понятное сообщение
Читаемость Понятные имена; нет закомментированного кода и print; функции небольшие
Безопасность Нет ключей и токенов в коде; токены не пишутся в логи
Проверки flutter analyze без предупреждений, dart format применён, тесты проходят

Типичные ошибки

  • Заучивать определения без понимания — первый же уточняющий вопрос «а почему?» ставит в тупик.
  • Рассказывать о проекте общими словами («делал приложение») вместо конкретики: какая задача, какое решение, что получилось.
  • Спорить с ревьюером вместо того, чтобы объяснить своё решение или согласиться.
  • Отправлять PR «на 2000 строк» без описания и скриншотов.
  • Писать замечания без причины («плохо», «переделай») — автор не поймёт, что именно не так.

Практика

  1. Запишите свой рассказ о себе на 2 минуты и прослушайте его. Ожидаемый результат: в рассказе есть стек, ваш лучший проект и задача, которую вы решили сами.
  2. Ответьте вслух, не подглядывая, на 10 вопросов из урока, затем сверьтесь с ответами. Ожидаемый результат: список тем, где ответ был неуверенным, — их повторите.
  3. Проведите мок-интервью с одногруппником: 20 минут вы спрашиваете, 20 — он. Ожидаемый результат: каждый получает обратную связь — что получилось и что подтянуть.
  4. Найдите в своём старом проекте 5 проблем по чек-листу и исправьте их отдельным PR с понятным описанием. Ожидаемый результат: PR, который можно отправить ментору на ревью.
  5. Сделайте ревью PR одногруппника: минимум 3 содержательных комментария с объяснением «почему» и пометьте мелочи как nit:. Ожидаемый результат: автор понимает, что и зачем исправить.

Итоги

  • Собеседование — это знакомство, теория, практика и ваши вопросы; мок-интервью снимает волнение.
  • Отвечая, объясняйте «почему», а не повторяйте определения; не знаете — рассуждайте вслух.
  • Ключевые темы: Dart (null safety, async, isolates), деревья и жизненный цикл Flutter, Bloc, Clean Architecture, DI, сеть.
  • Code review — проверка кода коллегами через Pull Request; PR должен быть маленьким и описанным.
  • Комментируйте код, а не человека, объясняйте причину и отделяйте важное от nit:.
  • Перед отправкой PR сделайте самопроверку по чек-листу и прогоните flutter analyze.
Отзыв