How FlClash Integrates the ClashMeta Core with Flutter on Android and Desktop
FlClash bridges its Flutter UI to the native ClashMeta Go core using JNI on Android and JSON-over-IPC on desktop, exposing both platforms through a unified CoreController API.
FlClash is an open-source proxy client maintained by chen08209 that combines the ClashMeta core with a cross-platform Flutter interface. The integration relies on two distinct native communication patterns to deliver the same Dart-level functionality across mobile and desktop operating systems.
Platform-Specific Integration Architecture
Android FFI and JNI Layer
On Android, the ClashMeta core compiles into a C shared library (libclash.so) using CGO. The Flutter layer communicates with this native library through a MethodChannel (com.follow.clash/service) that forwards calls to Kotlin/Java code, which then invokes the Go functions via JNI.
The entry point for these calls resides in android/core/src/main/java/com/follow/clash/core/Core.kt, where external declarations map to Go-exported C functions such as init and invokeAction. The Go side exposes these functions in core/lib.go and core/main_cgo.go, allowing the Dart layer to execute core actions within the same process.
Desktop IPC Process Model
For macOS, Linux, and Windows, FlClash spawns the ClashMeta core as a separate process. Flutter communicates through a JSON-over-IPC channel using Unix domain sockets on Unix-like systems and named pipes on Windows.
The CoreService class launches the Go binary (appPath.corePath) with the IPC address as an argument via Process.start. The IPCCoreTransport class in lib/core/transport.dart manages the socket or pipe connection, sending JSON-encoded Action objects and receiving ActionResult responses. The Go process, implemented in core/hub.go, reads these messages and dispatches them to the appropriate ClashMeta handlers.
The CoreController Abstraction
At the heart of the integration sits CoreController, a singleton defined in lib/core/controller.dart that abstracts platform differences behind a common interface. At runtime, it selects the appropriate implementation based on the host operating system.
if (system.isAndroid) {
_interface = coreLib!; // Android FFI path
} else {
_interface = coreService!; // Desktop IPC path
}
Both CoreLib (Android) and CoreService (desktop) implement CoreHandlerInterface, ensuring that methods like init(), getProxiesGroups(), and changeProxy() behave identically regardless of the underlying transport mechanism.
Android Implementation Details
The Android pathway follows a four-step delegation chain from Dart to Go:
- Flutter MethodChannel – The
Serviceclass inlib/plugins/service.dartsends a JSONActionthrough thecom.follow.clash/servicechannel. - Kotlin JNI Bridge – The
Coreobject inandroid/core/src/main/java/com/follow/clash/core/Core.ktreceives the JSON and calls the nativeinvokeActionC function. - Go Shared Library – The compiled
libclash.so(built fromcore/lib.go) dispatches the call to Go handlers such ashandleGetProxies. - Result Propagation – The native code returns a JSON string, which Kotlin passes back through the MethodChannel to resolve the original Dart
Future.
Desktop Implementation Details
The desktop flow establishes a persistent IPC connection between the Flutter UI and the Go binary:
- Process Spawning –
CoreService.start()inlib/core/service.dartlaunches the Go executable with the IPC address as a command-line argument. - Transport Initialization –
IPCCoreTransportopens the socket or pipe and signals readiness to the Flutter layer. - Action Invocation –
CoreService.invokeserializes theActionobject to JSON and writes it to the transport. - Go Processing – The Go process reads the message in
core/hub.go, executes the requested action (e.g.,handleChangeProxy), and writes anActionResultback to the socket. - Dart Completion – The transport reads the response and completes the awaiting Dart
Futurewith the result.
Practical Code Examples
Initialize the core and preload native resources:
await coreController.init(version);
await coreController.preload();
Fetch proxy groups with latency sorting:
final groups = await coreController.getProxiesGroups(
sortType: ProxiesSortType.latency,
delayMap: {},
selectedMap: {},
defaultTestUrl: 'https://www.gstatic.com/generate_204',
);
Change the active proxy selection:
await coreController.changeProxy(
ChangeProxyParams(groupName: 'Proxy', proxyName: 'US-01'),
);
Gracefully shutdown the core:
await coreController.shutdown(isUser: true);
Summary
- FlClash uses JNI/CGO on Android to load
libclash.sodirectly within the app process, satisfying platform restrictions for VPN services. - Desktop platforms use JSON-over-IPC to communicate with a separate Go process via sockets or named pipes, simplifying sandboxing and privilege separation.
- The
CoreControllersingleton inlib/core/controller.dartprovides a unified Dart API that transparently selects the correct transport mechanism at runtime. - Key source files include
android/core/src/main/java/com/follow/clash/core/Core.ktfor Android JNI,core/lib.gofor Go exports, andlib/core/transport.dartfor desktop IPC management.
Frequently Asked Questions
Why does FlClash use different integration methods for Android and Desktop?
Android requires in-process execution to satisfy platform restrictions and allow the VPN/TUN service to be driven directly via the FFI layer. Desktop systems can safely launch the core as a stand-alone process, simplifying sandboxing and allowing the core to run with its own privileges, particularly on Windows where helper services may be used.
How does the Dart layer handle platform detection?
The CoreController singleton checks system.isAndroid during initialization. If true, it instantiates CoreLib for FFI communication; otherwise, it instantiates CoreService for IPC communication. Both classes implement the same CoreHandlerInterface, ensuring the rest of the application remains platform-agnostic.
Can the FlClash core run independently of the Flutter UI on desktop?
Yes. On desktop platforms, the core runs as a separate Go binary spawned by CoreService via Process.start. It listens on a Unix domain socket or named pipe and can persist independently until explicitly shut down through the IPC channel.
What is the role of the Action and ActionResult classes in FlClash?
These classes define the JSON message protocol used across both platforms. Action encapsulates method calls and parameters sent from Dart to Go, while ActionResult wraps return values and errors sent back to Dart. This unified message format allows the same Go handlers in core/hub.go to service requests from both Android (via JNI delegation) and desktop (via IPC).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →