Flutter: How do I reset navigation stack on a Flutter engine?

433 views Asked by At

I'm attempting to add a Flutter module to a native iOS app. However, I'm having an issue where Flutter's navigation stack is maintained when presenting a FlutterViewController more than once (i.e. the details screen is shown instead of the landing page).

How do I reset the navigation stack while using a Flutter engine?

Here is the code for my demo.

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: 'example',
      routes: {
        'example': (context) => const LandingPage(),
      },
    );
  }
}

class LandingPage extends StatelessWidget {
  const LandingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Landing screen')),
      body: Center(
        child: TextButton(
          child: const Text('Go to details'),
          onPressed: () => _navigateToDetails(context),
        ),
      ),
    );
  }

  void _navigateToDetails(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => const DetailsPage()),
    );
  }
}

class DetailsPage extends StatelessWidget {
  const DetailsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details screen')),
      body: const Center(child: Text('Details')),
    );
  }
}

Here is my native Swift code.

@main
class AppDelegate: FlutterAppDelegate {
    
    lazy var sharedEngine = FlutterEngine(name: "shared")

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        sharedEngine.run();
        GeneratedPluginRegistrant.register(with: sharedEngine);
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

class ViewController: UIViewController {

    @IBAction private func onButtonTapped(_ sender: UIButton) {
        let page = FlutterViewController(
            engine: AppDelegate.current.sharedEngine,
            nibName: nil,
            bundle: nil
        )
        present(page, animated: true)
    }
}
1

There are 1 answers

0
AudioBubble On BEST ANSWER

I ended up solving this by dismissing the modal manually.

I added a close button to the AppBar only if running on iOS, which invokes a method on a MethodChannel. I created a custom ViewController that will listen for calls to dismiss_modal. The key is to present this modal by setting modalPresentationStyle to .overCurrentContext.

class LandingPage extends StatelessWidget {

  static const platform = MethodChannel('dev.example/native_channel');
  
  const LandingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Landing screen'),
        leading: _closeButton,
      ),
      body: Center(
        child: TextButton(
          child: const Text('Go to details'),
          onPressed: () => _navigateToDetails(context),
        ),
      ),
    );
  }

  Widget? get _closeButton {
    if (defaultTargetPlatform != TargetPlatform.iOS) {
      return null;
    }
    return IconButton(
      icon: const Icon(Icons.close),
      onPressed: () => _dismissModal(),
    );
  }

  void _dismissModal() async {
    try {
      await platform.invokeMethod('dismiss_modal');
    } on PlatformException {
      return;
    }
  }

  void _navigateToDetails(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => const DetailsPage()),
    );
  }
}
class CustomFlutterViewController: FlutterViewController {

    private lazy var methodChannel = FlutterMethodChannel(
        name: "dev.example/native_channel",
        binaryMessenger: binaryMessenger
    )

    override func viewDidLoad() {
        super.viewDidLoad()
        methodChannel.setMethodCallHandler { [weak self] methodCall, result in
            switch methodCall.method {
            case "dismiss_modal":
                self?.dismiss(animated: true)
                result(nil)
            default:
                result(FlutterMethodNotImplemented)
            }
        }
    }
}

class ViewController: UIViewController {

    @IBAction private func onButtonTapped(_ sender: UIButton) {
        let viewController = CustomFlutterViewController(
            engine: AppDelegate.current.sharedEngine,
            nibName: nil,
            bundle: nil
        )
        viewController.modalPresentationStyle = .overCurrentContext
        present(viewController, animated: true)
    }
}