Mobile Mastery
Skill ID: coding/mobile-mastery
Domain: Mobile Development (iOS, Android, Cross-Platform)
Comprehensive mobile development covering Flutter, React Native, mobile security, cross-platform patterns, and build/deploy pipelines.
1. Flutter
graph TD
A[MaterialApp] --> B[Scaffold]
B --> C[AppBar]
B --> D[Body: Column]
B --> E[FloatingActionButton]
D --> F[Consumer<UserModel>]
D --> G[Expanded]
F --> H[ListView.builder]
G --> I[Text]
H --> J[ListTile]
C --> L[IconButton]
style A fill:#027DFD,color:#fff
style F fill:#FF9800,color:#000
Widget categories: Structural (Scaffold, AppBar), Layout (Column, Row, Stack, Expanded), Content (Text, Image), Interaction (GestureDetector, TextField), Animation (AnimatedContainer, Hero), Painting (DecoratedBox, Transform).
State Management: Provider & Riverpod
// Provider (classic)
class CartModel extends ChangeNotifier {
final List- _items = [];
List
- get items => UnmodifiableListView(_items);
int get totalPrice => _items.fold(0, (sum, i) => sum + i.price);
void add(Item item) { _items.add(item); notifyListeners(); }
void clear() { _items.clear(); notifyListeners(); }
}
void main() => runApp(
ChangeNotifierProvider(create: (_) => CartModel(), child: const MyApp()),
);
class CartTotal extends StatelessWidget {
@override Widget build(BuildContext context) => Consumer(
builder: (_, cart, __) => Text('Total: \$${cart.totalPrice}'),
);
}
// Riverpod (modern, compile-safe)
final cartProvider = NotifierProvider>(CartNotifier.new);
final totalPriceProvider = Provider((ref) =>
ref.watch(cartProvider).fold(0, (sum, i) => sum + i.price));
class CartNotifier extends Notifier> {
@override List- build() => [];
void add(Item item) => state = [...state, item];
void clear() => state = [];
}
class CartTotal extends ConsumerWidget {
@override Widget build(_, WidgetRef ref) =>
Text('Total: \$${ref.watch(totalPriceProvider)}');
}
// Dart side
class NativeApi {
static const _channel = MethodChannel('com.app/native_api');
static Future getDeviceId() async {
try { return await _channel.invokeMethod('getDeviceId'); }
on PlatformException catch (e) { debugPrint('Error: ${e.message}'); return null; }
}
static Stream observeNetwork() =>
const EventChannel('com.app/network').receiveBroadcastStream().cast();
}
// Android (Kotlin) — MethodChannel handler
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(engine: FlutterEngine) {
super.configureFlutterEngine(engine)
MethodChannel(engine.dartExecutor.binaryMessenger, "com.app/native_api")
.setMethodCallHandler { call, result ->
when (call.method) {
"getDeviceId" -> result.success(
Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID))
else -> result.notImplemented()
}
}
}
}
// iOS (Swift) — MethodChannel handler
@main class AppDelegate: FlutterAppDelegate {
override func application(_ app: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let ctrl = window?.rootViewController as? FlutterViewController
FlutterMethodChannel(name: "com.app/native_api", binaryMessenger: ctrl!.binaryMessenger)
.setMethodCallHandler { call, result in
if call.method == "getDeviceId" { result(UIDevice.current.identifierForVendor?.uuidString ?? "") }
else { result(FlutterMethodNotImplemented) }
}
return super.application(app, didFinishLaunchingWithOptions: opts)
}
}
FFI for performance-critical code:
import 'dart:ffi';
final DynamicLibrary _lib = DynamicLibrary.open('libprocessing.so');
final int Function(Pointer, int) _process =
_lib.lookupFunction, Int32)>,
int Function(Pointer, int)>('process_image');
2. React Native
Hooks Pattern
import {useState, useCallback, useEffect} from 'react';
import TouchID from 'react-native-touch-id';
import * as Keychain from 'react-native-keychain';
export function useBiometricAuth() {
const [state, setState] = useState({available: false, loading: false, error: null as string|null});
useEffect(() => {
TouchID.isSupported()
.then(() => setState(s => ({...s, available: true})))
.catch(() => setState(s => ({...s, available: false})));
}, []);
const authenticate = useCallback(async () => {
setState(s => ({...s, loading: true, error: null}));
try {
await TouchID.authenticate('Verify identity');
const creds = await Keychain.getGenericPassword({service: 'auth_token'});
setState(s => ({...s, loading: false}));
return creds;
} catch (err: any) {
setState(s => ({...s, loading: false, error: err.message}));
return null;
}
}, []);
return {...state, authenticate};
}
Navigation (React Navigation v6+)
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
type RootStackParamList = { Home: undefined; Profile: {userId: string} };
const Stack = createNativeStackNavigator();
export default function App() {
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: { screens: { Home: 'home', Profile: 'profile/:userId' } },
};
return (
);
}
Native Modules & Bridge Architecture
graph LR
subgraph "JavaScript Realm"
A[React Component] --> B[JS Bridge Module]
end
subgraph "Bridge — Async Message Queue"
C[JSON-serialized Messages]
end
subgraph "Native Realm"
D[iOS: Swift / ObjC]
E[Android: Kotlin / Java]
end
B -->|serialize + queue| C
C -->|dispatch| D
C -->|dispatch| E
D -->|callback/promise| C
E -->|callback/promise| C
C -->|resolve| B
style C fill:#FFD600,color:#000
style A fill:#61DAFB,color:#000
style D fill:#FC3D39,color:#fff
style E fill:#3DDC84,color:#000
// Android native module
class NativePaymentsModule(ctx: ReactApplicationContext) : ReactContextBaseJavaModule(ctx) {
override fun getName() = "NativePayments"
@ReactMethod
fun processPayment(amount: Double, currency: String, promise: Promise) {
try { promise.resolve(PaymentGateway.charge(amount, currency).toJson()) }
catch (e: Exception) { promise.reject("PAYMENT_FAILED", e.message, e) }
}
}
TurboModule (New Architecture):
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import {TurboModuleRegistry} from 'react-native';
export interface Spec extends TurboModule {
processPayment(amount: number, currency: string): Promise;
}
export default TurboModuleRegistry.getEnforcing('NativePayments');
Expo vs Bare Workflow
| Feature | Expo Managed | Expo Bare | Bare RN |
|---|---|---|---|
| Native code editing | ❌ | ✅ | ✅ |
| Custom native modules | Config plugins | ✅ | ✅ |
| OTA updates (EAS) | ✅ | ✅ | Manual |
| Cloud build (EAS) | ✅ | ✅ | Manual |
| App size | Larger | Optimized | Optimized |
3. Mobile Security
Secure Storage
// Flutter — flutter_secure_storage
class SecureStore {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessible.firstUnlockThisDeviceOnly),
);
static Future save(String k, String v) => _storage.write(key: k, value: v);
static Future read(String k) => _storage.read(key: k);
static Future delete(String k) => _storage.delete(key: k);
}
// React Native — react-native-keychain
export async function storeToken(token: string) {
await Keychain.setGenericPassword('auth', token, {
service: 'com.app.auth',
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
}
export async function retrieveToken() {
const c = await Keychain.getGenericPassword({service: 'com.app.auth'});
return c?.password ?? null;
}
Certificate Pinning
// Flutter — dio + custom validation
final dio = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () => HttpClient(context: SecurityContext.defaultContext),
validateCertificate: (cert, host, port) {
if (cert == null) return false;
final hash = sha256.convert(cert.der).toString();
return hash == 'expectedSha256Fingerprint';
},
);
// Android — OkHttp CertificatePinner
val pinner = CertificatePinner.Builder()
.add("api.myapp.com", "sha256/ABC123...")
.add("api.myapp.com", "sha256/DEF456...") // backup pin
.build()
val client = OkHttpClient.Builder().certificatePinner(pinner).build()
Biometric Authentication
// Flutter — local_auth
class BiometricAuth {
static final _auth = LocalAuthentication();
static Future isAvailable() async =>
await _auth.isDeviceSupported() && await _auth.canCheckBiometrics;
static Future authenticate({String reason = 'Verify identity'}) =>
_auth.authenticate(localizedReason: reason,
options: const AuthenticationOptions(stickyAuth: true, biometricOnly: true));
static Future> enrolledTypes() =>
_auth.getAvailableBiometrics();
}
Deep Link Validation
class DeepLinkHandler {
static const _allowedHosts = {'myapp.com', 'app.myapp.com'};
static const _allowedSchemes = {'myapp', 'https'};
static Uri? validate(String url) {
final uri = Uri.tryParse(url);
if (uri == null || !_allowedSchemes.contains(uri.scheme) ||
!_allowedHosts.contains(uri.host)) return null;
if (!RegExp(r'^/(product/\d+|profile/\w+|reset/[a-f0-9]{32})$')
.hasMatch(uri.path)) return null;
final allowedParams = {'ref', 'utm_source', 'token'};
if (uri.queryParameters.keys.any((k) => !allowedParams.contains(k))) return null;
return uri;
}
}
// React Native deep link validation
const ALLOWED = [/^\/product\/\d+$/, /^\/profile\/[a-zA-Z0-9_-]+$/];
export function validateDeepLink(url: string): URL | null {
try {
const p = new URL(url);
if (!['myapp','https'].includes(p.protocol.replace(':',''))) return null;
if (!['myapp.com','app.myapp.com'].includes(p.hostname)) return null;
return ALLOWED.some(r => r.test(p.pathname)) ? p : null;
} catch { return null; }
}
OWASP Mobile Top 10 (2024)
| # | Risk | Mitigation |
|---|---|---|
| M1 | Improper Platform Usage | Keychain/Keystore; validate all intent/URL inputs |
| M2 | Insecure Data Storage | Encrypt at rest; never log PII; wipe temp files |
| M3 | Insecure Communication | TLS 1.3; certificate pinning; no cleartext |
| M4 | Insecure Authentication | Biometric + refresh-token rotation; no stored passwords |
| M5 | Insufficient Cryptography | AES-256-GCM; ECDSA-P256; never MD5/SHA1 |
| M6 | Insecure Authorization | Server-side authz; role-based; no client-only checks |
| M7 | Client Code Quality | ProGuard/R8 obfuscation; static analysis in CI |
| M8 | Code Tampering | Bundle integrity checks; root/jailbreak detection |
| M9 | Reverse Engineering | Obfuscation; symbol stripping; anti-debug flags |
| M10 | Extraneous Functionality | Remove debug logs; no hardcoded keys; strip backdoors |
lib/
├── core/ # Shared business logic (models, repos, usecases)
├── platform/ # Platform abstraction (interface + mobile/web impls)
├── features/ # Shared VM + platform-specific screens per feature
└── di/ # Dependency injection (get_it / injectable)
abstract class PlatformAdapter {
String get platformName;
bool get hasBiometrics;
Future openAppSettings();
}
class MobileAdapter implements PlatformAdapter {
@override String get platformName => Platform.isIOS ? 'iOS' : 'Android';
@override bool get hasBiometrics => true;
@override Future openAppSettings() => OpenAppSettings.openAppSettings();
}
Responsive Layout
class ResponsiveBuilder extends StatelessWidget {
final Widget mobile; final Widget? tablet; final Widget? desktop;
const ResponsiveBuilder({super.key, required this.mobile, this.tablet, this.desktop});
@override Widget build(BuildContext context) => LayoutBuilder(builder: (_, c) {
if (c.maxWidth >= 1024 && desktop != null) return desktop!;
if (c.maxWidth >= 600 && tablet != null) return tablet!;
return mobile;
});
}
// React Native responsive hook
export function useBreakpoint(): 'mobile' | 'tablet' | 'desktop' {
const {width} = useWindowDimensions();
return width >= 1024 ? 'desktop' : width >= 600 ? 'tablet' : 'mobile';
}
Accessibility
// Flutter — semantic labeling
Semantics(label: 'Add to cart', button: true,
hint: 'Adds product to your shopping cart',
child: IconButton(icon: const Icon(Icons.add_shopping_cart), onPressed: _addToCart))
// React Native accessibility
Add to Cart
5. Build & Deploy
Deployment Pipeline
graph LR
A[Developer Push] --> B[CI Trigger]
B --> C[Lint + Unit Tests]
C --> D[Build iOS .ipa]
C --> E[Build Android .aab]
D --> F[iOS Test Suite]
E --> G[Android Test Suite]
F --> H[Code Signing]
G --> I[Code Signing + Align]
H --> J[App Store Connect]
I --> K[Play Console]
J --> L[TestFlight]
K --> M[Internal Track]
L --> N[Phased Release]
M --> N
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style H fill:#FF9800,color:#000
style I fill:#FF9800,color:#000
style N fill:#9C27B0,color:#fff
Fastlane Automation
default_platform(:ios)
platform :ios do
lane :release do
ensure_git_status_clean
increment_build_number(build_number: ENV["CI_BUILD_NUMBER"])
run_tests(scheme: "MyApp", devices: ["iPhone 15"])
build_app(workspace: "MyApp.xcworkspace", scheme: "MyApp",
export_method: "app-store",
export_options: { provisioningProfiles: { "com.myapp.prod" => "MyApp_AppStore" } })
upload_to_app_store(skip_metadata: true, distribute_external: true, groups: ["beta"])
end
end
platform :android do
lane :release do
ensure_git_status_clean
gradle(task: "testProdRelease")
gradle(task: "bundleProdRelease", properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"] })
upload_to_play_store(track: "internal",
aab: "android/app/build/outputs/bundle/prodRelease/app-prod-release.aab")
end
end
CI/CD (GitHub Actions)
name: Mobile CI/CD
on:
push: { branches: [main] }
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with: { flutter-version: '3.22.0', channel: 'stable' }
- run: flutter pub get
- run: dart analyze --fatal-infos
- run: flutter test --coverage
- uses: codecov/codecov-action@v3
deploy-ios:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: brew install fastlane && bundle install
- env:
MATCH_PASSWORD: "${{ secrets.MATCH_PASSWORD }}"
APP_STORE_CONNECT_API_KEY_ID: "${{ secrets.ASC_KEY_ID }}"
APP_STORE_CONNECT_ISSUER_ID: "${{ secrets.ASC_ISSUER_ID }}"
APP_STORE_CONNECT_API_KEY: "${{ secrets.ASC_PRIVATE_KEY }}"
CI_BUILD_NUMBER: "${{ github.run_number }}"
run: bundle exec fastlane ios release
deploy-android:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- env:
KEYSTORE_PATH: "${{ secrets.KEYSTORE_PATH }}"
KEYSTORE_PASSWORD: "${{ secrets.KEYSTORE_PASSWORD }}"
KEY_ALIAS: "${{ secrets.KEY_ALIAS }}"
KEY_PASSWORD: "${{ secrets.KEY_PASSWORD }}"
CI_BUILD_NUMBER: "${{ github.run_number }}"
run: bundle exec fastlane android release
Code Signing
# iOS — fastlane match (shared signing via encrypted git repo)
fastlane match appstore -y
CI uses App Store Connect API key — no 2FA bottleneck
Android — generate upload keystore
keytool -genkeypair -v -keystore upload.keystore -alias upload \
-keyalg RSA -keysize 2048 -validity 10000 -storetype PKCS12
OTA Updates
// Flutter — Shorebird (patches Dart heap at runtime)
// CLI: shorebird patch --release-version 1.0.0
import 'package:shorebird_code_push/shorebird_code_push.dart';
Future checkPatch() async {
final status = await ShorebirdUpdater().checkForUpdate();
if (status == UpdateStatus.restartRequired) {
// Prompt user to restart the app
}
}
// React Native — EAS Update
// Push: eas update --branch production --message "Fix login crash"
import * as Updates from 'expo-updates';
export function useOTAUpdate() {
useEffect(() => { (async () => {
const {isAvailable} = await Updates.checkForUpdateAsync();
if (isAvailable) {
await Updates.fetchUpdateAsync();
Alert.alert('Update Ready', 'Restart to apply?', [
{text: 'Later', style: 'cancel'},
{text: 'Restart', onPress: () => Updates.reloadAsync()},
]);
}
})(); }, []);
}
Quick Decision Matrix
| Decision | Flutter | React Native |
|---|---|---|
| UI fidelity | Pixel-perfect custom | Native feel by default |
| Performance | Higher (Skia/Impeller) | Good (Hermes + Fabric) |
| Team skill | Dart knowledge | JS/TS ecosystem |
| Hot reload | ✅ Stateful | ✅ Fast refresh |
| Native modules | Channels / FFI | Bridge / TurboModules |
| Desktop/web | ✅ Official | Community (rn-web) |
| OTA updates | Shorebird | EAS Update |