Глобальное логирование ошибок с отправкой в Telegram

от @Valerka_P
Давно хотел добавить красивый логер. Внедрить просто - добавляете кастомные файл и правите main. А вот такой красивый экран с ошибками появится у вас если потрясти устройство ,)

А вот такой красивый экран с ошибками появится у вас если потрясти устройство
photo_2026-01-15_14-56-17.jpg

Глобальное логирование ошибок с отправкой в Telegram

Описание решения

Система логирования на базе пакета talker_flutter, которая:

  • Перехватывает все ошибки приложения (синхронные и асинхронные)
  • Автоматически отправляет критические ошибки (exceptions) в Telegram бот
  • Предоставляет экран просмотра логов (TalkerScreen) для отладки
  • Ограничивает доступ к TalkerScreen по списку разрешённых user ID
  • Открывает TalkerScreen по жесту "тряска телефона" (shake)

Архитектура

┌─────────────────────────────────────────────────────────────────┐
│                         main.dart                                │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    runZonedGuarded                         │  │
│  │         (перехват асинхронных ошибок)                      │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │              FlutterError.onError                    │  │  │
│  │  │         (перехват ошибок Flutter)                    │  │  │
│  │  │  ┌───────────────────────────────────────────────┐  │  │  │
│  │  │  │               ShakeGesture                     │  │  │  │
│  │  │  │    (открытие TalkerScreen при тряске)          │  │  │  │
│  │  │  │  ┌─────────────────────────────────────────┐  │  │  │  │
│  │  │  │  │            MaterialApp                   │  │  │  │  │
│  │  │  │  │         (всё приложение)                 │  │  │  │  │
│  │  │  │  └─────────────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      TalkerService                               │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                      Talker                                │  │
│  │              (логирование в консоль)                       │  │
│  └───────────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │               TelegramTalkerObserver                       │  │
│  │         (отправка exceptions в Telegram)                   │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │  Telegram Bot   │
                    │     API         │
                    └─────────────────┘

Что отправляется в Telegram

Тип Отправка Описание
exception ✅ Да Критические ошибки (try-catch exceptions)
error ✅ Да FlutterError (ошибки рендеринга и т.д.)
warning ❌ Нет Только в локальные логи
info ❌ Нет Только в локальные логи
debug ❌ Нет Только в локальные логи

Формат сообщения в Telegram

🚨 EXCEPTION в Hoof Master

👤 Пользователь:
• ID: 541c2bfe-bddb-4cb7-8308-90a054961fa6
• Имя: Иван Иванов
• Email: ivan@example.com

❌ Ошибка:
FormatException: Invalid date format

📝 Сообщение:
Ошибка при парсинге даты

📍 Stack Trace:
#0      DateParser.parse (package:app/utils/date.dart:42:10)
#1      _FormWidgetState.submit (package:app/widgets/form.dart:123:15)
...

🕐 Время: 2026-01-09T12:30:45.123456

Требования

Зависимости в pubspec.yaml

dependencies:
  # Логирование и отображение логов
  talker_flutter: ^4.6.1

  # Распознавание жеста тряски телефона
  shake_gesture: ^2.0.0

  # HTTP запросы (обычно уже есть)
  http: ^1.4.0

Полный код решения

Файл: lib/custom_code/TalkerService.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:talker_flutter/talker_flutter.dart';
import '/app_state.dart';

/// ============================================================================
/// КОНФИГУРАЦИЯ - Telegram бот и доступы
/// ============================================================================
/// Токен Telegram бота для отправки ошибок
const String _telegramBotToken = 'YOUR_BOT_TOKEN_HERE';

/// Chat ID куда отправлять ошибки (группа/канал/личный чат)
const String _telegramChatId = 'YOUR_CHAT_ID_HERE';

/// Список user ID которым разрешён доступ к TalkerScreen
/// Добавляйте сюда UUID пользователей из вашей базы данных
const List<String> _allowedUserIds = [
  'user-uuid-1',
  'user-uuid-2',
];

/// ============================================================================
/// TalkerService - синглтон сервис логирования
/// ============================================================================
/// Предоставляет:
/// - Глобальный экземпляр Talker для логирования
/// - Автоматическую отправку критических ошибок в Telegram
/// - Доступ к TalkerScreen для разрешённых пользователей
/// ============================================================================
class TalkerService {
  /// Приватный конструктор для паттерна Singleton
  TalkerService._internal();

  /// Единственный экземпляр сервиса
  static final TalkerService _instance = TalkerService._internal();

  /// Фабричный конструктор возвращает единственный экземпляр
  factory TalkerService() => _instance;

  /// Экземпляр Talker - основной логгер
  late final Talker _talker;

  /// Геттер для доступа к Talker из любого места приложения
  Talker get talker => _talker;

  /// Флаг инициализации
  bool _isInitialized = false;

  /// ============================================================================
  /// Инициализация сервиса
  /// ============================================================================
  /// Вызывается один раз при старте приложения в main()
  /// Создаёт Talker с TelegramObserver для отправки ошибок
  /// ============================================================================
  void initialize() {
    if (_isInitialized) return;
    _isInitialized = true;

    /// Создаём observer для отправки ошибок в Telegram
    final telegramObserver = TelegramTalkerObserver();

    /// Инициализируем Talker с настройками
    _talker = TalkerFlutter.init(
      /// Observer для отправки ошибок в Telegram
      observer: telegramObserver,

      /// Настройки логирования
      settings: TalkerSettings(
        /// Включаем логирование в консоль
        enabled: true,

        /// Используем цветной вывод в консоль
        useConsoleLogs: true,

        /// Максимальное количество записей в истории
        maxHistoryItems: 1000,
      ),
    );

    /// Логируем успешную инициализацию
    _talker.info('TalkerService инициализирован');

    /// ========================================================================
    /// ТЕСТОВАЯ ОШИБКА - раскомментировать для проверки отправки в Telegram
    /// ========================================================================
    // Future.delayed(const Duration(seconds: 3), () {
    //   logException(
    //     Exception('Тестовая ошибка для проверки Telegram бота'),
    //     StackTrace.current,
    //     'Тест отправки в Telegram',
    //   );
    // });
  }

  /// ============================================================================
  /// Проверка доступа пользователя к TalkerScreen
  /// ============================================================================
  /// Возвращает true если текущий пользователь есть в списке _allowedUserIds
  /// ============================================================================
  bool hasAccess() {
    final currentUserId = FFAppState().user.id;
    return _allowedUserIds.contains(currentUserId);
  }

  /// ============================================================================
  /// Открыть TalkerScreen (экран логов)
  /// ============================================================================
  /// Проверяет права доступа и открывает экран если пользователь разрешён.
  /// Возвращает true если экран был открыт, false если доступ запрещён.
  /// ============================================================================
  bool openTalkerScreen(BuildContext context) {
    /// Проверяем права доступа
    if (!hasAccess()) {
      _talker.warning('Попытка доступа к TalkerScreen без прав');
      return false;
    }

    /// Открываем экран логов
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => TalkerScreen(
          talker: _talker,

          /// Кастомизация темы экрана
          theme: TalkerScreenTheme(
            backgroundColor: Colors.grey[900]!,
            cardColor: Colors.grey[850] ?? Colors.grey[800]!,
            textColor: Colors.white,

            /// Цвета для разных типов логов
            logColors: {
              TalkerLogType.error.key: Colors.redAccent,
              TalkerLogType.critical.key: Colors.red,
              TalkerLogType.exception.key: Colors.deepOrange,
              TalkerLogType.warning.key: Colors.orange,
              TalkerLogType.info.key: Colors.blue,
              TalkerLogType.debug.key: Colors.grey,
              TalkerLogType.verbose.key: Colors.grey[600]!,
            },
          ),
        ),
      ),
    );

    _talker.info('TalkerScreen открыт');
    return true;
  }
}

/// ============================================================================
/// TelegramTalkerObserver - отправка ошибок в Telegram
/// ============================================================================
/// Observer который слушает все события Talker и отправляет
/// критические ошибки (exceptions) в Telegram бот.
/// ============================================================================
class TelegramTalkerObserver extends TalkerObserver {
  /// ============================================================================
  /// Обработка Exception (критических ошибок)
  /// ============================================================================
  /// Вызывается когда происходит exception в приложении.
  /// Отправляет подробную информацию в Telegram.
  /// ============================================================================
  
  void onException(TalkerException exception) {
    _sendToTelegram(
      type: 'EXCEPTION',
      message: exception.message ?? 'Нет сообщения',
      error: exception.exception?.toString() ?? 'Неизвестная ошибка',
      stackTrace: exception.stackTrace?.toString(),
    );
  }

  /// ============================================================================
  /// Обработка Error (ошибок Flutter)
  /// ============================================================================
  /// Вызывается при FlutterError и других критических ошибках.
  /// ============================================================================
  
  void onError(TalkerError err) {
    _sendToTelegram(
      type: 'ERROR',
      message: err.message ?? 'Нет сообщения',
      error: err.error?.toString() ?? 'Неизвестная ошибка',
      stackTrace: err.stackTrace?.toString(),
    );
  }

  /// ============================================================================
  /// Отправка сообщения в Telegram
  /// ============================================================================
  /// Формирует сообщение с информацией об ошибке и отправляет
  /// через Telegram Bot API.
  /// ============================================================================
  Future<void> _sendToTelegram({
    required String type,
    required String message,
    required String error,
    String? stackTrace,
  }) async {
    try {
      /// Получаем информацию о пользователе из состояния приложения
      final userId = FFAppState().user.id;
      final userName = FFAppState().user.name;
      final userEmail = FFAppState().user.email;

      /// Формируем текст сообщения
      /// Ограничиваем длину stackTrace чтобы не превысить лимит Telegram (4096 символов)
      final truncatedStackTrace = stackTrace != null && stackTrace.length > 1500
          ? '${stackTrace.substring(0, 1500)}...\n[обрезано]'
          : stackTrace;

      /// Экранируем специальные символы Markdown
      final safeError = _escapeMarkdown(error);
      final safeMessage = _escapeMarkdown(message);
      final safeName = _escapeMarkdown(userName.isNotEmpty ? userName : 'Неизвестно');
      final safeEmail = _escapeMarkdown(userEmail.isNotEmpty ? userEmail : 'Неизвестно');

      final text = '''
🚨 *$type* в MyApp

👤 *Пользователь:*
• ID: `$userId`
• Имя: $safeName
• Email: $safeEmail

❌ *Ошибка:*
$safeError

📝 *Сообщение:*
$safeMessage

📍 *Stack Trace:*
${truncatedStackTrace ?? 'Нет stacktrace'}

🕐 *Время:* ${DateTime.now().toIso8601String()}
''';

      /// URL Telegram Bot API
      final url = Uri.parse(
        'https://api.telegram.org/bot$_telegramBotToken/sendMessage',
      );

      /// Создаём клиент с таймаутом
      final client = http.Client();
      try {
        /// Отправляем POST запрос с таймаутом
        final response = await client
            .post(
              url,
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
              },
              body: {
                'chat_id': _telegramChatId,
                'text': text,
                'parse_mode': 'Markdown',
              },
            )
            .timeout(const Duration(seconds: 10));

        /// Логируем результат для отладки
        if (response.statusCode != 200) {
          print('Telegram API error: ${response.statusCode} - ${response.body}');
        } else {
          print('Telegram: сообщение отправлено успешно');
        }
      } finally {
        client.close();
      }
    } catch (e) {
      /// Если не удалось отправить в Telegram - логируем локально
      /// Не выбрасываем ошибку чтобы не создать бесконечный цикл
      print('Ошибка отправки в Telegram: $e');
    }
  }

  /// Экранирование специальных символов Markdown для Telegram
  String _escapeMarkdown(String text) {
    return text
        .replaceAll('_', '\\_')
        .replaceAll('*', '\\*')
        .replaceAll('[', '\\[')
        .replaceAll(']', '\\]')
        .replaceAll('`', '\\`');
  }
}

/// ============================================================================
/// Глобальные функции-хелперы для удобного логирования
/// ============================================================================

/// Логирование информационного сообщения
void logInfo(String message) => TalkerService().talker.info(message);

/// Логирование предупреждения
void logWarning(String message) => TalkerService().talker.warning(message);

/// Логирование ошибки (НЕ отправляется в Telegram)
void logError(String message) => TalkerService().talker.error(message);

/// Логирование и обработка exception (отправляется в Telegram)
void logException(Object exception, [StackTrace? stackTrace, String? message]) {
  TalkerService().talker.handle(exception, stackTrace, message);
}

/// Логирование debug сообщения
void logDebug(String message) => TalkerService().talker.debug(message);

Файл: lib/main.dart

ВАЖНО: При использовании MaterialApp.router с GoRouter НЕ добавляйте
дополнительный Navigator
в builder. Это блокирует доступ к GoRouter
из дочерних виджетов и вызывает ошибку "No GoRouter found in context".

import 'dart:async';
import 'dart:ui' as ui;

import 'package:provider/provider.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:shake_gesture/shake_gesture.dart';

// Ваши импорты...
import '/app_state.dart';
import '/custom_code/TalkerService.dart';

void main() async {
  /// ============================================================================
  /// runZonedGuarded оборачивает всё приложение для перехвата
  /// асинхронных ошибок которые не ловятся через try-catch
  /// ============================================================================
  runZonedGuarded<Future<void>>(
    () async {
      WidgetsFlutterBinding.ensureInitialized();

      /// ========================================================================
      /// Инициализация TalkerService - должен быть первым!
      /// ========================================================================
      /// Это позволяет логировать все последующие этапы инициализации
      /// и перехватывать ошибки с самого начала
      /// ========================================================================
      TalkerService().initialize();

      /// ========================================================================
      /// Настройка глобального обработчика ошибок Flutter
      /// ========================================================================
      /// FlutterError.onError перехватывает ошибки рендеринга,
      /// widget lifecycle и другие Flutter-специфичные ошибки
      /// ========================================================================
      FlutterError.onError = (FlutterErrorDetails details) {
        /// Логируем ошибку в Talker (будет отправлена в Telegram)
        TalkerService().talker.handle(
              details.exception,
              details.stack,
              'FlutterError: ${details.context?.toString() ?? "Нет контекста"}',
            );
      };

      // Ваша инициализация...
      // await YourService.initialize();

      final appState = FFAppState();
      await appState.initializePersistedState();

      /// Логируем успешный запуск приложения
      logInfo('Приложение успешно инициализировано');

      runApp(ChangeNotifierProvider(
        create: (context) => appState,
        child: MyApp(),
      ));
    },

    /// ========================================================================
    /// Обработчик асинхронных ошибок
    /// ========================================================================
    /// Перехватывает ошибки в Future, Stream, Timer и других
    /// асинхронных операциях которые не были обработаны
    /// ========================================================================
    (error, stackTrace) {
      /// Логируем ошибку в Talker (будет отправлена в Telegram)
      TalkerService().talker.handle(
            error,
            stackTrace,
            'Необработанная асинхронная ошибка',
          );
    },
  );
}

class MyApp extends StatefulWidget {
  
  State<MyApp> createState() => _MyAppState();

  static _MyAppState of(BuildContext context) =>
      context.findAncestorStateOfType<_MyAppState>()!;
}

/// Кастомный ScrollBehavior для поддержки скроллинга стилусом на планшетах
class MyAppScrollBehavior extends MaterialScrollBehavior {
  
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
        PointerDeviceKind.stylus,  // Поддержка стилуса для скролла
      };
}

class _MyAppState extends State<MyApp> {
  late GoRouter _router;

  
  void initState() {
    super.initState();
    // Инициализация роутера (в реальном коде используется createRouter)
    _router = createRouter(AppStateNotifier.instance);
  }

  /// ============================================================================
  /// Обработчик жеста Shake (тряска телефона)
  /// ============================================================================
  /// Открывает TalkerScreen если у пользователя есть права доступа.
  ///
  /// ВАЖНО: Используем _router.routerDelegate.navigatorKey.currentContext
  /// для получения контекста, а НЕ отдельный Navigator с GlobalKey.
  /// Дополнительный Navigator блокирует доступ к GoRouter!
  /// ============================================================================
  void _onShake() {
    final context = _router.routerDelegate.navigatorKey.currentContext;
    if (context != null) {
      final opened = TalkerService().openTalkerScreen(context);
      if (!opened) {
        /// Если доступ запрещён - можно показать snackbar (опционально)
        logDebug('Shake detected, но доступ к TalkerScreen запрещён');
      }
    }
  }

  
  Widget build(BuildContext context) {
    /// ========================================================================
    /// ShakeGesture оборачивает приложение для открытия TalkerScreen
    /// при тряске телефона (только для разрешённых пользователей)
    /// ========================================================================
    return ShakeGesture(
      onShake: _onShake,
      child: MaterialApp.router(
        // Ваши настройки MaterialApp.router...
        scrollBehavior: MyAppScrollBehavior(),
        routerConfig: _router,

        /// ВАЖНО: НЕ используйте builder с дополнительным Navigator!
        /// Это заблокирует GoRouter и вызовет ошибку
        /// "No GoRouter found in context" в дочерних виджетах.
        ///
        /// ❌ НЕПРАВИЛЬНО:
        /// builder: (context, child) {
        ///   return Navigator(
        ///     key: _navigatorKey,
        ///     onGenerateRoute: (_) => MaterialPageRoute(
        ///       builder: (_) => child ?? const SizedBox.shrink(),
        ///     ),
        ///   );
        /// },
        ///
        /// ✅ ПРАВИЛЬНО: не использовать builder вообще,
        /// или использовать для других целей (например, баннер):
        /// builder: (context, child) {
        ///   return Stack(children: [child!, const NoInternetBanner()]);
        /// },
      ),
    );
  }
}

Инструкция по интеграции

Шаг 1: Создать Telegram бота

  1. Откройте Telegram и найдите @BotFather

  2. Отправьте команду /newbot

  3. Следуйте инструкциям - укажите имя и username бота

  4. Получите токен бота (формат: 123456789:ABCdefGHI...)

  5. Создайте группу/канал и добавьте туда бота

  6. Получите chat_id:

    • отправьте сообщение в группу и откройте: https://api.telegram.org/bot<TOKEN>/getUpdates

Шаг 2: Добавить зависимости

flutter pub add talker_flutter shake_gesture

Или в pubspec.yaml:

dependencies:
  talker_flutter: ^4.6.1
  shake_gesture: ^2.0.0

Шаг 3: Создать TalkerService.dart

Скопируйте код выше в lib/custom_code/TalkerService.dart и замените:

  • YOUR_BOT_TOKEN_HERE на токен вашего бота
  • YOUR_CHAT_ID_HERE на ваш chat_id
  • Список _allowedUserIds на UUID ваших пользователей

Шаг 4: Интегрировать в main.dart

  1. Добавьте импорт TalkerService
  2. Оберните main() в runZonedGuarded
  3. Инициализируйте TalkerService первым
  4. Настройте FlutterError.onError
  5. Оберните приложение в ShakeGesture

Шаг 5: Тестирование

Раскомментируйте тестовый код в TalkerService.initialize():

Future.delayed(const Duration(seconds: 3), () {
  logException(
    Exception('Тестовая ошибка для проверки Telegram бота'),
    StackTrace.current,
    'Тест отправки в Telegram',
  );
});

Запустите приложение и проверьте Telegram - через 3 секунды должно прийти сообщение.


Использование в коде

Логирование (не отправляется в Telegram)

import '/custom_code/TalkerService.dart';

// Информационное сообщение
logInfo('Пользователь вошёл в систему');

// Предупреждение
logWarning('Кэш устарел, обновляем...');

// Ошибка (без отправки в Telegram)
logError('Не удалось загрузить изображение');

// Debug (только для разработки)
logDebug('API response: $response');

Логирование с отправкой в Telegram

try {
  await someRiskyOperation();
} catch (e, stackTrace) {
  // Отправится в Telegram!
  logException(e, stackTrace, 'Ошибка в someRiskyOperation');
}

Прямой доступ к Talker

final talker = TalkerService().talker;

// Все методы Talker доступны
talker.info('Info');
talker.warning('Warning');
talker.error('Error');
talker.handle(exception, stackTrace, 'Message');

// Кастомные логи
talker.logTyped(YourCustomLog('message'));

Открытие TalkerScreen программно

// Откроет только если пользователь в списке разрешённых
TalkerService().openTalkerScreen(context);

// Проверка доступа
if (TalkerService().hasAccess()) {
  // Показать кнопку "Логи" в меню
}

Кастомизация

Изменить тему TalkerScreen

TalkerScreen(
  talker: _talker,
  theme: TalkerScreenTheme(
    backgroundColor: Colors.black,
    cardColor: Colors.grey[900]!,
    textColor: Colors.white,
    logColors: {
      TalkerLogType.error.key: Colors.red,
      TalkerLogType.warning.key: Colors.amber,
      TalkerLogType.info.key: Colors.lightBlue,
    },
  ),
)

Добавить больше данных в Telegram

В методе _sendToTelegram можно добавить:

  • Версию приложения
  • Модель устройства
  • Версию ОС
  • Текущий экран/роут
import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';

// В _sendToTelegram:
final packageInfo = await PackageInfo.fromPlatform();
final deviceInfo = await DeviceInfoPlugin().androidInfo; // или iosInfo

final text = '''
🚨 *$type*

📱 *Устройство:*
• Модель: ${deviceInfo.model}
• Android: ${deviceInfo.version.release}
• App: ${packageInfo.version}+${packageInfo.buildNumber}

// остальное...
''';

Изменить жест открытия

Вместо shake можно использовать:

  • Long press на определённом элементе
  • Секретная комбинация тапов
  • Скрытая кнопка в настройках
// Пример: long press на версии приложения
GestureDetector(
  onLongPress: () => TalkerService().openTalkerScreen(context),
  child: Text('v1.0.0'),
)

Безопасность

Что НЕ должно попадать в логи

  • Пароли пользователей
  • Токены авторизации
  • Номера карт и CVV
  • Персональные данные (по GDPR)

Рекомендации

  1. Не логируйте тела запросов с чувствительными данными
  2. Маскируйте токены в логах: token: "eyJ...***"
  3. Ограничьте список _allowedUserIds только разработчиками
  4. Храните токен бота безопасно (не в публичных репозиториях)

Troubleshooting

Сообщения не приходят в Telegram

  1. Проверьте токен бота - он должен быть актуальным
  2. Проверьте chat_id - для групп он отрицательный
  3. Убедитесь что бот добавлен в группу/канал
  4. Проверьте логи: Telegram: сообщение отправлено успешно или ошибка

TalkerScreen не открывается при тряске

  1. Проверьте что user ID в списке _allowedUserIds
  2. Убедитесь что ShakeGesture обёрнут правильно
  3. На эмуляторе тряска может не работать - используйте реальное устройство

Ошибки не логируются

  1. Убедитесь что TalkerService().initialize() вызывается первым в main()
  2. Проверьте что FlutterError.onError настроен
  3. Проверьте что runZonedGuarded оборачивает весь код

Ошибка "No GoRouter found in context"

Симптомы: При навигации из popup/modal или после закрытия диалога
появляется ошибка No GoRouter found in context.

Причина: В MaterialApp.router использован builder с дополнительным
Navigator, который перехватывает контекст и блокирует доступ к GoRouter.

Решение:

  1. Удалите дополнительный Navigator из builder
  2. Используйте _router.routerDelegate.navigatorKey.currentContext для
    получения контекста вместо собственного GlobalKey
// ❌ НЕПРАВИЛЬНО - блокирует GoRouter:
builder: (context, child) {
  return Navigator(
    key: _navigatorKey,
    onGenerateRoute: (_) => MaterialPageRoute(
      builder: (_) => child ?? const SizedBox.shrink(),
    ),
  );
},

// ✅ ПРАВИЛЬНО - используем контекст из GoRouter:
void _onShake() {
  final context = _router.routerDelegate.navigatorKey.currentContext;
  if (context != null) {
    TalkerService().openTalkerScreen(context);
  }
}

Автор

Решение разработано на базе пакета talker_flutter.

Дата: Январь 2026