一、先搞懂咱们要解决的核心问题

很多做混合开发的朋友都会碰到一个闹心的事儿: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),但这两个生命周期是完全独立的,没有任何默认的关联。

举个常见的异常场景:

  1. 原生页面A跳转到Flutter页面B,Flutter页面B里有一个计时器,每秒更新一次时间;
  2. 现在按返回键,回到原生页面A,Flutter页面B被安卓系统暂停(Activity的onPause被调用),但Flutter页面B的dispose方法还没被调用,计时器还在继续跑;
  3. 过了一会儿,安卓系统内存不够,把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 应用场景

咱们解决的这两个问题,几乎所有的混合开发项目都会用到,具体的场景包括:

  1. 原生App部分页面用Flutter重构,比如原来的原生“我的”页面改成Flutter的,需要兼容原来的返回逻辑;
  2. 用Flutter开发独立的功能模块,比如电商App的支付模块、直播模块,需要和原生的主页面、其他原生模块协同;
  3. 大型项目的跨端开发,同时维护安卓、iOS和Flutter代码,需要统一的页面管理逻辑。

4.2 技术优缺点

优点

  1. 解决了混合开发中最常见的返回键冲突问题,让页面跳转逻辑统一;
  2. 绑定了原生和Flutter的生命周期,避免了因为生命周期不搭导致的崩溃和数据异常;
  3. 代码逻辑清晰,只需要在原生的FlutterActivity和Flutter的主页面做少量修改,不影响原来的业务逻辑;
  4. 可扩展性强,除了返回键和生命周期,还可以用MethodChannel实现更多的原生和Flutter的通信,比如原生获取Flutter的状态、Flutter调用原生的功能。

缺点

  1. 增加了少量的代码量,需要维护原生和Flutter两端的MethodChannel,容易出现名字不一致的问题(比如原生的MethodChannel名字和Flutter的不一样,导致通信失败);
  2. 通信是异步的,在一些对返回速度要求很高的场景,可能会有轻微的延迟(不过一般感觉不到);
  3. 如果Flutter内部的路由逻辑很复杂(比如嵌套路由),需要更复杂的canPop判断逻辑,增加了维护难度。

4.3 注意事项

  1. MethodChannel的名字一定要原生和Flutter完全一致,包括大小写,最好定义成常量,避免写错;
  2. 一定要在Flutter页面销毁时,取消所有的定时器、监听器等资源,避免内存泄漏;
  3. 原生端的onBackPressed方法是安卓特有的,iOS端的返回逻辑不一样,需要单独处理;
  4. 如果Flutter用的是嵌套路由(比如Navigator 2.0),原来的canPop判断逻辑可能不适用,需要根据实际的路由结构调整;
  5. 测试时一定要覆盖所有的场景,比如从Flutter页面跳转到原生页面、从原生页面跳转到Flutter页面、后台切换、内存回收等场景,避免遗漏异常。

五、文章总结

混合开发中,安卓返回键和Flutter路由的冲突、Navigator和Activity生命周期的协同异常,本质上是因为原生系统和Flutter框架是两个独立的体系,各自管理自己的页面和生命周期,没有默认的关联。解决的核心思路就是“通信”:用MethodChannel把原生和Flutter连接起来,让原生系统知道Flutter内部的页面状态,让Flutter知道原生系统的生命周期变化,从而让两个体系协同工作。

咱们通过两个完整的示例,一步步解决了这两个问题,从最基础的返回键拦截,到复杂的生命周期绑定,每一步都有具体的代码和解释。只要掌握了这个核心思路,不管是简单的混合开发还是复杂的大型项目,都可以灵活调整,解决类似的问题。