Как разрабатывал кроссплатформенное приложение в продакшн?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Опыт разработки кроссплатформенного приложения в Production
Это вопрос о практическом опыте и методологии. Расскажу о реальных проектах, которые я разрабатывал, и что я на них научился.
1. Подход к архитектуре: от идеи к коду
Проект: E-commerce приложение для iOS и Android
При начале работы я всегда:
-
Анализирую требования
- Какие платформы? (iOS, Android, Web?)
- Какие версии ОС? (minSdkVersion для Android, iOS version)
- Какая производительность нужна?
- Какие особенности платформ критичны?
-
Выбираю архитектуру
- 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. Ключевые уроки из практики
✅ ДО:
- Архитектура сначала - Clean Architecture + DI
- Тесты на ранних этапах - TDD помогает
- Обработка ошибок везде - даже в обычных операциях
- Профилирование - не додумывай, профилируй
- Версионирование API - легче обновлять
- Локализация с начала - сложно добавлять потом
- Документация - для новых members
❌ ИЗБЕГАЙ:
- Hardcode'ов и magic strings
- Глубокого вложения виджетов
- Синхронных операций в UI
- Пропуска обработки ошибок
- Развёртывания без тестов
- Игнорирования warnings компилятора
13. Инструменты, которые я использую
// Основные пакеты
futtertoast // Уведомления
package_info // Версия приложения
path_provider // Доступ к файловой системе
device_info // Информация об устройстве
connectivity // Проверка интернета
shared_preferences // Настройки
getx // Альтернатива Provider
riverpod // Мощное управление состоянием
Вывод
Мой опыт разработки кроссплатформенных приложений:
- Архитектура важнее, чем фичи
- Тесты экономят время
- Кроссплатформенность требует внимания к деталям
- Профилирование - не опция, а необходимость
- CI/CD - критична для Production
- Мониторинг - узнаёшь о проблемах раньше юзеров
- Documentation и Code review - инвестиции в качество
Лучшее приложение — это не то, которое работает, а то, которое масштабируется, тестируется и поддерживается легко.