← Назад к вопросам

Как разрабатывал кроссплатформенное приложение в продакшн?

1.6 Junior🔥 121 комментариев
#State Management#Архитектура Flutter

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Опыт разработки кроссплатформенного приложения в Production

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

1. Подход к архитектуре: от идеи к коду

Проект: E-commerce приложение для iOS и Android

При начале работы я всегда:

  1. Анализирую требования

    • Какие платформы? (iOS, Android, Web?)
    • Какие версии ОС? (minSdkVersion для Android, iOS version)
    • Какая производительность нужна?
    • Какие особенности платформ критичны?
  2. Выбираю архитектуру

    • Clean Architecture + BLoC для управления состоянием
    • Слоистая структура: Presentation → Application → Domain → Data
    • Dependency Injection через GetIt
// Примерная структура проекта
lib/
├── main.dart
├── config/
│   ├── theme.dart
│   ├── routes.dart
│   └── di_setup.dart      // Dependency Injection
│
├── core/                   // Общий код
│   ├── models/
│   ├── exceptions/
│   ├── utils/
│   └── constants/
│
└── features/               # Каждая фича - отдельный модуль
    ├── auth/
    │   ├── presentation/
    │   ├── domain/
    │   └── data/
    └── products/
        ├── presentation/
        ├── domain/
        └── data/

2. Работа с Native код

Проблема: Камера требует разных разрешений на iOS/Android

// iOS требует запись в Info.plist
// Android требует запись в AndroidManifest.xml
// Flutter: используем пакеты

import 'package:camera/camera.dart';
import 'package:permission_handler/permission_handler.dart';

class CameraService {
  Future<bool> requestCameraPermission() async {
    final status = await Permission.camera.request();
    return status.isGranted;
  }

  Future<void> initCamera() async {
    // Получаем доступные камеры
    final cameras = await availableCameras();
    // Логика инициализации
  }
}

Практика: всегда проверяй требования каждой платформы отдельно.

3. Хранение данных

Проблема: Локальное кэширование и синхронизация

// Использую SQLite + Hive для разных целей
// SQLite - для сложных данных
// Hive - для быстрого кэша (ключ-значение)

import 'package:hive/hive.dart';
import 'sqflite/sqflite.dart';

class DataPersistence {
  // Быстрое кэширование
  Future<void> cacheUserPreferences(UserPreferences prefs) async {
    final box = await Hive.openBox('preferences');
    box.put('theme', prefs.theme);
    box.put('language', prefs.language);
  }

  // Комплексные данные
  Future<void> saveUserData(User user) async {
    final db = await openDatabase('app.db');
    await db.insert('users', user.toJson());
  }

  // Синхронизация с сервером
  Future<void> syncWithBackend() async {
    try {
      final localData = await getLocalData();
      final response = await api.sync(localData);
      // Обновить локальные данные
    } catch (e) {
      // Если нет интернета - работаем с кэшем
      print('Offline mode');
    }
  }
}

4. Работа с API

Проблема: Обработка ошибок, retry логика, timeout

import 'package:dio/dio.dart';

class ApiClient {
  final dio = Dio();

  ApiClient() {
    // Retry interceptor
    dio.interceptors.add(
      RetryInterceptor(
        dio: dio,
        logPrint: print,
        maxRetries: 3,
      ),
    );

    // Timeout для разных типов запросов
    dio.options.connectTimeout = Duration(seconds: 10);
    dio.options.receiveTimeout = Duration(seconds: 30);
  }

  Future<T> request<T>(
    String url, {
    required String method,
    Map<String, dynamic>? data,
    T Function(Map<String, dynamic>)? parser,
  }) async {
    try {
      final response = await dio.request(
        url,
        options: Options(method: method),
        data: data,
      );

      if (response.statusCode == 200) {
        return parser!(response.data);
      } else if (response.statusCode == 401) {
        // Refresh token
        await _refreshToken();
        // Retry запрос
        return request(url, method: method, data: data, parser: parser);
      } else {
        throw ApiException('${response.statusCode}');
      }
    } on DioException catch (e) {
      if (e.type == DioExceptionType.connectionTimeout) {
        throw TimeoutException('Connection timeout');
      } else if (e.type == DioExceptionType.unknown) {
        throw NetworkException('No internet connection');
      }
      rethrow;
    }
  }
}

5. Работа с изображениями

Проблема: Оптимизация памяти, кэширование, разные размеры

class ImageManager {
  // Кэш изображений в памяти
  final imageCache = ImageCache();

  // Загрузка с кэшированием
  Future<File> downloadAndCacheImage(String url) async {
    // Проверить локальный файл
    final file = File('${appDir.path}/${hash(url)}');
    if (file.existsSync()) {
      return file;
    }

    // Скачать
    final response = await http.get(Uri.parse(url));
    await file.writeAsBytes(response.bodyBytes);
    return file;
  }

  // Оптимизировать размер для экрана
  Future<Image> optimizeImage(File imageFile) async {
    // Compress если нужно
    final compressedFile = await compressImage(imageFile);
    return Image.file(compressedFile);
  }
}

6. Тестирование

Проблема: Как тестировать кроссплатформенно?

// Unit тесты - тестируют бизнес логику
test('User login should return token', () async {
  final authRepo = MockAuthRepository();
  when(authRepo.login('email', 'password'))
    .thenAnswer((_) async => Token(value: 'token123'));
  
  expect(
    await authRepo.login('email', 'password'),
    Token(value: 'token123'),
  );
});

// Widget тесты - тестируют UI
testWidgets('Login screen shows error on wrong password', (tester) async {
  await tester.pumpWidget(MyApp());
  await tester.enterText(find.byType(TextField), 'wrong@email.com');
  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();
  
  expect(find.text('Wrong password'), findsOneWidget);
});

// Integration тесты - тестируют весь flow
void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  testWidgets('Full login flow', (tester) async {
    await tester.pumpWidget(MyApp());
    // Полный flow от входа до главного экрана
  });
}

7. Работа с платформоспецифичным кодом

Проблема: Push notifications, разные ОС требуют разных подходов

// Dart код
import 'package:firebase_messaging/firebase_messaging.dart';

class PushNotificationService {
  static const platform = MethodChannel('com.example.app/notifications');

  Future<void> initPushNotifications() async {
    // Firebase работает автоматически
    // Но для нативного кода нужны обработчики

    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Got a message whilst in the foreground');
    });

    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('User opened notification');
    });
  }

  Future<void> requestPermission() async {
    // iOS требует явного запроса
    await FirebaseMessaging.instance.requestPermission();
  }
}
// Kotlin для Android
FirebaseMessaging.getInstance().isAutoInitEnabled = true
// Swift для iOS
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in }

8. Производительность в Production

Проблема: Приложение медленное на старых устройствах

// Профилирование
flutter run --profile
// В DevTools: Performance tab

// Оптимизация
class ProductList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      // ❌ ПЛОХО: загружает все сразу
      // itemCount: 10000,
      
      // ✅ ХОРОШО: ленивая загрузка
      itemCount: items.length + 1,
      itemBuilder: (context, index) {
        if (index == items.length) {
          // Загрузить ещё
          context.read<ProductBloc>().add(FetchMore());
          return LoadingIndicator();
        }
        return ProductTile(items[index]);
      },
    );
  }
}

9. Release build и deployment

iOS:

flutter build ios --release
cd ios
pod install
xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release archive

Android:

flutter build appbundle --release
# Или APK
flutter build apk --release --split-per-abi

10. Мониторинг в Production

import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_analytics/firebase_analytics.dart';

void main() async {
  await Firebase.initializeApp();

  // Crashlytics
  FlutterError.onError = (errorDetails) {
    FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
  };

  // Analytics
  FirebaseAnalytics.instance.logEvent(
    name: 'product_viewed',
    parameters: {'product_id': productId},
  );

  runApp(MyApp());
}

11. CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v2

      - run: flutter pub get
      - run: flutter test
      - run: flutter build apk --release
      - run: flutter build ios --release
      
      # Upload to stores
      - run: bundle exec fastlane android deploy
      - run: bundle exec fastlane ios deploy

12. Ключевые уроки из практики

✅ ДО:

  1. Архитектура сначала - Clean Architecture + DI
  2. Тесты на ранних этапах - TDD помогает
  3. Обработка ошибок везде - даже в обычных операциях
  4. Профилирование - не додумывай, профилируй
  5. Версионирование API - легче обновлять
  6. Локализация с начала - сложно добавлять потом
  7. Документация - для новых members

❌ ИЗБЕГАЙ:

  1. Hardcode'ов и magic strings
  2. Глубокого вложения виджетов
  3. Синхронных операций в UI
  4. Пропуска обработки ошибок
  5. Развёртывания без тестов
  6. Игнорирования warnings компилятора

13. Инструменты, которые я использую

// Основные пакеты
futtertoast       // Уведомления
package_info      // Версия приложения
path_provider     // Доступ к файловой системе
device_info       // Информация об устройстве
connectivity      // Проверка интернета
shared_preferences // Настройки
getx              // Альтернатива Provider
riverpod          // Мощное управление состоянием

Вывод

Мой опыт разработки кроссплатформенных приложений:

  1. Архитектура важнее, чем фичи
  2. Тесты экономят время
  3. Кроссплатформенность требует внимания к деталям
  4. Профилирование - не опция, а необходимость
  5. CI/CD - критична для Production
  6. Мониторинг - узнаёшь о проблемах раньше юзеров
  7. Documentation и Code review - инвестиции в качество

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

Как разрабатывал кроссплатформенное приложение в продакшн? | PrepBro