Mobile Development 21 min read

Flutter-based Refactoring of QTalk: Architecture, Challenges, and Solutions

QTalk, Qunar's internal IM tool, was re‑engineered using Flutter to unify Android, iOS, QT, and desktop platforms, addressing code duplication, low development efficiency, and architectural complexity through a new data‑logic‑UI framework, mixed‑stack integration, state‑management choices, and performance optimizations.

Qunar Tech Salon
Qunar Tech Salon
Qunar Tech Salon
Flutter-based Refactoring of QTalk: Architecture, Challenges, and Solutions

1. Original QTalk Product Framework

QTalk is Qunar's internal instant‑messaging (IM) tool that also integrates OA approval, access‑control punch‑in, leave requests, meeting‑room booking, and a social feed, facilitating paperless office workflows and internal communication.

2. Why Choose Flutter

Flutter offers high rendering performance and eliminates platform differences by using its own rendering engine, providing game‑engine‑like efficiency while keeping the rendering model similar to native platforms. It supports long connections, long lists, and web views, and its hybrid capabilities in Flutter 2.0 meet QTalk's needs.

3. Flutter Version QTalk Code Framework

The new architecture consists of a data layer (push, HTTP, long‑connection) that produces IMMessage objects, a logic layer that distributes data via a subscriber pattern, and a UI layer that renders the messages. This design improves code reuse across Android, iOS, macOS, Windows, and Linux.

4. Problems Encountered When Refactoring QT Mobile with Flutter

Two main hybrid approaches were evaluated:

FlutterBoost

FlutterEngineGroup

Advantages

iOS shared memory, easy data transfer between pages

Official support, minimal code intrusion, negligible performance impact, each extra engine adds ~180 KB memory

Disadvantages

High upgrade cost, large engine size, iOS memory consumption

iOS layer cannot share memory, direct calls are cumbersome

The team ultimately adopted a third approach: using PlatformView to embed ReactNative pages inside Flutter, allowing Flutter routing to control navigation while keeping ReactNative lifecycle isolated.

// Flutter calls native
const MethodChannel _channel = const MethodChannel('com.mqunar.flutterQTalk/rn_bridge'); // register channel
_channel.invokeMapMethod('onWorkbenchShow', {});
// Native calls Flutter
_channel.setMethodCallHandler((MethodCall call) async {
var classAndMethod = call.method.split('.');
var className = classAndMethod.first;
if (mRnBridgeHandlers[className] == null) {
throw Exception('not found method response');
}
RNBridgeModule bridgeModule = mRnBridgeHandlers[className]!;
return bridgeModule.handleBridge(call);
});

On the Flutter side, the mixed view is created with PlatformViewLink (Android) or UiKitView (iOS), passing parameters via StandardMessageCodec and rendering the combined page seamlessly.

Widget getReactRootView(ReactNativePageState state, Dispatch dispatch, ViewService viewService) {
if (defaultTargetPlatform == TargetPlatform.android) {
return PlatformViewLink(
viewType: VIEW_TYPE,
surfaceFactory: (context, controller) => AndroidViewSurface(controller: controller as AndroidViewController, gestureRecognizers: const
>{}, hitTestBehavior: PlatformViewHitTestBehavior.opaque),
onCreatePlatformView: (params) => PlatformViewsService.initSurfaceAndroidView(id: params.id, viewType: VIEW_TYPE, layoutDirection: TextDirection.ltr, creationParams: state.params, creationParamsCodec: StandardMessageCodec())..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)..create(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(viewType: VIEW_TYPE, creationParams: state.params, creationParamsCodec: const StandardMessageCodec());
} else {
return Text("Placeholder");
}
}

5. Data‑Management Solutions

The team evaluated several state‑management libraries (provider, BLoC, mobx, redux, fish‑redux) and settled on a combination of fish‑redux for the logic layer and an EventBus for communication between the database and the IMMessage module. This approach yields high code reuse (≈80 % UI reuse) and reduces development effort by about 50 %.

6. ListView Issues and Fixes

Flutter's ListView does not support index‑based jumps for variable‑height items, leading to jump glitches. The solution is to use scrollable_positioned_list , which separates height calculation from rendering, and to adjust jump offsets programmatically.

void _jumpTo({@required int index, double offset}) {
// calculate target offset
var jumpOffset = 0 + offset;
controller.jumpTo(jumpOffset);
// post‑frame correction
WidgetsBinding.instance.addPostFrameCallback((ts) {
var offset = min(jumpOffset, controller.position.maxScrollExtent);
if (controller.offset != offset) {
controller.jumpTo(offset);
}
});
}

7. iOS Keyboard Height Issue

SafeArea bottom height changes when the keyboard appears, causing layout jitter. The fix records the original SafeArea bottom value at app start and uses that stored height for the chat input area, avoiding overlap with the navigation bar.

8. Debugging Dart and Native Code Together

Since Dart and native code are compiled separately, simultaneous debugging is achieved by launching the app from the native side and then attaching to the Dart isolate with flutter attach .

9. Desktop (QT) Challenges and Solutions

Desktop reuse is enabled by the same fish‑redux components, with connectors adapting mobile pages to desktop windows. Multi‑window support and native capabilities are provided via the NativeShell framework, which uses a Rust‑based multi‑engine architecture to manage windows and bridge platform APIs.

Packaging issues (null‑safety in Rust Cargo scripts and oversized binaries) were resolved by adjusting the build scripts to include only necessary DLLs/frameworks.

10. Event Loop Bottleneck on PC

When many sub‑windows send messages to the main isolate, the event queue can become overloaded, causing UI stalls. A dispatch layer now batches requests, limits per‑frame command counts, and throttles excess messages, sending failure notifications for retries.

11. Technical Outcomes

Development effort reduced by >50 % and cycle time halved.

iOS package size reduced from 200 MB to 117 MB; Android from 44.9 MB to 27.5 MB.

Cold‑start time improved from 2.6 s to 1.5 s (≈42 % faster).

Memory usage comparable to native implementations.

12. Future Work

Complete long‑connection stability on mobile, finish Windows‑specific features (database selection, screenshot, keyboard handling, native SDK integration), and assist other large clients in migrating to Flutter, leveraging the lessons learned from QTalk.

FlutterMobile DevelopmentCross-platformArchitectureState ManagementHybrid Integration
Qunar Tech Salon
Written by

Qunar Tech Salon

Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.