• TAGES
  • Блог
  • Flutter. На пути к совершенству

Flutter. На пути к совершенству

Добро пожаловать в волшебный мир программирования, где каждый разработчик – не просто кодер, а настоящий магистр заклинаний и цифровых чар! Перед вами лежит карта неисследованных земель, полных таинственных «кодовых джунглей», где скрывается легендарный кракен «кодовых глубин» и много других заморских чудес.

Обладая значительным опытом в разработке энтерпрайз мобильных приложений в TAGES, я хочу поделиться проблемами, которые часто встречаются на код-ревью у начинающих разработчиков.

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

Кодовые джунгли

В этом разделе мы совершим экскурс в таинственные дебри чужого кода — место, куда не каждому разработчику удается войти без потерь и выйти без шрамов.

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

Прежде чем прийти во Flutter/Dart, у меня уже имелся опыт нативной разработки приложений на Swift и Kotlin, поэтому, незаметно для себя, я перешагнул низкий порог вхождения в Dart и начал использовать знакомые практики с других языков, которые имели более строгие правила и принуждали писать чистый, аккуратный и структурированный код.

В кодовых джунгли представлять угрозу другим разработчикам может «спагетти-код» (термин происходит от сравнения с тарелкой спагетти, где все макаронины перепутаны и сложно выделить одну отдельную нить).

Словно в густых джунглях, в слое представления Flutter без правильной организации и структурирования можно легко потеряться из-за многообразия виджетов и вариантов их взаимодействия. Для большей наглядности представим страницу с полем ввода имени и кнопкой для входа в приложение — они будут обрабатывать события от пользователя.


Страница авторизации

Для лучшего понимания, мы опустим тонкости управления состоянием, тем самым сделав акцент на проблеме, а не на подходе:

main.dart

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: AuthScreen()));

auth_screen.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign In')),
      body: const AuthView(),
    );
  }
}

auth_view.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: CustomScrollView(
        physics: const ClampingScrollPhysics(),
        slivers: [
          SliverList.list(
            children: [
              TextField(
                autofocus: true,
                decoration: const InputDecoration(hintText: 'Your username'),
                onChanged: (value) => debugPrint('Username changed: $value'),
              ),
              const SizedBox(height: 16.0),
            ],
          ),
          SliverFillRemaining(
            hasScrollBody: false,
            child: Align(
              alignment: Alignment.bottomCenter,
              child: FractionallySizedBox(
                widthFactor: 1.0,
                child: FilledButton.tonal(
                  onPressed: () => ScaffoldMessenger.of(context)
                    ..hideCurrentSnackBar()
                    ..showSnackBar(
                      const SnackBar(content: Text('Sign in pressed')),
                    ),
                  child: const Text('Sign in'),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

С виду все хорошо, но можно заметить, что auth_view.dart перенимает очень большую ответственность у auth_screen.dart, заставляя разработчика идти вглубь дерева виджетов, разбираясь с версткой и со всеми событиями, которые могут исходить от пользователя. Это приводит к тому, что разработчик не видит всей картины происходящего, а вложенные виджеты начинают самостоятельно принимать решения.

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

auth_screen.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign In')),
      body: AuthView(
        onUsernameChanged: (value) => debugPrint('Username changed: $value'),
        onSignedIn: () => ScaffoldMessenger.of(context)
          ..hideCurrentSnackBar()
          ..showSnackBar(const SnackBar(content: Text('Sign in pressed'))),
      ),
    );
  }
}

auth_view.dart

import 'package:flutter/material.dart';

class AuthView extends StatelessWidget {
  const AuthView({
    super.key,
    required this.onUsernameChanged,
    required this.onSignedIn,
  });

  final ValueChanged<String>? onUsernameChanged;
  final VoidCallback? onSignedIn;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: CustomScrollView(
        physics: const ClampingScrollPhysics(),
        slivers: [
          SliverList.list(
            children: [
              TextField(
                autofocus: true,
                decoration: const InputDecoration(hintText: 'Your username'),
                onChanged: onUsernameChanged,
              ),
              const SizedBox(height: 16.0),
            ],
          ),
          SliverFillRemaining(
            hasScrollBody: false,
            child: Align(
              alignment: Alignment.bottomCenter,
              child: FractionallySizedBox(
                widthFactor: 1.0,
                child: FilledButton.tonal(
                  onPressed: onSignedIn,
                  child: const Text('Sign in'),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Теперь уровень представления прекрасно отражает все события от пользователя, а вложенные виджеты больше не представляют угрозу связанности, поскольку auth_screen.dart является единственным адаптером, принимающим события из UI и передающим их одному из подходов по управлению состоянием.

Всегда помните: чтобы не заблудиться в джунглях Flutter, крайне важно придерживаться лучших практик и подходов разработки. Использование чистой архитектуры, хорошо продуманных паттернов проектирования и следование принципам SOLID позволяет создавать структурированные и удобные для навигации приложения. Это как компас и карта в путешествии по джунглям: они помогают разработчику ориентироваться среди сложной иерархии виджетов и не теряться в глубине кодовой растительности, обеспечивая эффективную и гибкую разработку.

Кракен «Кодовых глубин»

В мрачных глубинах разработки программного обеспечения скрываются многие чудовища, но ни одно не вызывает столько трепета, как кракен «Кодовых глубин». Это существо — воплощение оверинжиниринга, его многочисленные щупальца запутывают логику и захламляют архитектуру.

В данном разделе мы отправимся в погоню за этим зловещим монстром, чтобы раскрыть некоторые его секреты и научиться избегать ловушек оверинжиниринга. Мы исследуем глубины кода, где затаился кракен, и выясним, как его хитроумные уловки могут подстерегать нас на каждом шагу разработки. Сейчас наша цель — освоить методы, которые позволят нам избежать перегрузки кода, а также достичь эффективности и простоты в наших технических решениях.

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

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

Разберем сценарий: у пользователя есть возможность пройти процесс аутентификации в приложении. В чистой архитектуре это выглядело бы так:


Диаграмма классов. Процесс аутентификации

auth_client.dart

abstract class IAuthClient {
  const IAuthClient();

  Future<void> login(String name);

  Future<void> logout();
}

class AuthClient implements IAuthClient {
  const AuthClient();

  @override
  Future<void> login(String name) async {
    // TODO: Send a login request
    throw UnimplementedError();
  }

  @override
  Future<void> logout() async {
    // TODO: Send a logout request
    throw UnimplementedError();
  }
}

auth_repository.dart

abstract class IAuthRepository {
  const IAuthRepository();

  Future<void> insertUser(String name);

  Future<void> deleteUser();
}

class AuthRepository implements IAuthRepository {
  const AuthRepository();

  @override
  Future<void> deleteUser() {
    // TODO: Implement delete user
    throw UnimplementedError();
  }

  @override
  Future<void> insertUser(String name) {
    // TODO: Implement insert user
    throw UnimplementedError();
  }
}

auth_service.dart

abstract class IAuthService {
  const IAuthService();

  Future<void> login(String name);

  Future<void> logout();
}

class AuthService implements IAuthService {
  const AuthService(this._client, this._repository);

  final IAuthClient _client;
  final IAuthRepository _repository;

  @override
  Future<void> login(String name) async {
    await _client.login(name);
    await _repository.insertUser(name);
    // TODO: Implement user login
  }

  @override
  Future<void> logout() async {
    await _client.logout();
    await _repository.deleteUser();
    // TODO: Implement user logout
  }
}

В этом примере IAuthClient и IAuthRepository и IAuthService выступают в качестве спецификаций, которых не заботит конкретная реализация. Мы можем создать несколько реализаций, которые будут работать с разными протоколами и источниками данных, имея при этом единый контракт, но так ли нужна эта гибкость, если у пользователя не будет альтернативного способа входа?

Это классическое проявление оверинжиниринга, для избежания которого наша реализация должна выглядеть следующим образом:


Диаграмма классов. Процесс аутентификации (упрощенный вид)

auth_client.dart

class AuthClient {
  const AuthClient();

  Future<void> login(String name) async {
    // TODO: Send a login request
    throw UnimplementedError();
  }

  Future<void> logout() async {
    // TODO: Send a logout request
    throw UnimplementedError();
  }
}

auth_repository.dart

class AuthRepository {
  const AuthRepository();

  Future<void> deleteUser() {
    // TODO: Implement delete user
    throw UnimplementedError();
  }

  Future<void> insertUser(String name) {
    // TODO: Implement insert user
    throw UnimplementedError();
  }
}

auth_service.dart

class AuthService {
  const AuthService(this._client, this._repository);

  final AuthClient _client;
  final AuthRepository _repository;

  Future<void> login(String name) async {
    await _client.login(name);
    await _repository.insertUser(name);
    // TODO: Implement user login
  }

  Future<void> logout() async {
    await _client.logout();
    await _repository.deleteUser();
    // TODO: Implement user logout
  }
}

Мы избавились от абстракций и теперь используем реализации напрямую, мы мало что потеряли, потому что у нас остается возможность использовать неявные интерфейсы в Dart, чтобы заимплементировать класс и написать совершенно другую реализацию.

Конечно, лучшим решением в данной ситуации будет дождаться момента, когда появится потребность в расширении функционала. И вот только тогда, сделать все по красоте, обеспечив необходимый уровень обслуживаемости.

Избыточность в модели данных
В эпоху цифровизации и накопления данных, модели стали фундаментом, на котором строятся информационные системы. Однако, несмотря на их важность, существует тенденция перегружать модели лишними данными, что может привести к значительным проблемам в архитектуре и производительности приложений.

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

Представим модель пользователя, которая может возвращаться в ответе сетевого запроса:


Диаграмма классов. Пользователь

user.dart

class User {
  const User({
    required this.id,
    required this.name,
    required this.phone,
    required this.createdAt,
  });

  final int id;
  final String name;
  final String phone;
  final DateTime createdAt;

  User copyWith({
    int? id,
    String? name,
    String? phone,
    DateTime? createdAt,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      phone: phone ?? this.phone,
      createdAt: createdAt ?? this.createdAt,
    );
  }
}

В зависимости от требований, верстки и многих других аспектов, некоторые поля модели могут не использоваться очень долгое время в разрабатываемом приложении.

К примеру, если на текущий момент на странице профиля будет отображаться только идентификатор, имя и телефон пользователя, а на странице редактирования профиля можно поменять только имя – то логично предположить, что не все поля должны быть задействованы:


Диаграмма классов. Пользователь (упрощенная модель)

user.dart

class User {
  const User({
    required this.id,
    required this.name,
    required this.phone,
  });

  final int id;
  final String name;
  final String phone;

  User copyWith({String? name}) =>
      User(id: id, name: name ?? this.name, phone: phone);
}

Модель значительно уменьшилась в размере и соответствует только текущим требованиям, избегая перегруженности полями, которые не участвуют в бизнес-логике, поддержка такого кода происходит значительно быстрее, а еще вместе с этим оптимизируется работа со списками в ОЗУ.

В оправдание оверинжиниринга
Существуют случаи, когда оверинжиниринг может быть оправдан. Например:

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

При этом, следует избегать оверинжиниринга в следующих случаях:

  1. Когда проект ограничен по времени и бюджету, и основная функциональность должна быть быстро представлена на рынке.
  2. Когда функциональность или возможности, которые добавляются, не будут использоваться конечными пользователями или не соответствуют их потребностям.
  3. Когда чрезмерное проектирование и усилия могут замедлить разработку и внедрение, не принося явной ценности проекту.

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

Жонглирование ошибками

Когда не все идет по плану, «Ой-ой-опаньки!» становится частью нашего ежедневного лексикона. В этой части, мы погрузимся в искусство обработки ошибок, которое часто остается незамеченным, но на самом деле является ключевым элементом создания надежных и устойчивых приложений.

Скучные и предсказуемые приложения обрабатывают ошибки тихо и без фанфар, но мы пойдем необычным путем. Здесь вы научитесь не просто «ловить» исключения, но и делать это с умом, изяществом и, возможно, с щепоткой магии!

Существуют распространенные проблемы, с которыми сталкиваются разработчики при обработке ошибок. Они включают в себя необработанные исключения, отсутствие подробного логирования, отсутствие реакции на ошибку, неинформативные сообщения и многое другое. Разберемся, как работать со всем этим правильно.

Локализация ошибок
В первую очередь важно настроить систему отображения ошибок таким образом, чтобы они были ясны и доступны для понимания конечным пользователям. В качестве решения мы воспользуемся абстрактным классом локализации, однако вы также можете руководствоваться официальными советами по интернационализации приложений, где такой класс может генерироваться на основе спецификации (Application Resource Bundle):


Диаграмма классов. Локализация

localization.dart

abstract class Localization {
  const Localization();

  String get unimplementedError;

  String get unknownError;
}

class English extends Localization {
  const English();

  @override
  String get unimplementedError => 'Method not implemented';

  @override
  String get unknownError => 'An unknown error has occurred';
}

Типизация ошибок
При разработке огромную роль имеет типизация ошибок, поскольку она позволяет разработчикам лучше понимать природу проблемы и принимать меры для ее устранения, значительно улучшая пользовательский опыт, обеспечивая более информативные сообщения об ошибках и пути их решения.

Для начала выстраивания типизации достаточно создать абстрактный класс ошибки, который будет являться базовым для всех других:


Диаграмма классов. Обработка ошибок

error.dart

abstract class Error {
  const Error();

  factory Error.fromException(Object? e) {
    if (e is Error) return e;
    return UnknownError(e.toString());
  }

  String toLocale(Localization l);

  @override
  String toString() => '[$runtimeType]';
}

class UnimplementedError extends Error {
  const UnimplementedError();

  @override
  String toLocale(Localization l) => l.unimplementedError;
}

class UnknownError extends Error {
  const UnknownError([this.message]);

  final String? message;

  @override
  String toLocale(Localization l) => l.unknownError;

  @override
  String toString() => message ?? super.toString();
}

Журналирование ошибок
Журналирование ошибок – это ключевой процесс в разработке программного обеспечения, позволяющий отслеживать и устранять проблемы в системах различного уровня. Ошибки могут быть классифицированы по разным уровням в зависимости от их серьезности и влияния на работу системы.

Начать агрегировать логи можно при помощи пакета logging. Он позволяет настроить уровень ведения журнала и добавить обработчик для вывода сообщений. Первым делом создадим error_service.dart, который будет имитировать и логировать возникающие исключения:


Диаграмма классов. Сервис имитации ошибок

error_service.dart

import 'package:logging/logging.dart';

class ErrorService {
  const ErrorService();

  // Создает именованный журнал для логирования
  static final _log = Logger('ErrorService');

  void first() {
    try {
      // Выводит сообщение с уровнем [Level.INFO]
      _log.info('First method called');
      // Кидает исключение [UnimplementedError]
      throw const UnimplementedError();
    } catch (e, s) {
      // Выводит сообщение с уровнем [Level.SHOUT]
      _log.shout('Failed to call first method', e, s);
      // Пробрасывает исключение наружу
      rethrow;
    }
  }

  void second() {
    try {
      // Выводит сообщение с уровнем [Level.INFO]
      _log.info('Second method called');
      // Кидает исключение [ArgumentError]
      throw ArgumentError();
    } catch (e, s) {
      // Выводит сообщение с уровнем [Level.SEVERE]
      _log.severe('Failed to call second method', e, s);
      // Пробрасывает исключение наружу
      rethrow;
    }
  }

  Future<void> third() async {
    _log.info('Third method called');
    // Имитирует асинхронную операцию
    await Future.delayed(const Duration(seconds: 1));
    // Кидает исключение с текстом ошибки
    throw const UnknownError('You should always handle exceptions!');
  }
}

Далее необходимо настроить общий уровень логирования и проработать все необработанные исключения, которые могут возникнуть:

main.dart

import 'dart:async';

import 'package:logging/logging.dart';

void main() {
  hierarchicalLoggingEnabled = true;
  // Выводит события для всех уровней
  Logger.root.level = Level.ALL;
  // Подписывает на журнал событий
  Logger.root.onRecord.listen(_listener);

  // Отлавливает и выводит необработанные исключения
  return runZonedGuarded(
    () => runApp(
      localization: const English(),
      service: const ErrorService(),
      logger: Logger('App'),
    ),
    (e, s) => Logger.root.shout('Unhandled exception.', e, s),
  );
}

void _listener(LogRecord record) {
  final builder = ['${record.level.name}: ${record.time}'];
  if (record.loggerName.isNotEmpty) builder.add('[${record.loggerName}]');
  if (record.message.isNotEmpty) builder.add(record.message);

  final error = record.error;
  if (error != null) builder.add(record.error.toString());
  print(builder.join(' '));
  final stackTrace = record.stackTrace;
  if (stackTrace != null) print(record.stackTrace.toString());
}

Вызвав первый метод в error_service.dart, будет выдано исключение [UnimplementedError]. В этом случае для разработчика будет доступна подробная трассировка стека об ошибке, а для пользователя необходимо скрыть детали и вывести понятное сообщение:

app.dart

void runApp({
  required Localization localization,
  required ErrorService service,
  required Logger logger,
}) {
  try {
    service.first();
  } catch (e) {
    final error = Error.fromException(e);
    // Выводит локализованное сообщение для пользователя
    logger.fine(error.toLocale(localization));
  }
}

console

INFO: 2024-01-22 12:00:27.580007 [ErrorService] First method called
SHOUT: 2024-01-22 12:00:27.582901 [ErrorService] Failed to call first method [UnimplementedError]
#0 ErrorService.first (package:example/main.dart:65:7)
#1 runApp (package:example/main.dart:45:13)
#2 main. (package:example/main.dart:18:11)
#3 _rootRun (dart:async/zone.dart:1399:13)
#4 _CustomZone.run (dart:async/zone.dart:1301:19)
#5 _runZoned (dart:async/zone.dart:1804:10)
#6 runZonedGuarded (dart:async/zone.dart:1792:12)
#7 main (package:example/main.dart:17:10)
#8 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:297:19)
#9 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

FINE: 2024-01-22 12:00:27.582901 [App] Method not implemented

Process finished with exit code 0

После вызова второго метода, будет выдано неизвестное исключение [ArgumentError]. В таком случае для пользователя необходимо донести информацию о неизвестной ошибке, потому что приложение не ожидало ее получить. К счастью, это происходит просто:

app.dart

void runApp({
  required Localization localization,
  required ErrorService service,
  required Logger logger,
}) {
  try {
    service.second();
  } catch (e) {
    final error = Error.fromException(e);
    // Выводит локализованное сообщение
    logger.fine(error.toLocale(localization));
  }
}

console

INFO: 2024-01-22 12:00:37.456504 [ErrorService] Second method called
SEVERE: 2024-01-22 12:00:37.458498 [ErrorService] Failed to call second method Invalid argument(s)
#0 ErrorService.second (package:example/main.dart:78:7)
#1 runApp (package:example/main.dart:45:13)
#2 main. (package:example/main.dart:18:11)
#3 _rootRun (dart:async/zone.dart:1399:13)
#4 _CustomZone.run (dart:async/zone.dart:1301:19)
#5 _runZoned (dart:async/zone.dart:1804:10)
#6 runZonedGuarded (dart:async/zone.dart:1792:12)
#7 main (package:example/main.dart:17:10)
#8 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:297:19)
#9 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

FINE: 2024-01-22 12:00:37.458498 [App] An unknown error has occurred

Process finished with exit code 0

Третий случай показывает работу отлавливания необработанных исключений. Если в любом месте приложения будет получено необработанное исключение, то это обязательно будет зафиксировано в консоли:

app.dart

void runApp({
  required Localization localization,
  required ErrorService service,
  required Logger logger,
}) async {
  await service.third();
}

console

INFO: 2024-01-22 12:03:41.039678 [ErrorService] Third method called
SHOUT: 2024-01-22 12:03:42.062982 Unhandled exception. You should always handle exceptions!
#0 ErrorService.third (package:example/main.dart:87:5)
#1 runApp (package:example/main.dart:45:3)

Process finished with exit code 0

Самое важное:

  1. Исключения могут возникать на любом уровне, главное – правильно их категоризировать и фиксировать. Не игнорируйте их.
  2. Необходимо использовать разные уровни логирования: информационные, предупреждения или ошибки, чтобы определить важность каждого события в консоли.
  3. Возникающие исключения должны быть понятны пользователю.
  4. Исключениями можно управлять, чтобы повысить стабильность кода. Например, можно проверить ошибку на определенный тип и предоставить пользователю возможность повторить запрос.
  5. В продуктивной среде необходимо ограничивать логирование ошибок и чувствительных данных. Убедитесь, что не логируете сетевые запросы с конфиденциальными данными пользователей.

Стоит помнить, что правильное отслеживание работы приложения играет огромную роль в выявлении и исправлении багов, что положительно влияет на стабильность приложения.

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

Для быстрой интеграции могут понадобиться следующие пакеты:

Список интеграций с другими популярными пакетами доступен здесь. Для реализации понадобится проинициализировать зависимость, и дело в шляпе:

main.dart

import 'dart:async';

import 'package:logging/logging.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry_logging/sentry_logging.dart';

void main() {
  hierarchicalLoggingEnabled = true;
  // Выводит события для всех уровней
  Logger.root.level = Level.ALL;
  // Подписывает на журнал событий
  Logger.root.onRecord.listen(_listener);

  // Отлавливает и выводит необработанные исключения
  return runZonedGuarded(
    () async {
      // Инициализирует сервис удаленного мониторинга Sentry
      await Sentry.init(
        (options) => options
          ..dsn = 'https://example@sentry.io/example'
          // Бесшовная интеграция с пакетом logging
          ..addIntegration(LoggingIntegration()),
      );
      return runApp(
        localization: const English(),
        service: const ErrorService(),
        logger: Logger('App'),
      );
    },
    (e, s) => Logger.root.shout('Unhandled exception.', e, s),
  );
}

После этого все события из консоли будут доступны в Sentry с подробной информацией об устройстве, платформе, пользователе и т.д.

Теперь, когда вы вооружены знаниями и навыками, чтобы превращать потенциальные катастрофы в возможности для улучшения вашего приложения, впереди вас ждет мир, где каждая ошибка – это шанс научиться чему-то новому.

Помните, что «ой-ой-опаньки!» не всегда предвестник беды; иногда это трамплины к инновациям. Сохраняйте спокойствие, обрабатывайте ошибки с умом, и пусть код будет столь же устойчивым, сколь и ваше чувство юмора!

Кодогенерация

В этой части мы прокатимся на волшебном ковре-самолете, где каждый разработчик, который использует кодогенерацию, имеет личного Джинна в волшебной лампе! Этот раздел — входной билет в парк удивительных технологий, где кодогенерация, используемая разработчиками, выходит на арену, жонглирует декораторами и генерирует код прямо перед вашими глазами, но перед началом проясним момент:

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

Как правило, кодогенерация прежде всего направлена на решение следующих задач:

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

Особенности, которыми обладает сгенерированный код:

  1. Код может быть сложным для человеческого восприятия.
  2. Код может не соответствовать принятым стандартам и лучшим практикам.

Хотя у сгенерированного кода есть недостатки, но они часто смягчаются тем фактом, что разработчики обычно не читают и не поддерживают его напрямую. Вместо этого они управляют кодом на более высоком (абстрактном) уровне, на основе которого строится процесс генерации. Такой подход часто используется для создания бойлерплейт кода (кода, который должен быть включен во многих местах с небольшими изменениями).

Кодогенерация в Dart, увы, не может похвастаться своим быстродействием (даже если следовать рекомендациям). В больших проектах Flutter/Dart кодогенерация может начать влиять на скорость разработки, но только не в положительную сторону, а в отрицательную, поэтому рано или поздно, команда сталкивается с этой проблемой.

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

Прежде всего, чтобы решить эту проблему, нужно понять, насколько эффективно используются текущие зависимости проекта и можно ли обойтись без них. Приведу пример, который помог снизить влияние от кодогенерации:

  1. freezed. Использовался для всех моделей, очень сильно был завязан в контексте управления состоянием, был заменен на equatable, при необходимости конструкции пишутся вручную или сниппетом.
  2. retrofit. Обертка над dio, позволяющая уложиться в пару строчек. Кодогенерация не сильно ускоряла разработку, поэтому был сделан переход в сторону прямого общения с dio.
  3. go_router_builder или auto_route_generator. При изменении маршрутизации приходится каждый раз запускать кодогенерацию, что негативно сказывается на общем времени разработки, поэтому избавление от зависимостей ускорило этот момент.

Зависимости, которые остались на кодогенерации:

  1. injectable + get_it. Зависимости очень часто меняются, в результате чего поддерживать их взаимосвязь в DI-контейнере довольно утомительно.
  2. json_serializable + json_annotation. Используются для минимизации ошибок от человеческого фактора.

А теперь, давайте спустимся на землю с пониманием, что волшебство автоматизации — это не всегда универсальный рецепт. Как и любой иллюзионист, который иногда может запутаться в собственных фокусах, так и мы, при использовании кодогенерации можем прийти к ситуации, когда создание кода в автоматическом режиме будет забирать больше времени, нежели его ручное написание.

Так что, дорогие читатели, будьте готовы к тому, что иногда Джинн из лампы может быть не таким уж послушным, и вам придется взять старый добрый редактор кода в руки и воплотить свои идеи самостоятельно. Однако не бойтесь экспериментировать и искать баланс между кодогенерацией и ручным написанием кода, поскольку именно в этом балансе рождается истинное мастерство.

Резюмируя

  • Уделяйте внимание качеству кода. Старайтесь писать код так, чтобы он был прозрачен и понятен для других разработчиков.
  • Не уходите в оверинжиниринг. Ищите баланс между достижением необходимой функциональности и качеством продукта.
  • Следите за всеми возникающими исключениями и обрабатывайте их, повышая стабильность приложения.
  • Правильно оценивайте возможности кодогенерации. Не нагружайте кодогенерацией те части приложения, которые могут обойтись без нее.

Заключение

Flutter — это мощный инструмент, который может значительно упростить процесс разработки приложений. Он предлагает решения для многих распространенных проблем, с которыми сталкиваются разработчики. Однако, как и любой инструмент, он требует правильного использования и понимания.

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

В конечном счете, качество кода в Flutter напрямую зависит от способности разработчика применять лучшие практики, правильно использовать кодогенерацию и обеспечивать эффективную обработку ошибок. Овладение этими аспектами является ключом к созданию высокопроизводительных, масштабируемых и надежных кроссплатформенных приложений, которые будут положительно восприняты пользователями и выдержат испытание временем.

Источник: Хабр

Автор: Данил Абдрафиков (разработчик мобильных приложений TAGES)

Назад