Flutter Add-to-App:宿主App与Flutter模块的路由栈同步机制
各位同学,大家好!今天我们来深入探讨Flutter Add-to-App架构中一个至关重要的问题:宿主App与Flutter模块的路由栈同步。在Add-to-App场景下,Flutter模块并非独立运行,而是嵌入到现有的原生App中。这就要求原生App和Flutter模块能够协同工作,特别是在导航方面。如果两者路由栈不同步,会导致页面跳转异常、状态丢失等问题,严重影响用户体验。
1. Add-to-App架构下的路由挑战
在传统的Flutter应用中,Flutter Engine负责管理整个应用的路由栈。但在Add-to-App架构下,情况变得复杂。原生App拥有自己的路由栈(例如,Android的Activity栈,iOS的UIViewController栈),而Flutter模块也拥有自己的Navigator管理的路由栈。
两者独立运行,互不感知,必然会产生以下问题:
- 页面跳转不一致: 用户在原生App中点击某个按钮,需要跳转到Flutter页面,然后再从Flutter页面返回原生App。如果两者路由栈没有同步,返回操作可能无法回到预期的原生页面。
- 状态丢失: 原生页面携带的数据传递给Flutter页面,Flutter页面修改了这些数据,返回原生页面后,原生页面可能无法感知这些修改,导致状态丢失。
- Deep Link处理: 当App通过Deep Link启动并需要直接跳转到Flutter页面时,如何正确地将Deep Link信息传递给Flutter模块,并确保原生App和Flutter模块的路由栈状态一致。
2. 实现路由栈同步的关键技术点
为了解决上述问题,我们需要在原生App和Flutter模块之间建立一套有效的路由栈同步机制。主要涉及以下几个关键技术点:
- 路由事件监听: 原生App和Flutter模块需要能够监听彼此的路由事件(例如,页面push、pop)。
- 路由信息传递: 原生App需要能够将路由信息(例如,页面名称、参数)传递给Flutter模块,反之亦然。
- 路由栈管理: 原生App和Flutter模块需要能够根据接收到的路由事件和信息,更新各自的路由栈。
3. 基于Platform Channel的实现方案
一种常见的实现方案是基于Flutter的Platform Channel。 Platform Channel 允许原生代码和 Flutter 代码之间进行双向通信。 我们可以利用它来监听和传递路由事件和信息。
3.1 定义Platform Channel
首先,我们需要定义一个Platform Channel,用于原生App和Flutter模块之间的通信。
// Flutter端
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('my_app/route');
@override
void initState() {
super.initState();
// 监听Flutter路由事件
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).didPushRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "push");
return true;
};
Navigator.of(context).didPopRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "pop");
return true;
};
Navigator.of(context).didReplaceRoute = (Route<dynamic> newRoute, Route<dynamic>? oldRoute) {
_sendRouteMessageToNative(newRoute.settings.name!, "replace");
return true;
};
Navigator.of(context).didRemoveRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "remove");
return true;
};
});
}
Future<void> _sendRouteMessageToNative(String routeName, String action) async {
try {
final String result = await platform.invokeMethod('routeChanged', {'routeName': routeName, 'action': action});
print('Native received: $result');
} on PlatformException catch (e) {
print("Failed to invoke: '${e.message}'.");
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(title: 'Flutter Demo Home Page'),
'/second': (context) => SecondPage(),
},
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: Text('Go to Second Page'),
),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text('This is the second page.'),
),
);
}
}
// Android端 (Kotlin)
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
class MainActivity : AppCompatActivity() {
private val CHANNEL = "my_app/route"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Pre-warm the FlutterEngine
val flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "routeChanged") {
val routeName = call.argument<String>("routeName")
val action = call.argument<String>("action")
// 在这里处理路由变化事件
println("Received route change: routeName=$routeName, action=$action")
result.success("Route change received in Native!")
} else {
result.notImplemented()
}
}
// Launch Flutter Activity
findViewById<android.widget.Button>(R.id.button).setOnClickListener {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
)
}
}
}
3.2 监听Flutter路由事件
在Flutter端,我们需要监听Navigator的路由事件,并在页面push、pop等操作发生时,通过Platform Channel将路由信息发送给原生App。
// Flutter端
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('my_app/route');
@override
void initState() {
super.initState();
// 监听Flutter路由事件
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).didPushRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "push");
return true;
};
Navigator.of(context).didPopRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "pop");
return true;
};
Navigator.of(context).didReplaceRoute = (Route<dynamic> newRoute, Route<dynamic>? oldRoute) {
_sendRouteMessageToNative(newRoute.settings.name!, "replace");
return true;
};
Navigator.of(context).didRemoveRoute = (Route<dynamic> route, Route<dynamic>? previousRoute) {
_sendRouteMessageToNative(route.settings.name!, "remove");
return true;
};
});
}
Future<void> _sendRouteMessageToNative(String routeName, String action) async {
try {
final String result = await platform.invokeMethod('routeChanged', {'routeName': routeName, 'action': action});
print('Native received: $result');
} on PlatformException catch (e) {
print("Failed to invoke: '${e.message}'.");
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(title: 'Flutter Demo Home Page'),
'/second': (context) => SecondPage(),
},
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: Text('Go to Second Page'),
),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: Text('This is the second page.'),
),
);
}
}
3.3 原生App处理路由事件
在原生App端,我们需要监听Platform Channel的事件,并根据接收到的路由信息,更新原生App的路由栈。
// Android端 (Kotlin)
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
class MainActivity : AppCompatActivity() {
private val CHANNEL = "my_app/route"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Pre-warm the FlutterEngine
val flutterEngine = FlutterEngine(this)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "routeChanged") {
val routeName = call.argument<String>("routeName")
val action = call.argument<String>("action")
// 在这里处理路由变化事件
println("Received route change: routeName=$routeName, action=$action")
result.success("Route change received in Native!")
} else {
result.notImplemented()
}
}
// Launch Flutter Activity
findViewById<android.widget.Button>(R.id.button).setOnClickListener {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
)
}
}
}
3.4 路由信息传递
除了监听路由事件,我们还需要能够传递路由信息(例如,页面参数)。这可以通过Platform Channel的invokeMethod方法实现。
// Flutter端
Future<void> _sendRouteMessageToNative(String routeName, String action, {Map<String, dynamic>? arguments}) async {
try {
final String result = await platform.invokeMethod('routeChanged', {'routeName': routeName, 'action': action, 'arguments': arguments});
print('Native received: $result');
} on PlatformException catch (e) {
print("Failed to invoke: '${e.message}'.");
}
}
// 在页面跳转时,传递参数
Navigator.pushNamed(context, '/second', arguments: {'id': 123, 'name': 'test'});
// Android端 (Kotlin)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "routeChanged") {
val routeName = call.argument<String>("routeName")
val action = call.argument<String>("action")
val arguments = call.argument<Map<String, Any>>("arguments")
// 在这里处理路由变化事件
println("Received route change: routeName=$routeName, action=$action, arguments=$arguments")
result.success("Route change received in Native!")
} else {
result.notImplemented()
}
}
4. 路由栈同步策略
基于上述技术,我们可以制定不同的路由栈同步策略。以下是一些常见的策略:
4.1 完全同步
原生App和Flutter模块维护完全相同的路由栈。当任何一方发生路由变化时,都会通知对方更新自己的路由栈。这种策略的优点是路由栈状态一致性高,但实现较为复杂,需要处理各种边界情况。
4.2 部分同步
只同步部分关键路由。例如,只同步原生App跳转到Flutter模块的入口页面,以及Flutter模块返回原生App的出口页面。这种策略的优点是实现简单,但可能无法处理所有路由场景。
4.3 基于事件驱动的同步
原生App和Flutter模块不直接维护对方的路由栈,而是通过事件驱动的方式进行同步。例如,当原生App需要跳转到Flutter页面时,会发送一个事件给Flutter模块,Flutter模块根据事件信息创建相应的页面。这种策略的优点是解耦性好,但需要设计一套完善的事件机制。
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 完全同步 | 路由栈状态一致性高,用户体验好 | 实现复杂,需要处理各种边界情况 | 对路由一致性要求非常高的场景,例如金融、支付等 |
| 部分同步 | 实现简单,对现有代码改动较小 | 可能无法处理所有路由场景,例如Deep Link | Flutter模块只是作为原生App的一部分功能模块,路由场景比较简单的场景 |
| 事件驱动 | 解耦性好,原生App和Flutter模块可以独立演进 | 需要设计一套完善的事件机制,增加了开发成本 | 原生App和Flutter模块需要高度解耦,并且路由场景比较复杂的场景 |
5. Deep Link处理
Deep Link是指通过URL Scheme或Universal Link直接启动App并跳转到指定页面的技术。在Add-to-App架构下,Deep Link的处理需要特别注意。
5.1 原生App处理Deep Link
原生App接收到Deep Link后,需要判断是否需要跳转到Flutter页面。如果需要,需要将Deep Link信息传递给Flutter模块。
5.2 Flutter模块处理Deep Link信息
Flutter模块接收到Deep Link信息后,需要根据信息创建相应的页面,并更新自己的路由栈。同时,还需要通知原生App,更新原生App的路由栈,保持两者同步。
// Android端 (Kotlin)
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val data = intent?.data
if (data != null) {
val deepLink = data.toString()
// 将Deep Link信息传递给Flutter模块
MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CHANNEL).invokeMethod("handleDeepLink", deepLink)
}
}
// Flutter端
static const platform = const MethodChannel('my_app/route');
void main() {
runApp(MyApp());
platform.setMethodCallHandler((MethodCall call) async {
if (call.method == 'handleDeepLink') {
final String deepLink = call.arguments;
// 处理Deep Link信息,更新路由栈
_handleDeepLink(deepLink);
}
});
}
void _handleDeepLink(String deepLink) {
// 解析Deep Link
// ...
// 跳转到指定页面
Navigator.pushNamed(context, '/deep_link_page', arguments: {'deepLink': deepLink});
}
6. 常见问题与解决方案
6.1 Flutter模块启动时路由栈丢失
当原生App重新启动Flutter模块时,Flutter模块的路由栈可能会丢失。这是因为Flutter Engine默认情况下不会持久化路由栈信息。
解决方案:
- 在Flutter Engine启动时,从持久化存储中恢复路由栈信息。
- 每次原生App跳转到Flutter模块时,都传递当前原生App的路由栈信息给Flutter模块,由Flutter模块重建路由栈。
6.2 原生App和Flutter模块路由栈不一致
由于各种原因,原生App和Flutter模块的路由栈可能会出现不一致的情况。
解决方案:
- 在原生App和Flutter模块之间建立一套完善的路由栈同步机制,尽可能减少路由栈不一致的可能性。
- 在出现路由栈不一致时,提供一种修复机制,例如,强制同步路由栈。
6.3 性能问题
频繁地通过Platform Channel进行通信可能会影响性能。
解决方案:
- 减少不必要的通信,例如,只同步关键路由。
- 使用更高效的通信机制,例如,使用共享内存。
7. 总结
今天,我们深入探讨了Flutter Add-to-App架构中路由栈同步的问题,并讨论了基于Platform Channel的实现方案。重点在于理解原生平台和Flutter引擎各自的路由管理方式,并在二者之间建立可靠的通信机制,同步路由事件和数据。最后,根据实际业务场景选择合适的路由同步策略,保证用户体验的一致性和流畅性。
希望今天的讲解能够帮助大家更好地理解和应用Flutter Add-to-App架构。谢谢大家!