Flutter add-to-app 架构:宿主 App 与 Flutter 模块的路由栈同步机制

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 ChannelPlatform 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 ChannelinvokeMethod方法实现。

// 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架构。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注