一、先搞懂咱们要解决的核心问题
很多做混合开发的朋友都会碰到一个闹心的事儿:App里一半页面是安卓原生的,一半是Flutter写的,点安卓的返回键时,要么直接退App了,要么Flutter页面乱跳,更糟的是原生页面和Flutter页面的生命周期完全不搭,比如Flutter页面还没销毁,原生页面就被关了,导致数据乱了或者崩溃。咱们今天就把这个问题拆得明明白白,一步步解决。
1.1 先理清楚几个基础概念(怕你懵,用大白话讲)
首先得说两个最容易混的东西:安卓的返回键和Flutter的路由。 安卓的返回键是系统级的,按了之后,安卓系统会给当前显示的页面发一个“要关闭”的信号,原生页面(叫Activity)自己决定是关还是跳;而Flutter的路由是Flutter自己管的,比如你在Flutter里写Navigator.push,是Flutter自己在它的页面栈里加页面,安卓系统根本不知道Flutter内部的页面有多少层。 举个简单的例子:你用Flutter写了一个“我的”页面,点进去有个“设置”页面,这两个页面都在Flutter里,安卓系统只知道现在显示的是Flutter的根页面(就是那个装所有Flutter页面的原生容器,叫FlutterActivity),根本不知道Flutter里还有“设置”页面。所以你按返回键,安卓系统只会问FlutterActivity:“你要关吗?”,不会问Flutter里的“设置”页面。
二、先解决第一个问题:安卓返回键和Flutter路由的冲突
咱们先做一个最简单的混合开发场景:安卓有一个原生页面A,点一个按钮跳转到Flutter页面B(B里有Flutter的子页面C)。现在按返回键,正常逻辑应该是:如果当前在C,按返回键回B;如果在B,按返回键回A;如果在A,按返回键退App。但实际测试会发现,不管在B还是C,按返回键直接退App了,这就是冲突的核心。
2.1 冲突的根源:安卓系统不知道Flutter内部的路由
刚才说了,安卓系统只认FlutterActivity(装Flutter页面的原生容器),所以当FlutterActivity显示的时候,按返回键,安卓系统会先调用FlutterActivity里的onBackPressed方法(原生页面处理返回键的方法),默认的onBackPressed方法是直接关这个Activity,所以不管Flutter里有多少层,直接关了,就退App了。
2.2 解决办法:拦截安卓返回键,告诉Flutter先处理
咱们要做的就是:在FlutterActivity里,拦截返回键,先问Flutter:“你内部有没有要处理的返回?”,如果Flutter说“有”,就让Flutter自己处理(比如从C回B);如果Flutter说“没有”,再按原来的逻辑关Activity(比如从B回A)。
咱们用一个完整的示例来演示,这个示例是原生安卓(Java)+Flutter的混合开发,先明确技术栈: 技术栈:原生安卓(Java)、Flutter 3.16、FlutterActivity
2.2.1 第一步:写Flutter端的路由和返回处理
首先,Flutter端要做两件事:一是自己管理页面栈,二是给原生端暴露一个方法,告诉原生端“我内部有没有可以返回的页面”。
先写Flutter的主页面(main.dart):
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 注册一个MethodChannel,用来和原生端通信
home: const MyHomePage(title: 'Flutter页面B'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// 定义MethodChannel,名字要和原生端的一致
static const MethodChannel _channel = MethodChannel('com.example.flutter/back');
@override
void initState() {
super.initState();
// 给原生端的调用注册处理方法:原生端问“有没有可以返回的页面”,我们要回答
_channel.setMethodCallHandler((call) async {
if (call.method == 'canPop') {
// Navigator.canPop(context) 是Flutter的方法,判断当前路由栈能不能回退
return Navigator.canPop(context);
}
return false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('这是Flutter页面B'),
ElevatedButton(
onPressed: () {
// 跳转到Flutter子页面C
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PageC(title: 'Flutter页面C'),
),
);
},
child: const Text('跳转到页面C'),
),
],
),
),
);
}
}
// Flutter子页面C
class PageC extends StatelessWidget {
const PageC({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: const Center(
child: Text('这是Flutter页面C'),
),
);
}
}
2.2.2 第二步:写原生端的FlutterActivity,拦截返回键
原生端的FlutterActivity(MainActivity.java)要做的就是:重写返回键的处理方法,先调用Flutter端的canPop方法,判断Flutter能不能回退。
package com.example.myapp;
import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
// MethodChannel的名字要和Flutter端的完全一致
private static final String CHANNEL = "com.example.flutter/back";
private MethodChannel methodChannel;
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// 初始化MethodChannel,用来和Flutter端通信
methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL);
}
// 重写安卓返回键的处理方法,这个方法是安卓原生的,按返回键就会触发
@Override
public void onBackPressed() {
// 调用Flutter端的canPop方法,判断Flutter内部能不能回退
methodChannel.invokeMethod("canPop", null, new MethodChannel.Result() {
@Override
public void success(Object result) {
// result是Flutter端返回的boolean值:true表示能回退,false表示不能
boolean canPop = (boolean) result;
if (canPop) {
// 如果Flutter能回退,就调用Flutter端的pop方法,让Flutter自己处理返回
methodChannel.invokeMethod("pop", null);
} else {
// 如果Flutter不能回退,就按原来的逻辑关Activity,回到原生页面A
MainActivity.super.onBackPressed();
}
}
@Override
public void error(String s, String s1, Object o) {
// 如果调用出错,直接关Activity
MainActivity.super.onBackPressed();
}
@Override
public void notImplemented() {
// 如果Flutter端没有实现这个方法,直接关Activity
MainActivity.super.onBackPressed();
}
});
}
}
2.2.3 第三步:补全Flutter端的pop方法
刚才原生端调用了Flutter端的pop方法,咱们要在Flutter端实现这个方法,让Flutter自己处理回退: 在Flutter的_MyHomePageState的initState里,给setMethodCallHandler加一个处理pop的逻辑:
// 给原生端的调用注册处理方法:原生端问“有没有可以返回的页面”,我们要回答
_channel.setMethodCallHandler((call) async {
if (call.method == 'canPop') {
return Navigator.canPop(context);
} else if (call.method == 'pop') {
// 原生端让Flutter自己处理返回,调用pop方法
Navigator.pop(context);
}
return false;
});
现在测试一下:在页面C按返回键,会回到页面B;在页面B按返回键,会回到原生页面A;在页面A按返回键,退App,完全符合预期。
三、第二个问题:Navigator和Activity生命周期的协同异常
刚才解决了返回键的问题,还有一个更隐蔽的问题:原生页面和Flutter页面的生命周期不搭,导致数据异常或者崩溃。
3.1 异常的根源:两个生命周期完全独立
安卓的Activity有自己的生命周期(比如onCreate、onStart、onResume、onPause、onStop、onDestroy),Flutter的页面也有自己的生命周期(比如initState、didChangeDependencies、build、dispose),但这两个生命周期是完全独立的,没有任何默认的关联。
举个常见的异常场景:
- 原生页面A跳转到Flutter页面B,Flutter页面B里有一个计时器,每秒更新一次时间;
- 现在按返回键,回到原生页面A,Flutter页面B被安卓系统暂停(Activity的onPause被调用),但Flutter页面B的dispose方法还没被调用,计时器还在继续跑;
- 过了一会儿,安卓系统内存不够,把FlutterActivity销毁了(Activity的onDestroy被调用),但Flutter页面B的dispose方法还是没被调用,计时器还在跑,这时候就会报错:“setState called after dispose”,因为Flutter页面已经被销毁了,还在更新UI。
3.2 解决办法:把两个生命周期绑定起来
咱们要做的就是:当原生Activity的生命周期变化时,通知Flutter,让Flutter也同步更新自己的状态;反过来,当Flutter页面的生命周期变化时,也通知原生Activity,让原生Activity同步更新状态。
咱们还是用刚才的示例来改造,先解决刚才的计时器问题:
3.2.1 第一步:Flutter端添加生命周期监听
Flutter端要监听自己的生命周期,同时监听原生Activity的生命周期变化。
先改造Flutter的_MyHomePageState:
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
static const MethodChannel _channel = MethodChannel('com.example.flutter/back');
// 定义一个计时器
Timer? _timer;
// 定义一个变量,标记Flutter页面是否可见
bool _isVisible = true;
@override
void initState() {
super.initState();
// 注册WidgetsBindingObserver,用来监听Flutter的生命周期
WidgetsBinding.instance.addObserver(this);
// 注册MethodChannel的处理方法
_channel.setMethodCallHandler((call) async {
if (call.method == 'canPop') {
return Navigator.canPop(context);
} else if (call.method == 'pop') {
Navigator.pop(context);
} else if (call.method == 'onPause') {
// 原生Activity暂停时,通知Flutter
_isVisible = false;
// 暂停计时器
_timer?.cancel();
} else if (call.method == 'onResume') {
// 原生Activity恢复时,通知Flutter
_isVisible = true;
// 重新启动计时器
_startTimer();
}
return false;
});
// 启动计时器
_startTimer();
}
// 启动计时器的方法
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
// 只有页面可见时才更新UI
if (_isVisible && mounted) {
setState(() {});
}
});
}
// 监听Flutter的生命周期变化,这个方法是WidgetsBindingObserver的方法
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
// Flutter页面恢复时,标记为可见,启动计时器
_isVisible = true;
_startTimer();
break;
case AppLifecycleState.paused:
// Flutter页面暂停时,标记为不可见,暂停计时器
_isVisible = false;
_timer?.cancel();
break;
case AppLifecycleState.detached:
// Flutter页面销毁时,取消计时器
_timer?.cancel();
break;
default:
break;
}
}
@override
void dispose() {
// 取消WidgetsBindingObserver的注册
WidgetsBinding.instance.removeObserver(this);
// 取消计时器
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('这是Flutter页面B'),
// 显示计时器的时间
Text('计时器:${DateTime.now().second}'),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PageC(title: 'Flutter页面C'),
),
);
},
child: const Text('跳转到页面C'),
),
],
),
),
);
}
}
3.2.2 第二步:原生端在生命周期变化时通知Flutter
原生端的MainActivity要重写自己的生命周期方法,在onPause和onResume时,通知Flutter。
改造原生端的MainActivity.java:
package com.example.myapp;
import android.os.Bundle;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.example.flutter/back";
private MethodChannel methodChannel;
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
methodChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL);
}
// 重写原生Activity的onPause方法,当Activity暂停时触发
@Override
protected void onPause() {
super.onPause();
// 通知Flutter,原生Activity暂停了
if (methodChannel != null) {
methodChannel.invokeMethod("onPause", null);
}
}
// 重写原生Activity的onResume方法,当Activity恢复时触发
@Override
protected void onResume() {
super.onResume();
// 通知Flutter,原生Activity恢复了
if (methodChannel != null) {
methodChannel.invokeMethod("onResume", null);
}
}
// 重写原生Activity的onDestroy方法,当Activity销毁时触发
@Override
protected void onDestroy() {
super.onDestroy();
// 通知Flutter,原生Activity销毁了
if (methodChannel != null) {
methodChannel.invokeMethod("onDestroy", null);
}
}
@Override
public void onBackPressed() {
methodChannel.invokeMethod("canPop", null, new MethodChannel.Result() {
@Override
public void success(Object result) {
boolean canPop = (boolean) result;
if (canPop) {
methodChannel.invokeMethod("pop", null);
} else {
MainActivity.super.onBackPressed();
}
}
@Override
public void error(String s, String s1, Object o) {
MainActivity.super.onBackPressed();
}
@Override
public void notImplemented() {
MainActivity.super.onBackPressed();
}
});
}
}
现在测试一下:在页面B按返回键回到原生页面A,计时器会暂停;再回到页面B,计时器会重新启动;退App后,计时器会被彻底取消,不会再报错了。
四、应用场景、优缺点和注意事项
4.1 应用场景
咱们解决的这两个问题,几乎所有的混合开发项目都会用到,具体的场景包括:
- 原生App部分页面用Flutter重构,比如原来的原生“我的”页面改成Flutter的,需要兼容原来的返回逻辑;
- 用Flutter开发独立的功能模块,比如电商App的支付模块、直播模块,需要和原生的主页面、其他原生模块协同;
- 大型项目的跨端开发,同时维护安卓、iOS和Flutter代码,需要统一的页面管理逻辑。
4.2 技术优缺点
优点
- 解决了混合开发中最常见的返回键冲突问题,让页面跳转逻辑统一;
- 绑定了原生和Flutter的生命周期,避免了因为生命周期不搭导致的崩溃和数据异常;
- 代码逻辑清晰,只需要在原生的FlutterActivity和Flutter的主页面做少量修改,不影响原来的业务逻辑;
- 可扩展性强,除了返回键和生命周期,还可以用MethodChannel实现更多的原生和Flutter的通信,比如原生获取Flutter的状态、Flutter调用原生的功能。
缺点
- 增加了少量的代码量,需要维护原生和Flutter两端的MethodChannel,容易出现名字不一致的问题(比如原生的MethodChannel名字和Flutter的不一样,导致通信失败);
- 通信是异步的,在一些对返回速度要求很高的场景,可能会有轻微的延迟(不过一般感觉不到);
- 如果Flutter内部的路由逻辑很复杂(比如嵌套路由),需要更复杂的canPop判断逻辑,增加了维护难度。
4.3 注意事项
- MethodChannel的名字一定要原生和Flutter完全一致,包括大小写,最好定义成常量,避免写错;
- 一定要在Flutter页面销毁时,取消所有的定时器、监听器等资源,避免内存泄漏;
- 原生端的onBackPressed方法是安卓特有的,iOS端的返回逻辑不一样,需要单独处理;
- 如果Flutter用的是嵌套路由(比如Navigator 2.0),原来的canPop判断逻辑可能不适用,需要根据实际的路由结构调整;
- 测试时一定要覆盖所有的场景,比如从Flutter页面跳转到原生页面、从原生页面跳转到Flutter页面、后台切换、内存回收等场景,避免遗漏异常。
五、文章总结
混合开发中,安卓返回键和Flutter路由的冲突、Navigator和Activity生命周期的协同异常,本质上是因为原生系统和Flutter框架是两个独立的体系,各自管理自己的页面和生命周期,没有默认的关联。解决的核心思路就是“通信”:用MethodChannel把原生和Flutter连接起来,让原生系统知道Flutter内部的页面状态,让Flutter知道原生系统的生命周期变化,从而让两个体系协同工作。
咱们通过两个完整的示例,一步步解决了这两个问题,从最基础的返回键拦截,到复杂的生命周期绑定,每一步都有具体的代码和解释。只要掌握了这个核心思路,不管是简单的混合开发还是复杂的大型项目,都可以灵活调整,解决类似的问题。
评论
围绕“Flutter混合开发中Android返回键与路由拦截逻辑,Navigator与Activity生命周期协同异常梳理”参与讨论