From 4264aecc696591c98ce571f15a2a90869a638aac Mon Sep 17 00:00:00 2001 From: ChauVanSang Date: Mon, 8 Dec 2025 22:27:33 +0700 Subject: [PATCH 1/2] add document for hydrid projet --- HYBRID_INTEGRATION.md | 838 ++++++++++++++++++++++++++++++++++++++++++ QUICK_REFERENCE.md | 281 ++++++++++++++ 2 files changed, 1119 insertions(+) create mode 100644 HYBRID_INTEGRATION.md create mode 100644 QUICK_REFERENCE.md diff --git a/HYBRID_INTEGRATION.md b/HYBRID_INTEGRATION.md new file mode 100644 index 0000000..1f60550 --- /dev/null +++ b/HYBRID_INTEGRATION.md @@ -0,0 +1,838 @@ +# Kotlin-Flutter Hybrid Integration Documentation + +## Table of Contents +- [Overview](#overview) +- [Architecture](#architecture) +- [Communication Channels](#communication-channels) +- [Android Native Side](#android-native-side) +- [Flutter Side](#flutter-side) +- [Navigation Flow](#navigation-flow) +- [Event Handling](#event-handling) +- [Example Use Cases](#example-use-cases) +- [Best Practices](#best-practices) + +--- + +## Overview + +This project implements a hybrid Android application that integrates native Kotlin with Flutter Module. The architecture enables bidirectional communication between native Android (written in Kotlin with Jetpack Compose) and Flutter UI components. + +### Key Features +- **Bidirectional Communication**: Native ↔ Flutter event exchange +- **Shared Flutter Engine**: Single FlutterEngine cached and reused across the app +- **Dependency Injection**: Dagger Hilt (Android) and GetIt (Flutter) +- **State Management**: BLoC pattern in Flutter, ViewModel in Android +- **Navigation**: Seamless transition between native and Flutter screens + +--- + +## Architecture + +### High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Android Native Layer │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ DashboardActivity (Jetpack Compose) │ │ +│ │ - Display appliances list │ │ +│ │ - Navigate to Flutter screens │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ HybridFlutterActivity │ │ +│ │ - Hosts FlutterFragment │ │ +│ │ - Registers event listeners │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────┼────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ViewModel │ │ EventBus │ │ NavigateService│ │ +│ └──────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────┘ + │ + │ MethodChannel / EventChannel + │ +┌─────────────────────────────────────────────────────────┐ +│ Flutter Module │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ HybridBusService │ │ +│ │ - Receives events from native │ │ +│ │ - Sends events to native │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ BLoC Layer (HybridBloc, ApplianceDetailBloc) │ │ +│ │ - Business logic │ │ +│ │ - Event handling │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ UI Layer (HomePage, ApplianceDetailPage) │ │ +│ │ - Flutter widgets │ │ +│ │ - User interaction │ │ +│ └──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Communication Channels + +The integration uses Flutter's Platform Channels for bidirectional communication: + +### 1. MethodChannel (`hybrid_channel`) +- **Purpose**: Bidirectional method invocation +- **Android → Flutter**: Navigate to routes, pass data +- **Flutter → Android**: Navigate back, send events + +### 2. EventChannel (`hybrid_event_channel`) +- **Purpose**: Stream events from Android to Flutter +- **Status**: Currently set up but not actively used (can be extended) + +### Communication Protocol + +``` +Android Native Flutter +───────────────── ─────── + │ │ + │ sendEventToHydrid() │ + ├──────────────────────────────>│ + │ MethodChannel.invokeMethod │ + │ ("navigate", data) │ + │ │ + │ setMethodCallHandler() + │ │ + │ │ + │ sendEventToNative() │ + │<──────────────────────────────┤ + │ MethodChannel.invokeMethod │ + │ ("navigateBack", data) │ + │ │ +``` + +--- + +## Android Native Side + +### 1. FlutterEngine Setup + +**File**: [FlutterEngineModule.kt](app/src/main/java/com/example/myapplication/di/FlutterEngineModule.kt) + +```kotlin +@Module +@InstallIn(SingletonComponent::class) +object FlutterEngineModule { + @Provides + @Singleton + fun provideFlutterEngine(@ApplicationContext context: Context): FlutterEngine { + val engine = FlutterEngine(context).apply { + dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + } + FlutterEngineCache.getInstance().put(Constants.flutterEngineName, engine) + return engine + } +} +``` + +**Key Points**: +- Single FlutterEngine instance created at app startup +- Pre-warms the Dart runtime for faster Flutter screen loading +- Cached for reuse across multiple FlutterFragment instances +- Managed by Dagger Hilt for dependency injection + +### 2. HybridEventBus + +**File**: [HybridEventBus.kt](app/src/main/java/com/example/myapplication/core/service/HybridEventBus.kt) + +```kotlin +interface HybridEventBus { + fun sendEventToHydrid(event: String, data: Any?) + fun register() + fun clearQueue() + val hydridEventFlow: Flow +} +``` + +**Responsibilities**: +- **Sending Events**: `sendEventToHydrid()` sends events to Flutter via MethodChannel +- **Receiving Events**: Listens for Flutter events via `setMethodCallHandler` +- **Event Queue**: Queues events before channel registration to prevent loss +- **Flow API**: Exposes `hydridEventFlow` for reactive event handling + +**Event Flow**: +1. Events are queued if channel not initialized +2. Once `register()` is called, channel is set up +3. Queued events are flushed to Flutter +4. Incoming Flutter events are emitted via `MutableSharedFlow` + +### 3. HybridFlutterActivity + +**File**: [HybridFlutterActivity.kt](app/src/main/java/com/example/myapplication/ui/feature/hydrid/HybridFlutterActivity.kt) + +```kotlin +@AndroidEntryPoint +class HybridFlutterActivity : FragmentActivity() { + private val hydridViewModel: HydridFlutterViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_hybrid_flutter) + + // Listen for close event + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + hydridViewModel.closeActivity.collect { + finish() + } + } + } + + // Create FlutterFragment + val fragment = FlutterFragment.withCachedEngine(Constants.flutterEngineName).build() + + supportFragmentManager.commit { + replace(R.id.flutter_container, fragment, flutterFragmentTag) + } + + hydridViewModel.registerEvent() + } +} +``` + +**Key Points**: +- Hosts `FlutterFragment` in a container +- Uses cached FlutterEngine for performance +- Registers event listeners via ViewModel +- Listens for `navigateBack` events to close activity + +### 4. HydridFlutterViewModel + +**File**: [HydridFlutterViewModel.kt](app/src/main/java/com/example/myapplication/ui/feature/hydrid/viewmodel/HydridFlutterViewModel.kt) + +```kotlin +@HiltViewModel +class HydridFlutterViewModel @Inject constructor( + private val repository: ApplianceRepository, + private val hydridNavigateService: HydridNavigateService, + private val hydridEventBus: HybridEventBus +) : ViewModel() { + + private val _closeActivity = MutableSharedFlow(replay = 0) + val closeActivity = _closeActivity.asSharedFlow() + + fun registerEvent() { + hydridEventBus.register() + listenEventFromFlutter() + } + + fun listenEventFromFlutter() { + viewModelScope.launch { + hydridEventBus.hydridEventFlow.collect { event -> + when (event.name) { + "navigateBack" -> _closeActivity.emit(Unit) + } + } + } + } +} +``` + +**Responsibilities**: +- Registers HybridEventBus +- Listens for Flutter events +- Handles navigation commands (e.g., `navigateBack`) + +### 5. Navigation Service + +**File**: [HydridNavigateService.kt](app/src/main/java/com/example/myapplication/core/service/HydridNavigateService.kt) + +```kotlin +interface HydridNavigateService { + fun navigateToHybridRouteWithParameters( + routeContext: Context?, + route: String, + parameters: Map + ) +} + +class FlutterNavigateService @Inject constructor( + private val hydridEventBus: HybridEventBus, + private val flutterEngine: FlutterEngine +) : HydridNavigateService { + + override fun navigateToHybridRouteWithParameters( + routeContext: Context?, + route: String, + parameters: Map + ) { + val newParameters = mutableMapOf() + newParameters.putAll(parameters) + newParameters["routeName"] = route + hydridEventBus.sendEventToHydrid("navigate", data = newParameters) + } +} +``` + +**Usage**: +```kotlin +// In DashBoardViewModel +fun goToApplianceDetail(context: Context, appliance: Appliance) { + val hydridIntent = Intent(context, HybridFlutterActivity::class.java) + context.startActivity(hydridIntent) + + hydridNavigateService.navigateToHybridRouteWithParameters( + route = "/appliance/${appliance.id}", + routeContext = null, + parameters = appliance.toMap() + ) +} +``` + +--- + +## Flutter Side + +### 1. Dependency Injection + +**File**: [injection_container.dart](flutter_module/lib/core/di/injection_container.dart) + +```dart +import 'package:get_it/get_it.dart'; + +final getIt = GetIt.instance; + +void setupLocator() { + getIt.registerLazySingleton(() => HybridBusServiceImpl()); + getIt.registerLazySingleton(() => NavigationServiceImpl()); +} +``` + +### 2. HybridBusService + +**File**: [hybrid_bus_service.dart](flutter_module/lib/core/services/hybrid_bus_service.dart) + +```dart +abstract class HybridBusService { + Future sendEventToNative(String event, Map data); + void registerListener(void Function(String event, Map data) onEvent); +} + +class HybridBusServiceImpl implements HybridBusService { + static const String _methodChannelName = 'hybrid_channel'; + final MethodChannel _methodChannel; + + HybridBusServiceImpl() : _methodChannel = const MethodChannel(_methodChannelName); + + @override + Future sendEventToNative(String event, Map data) async { + await _methodChannel.invokeMethod(event, data); + } + + @override + void registerListener(void Function(String event, Map data) onEvent) { + _methodChannel.setMethodCallHandler((call) async { + final args = call.arguments as Map? ?? {}; + onEvent(call.method, Map.from(args)); + }); + } +} +``` + +### 3. BLoC Integration + +**File**: [hybrid_bloc.dart](flutter_module/lib/ui/home/bloc/hybrid_bloc.dart) + +```dart +class HybridBloc extends Bloc { + final HybridBusService _hybridBusService; + final NavigationService _navigationService; + + HybridBloc(this._hybridBusService, this._navigationService) + : super(const HybridState.initial()) { + + // Register listener for native events + _hybridBusService.registerListener((event, data) { + if (event == 'navigate') { + final routeName = data['routeName'] as String?; + if (routeName != null) { + _navigationService.navigateTo(routeName, arguments: data); + } + } + }); + + on((event, emit) async { + // Handle events + }); + } +} +``` + +### 4. Navigation Service + +**File**: [navigation_service.dart](flutter_module/lib/core/services/navigation_service.dart) + +```dart +abstract class NavigationService { + Future? navigateTo(String routeName, {Object? arguments}); + void goBack(); +} + +class NavigationServiceImpl implements NavigationService { + @override + Future? navigateTo(String routeName, {Object? arguments}) { + return App.navigatorKey.currentState?.pushNamed(routeName, arguments: arguments); + } + + @override + void goBack() { + SystemNavigator.pop(); // Closes the Flutter activity + } +} +``` + +### 5. Route Mapping + +**File**: [route_mapper.dart](flutter_module/lib/core/routes/route_mapper.dart) + +```dart +class RouteMapper { + static Route onGenerateRoute(RouteSettings settings) { + final name = settings.name ?? '/'; + + if (name.startsWith(AppRoutes.applianceDetail)) { + final arguments = settings.arguments; + if (arguments is Map) { + final appliance = Appliance.fromJson(arguments); + return MaterialPageRoute( + builder: (_) => ApplianceDetailPage(appliance: appliance), + settings: settings, + ); + } + } + + return MaterialPageRoute(builder: (_) => HomePage(), settings: settings); + } +} +``` + +--- + +## Navigation Flow + +### Native → Flutter Navigation + +**Scenario**: User taps an appliance card in the native dashboard + +``` +┌─────────────────────────────────────────────────────────┐ +│ Step 1: User taps appliance card │ +│ Location: DashboardActivity (Compose) │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 2: DashBoardViewModel.goToApplianceDetail() │ +│ - Start HybridFlutterActivity │ +│ - Call HydridNavigateService │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 3: Send "navigate" event via MethodChannel │ +│ Data: { routeName: "/appliance/1", ...applianceData } │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 4: Flutter receives event in HybridBloc │ +│ - Parse routeName and data │ +│ - Call NavigationService.navigateTo() │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 5: RouteMapper generates route │ +│ - Parse Appliance from JSON │ +│ - Navigate to ApplianceDetailPage │ +└─────────────────────────────────────────────────────────┘ +``` + +### Flutter → Native Navigation + +**Scenario**: User taps back button in Flutter screen + +``` +┌─────────────────────────────────────────────────────────┐ +│ Step 1: User taps back button │ +│ Location: ApplianceDetailPage │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 2: ApplianceDetailBloc emits navigateBack event │ +│ - Calls HybridBusService.sendEventToNative() │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 3: MethodChannel sends "navigateBack" to Android │ +│ Data: { } │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 4: HybridEventBus receives event │ +│ - Emits to hydridEventFlow │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 5: HydridFlutterViewModel handles event │ +│ - Emits to closeActivity flow │ +└─────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Step 6: HybridFlutterActivity calls finish() │ +│ - Activity closes, returns to native dashboard │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Event Handling + +### Event Types + +| Event Name | Direction | Purpose | Data | +|-----------------|------------------|----------------------------------|-----------------------------------------| +| `navigate` | Android → Flutter| Navigate to Flutter route | `{ routeName: String, ...params }` | +| `navigateBack` | Flutter → Android| Close Flutter screen | `{}` | + +### Adding New Events + +#### 1. Android Side + +**Sending Event to Flutter**: +```kotlin +// In any service/viewmodel with HybridEventBus +hybridEventBus.sendEventToHydrid("customEvent", mapOf( + "key1" to "value1", + "key2" to 123 +)) +``` + +**Handling Event from Flutter**: +```kotlin +// In HydridFlutterViewModel +fun listenEventFromFlutter() { + viewModelScope.launch { + hydridEventBus.hydridEventFlow.collect { event -> + when (event.name) { + "navigateBack" -> _closeActivity.emit(Unit) + "customEvent" -> handleCustomEvent(event.data) + } + } + } +} +``` + +#### 2. Flutter Side + +**Sending Event to Android**: +```dart +// In any BLoC/Service +await hybridBusService.sendEventToNative('customEvent', { + 'key1': 'value1', + 'key2': 123, +}); +``` + +**Handling Event from Android**: +```dart +// In HybridBloc or any BLoC +_hybridBusService.registerListener((event, data) { + switch (event) { + case 'navigate': + // Handle navigation + break; + case 'customEvent': + // Handle custom event + handleCustomEvent(data); + break; + } +}); +``` + +--- + +## Example Use Cases + +### Use Case 1: Opening Appliance Detail + +**Flow**: User clicks appliance card → Opens Flutter detail screen + +**Android Code**: +```kotlin +// DashBoardViewModel.kt +fun goToApplianceDetail(context: Context, appliance: Appliance) { + // Start Flutter activity + val hydridIntent = Intent(context, HybridFlutterActivity::class.java) + context.startActivity(hydridIntent) + + // Send navigation event with data + hydridNavigateService.navigateToHybridRouteWithParameters( + route = "/appliance/${appliance.id}", + routeContext = null, + parameters = appliance.toMap() + ) +} +``` + +**Flutter Code**: +```dart +// HybridBloc.dart +_hybridBusService.registerListener((event, data) { + if (event == 'navigate') { + final routeName = data['routeName'] as String?; + if (routeName != null) { + _navigationService.navigateTo(routeName, arguments: data); + } + } +}); + +// route_mapper.dart +static Route onGenerateRoute(RouteSettings settings) { + if (settings.name?.startsWith(AppRoutes.applianceDetail) == true) { + final arguments = settings.arguments as Map; + final appliance = Appliance.fromJson(arguments); + return MaterialPageRoute( + builder: (_) => ApplianceDetailPage(appliance: appliance), + ); + } + return MaterialPageRoute(builder: (_) => HomePage()); +} +``` + +### Use Case 2: Back Navigation + +**Flow**: User taps back button in Flutter → Closes Flutter screen → Returns to native + +**Flutter Code**: +```dart +// ApplianceDetailBloc.dart +on((event, emit) async { + event.when( + navigateBack: () async { + await _hybridBusService.sendEventToNative('navigateBack', {}); + }, + ); +}); +``` + +**Android Code**: +```kotlin +// HydridFlutterViewModel.kt +fun listenEventFromFlutter() { + viewModelScope.launch { + hydridEventBus.hydridEventFlow.collect { event -> + when (event.name) { + "navigateBack" -> _closeActivity.emit(Unit) + } + } + } +} + +// HybridFlutterActivity.kt +lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + hydridViewModel.closeActivity.collect { + finish() // Close activity + } + } +} +``` + +--- + +## Best Practices + +### 1. Performance Optimization + +**Use Cached FlutterEngine**: +```kotlin +// Pre-warm engine at app startup +FlutterEngineModule.provideFlutterEngine(context) + +// Reuse in activities +FlutterFragment.withCachedEngine(Constants.flutterEngineName).build() +``` + +**Benefits**: +- Faster Flutter screen load times +- Reduced memory usage +- Shared state between Flutter screens + +### 2. Event Queue Management + +**Problem**: Events sent before MethodChannel initialization are lost + +**Solution**: Queue events and flush after registration +```kotlin +// HybridEventBus.kt +private val eventQueue = mutableListOf() + +override fun sendEventToHydrid(event: String, data: Any?) { + if (!isInit) { + eventQueue.add(NativeEvent(name = event, data = data)) + return + } + methodChannel.invokeMethod(event, data) +} + +override fun register() { + // ... setup channels + isInit = true + + // Flush queued events + eventQueue.forEach { event -> + sendEventToHydrid(event.name, event.data) + } + eventQueue.clear() +} +``` + +### 3. Type Safety + +**Use strongly-typed models**: +```kotlin +// Android +data class Appliance( + val id: String, + val name: String, + // ... +) { + fun toMap(): Map = mapOf( + "id" to id, + "name" to name, + // ... + ) +} +``` + +```dart +// Flutter +class Appliance { + final String id; + final String name; + + factory Appliance.fromJson(Map json) { + return Appliance( + id: json['id'] as String, + name: json['name'] as String, + ); + } +} +``` + +### 4. Error Handling + +**Handle MethodChannel errors gracefully**: +```dart +try { + await _methodChannel.invokeMethod(event, data); +} on PlatformException catch (e) { + debugPrint('Error sending event to native: ${e.message}'); + // Handle error +} +``` + +### 5. Lifecycle Management + +**Register/unregister listeners properly**: +```kotlin +// In HybridFlutterActivity +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + hydridViewModel.registerEvent() +} + +override fun onDestroy() { + super.onDestroy() + hydridEventBus.clearQueue() +} +``` + +### 6. Testing + +**Mock Platform Channels in tests**: +```dart +// In tests +setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + const MethodChannel('hybrid_channel').setMockMethodCallHandler((call) async { + // Mock responses + }); +}); +``` + +--- + +## Troubleshooting + +### Common Issues + +#### 1. FlutterEngine not found +**Error**: `The requested FlutterEngine was not found` + +**Solution**: Ensure engine is cached before creating FlutterFragment +```kotlin +FlutterEngineModule.provideFlutterEngine(context) +``` + +#### 2. Events not received +**Error**: Flutter doesn't receive navigation events + +**Solution**: +- Check channel name matches on both sides +- Call `hybridEventBus.register()` in Activity +- Verify MethodChannel is set up before sending events + +#### 3. Activity doesn't close +**Error**: `navigateBack` event sent but activity stays open + +**Solution**: Verify event handler is registered and flow is collected: +```kotlin +lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + hydridViewModel.closeActivity.collect { finish() } + } +} +``` + +--- + +## Summary + +This hybrid architecture provides: +- **Seamless Integration**: Native Kotlin UI with Flutter screens +- **Bidirectional Communication**: Events flow both ways via MethodChannel +- **Performance**: Cached FlutterEngine for fast load times +- **Maintainability**: Clear separation of concerns with DI +- **Scalability**: Easy to add new events and navigation routes + +### Key Files Reference + +**Android**: +- [FlutterEngineModule.kt](app/src/main/java/com/example/myapplication/di/FlutterEngineModule.kt) - FlutterEngine DI +- [HybridEventBus.kt](app/src/main/java/com/example/myapplication/core/service/HybridEventBus.kt) - Event communication +- [HybridFlutterActivity.kt](app/src/main/java/com/example/myapplication/ui/feature/hydrid/HybridFlutterActivity.kt) - Flutter host +- [HydridNavigateService.kt](app/src/main/java/com/example/myapplication/core/service/HydridNavigateService.kt) - Navigation helper + +**Flutter**: +- [main.dart](flutter_module/lib/main.dart) - Entry point +- [hybrid_bus_service.dart](flutter_module/lib/core/services/hybrid_bus_service.dart) - Platform channel wrapper +- [hybrid_bloc.dart](flutter_module/lib/ui/home/bloc/hybrid_bloc.dart) - Event handling BLoC +- [route_mapper.dart](flutter_module/lib/core/routes/route_mapper.dart) - Route configuration + +--- + +**Last Updated**: 2025-12-08 diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md new file mode 100644 index 0000000..bc790c8 --- /dev/null +++ b/QUICK_REFERENCE.md @@ -0,0 +1,281 @@ +# Kotlin-Flutter Integration Quick Reference + +## Communication Channels + +### Channel Names +``` +MethodChannel: "hybrid_channel" +EventChannel: "hybrid_event_channel" (setup but unused) +``` + +## Sending Events + +### Android → Flutter + +```kotlin +// Option 1: Via HybridEventBus +@Inject lateinit var hybridEventBus: HybridEventBus + +hybridEventBus.sendEventToHydrid("eventName", mapOf( + "key" to "value" +)) + +// Option 2: Via NavigateService +@Inject lateinit var navigateService: HydridNavigateService + +navigateService.navigateToHybridRouteWithParameters( + route = "/route/path", + routeContext = null, + parameters = mapOf("key" to "value") +) +``` + +### Flutter → Android + +```dart +// Inject service +final hybridBusService = getIt(); + +// Send event +await hybridBusService.sendEventToNative('eventName', { + 'key': 'value', +}); +``` + +## Receiving Events + +### Android Receiving from Flutter + +```kotlin +// In ViewModel +@HiltViewModel +class MyViewModel @Inject constructor( + private val hybridEventBus: HybridEventBus +) : ViewModel() { + + init { + hybridEventBus.register() // Important! + listenToFlutter() + } + + private fun listenToFlutter() { + viewModelScope.launch { + hybridEventBus.hydridEventFlow.collect { event -> + when (event.name) { + "eventName" -> handleEvent(event.data) + } + } + } + } +} +``` + +### Flutter Receiving from Android + +```dart +// In BLoC constructor +class MyBloc extends Bloc { + final HybridBusService _hybridBusService; + + MyBloc(this._hybridBusService) : super(Initial()) { + // Register listener + _hybridBusService.registerListener((event, data) { + switch (event) { + case 'navigate': + handleNavigation(data); + break; + case 'customEvent': + handleCustomEvent(data); + break; + } + }); + } +} +``` + +## Navigation Patterns + +### Native → Flutter Screen + +```kotlin +// 1. Start Flutter activity +val intent = Intent(context, HybridFlutterActivity::class.java) +context.startActivity(intent) + +// 2. Send navigation event with data +hydridNavigateService.navigateToHybridRouteWithParameters( + route = "/target/route", + routeContext = null, + parameters = myData.toMap() +) +``` + +### Flutter → Native (Back) + +```dart +// Send back event +await hybridBusService.sendEventToNative('navigateBack', {}); +``` + +## Dependency Injection + +### Android (Hilt) + +```kotlin +// Module +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun provideHybridEventBus(engine: FlutterEngine): HybridEventBus = + FlutterHydridEventBus(engine) +} + +// Usage +@HiltViewModel +class MyViewModel @Inject constructor( + private val hybridEventBus: HybridEventBus +) : ViewModel() +``` + +### Flutter (GetIt) + +```dart +// Setup +void setupLocator() { + getIt.registerLazySingleton( + () => HybridBusServiceImpl() + ); +} + +// Usage +final hybridBusService = getIt(); +``` + +## Common Events + +| Event | Direction | Data | Purpose | +|----------------|-----------|---------------------------------------|----------------------------| +| `navigate` | A → F | `{ routeName, ...params }` | Navigate to Flutter route | +| `navigateBack` | F → A | `{}` | Close Flutter screen | + +## FlutterEngine Management + +### Setup (in Application) + +```kotlin +@Provides +@Singleton +fun provideFlutterEngine(@ApplicationContext context: Context): FlutterEngine { + val engine = FlutterEngine(context).apply { + dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + } + FlutterEngineCache.getInstance().put(Constants.flutterEngineName, engine) + return engine +} +``` + +### Usage (in Activity) + +```kotlin +// Use cached engine +val fragment = FlutterFragment + .withCachedEngine(Constants.flutterEngineName) + .build() + +supportFragmentManager.commit { + replace(R.id.flutter_container, fragment) +} +``` + +## Checklist for New Feature + +### Android Side +- [ ] Define event name constant +- [ ] Create data model with `toMap()` method +- [ ] Add event handler in ViewModel +- [ ] Send event via `HybridEventBus` or `NavigateService` +- [ ] Register listener if receiving events from Flutter + +### Flutter Side +- [ ] Define route in `AppRoutes` +- [ ] Create model with `fromJson()` factory +- [ ] Add route handler in `RouteMapper` +- [ ] Register listener in BLoC +- [ ] Send events via `HybridBusService` + +## Debugging Tips + +### Check Event Flow +```kotlin +// Android - Add logs +override fun sendEventToHydrid(event: String, data: Any?) { + Log.d("HybridEventBus", "Sending: $event -> $data") + methodChannel.invokeMethod(event, data) +} +``` + +```dart +// Flutter - Add logs +@override +Future sendEventToNative(String event, Map data) async { + debugPrint('Sending to native: $event -> $data'); + await _methodChannel.invokeMethod(event, data); +} +``` + +### Verify Channel Setup +```kotlin +// Check if MethodChannel is initialized +if (isInit) { + Log.d("HybridEventBus", "Channel ready") +} else { + Log.w("HybridEventBus", "Channel not initialized, queuing event") +} +``` + +## File Structure + +``` +app/src/main/java/com/example/myapplication/ +├── core/ +│ ├── service/ +│ │ ├── HybridEventBus.kt # Event communication +│ │ └── HydridNavigateService.kt # Navigation helper +│ └── MainApplication.kt +├── di/ +│ ├── FlutterEngineModule.kt # Flutter engine DI +│ └── ServiceModule.kt # Services DI +└── ui/ + └── feature/ + └── hydrid/ + ├── HybridFlutterActivity.kt # Flutter host + └── viewmodel/ + └── HydridFlutterViewModel.kt + +flutter_module/lib/ +├── core/ +│ ├── di/ +│ │ └── injection_container.dart # GetIt setup +│ ├── routes/ +│ │ ├── app_routes.dart # Route constants +│ │ └── route_mapper.dart # Route configuration +│ └── services/ +│ ├── hybrid_bus_service.dart # Platform channel +│ └── navigation_service.dart # Navigation helper +├── ui/ +│ ├── home/ +│ │ ├── bloc/ +│ │ │ └── hybrid_bloc.dart # Event handling +│ │ └── home_page.dart +│ └── appliance/ +│ └── appliance_detail_page.dart +└── main.dart # Entry point +``` + +--- + +**See [HYBRID_INTEGRATION.md](HYBRID_INTEGRATION.md) for detailed documentation** From 4c5023a8316c09d95b487664d1a615eaafcf6d49 Mon Sep 17 00:00:00 2001 From: ChauVanSang Date: Mon, 8 Dec 2025 22:38:26 +0700 Subject: [PATCH 2/2] Update readme document --- README.md | 449 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 447 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa1ec9c..341cc03 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,447 @@ -# sample_hydrid_android -An example demonstrating how to integrate Flutter (following Clean Architecture) with native Android (using the MVVM pattern), including the mechanism for exchanging data between the two layers. +# Hybrid Android-Flutter Application + +A production-ready hybrid mobile application demonstrating seamless integration between native Android (Kotlin + Jetpack Compose) and Flutter Module with bidirectional communication. + +## Overview + +This project showcases how to build a hybrid mobile application where: +- Native Android screens are built with **Jetpack Compose** and **MVVM architecture** +- Flutter screens follow **Clean Architecture** with **BLoC pattern** +- Both layers communicate bidirectionally via **Platform Channels** +- Shared **FlutterEngine** for optimal performance +- Complete **CI/CD** pipeline with GitHub Actions and CircleCI + +## Features + +- **Hybrid Navigation**: Seamless transitions between native and Flutter screens +- **Bidirectional Communication**: Native ↔ Flutter event exchange via MethodChannel +- **Appliance Management**: Browse appliances in native UI, view details in Flutter +- **State Management**: + - Android: MVVM with StateFlow/SharedFlow + - Flutter: BLoC pattern with Freezed +- **Dependency Injection**: + - Android: Dagger Hilt + - Flutter: GetIt +- **Testing**: Comprehensive unit tests for Flutter BLoC components +- **CI/CD**: Automated builds and testing with GitHub Actions & CircleCI + +## Tech Stack + +### Android Native +- **Language**: Kotlin +- **UI**: Jetpack Compose +- **Architecture**: MVVM (Model-View-ViewModel) +- **DI**: Dagger Hilt +- **Networking**: Retrofit + OkHttp +- **Async**: Coroutines + Flow +- **Build**: Gradle with Kotlin DSL + +### Flutter Module +- **Language**: Dart +- **Architecture**: Clean Architecture with BLoC +- **State Management**: flutter_bloc + Freezed +- **DI**: GetIt +- **Networking**: Dio +- **Testing**: mockito + bloc_test +- **Code Generation**: build_runner, freezed + +### Communication Layer +- **Platform Channels**: MethodChannel for bidirectional method invocation +- **Event Channel**: EventChannel for streaming (setup for future use) +- **Data Format**: JSON for cross-platform data serialization + +## Architecture + +### High-Level Architecture + +``` +┌──────────────────────────────────────────────────────┐ +│ Android Native Layer │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ DashboardActivity (Jetpack Compose) │ │ +│ │ - Appliance list │ │ +│ │ - Image carousel │ │ +│ │ - Support cards │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ Intent │ +│ ▼ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ HybridFlutterActivity │ │ +│ │ - Hosts FlutterFragment │ │ +│ │ - Manages lifecycle │ │ +│ └────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────┘ + │ + │ MethodChannel + ▼ +┌──────────────────────────────────────────────────────┐ +│ Flutter Module Layer │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ HomePage │ │ +│ │ - Flutter home screen │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ ApplianceDetailPage │ │ +│ │ - Appliance details │ │ +│ │ - Interactive controls │ │ +│ └────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────┘ +``` + +### Communication Flow + +``` +Android Native Platform Channel Flutter +───────────── ───────────────── ─────── +DashBoardViewModel + │ + │ 1. Start HybridFlutterActivity + ├──────────────────────────────────────────────> + │ + │ 2. Send "navigate" event + │ + appliance data + ├────────────────────────> MethodChannel + │ │ + │ │ 3. Receive event + │ └────────────────> HybridBloc + │ │ + │ │ 4. Navigate + │ │ to route + │ ▼ + │ ApplianceDetailPage + │ │ + │ 5. Send "navigateBack" │ + │ <────────────────────────┤ + │ 6. Receive event │ + │<─────────────────────────┤ + │ + │ 7. Close activity + │ finish() +``` + +## Project Structure + +``` +sample_hydrid_android/ +├── app/ # Android native app +│ ├── src/main/java/com/example/myapplication/ +│ │ ├── core/ +│ │ │ ├── MainApplication.kt +│ │ │ ├── service/ +│ │ │ │ ├── HybridEventBus.kt # Event communication +│ │ │ │ └── HydridNavigateService.kt +│ │ │ ├── theme/ # Compose theme +│ │ │ └── utils/ +│ │ ├── data/ +│ │ │ ├── model/ # Data models +│ │ │ ├── remote/ # API service +│ │ │ └── repository/ +│ │ ├── di/ # Dagger Hilt modules +│ │ │ ├── FlutterEngineModule.kt +│ │ │ ├── NetworkModule.kt +│ │ │ └── ServiceModule.kt +│ │ └── ui/ +│ │ ├── components/ # Reusable Compose components +│ │ └── feature/ +│ │ ├── dashboard/ # Native dashboard +│ │ └── hydrid/ # Flutter host activity +│ └── build.gradle.kts +│ +├── flutter_module/ # Flutter module +│ ├── lib/ +│ │ ├── core/ +│ │ │ ├── di/ +│ │ │ │ └── injection_container.dart +│ │ │ ├── routes/ +│ │ │ │ ├── app_routes.dart +│ │ │ │ └── route_mapper.dart +│ │ │ ├── services/ +│ │ │ │ ├── hybrid_bus_service.dart +│ │ │ │ └── navigation_service.dart +│ │ │ └── theme/ +│ │ ├── data/ # Data layer +│ │ ├── domain/ # Domain layer +│ │ │ ├── api/ +│ │ │ ├── model/ +│ │ │ └── usecases/ +│ │ └── ui/ # Presentation layer +│ │ ├── home/ +│ │ │ ├── bloc/ +│ │ │ └── home_page.dart +│ │ └── appliance/ +│ │ ├── bloc/ +│ │ └── appliance_detail_page.dart +│ ├── test/ # Unit tests +│ └── pubspec.yaml +│ +├── .github/workflows/ +│ └── build-apk.yml # GitHub Actions CI +├── .circleci/ +│ └── config.yml # CircleCI configuration +├── HYBRID_INTEGRATION.md # Detailed integration docs +├── QUICK_REFERENCE.md # Quick reference guide +└── README.md # This file +``` + +## Getting Started + +### Prerequisites + +- **Android Studio**: Arctic Fox or later +- **Flutter SDK**: 3.36.5 or later +- **Dart SDK**: Included with Flutter +- **JDK**: 17 or later +- **Gradle**: 8.0+ + +### Installation + +1. **Clone the repository** + ```bash + git clone https://github.com/yourusername/sample_hydrid_android.git + cd sample_hydrid_android + ``` + +2. **Set up Flutter module** + ```bash + cd flutter_module + flutter pub get + cd .. + ``` + +3. **Generate Flutter code** (for Freezed, Mockito, etc.) + ```bash + cd flutter_module + dart run build_runner build --delete-conflicting-outputs + cd .. + ``` + +4. **Open in Android Studio** + - Open the root project in Android Studio + - Wait for Gradle sync to complete + +### Building + +#### Debug Build +```bash +./gradlew assembleDebug +``` + +#### Release Build +```bash +./gradlew assembleRelease +``` + +#### Flutter Module Build +```bash +cd flutter_module +flutter build aar +``` + +## Testing + +### Run Flutter Tests +```bash +cd flutter_module +flutter test +``` + +### Run Flutter Tests with Coverage +```bash +cd flutter_module +flutter test --coverage +``` + +### Generate Flutter Mocks +```bash +cd flutter_module +dart run build_runner build --delete-conflicting-outputs +``` + +### Run Android Unit Tests +```bash +./gradlew test +``` + +### Run Android Instrumented Tests +```bash +./gradlew connectedAndroidTest +``` + +## CI/CD + +This project includes two CI/CD configurations: + +### GitHub Actions +- **Workflow**: [.github/workflows/build-apk.yml](.github/workflows/build-apk.yml) +- **Triggers**: Push to `main`, `develop` branches +- **Steps**: + 1. Checkout code + 2. Set up Java 17 + 3. Set up Flutter + 4. Install Flutter dependencies + 5. Generate code (build_runner) + 6. Run Flutter tests + 7. Build Android APK + 8. Upload APK artifact + +### CircleCI +- **Config**: [.circleci/config.yml](.circleci/config.yml) +- **Triggers**: Push to any branch +- **Steps**: + 1. Checkout code + 2. Restore Flutter cache + 3. Install Flutter SDK + 4. Install dependencies + 5. Generate code + 6. Run tests with coverage + 7. Build APK + 8. Store artifacts + +## Documentation + +### Detailed Documentation +- **[HYBRID_INTEGRATION.md](HYBRID_INTEGRATION.md)** - Complete guide to the hybrid architecture + - Communication channels + - Android native implementation + - Flutter module implementation + - Navigation patterns + - Event handling + - Best practices + - Troubleshooting + +### Quick Reference +- **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - Quick code snippets and patterns + - Event communication examples + - Navigation patterns + - Dependency injection + - Debugging tips + - Checklists + +## Key Concepts + +### FlutterEngine Caching +The app uses a cached FlutterEngine for better performance: +```kotlin +// Pre-warm at startup +val engine = FlutterEngine(context).apply { + dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) +} +FlutterEngineCache.getInstance().put(Constants.flutterEngineName, engine) + +// Reuse in activities +FlutterFragment.withCachedEngine(Constants.flutterEngineName).build() +``` + +### Event Communication +Events are sent via MethodChannel with JSON data: +```kotlin +// Android → Flutter +hybridEventBus.sendEventToHydrid("navigate", mapOf( + "routeName" to "/appliance/1", + "id" to "1", + "name" to "Refrigerator" +)) + +// Flutter → Android +await hybridBusService.sendEventToNative('navigateBack', {}); +``` + +### Navigation Pattern +1. Native screen starts `HybridFlutterActivity` +2. Activity sends navigation event with route and data +3. Flutter receives event and navigates to route +4. Flutter sends back event to close activity + +## Use Cases + +### Opening Appliance Details +1. User taps appliance card in native dashboard (Compose) +2. `DashBoardViewModel` starts `HybridFlutterActivity` +3. Sends "navigate" event with appliance data +4. Flutter `HybridBloc` receives event +5. Navigates to `ApplianceDetailPage` with data +6. User sees appliance details in Flutter UI + +### Returning to Native +1. User taps back button in Flutter screen +2. `ApplianceDetailBloc` sends "navigateBack" event +3. Android `HybridFlutterViewModel` receives event +4. Emits to `closeActivity` flow +5. `HybridFlutterActivity` finishes +6. User returns to native dashboard + +## Troubleshooting + +### Common Issues + +**Issue**: FlutterEngine not found +``` +Solution: Ensure FlutterEngineModule is providing the engine before +creating FlutterFragment +``` + +**Issue**: Events not received in Flutter +``` +Solution: +1. Verify channel name matches: "hybrid_channel" +2. Call hybridEventBus.register() in Activity +3. Check registerListener() is called in BLoC +``` + +**Issue**: Activity doesn't close on back +``` +Solution: Ensure closeActivity flow is collected in Activity: +lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + hydridViewModel.closeActivity.collect { finish() } + } +} +``` + +## Performance Considerations + +- **FlutterEngine caching**: Reduces cold start time from ~2s to ~200ms +- **Event queuing**: Prevents event loss during initialization +- **Lazy singletons**: Services are created only when needed +- **StateFlow**: Efficient state management with replay cache + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Code Style + +- **Android**: Follow [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) +- **Flutter**: Follow [Effective Dart](https://dart.dev/guides/language/effective-dart) +- Use meaningful variable names +- Add comments for complex logic +- Write unit tests for new features + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- Flutter team for the excellent Flutter Module architecture +- Dagger Hilt for powerful dependency injection +- BLoC pattern for predictable state management +- Jetpack Compose for modern Android UI + +## Contact + +For questions or feedback, please open an issue on GitHub. + +--- + +**Built with ❤️ using Kotlin, Flutter, and modern Android development practices**