Cross-Platform Development Ecosystem Tutorial
Introduction
Cross-platform development enables developers to create applications that run on multiple operating systems and devices using a single codebase or shared components. This tutorial explores the comprehensive ecosystem of tools, frameworks, and strategies for building cross-platform applications.
Overview
Cross-platform development has become essential in modern software development, allowing teams to maximize code reuse, reduce development costs, and reach broader audiences across different platforms and devices.
Benefits of Cross-Platform Development
- Cost Efficiency: Single codebase reduces development and maintenance costs
- Faster Time-to-Market: Deploy to multiple platforms simultaneously
- Consistent User Experience: Unified design and functionality across platforms
- Resource Optimization: One development team can target multiple platforms
- Easier Maintenance: Updates and bug fixes applied once
Cross-Platform Technology Stack
Frontend Technologies
- React Native: Mobile apps using React
- Flutter: Google's UI toolkit using Dart
- Ionic: Web-based mobile apps
- Xamarin: Microsoft's .NET-based framework
- Electron: Desktop apps using web technologies
- Progressive Web Apps (PWA): Web apps with native features
Backend Technologies
- Node.js: JavaScript runtime for servers
- Python: FastAPI, Django, Flask
- Go: High-performance concurrent applications
- Java: Spring Boot for enterprise applications
- C#/.NET: Microsoft's cross-platform framework
Development Tools
- Docker: Containerization for consistent environments
- GitHub Actions: CI/CD across platforms
- Firebase: Backend-as-a-Service platform
- MongoDB: Cross-platform NoSQL database
React Native Mobile Ecosystem
Project Setup and Architecture
# Create new React Native project
npx react-native@latest init CrossPlatformApp --template react-native-template-typescript
# Install essential dependencies
npm install @react-navigation/native @react-navigation/native-stack
npm install react-native-screens react-native-safe-area-context
npm install @react-native-async-storage/async-storage
npm install react-native-vector-icons
npm install @reduxjs/toolkit react-redux
// src/types/index.ts
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
preferences: UserPreferences;
}
export interface UserPreferences {
theme: 'light' | 'dark' | 'auto';
language: string;
notifications: boolean;
}
export interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
images: string[];
inStock: boolean;
}
export interface CartItem {
product: Product;
quantity: number;
}
State Management with Redux Toolkit
// src/store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { User } from '../../types';
import { apiService } from '../../services/api';
interface UserState {
currentUser: User | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
currentUser: null,
loading: false,
error: null,
};
export const loginUser = createAsyncThunk(
'user/login',
async (credentials: { email: string; password: string }) => {
const response = await apiService.login(credentials);
return response.data;
}
);
export const updateUserProfile = createAsyncThunk(
'user/updateProfile',
async (userData: Partial<User>) => {
const response = await apiService.updateUser(userData);
return response.data;
}
);
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
logout: (state) => {
state.currentUser = null;
state.error = null;
},
clearError: (state) => {
state.error = null;
},
updatePreferences: (state, action: PayloadAction<UserPreferences>) => {
if (state.currentUser) {
state.currentUser.preferences = action.payload;
}
},
},
extraReducers: (builder) => {
builder
.addCase(loginUser.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(loginUser.fulfilled, (state, action) => {
state.loading = false;
state.currentUser = action.payload;
})
.addCase(loginUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Login failed';
})
.addCase(updateUserProfile.fulfilled, (state, action) => {
state.currentUser = action.payload;
});
},
});
export const { logout, clearError, updatePreferences } = userSlice.actions;
export default userSlice.reducer;
Cross-Platform API Service
// src/services/api.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
interface ApiConfig {
baseURL: string;
timeout: number;
}
class ApiService {
private config: ApiConfig;
private token: string | null = null;
constructor() {
this.config = {
baseURL: Platform.select({
ios: 'https://api-ios.example.com',
android: 'https://api-android.example.com',
web: 'https://api.example.com',
default: 'https://api.example.com',
}),
timeout: 10000,
};
this.initializeToken();
}
private async initializeToken() {
try {
this.token = await AsyncStorage.getItem('authToken');
} catch (error) {
console.error('Error loading auth token:', error);
}
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ data: T; status: number }> {
const url = `${this.config.baseURL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
if (this.token) {
headers.Authorization = `Bearer ${this.token}`;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
try {
const response = await fetch(url, {
...options,
headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return { data, status: response.status };
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
async login(credentials: { email: string; password: string }) {
const response = await this.request('/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
if (response.data.token) {
this.token = response.data.token;
await AsyncStorage.setItem('authToken', this.token);
}
return response;
}
async logout() {
try {
await this.request('/auth/logout', { method: 'POST' });
} finally {
this.token = null;
await AsyncStorage.removeItem('authToken');
}
}
async getProducts(params?: {
category?: string;
search?: string;
page?: number;
limit?: number;
}) {
const queryParams = new URLSearchParams(params as any).toString();
const endpoint = `/products${queryParams ? `?${queryParams}` : ''}`;
return this.request(endpoint);
}
async updateUser(userData: any) {
return this.request('/user/profile', {
method: 'PUT',
body: JSON.stringify(userData),
});
}
// File upload with cross-platform support
async uploadFile(file: any, endpoint: string = '/upload') {
const formData = new FormData();
if (Platform.OS === 'web') {
formData.append('file', file);
} else {
formData.append('file', {
uri: file.uri,
type: file.type,
name: file.name,
} as any);
}
return fetch(`${this.config.baseURL}${endpoint}`, {
method: 'POST',
body: formData,
headers: {
'Content-Type': 'multipart/form-data',
...(this.token && { Authorization: `Bearer ${this.token}` }),
},
});
}
}
export const apiService = new ApiService();
Cross-Platform UI Components
// src/components/PlatformButton.tsx
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
Platform,
ViewStyle,
TextStyle,
} from 'react-native';
interface PlatformButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
}
export const PlatformButton: React.FC<PlatformButtonProps> = ({
title,
onPress,
variant = 'primary',
disabled = false,
style,
textStyle,
}) => {
return (
<TouchableOpacity
style={[
styles.button,
styles[variant],
Platform.OS === 'ios' && styles.iosButton,
Platform.OS === 'android' && styles.androidButton,
disabled && styles.disabled,
style,
]}
onPress={onPress}
disabled={disabled}
activeOpacity={0.7}
>
<Text style={[styles.text, styles[`${variant}Text`], textStyle]}>
{title}
</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
minHeight: 48,
},
iosButton: {
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
androidButton: {
elevation: 2,
borderRadius: 6,
},
primary: {
backgroundColor: '#007AFF',
},
secondary: {
backgroundColor: '#F2F2F7',
borderWidth: 1,
borderColor: '#C7C7CC',
},
danger: {
backgroundColor: '#FF3B30',
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
fontWeight: '600',
textAlign: 'center',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#007AFF',
},
dangerText: {
color: '#FFFFFF',
},
});
Platform-Specific Navigation
// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Platform } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { HomeScreen } from '../screens/HomeScreen';
import { ProductsScreen } from '../screens/ProductsScreen';
import { ProfileScreen } from '../screens/ProfileScreen';
import { CartScreen } from '../screens/CartScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: string;
switch (route.name) {
case 'Home':
iconName = focused ? 'home' : 'home-outline';
break;
case 'Products':
iconName = focused ? 'grid' : 'grid-outline';
break;
case 'Cart':
iconName = focused ? 'cart' : 'cart-outline';
break;
case 'Profile':
iconName = focused ? 'person' : 'person-outline';
break;
default:
iconName = 'circle';
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: Platform.select({
ios: '#007AFF',
android: '#2196F3',
default: '#007AFF',
}),
tabBarInactiveTintColor: 'gray',
headerStyle: {
backgroundColor: Platform.select({
ios: '#F8F9FA',
android: '#2196F3',
default: '#F8F9FA',
}),
},
headerTintColor: Platform.select({
ios: '#000',
android: '#FFF',
default: '#000',
}),
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Products" component={ProductsScreen} />
<Tab.Screen name="Cart" component={CartScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
};
export const AppNavigator = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Main"
component={TabNavigator}
options={{ headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
Flutter Cross-Platform Development
Project Structure and Setup
# pubspec.yaml
name: cross_platform_app
description: A cross-platform Flutter application
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: ">=3.10.0"
dependencies:
flutter:
sdk: flutter
# State management
bloc: ^8.1.2
flutter_bloc: ^8.1.3
# Navigation
go_router: ^12.0.0
# HTTP client
dio: ^5.3.2
# Storage
shared_preferences: ^2.2.2
hive: ^2.2.3
hive_flutter: ^1.1.0
# UI
cached_network_image: ^3.3.0
image_picker: ^1.0.4
# Platform-specific
device_info_plus: ^9.1.0
package_info_plus: ^4.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
build_runner: ^2.4.7
hive_generator: ^2.0.1
json_annotation: ^4.8.1
json_serializable: ^6.7.1
flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/
fonts:
- family: CustomFont
fonts:
- asset: assets/fonts/CustomFont-Regular.ttf
- asset: assets/fonts/CustomFont-Bold.ttf
weight: 700
Cross-Platform Data Models
// lib/models/user.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:hive/hive.dart';
part 'user.g.dart';
()
(typeId: 0)
class User extends HiveObject {
(0)
final String id;
(1)
final String name;
(2)
final String email;
(3)
final String? avatar;
(4)
final UserPreferences preferences;
User({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.preferences,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
User copyWith({
String? name,
String? email,
String? avatar,
UserPreferences? preferences,
}) {
return User(
id: id,
name: name ?? this.name,
email: email ?? this.email,
avatar: avatar ?? this.avatar,
preferences: preferences ?? this.preferences,
);
}
}
()
(typeId: 1)
class UserPreferences extends HiveObject {
(0)
final ThemeMode theme;
(1)
final String language;
(2)
final bool notifications;
UserPreferences({
required this.theme,
required this.language,
required this.notifications,
});
factory UserPreferences.fromJson(Map<String, dynamic> json) => _$UserPreferencesFromJson(json);
Map<String, dynamic> toJson() => _$UserPreferencesToJson(this);
}
enum ThemeMode {
light,
dark,
system
}
State Management with BLoC
// lib/bloc/user/user_bloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import '../models/user.dart';
import '../repositories/user_repository.dart';
part 'user_event.dart';
part 'user_state.dart';
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository _userRepository;
UserBloc({required UserRepository userRepository})
: _userRepository = userRepository,
super(const UserState()) {
on<UserLoginRequested>(_onLoginRequested);
on<UserLogoutRequested>(_onLogoutRequested);
on<UserProfileUpdateRequested>(_onProfileUpdateRequested);
on<UserPreferencesUpdateRequested>(_onPreferencesUpdateRequested);
}
Future<void> _onLoginRequested(
UserLoginRequested event,
Emitter<UserState> emit,
) async {
emit(state.copyWith(status: UserStatus.loading));
try {
final user = await _userRepository.login(
email: event.email,
password: event.password,
);
emit(state.copyWith(
status: UserStatus.authenticated,
user: user,
));
} catch (error) {
emit(state.copyWith(
status: UserStatus.unauthenticated,
error: error.toString(),
));
}
}
Future<void> _onLogoutRequested(
UserLogoutRequested event,
Emitter<UserState> emit,
) async {
try {
await _userRepository.logout();
emit(const UserState());
} catch (error) {
emit(state.copyWith(error: error.toString()));
}
}
Future<void> _onProfileUpdateRequested(
UserProfileUpdateRequested event,
Emitter<UserState> emit,
) async {
emit(state.copyWith(status: UserStatus.loading));
try {
final updatedUser = await _userRepository.updateProfile(event.userData);
emit(state.copyWith(
status: UserStatus.authenticated,
user: updatedUser,
));
} catch (error) {
emit(state.copyWith(
status: UserStatus.authenticated,
error: error.toString(),
));
}
}
Future<void> _onPreferencesUpdateRequested(
UserPreferencesUpdateRequested event,
Emitter<UserState> emit,
) async {
if (state.user != null) {
final updatedUser = state.user!.copyWith(preferences: event.preferences);
try {
await _userRepository.updatePreferences(event.preferences);
emit(state.copyWith(user: updatedUser));
} catch (error) {
emit(state.copyWith(error: error.toString()));
}
}
}
}
Cross-Platform Services
// lib/services/api_service.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ApiService {
late Dio _dio;
static const String _baseUrl = 'https://api.example.com';
ApiService() {
_dio = Dio(BaseOptions(
baseUrl: _baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Content-Type': 'application/json',
},
));
_setupInterceptors();
}
void _setupInterceptors() {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
// Add auth token
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('auth_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Handle token expiration
await _refreshToken();
return handler.resolve(await _dio.fetch(error.requestOptions));
}
handler.next(error);
},
),
);
}
Future<Response<T>> request<T>(
String path, {
String method = 'GET',
Map<String, dynamic>? queryParameters,
dynamic data,
Options? options,
}) async {
try {
return await _dio.request<T>(
path,
queryParameters: queryParameters,
data: data,
options: options?.copyWith(method: method) ?? Options(method: method),
);
} catch (e) {
throw _handleError(e);
}
}
Future<Response> uploadFile(
String path,
File file, {
Map<String, dynamic>? data,
ProgressCallback? onProgress,
}) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(file.path),
...?data,
});
return request(
path,
method: 'POST',
data: formData,
options: Options(
onSendProgress: onProgress,
),
);
}
Future<void> _refreshToken() async {
final prefs = await SharedPreferences.getInstance();
final refreshToken = prefs.getString('refresh_token');
if (refreshToken != null) {
try {
final response = await _dio.post('/auth/refresh', data: {
'refresh_token': refreshToken,
});
final newToken = response.data['access_token'];
await prefs.setString('auth_token', newToken);
} catch (e) {
// Redirect to login
await prefs.clear();
throw Exception('Session expired');
}
}
}
Exception _handleError(dynamic error) {
if (error is DioException) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.receiveTimeout:
return Exception('Connection timeout');
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
final message = error.response?.data['message'] ?? 'Unknown error';
return Exception('HTTP $statusCode: $message');
default:
return Exception('Network error');
}
}
return Exception('Unexpected error: $error');
}
}
Platform-Adaptive UI
// lib/widgets/platform_adaptive_button.dart
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class PlatformAdaptiveButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final ButtonStyle? style;
final bool isDestructive;
const PlatformAdaptiveButton({
Key? key,
required this.text,
this.onPressed,
this.style,
this.isDestructive = false,
}) : super(key: key);
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoButton(
onPressed: onPressed,
color: isDestructive ? CupertinoColors.destructiveRed : CupertinoColors.activeBlue,
child: Text(text),
);
}
return ElevatedButton(
onPressed: onPressed,
style: style ?? ElevatedButton.styleFrom(
backgroundColor: isDestructive ? Colors.red : Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
child: Text(text),
);
}
}
// lib/widgets/platform_loading_indicator.dart
class PlatformLoadingIndicator extends StatelessWidget {
final Color? color;
const PlatformLoadingIndicator({Key? key, this.color}) : super(key: key);
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoActivityIndicator(
color: color ?? CupertinoColors.activeBlue,
);
}
return CircularProgressIndicator(
color: color ?? Theme.of(context).primaryColor,
);
}
}
// lib/screens/adaptive_scaffold.dart
class AdaptiveScaffold extends StatelessWidget {
final String title;
final Widget body;
final List<Widget>? actions;
final Widget? floatingActionButton;
final Widget? drawer;
const AdaptiveScaffold({
Key? key,
required this.title,
required this.body,
this.actions,
this.floatingActionButton,
this.drawer,
}) : super(key: key);
Widget build(BuildContext context) {
if (Platform.isIOS) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(title),
trailing: actions != null ? Row(
mainAxisSize: MainAxisSize.min,
children: actions!,
) : null,
),
child: SafeArea(child: body),
);
}
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: actions,
),
body: body,
floatingActionButton: floatingActionButton,
drawer: drawer,
);
}
}
Electron Desktop Application
Project Setup and Configuration
// package.json
{
"name": "cross-platform-desktop-app",
"version": "1.0.0",
"description": "Cross-platform desktop application built with Electron",
"main": "dist/electron/main.js",
"scripts": {
"start": "concurrently \"npm run start:react\" \"wait-on http://localhost:3000 && npm run start:electron\"",
"start:react": "react-scripts start",
"start:electron": "electron .",
"build": "npm run build:react && npm run build:electron",
"build:react": "react-scripts build",
"build:electron": "tsc -p tsconfig.electron.json",
"pack": "electron-builder --dir",
"dist": "npm run build && electron-builder",
"dist:win": "npm run build && electron-builder --win",
"dist:mac": "npm run build && electron-builder --mac",
"dist:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"electron": "^27.0.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@reduxjs/toolkit": "^1.9.7",
"react-redux": "^8.1.3"
},
"devDependencies": {
"@types/electron": "^1.6.10",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"electron-builder": "^24.6.4",
"typescript": "^5.2.2",
"wait-on": "^7.2.0"
},
"build": {
"appId": "com.example.crossplatformapp",
"productName": "Cross Platform App",
"directories": {
"output": "dist"
},
"files": [
"build/**/*",
"node_modules/**/*",
"dist/electron/**/*"
],
"mac": {
"target": "dmg",
"category": "public.app-category.productivity"
},
"win": {
"target": "nsis",
"icon": "assets/icon.ico"
},
"linux": {
"target": "AppImage",
"category": "Office"
}
}
}
Electron Main Process
// src/electron/main.ts
import { app, BrowserWindow, Menu, shell, ipcMain, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import Store from 'electron-store';
import * as path from 'path';
interface AppConfig {
windowBounds: {
width: number;
height: number;
x?: number;
y?: number;
};
theme: 'light' | 'dark' | 'system';
autoLaunch: boolean;
}
class MainApplication {
private mainWindow: BrowserWindow | null = null;
private store: Store<AppConfig>;
constructor() {
this.store = new Store<AppConfig>({
defaults: {
windowBounds: { width: 1200, height: 800 },
theme: 'system',
autoLaunch: false,
},
});
this.setupEventHandlers();
}
private setupEventHandlers() {
app.on('ready', this.onReady.bind(this));
app.on('window-all-closed', this.onWindowAllClosed.bind(this));
app.on('activate', this.onActivate.bind(this));
app.on('second-instance', this.onSecondInstance.bind(this));
// IPC handlers
ipcMain.handle('app:get-version', () => app.getVersion());
ipcMain.handle('app:get-platform', () => process.platform);
ipcMain.handle('store:get', (_, key) => this.store.get(key));
ipcMain.handle('store:set', (_, key, value) => this.store.set(key, value));
ipcMain.handle('dialog:show-save', this.showSaveDialog.bind(this));
ipcMain.handle('dialog:show-open', this.showOpenDialog.bind(this));
}
private async onReady() {
await this.createMainWindow();
this.setupMenu();
this.setupAutoUpdater();
}
private async createMainWindow() {
const bounds = this.store.get('windowBounds');
this.mainWindow = new BrowserWindow({
...bounds,
minWidth: 800,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
show: false,
});
// Load the app
const isDev = process.env.NODE_ENV === 'development';
const url = isDev
? 'http://localhost:3000'
: `file://${path.join(__dirname, '../build/index.html')}`;
await this.mainWindow.loadURL(url);
// Show window when ready
this.mainWindow.once('ready-to-show', () => {
this.mainWindow?.show();
if (isDev) {
this.mainWindow?.webContents.openDevTools();
}
});
// Save window bounds on close
this.mainWindow.on('close', () => {
if (this.mainWindow) {
this.store.set('windowBounds', this.mainWindow.getBounds());
}
});
// Handle external links
this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
}
private setupMenu() {
const template: Electron.MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => this.mainWindow?.webContents.send('menu:new'),
},
{
label: 'Open',
accelerator: 'CmdOrCtrl+O',
click: () => this.mainWindow?.webContents.send('menu:open'),
},
{
label: 'Save',
accelerator: 'CmdOrCtrl+S',
click: () => this.mainWindow?.webContents.send('menu:save'),
},
{ type: 'separator' },
{
label: 'Exit',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
click: () => app.quit(),
},
],
},
{
label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' },
],
},
{
label: 'View',
submenu: [
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
{ label: 'Force Reload', accelerator: 'CmdOrCtrl+Shift+R', role: 'forceReload' },
{ label: 'Toggle Developer Tools', accelerator: 'F12', role: 'toggleDevTools' },
{ type: 'separator' },
{ label: 'Actual Size', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
{ label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
{ label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
{ type: 'separator' },
{ label: 'Toggle Fullscreen', accelerator: 'F11', role: 'togglefullscreen' },
],
},
{
label: 'Window',
submenu: [
{ label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' },
{ label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' },
],
},
{
label: 'Help',
submenu: [
{
label: 'About',
click: () => this.showAboutDialog(),
},
{
label: 'Check for Updates',
click: () => autoUpdater.checkForUpdatesAndNotify(),
},
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
private setupAutoUpdater() {
autoUpdater.checkForUpdatesAndNotify();
autoUpdater.on('update-available', () => {
dialog.showMessageBox(this.mainWindow!, {
type: 'info',
title: 'Update Available',
message: 'A new version is available. It will be downloaded in the background.',
buttons: ['OK'],
});
});
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox(this.mainWindow!, {
type: 'info',
title: 'Update Ready',
message: 'Update downloaded. The application will restart to apply the update.',
buttons: ['Restart Now', 'Later'],
}).then((result) => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
}
private async showSaveDialog() {
if (!this.mainWindow) return null;
const result = await dialog.showSaveDialog(this.mainWindow, {
filters: [
{ name: 'JSON Files', extensions: ['json'] },
{ name: 'All Files', extensions: ['*'] },
],
});
return result.canceled ? null : result.filePath;
}
private async showOpenDialog() {
if (!this.mainWindow) return null;
const result = await dialog.showOpenDialog(this.mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'JSON Files', extensions: ['json'] },
{ name: 'All Files', extensions: ['*'] },
],
});
return result.canceled ? null : result.filePaths[0];
}
private showAboutDialog() {
dialog.showMessageBox(this.mainWindow!, {
type: 'info',
title: 'About',
message: 'Cross Platform App',
detail: `Version: ${app.getVersion()}\nBuilt with Electron and React`,
buttons: ['OK'],
});
}
private onWindowAllClosed() {
if (process.platform !== 'darwin') {
app.quit();
}
}
private onActivate() {
if (BrowserWindow.getAllWindows().length === 0) {
this.createMainWindow();
}
}
private onSecondInstance() {
if (this.mainWindow) {
if (this.mainWindow.isMinimized()) {
this.mainWindow.restore();
}
this.mainWindow.focus();
}
}
}
// Prevent multiple instances
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
new MainApplication();
}
Docker Containerization
Multi-Platform Docker Setup
# Dockerfile
# Multi-stage build for cross-platform deployment
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci --only=production
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build the application
RUN npm run build
# Production image, copy all the files and run the app
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 appgroup
RUN adduser --system --uid 1001 appuser
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER appuser
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "start"]
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
platforms:
- linux/amd64
- linux/arm64
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:password@db:5432/appdb
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:15-alpine
platform: linux/amd64
environment:
- POSTGRES_DB=appdb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
redis:
image: redis:7-alpine
platform: linux/amd64
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
postgres_data:
redis_data:
CI/CD Pipeline for Cross-Platform
GitHub Actions Workflow
# .github/workflows/cross-platform-build.yml
name: Cross-Platform Build and Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lint
build-web:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build web application
run: npm run build:web
- name: Upload web build artifacts
uses: actions/upload-artifact@v3
with:
name: web-build
path: build/
build-mobile:
needs: test
strategy:
matrix:
platform: [ios, android]
runs-on: ${{ matrix.platform == 'ios' && 'macos-latest' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v3
- name: Setup React Native
uses: ./.github/actions/setup-react-native
- name: Build Android
if: matrix.platform == 'android'
run: |
cd android
./gradlew assembleRelease
- name: Build iOS
if: matrix.platform == 'ios'
run: |
cd ios
xcodebuild -workspace CrossPlatformApp.xcworkspace -scheme CrossPlatformApp -configuration Release
- name: Upload mobile build artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.platform }}-build
path: ${{ matrix.platform == 'android' && 'android/app/build/outputs/apk/release/' || 'ios/build/' }}
build-desktop:
needs: test
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build Electron app
run: npm run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload desktop build artifacts
uses: actions/upload-artifact@v3
with:
name: desktop-${{ matrix.os }}
path: dist/
deploy-web:
needs: build-web
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Download web build artifacts
uses: actions/download-artifact@v3
with:
name: web-build
path: build/
- name: Deploy to production
uses: ./.github/actions/deploy-web
with:
build-path: build/
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy-mobile:
needs: build-mobile
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to App Store Connect
if: contains(github.event.head_commit.message, '[deploy-ios]')
uses: ./.github/actions/deploy-ios
- name: Deploy to Google Play
if: contains(github.event.head_commit.message, '[deploy-android]')
uses: ./.github/actions/deploy-android
Best Practices and Architecture Patterns
Shared Business Logic
// src/core/services/BusinessLogicService.ts
export class BusinessLogicService {
static calculateTotal(items: CartItem[]): number {
return items.reduce((total, item) => {
return total + (item.product.price * item.quantity);
}, 0);
}
static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static formatPrice(amount: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
}).format(amount);
}
static calculateShipping(total: number, shippingMethod: string): number {
const shippingRates = {
standard: 5.99,
express: 12.99,
overnight: 24.99,
};
if (total > 100) {
return 0; // Free shipping over $100
}
return shippingRates[shippingMethod] || shippingRates.standard;
}
static generateOrderId(): string {
const timestamp = Date.now().toString(36);
const randomStr = Math.random().toString(36).substring(2, 7);
return `ORD-${timestamp}-${randomStr}`.toUpperCase();
}
static isValidCreditCard(cardNumber: string): boolean {
// Luhn algorithm implementation
const digits = cardNumber.replace(/\D/g, '');
let sum = 0;
let shouldDouble = false;
for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits.charAt(i), 10);
if (shouldDouble) {
digit *= 2;
if (digit > 9) {
digit -= 9;
}
}
sum += digit;
shouldDouble = !shouldDouble;
}
return sum % 10 === 0;
}
}
Platform Detection Utilities
// src/utils/PlatformUtils.ts
export class PlatformUtils {
static get isWeb(): boolean {
return typeof window !== 'undefined' && 'document' in window;
}
static get isMobile(): boolean {
return !this.isWeb && (this.isIOS || this.isAndroid);
}
static get isDesktop(): boolean {
return this.isElectron;
}
static get isIOS(): boolean {
return typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
}
static get isAndroid(): boolean {
return typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent);
}
static get isElectron(): boolean {
return typeof window !== 'undefined' && 'electronAPI' in window;
}
static get platform(): 'web' | 'ios' | 'android' | 'electron' {
if (this.isElectron) return 'electron';
if (this.isIOS) return 'ios';
if (this.isAndroid) return 'android';
return 'web';
}
static supportsFeature(feature: string): boolean {
const features = {
push_notifications: this.isWeb || this.isMobile,
file_system: this.isElectron,
camera: this.isMobile,
geolocation: this.isWeb || this.isMobile,
biometric_auth: this.isMobile,
native_sharing: this.isMobile,
};
return features[feature] || false;
}
static getStoragePrefix(): string {
return `crossplatform_${this.platform}_`;
}
static async requestPermission(permission: string): Promise<boolean> {
switch (permission) {
case 'camera':
if (this.isMobile) {
// Implementation would depend on platform-specific APIs
return true;
}
break;
case 'location':
if ('geolocation' in navigator) {
return new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
() => resolve(true),
() => resolve(false)
);
});
}
break;
case 'notifications':
if ('Notification' in window) {
const permission = await Notification.requestPermission();
return permission === 'granted';
}
break;
}
return false;
}
}
Testing Cross-Platform Applications
Unified Testing Strategy
// __tests__/cross-platform/BusinessLogic.test.ts
import { BusinessLogicService } from '../../src/core/services/BusinessLogicService';
import { CartItem, Product } from '../../src/types';
describe('BusinessLogicService', () => {
const mockProduct: Product = {
id: '1',
name: 'Test Product',
description: 'A test product',
price: 29.99,
category: 'Electronics',
images: [],
inStock: true,
};
const mockCartItem: CartItem = {
product: mockProduct,
quantity: 2,
};
describe('calculateTotal', () => {
it('should calculate correct total for single item', () => {
const result = BusinessLogicService.calculateTotal([mockCartItem]);
expect(result).toBe(59.98);
});
it('should calculate correct total for multiple items', () => {
const items = [
mockCartItem,
{ ...mockCartItem, quantity: 1 },
];
const result = BusinessLogicService.calculateTotal(items);
expect(result).toBe(89.97);
});
it('should return 0 for empty cart', () => {
const result = BusinessLogicService.calculateTotal([]);
expect(result).toBe(0);
});
});
describe('validateEmail', () => {
it('should validate correct email addresses', () => {
expect(BusinessLogicService.validateEmail('test@example.com')).toBe(true);
expect(BusinessLogicService.validateEmail('user+tag@domain.co.uk')).toBe(true);
});
it('should reject invalid email addresses', () => {
expect(BusinessLogicService.validateEmail('invalid-email')).toBe(false);
expect(BusinessLogicService.validateEmail('@domain.com')).toBe(false);
expect(BusinessLogicService.validateEmail('test@')).toBe(false);
});
});
describe('isValidCreditCard', () => {
it('should validate correct credit card numbers', () => {
expect(BusinessLogicService.isValidCreditCard('4532015112830366')).toBe(true);
expect(BusinessLogicService.isValidCreditCard('5425233430109903')).toBe(true);
});
it('should reject invalid credit card numbers', () => {
expect(BusinessLogicService.isValidCreditCard('1234567890123456')).toBe(false);
expect(BusinessLogicService.isValidCreditCard('invalid')).toBe(false);
});
});
});
Performance Optimization
Code Splitting and Lazy Loading
// src/utils/LazyLoader.ts
export class LazyLoader {
static async loadComponent(componentPath: string) {
try {
const module = await import(componentPath);
return module.default || module;
} catch (error) {
console.error(`Failed to load component: ${componentPath}`, error);
throw error;
}
}
static createLazyComponent(importFunction: () => Promise<any>) {
return React.lazy(() =>
importFunction().catch(error => {
console.error('Component loading failed:', error);
// Return a fallback component
return { default: () => React.createElement('div', null, 'Component failed to load') };
})
);
}
static async preloadComponents(componentPaths: string[]) {
const loadPromises = componentPaths.map(path => this.loadComponent(path));
return Promise.allSettled(loadPromises);
}
}
// Usage in React
const LazyProductList = LazyLoader.createLazyComponent(
() => import('../components/ProductList')
);
const LazyUserProfile = LazyLoader.createLazyComponent(
() => import('../components/UserProfile')
);
Conclusion
Cross-platform development enables efficient creation of applications that work across multiple platforms while maintaining code reuse and consistency. Key strategies include:
- Choose the Right Framework: Select frameworks that align with your team's expertise and project requirements
- Shared Business Logic: Extract platform-agnostic business logic into shared modules
- Platform-Specific Optimizations: Adapt UI and UX for each platform's conventions
- Unified Testing Strategy: Test business logic once, UI components per platform
- CI/CD Integration: Automate builds and deployments across all target platforms
- Performance Monitoring: Monitor performance across different platforms and devices
The cross-platform approach significantly reduces development time and maintenance overhead while reaching broader audiences across web, mobile, and desktop platforms.
Content Review
The content in this repository has been reviewed by chevp. Chevp is dedicated to ensuring that the information provided is accurate, relevant, and up-to-date, helping users to learn and implement programming skills effectively.
About the Reviewer
For more insights and contributions, visit chevp's GitHub profile: chevp's GitHub Profile.