Close App Confirmation Behaviour

Close App Confirmation Behaviour

Mobile + Desktop Close Flutter App Handling

·

5 min read

Default flutter Navigator.pop() will close the app when there is no stacked pages in the navigation.

Android back exit app

The sudden close app may make users feel strange as they may not aware this is the last page of the app. However, different platforms may have a slightly different behaviour on the confirmation, here is the action I usually implemented and how I do it in the flutter project.

Android

Most of the time, Toast message or Dialog will be implemented. Personally, I prefer toast because users don’t have to move their finger to proceed the exit. This avoid users leave app unexpectedly. By wrapping a WillPopScope widget to the home page widget, this allows me checking whether existing is the last page of the navigation stack. The reason why I have escaped IOS will explained in the IOS session.

@override
  Widget build(BuildContext context) {
    if (Platform.isIOS) {
      return _buildPage(context);
    }
    return WillPopScope(
      onWillPop: () {
        if (Navigator.canPop(context)) {
          return Future.value(true);
        }

        if (Platform.isAndroid && !_isGoingExit) {
          _isGoingExit = true;
          Fluttertoast.showToast(
              msg: 'Click again to exit', toastLength: Toast.LENGTH_LONG);
          Future.delayed(const Duration(seconds: 2), () {
            _isGoingExit = false;
          });
          return Future.value(false);
        }

        return Future.value(true);
      },
      child: _buildPage(context),
    );
  }

This is the result:

Android Toast Confirmation

IOS

For IOS, I have escaped the WillPopScoped because of 2 reasons. One is according the apple human interface guideline, it is not recommended to exit the app by in-app action including clicking a button, swipe back and other gesture. The manually call of exiting the app will causing the apple store to reject your app. Details can check the guideline. Another reason is that I will avoid using WillPopScope in IOS platform because it has issue on the IOS’s swipe back action. Implementing this will causing the swipe action not function. Thus, I will try to separate IOS platform when I need to use WillPopScope.

Desktop (Windows + Linux + MacOS)

In-App Exit

In app exit mostly happens when it is a full screen app. An alert dialog to confirm the exit will be useful.

Exit with Method Channel

await showDialog(
  context: context,
  builder: (dialogContext) {
    return AlertDialog(
      title: const Text('Are you sure to leave?'),
      actions: <Widget>[
        ElevatedButton(
          child: const Text('Confirm'),
          onPressed: () {
            Navigator.pop(dialogContext);
            if (!Navigator.canPop(context)) {
              _methodChannel.invokeMethod('exit');
            }
          },
        ),
        ElevatedButton(
          child: const Text('Cancel'),
          onPressed: () {
            Navigator.pop(dialogContext);
          },
        ),
      ],
    );
  },
);

Using method channel instead of Navigator.pop(context) to exit app because on desktop app it will become blank page when there is no stack in the navigation.

Desktop back blank

Thus, I have implemented the exit call with the method channel. More information about implementing method channel in different platform can reference to this article.

Windows

flutter::MethodChannel<> channel(
      flutter_controller_->engine()->messenger(), "cbl.tool.flutter_exit",
      &flutter::StandardMethodCodec::GetInstance());
channel.SetMethodCallHandler(
    [](const flutter::MethodCall<>& call,
        std::unique_ptr<flutter::MethodResult<>> result) {
            if (call.method_name() == "exit") {
                PostQuitMessage(0);
                result->Success(flutter::EncodableValue(true));
            }
            else {
                result->NotImplemented();
            }
    });

MacOS

let flutterChannel = FlutterMethodChannel(name: "cbl.tool.flutter_exit",
            binaryMessenger: flutterViewController.engine.binaryMessenger)
    flutterChannel.setMethodCallHandler({
        (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
        if (call.method == "exit") {
            self.close()
            result(true)
        } else {
            result(FlutterMethodNotImplemented)
        }
    })

Linux

// method channel callback
static void method_call_cb(FlMethodChannel* channel,
                           FlMethodCall* method_call,
                           gpointer user_data)
{
  g_autoptr(FlMethodResponse) response = nullptr;

  const gchar* method = fl_method_call_get_name(method_call);
  if (strcmp(method, "exit") == 0) {
    gtk_window_close(window);
    response = FL_METHOD_RESPONSE(fl_method_success_response_new(FlValue(true)));
  } else {   
    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
  }
  fl_method_call_respond(method_call, response, nullptr);
}

static void my_application_activate(GApplication* application) {
  ...
    FlEngine* engine = fl_view_get_engine(view);
    g_autoptr(FlBinaryMessenger) messenger = fl_engine_get_binary_messenger(engine);
  g_autoptr(FlMethodChannel) channel =
      fl_method_channel_new(messenger,
                            "cbl.tool.flutter_exit",
                            FL_METHOD_CODEC(fl_standard_method_codec_new()));
  fl_method_channel_set_method_call_handler(channel, 
                            method_call_cb, g_object_ref(view), g_object_unref);
}

Result

Result

Using flutter_window_close Plugin

flutter_window_close plugin provided the close method enable to close window on Windows, MacOS and Linux. This plugin can handle the close action from closeWindow() trigger or user close window but window’s exit button.

@override
void initState(){
    super.initState();
    /// add listener for close window action
    FlutterWindowClose.setWindowShouldCloseHandler(() async {
    return await showDialog(
        context: context,
        builder: (dialogContext) {
          return AlertDialog(
          title: const Text('Are you sure to leave?'),
          actions: [
            ElevatedButton(
            onPressed: () => Navigator.pop(dialogContext, true),
            child: const Text('Confirm')),
            ElevatedButton(
            onPressed: () => Navigator.pop(dialogContext, false),
            child: const Text('Cancel')),
          ]);
      });
    });
}

/// call inside exit button
FlutterWindowClose.closeWindow();

Window Exit

Exit with Method Channel

Referencing the flutter_window_close plugin, the close button action can get via the listener of different platform. After receiving the signal, method channel can be used to let dart handle UI asking confirmation.

@override
void initState(){
    super.initState();
    channel.setMethodCallHandler((call) async {
        if (call.method == 'onWindowClose') {
                    /// handle UI and later call exit app like previous session
                }        
    });
}

Windows

For Windows, it is listening the WM_CLOSE signal to determent the window is starting to close. By returning the 0 to intercept the close action and use method channel to ask flutter part for confirmation action.

WindowCloseWndProc(HWND hWnd, UINT iMessage, WPARAM wparam, LPARAM lparam)
{
    if (iMessage == WM_CLOSE) {
        auto args = std::make_unique<flutter::EncodableValue>(nullptr);
        channel_->InvokeMethod("onWindowClose", std::move(args));
        return 0;
    }
    return oldProc(hWnd, iMessage, wparam, lparam);
}

MacOS

For MacOS, overriding the windowShouldClose method allow us to know the window is starting to close. Similarly, returning false to intercept the window to hold and using method channel to let flutter part for confirmation action.

public func windowShouldClose(_ sender: NSWindow) -> Bool {
    notificationChannel?.invokeMethod("onWindowClose", arguments: nil)
    return false
}

Linux

Similarly, by listening to the delete_event signal and method channel to trigger the flutter part UI handling.

main_window_close(GtkWidget* window, gpointer data)
{
    FlValue* value = fl_value_new_null();
    fl_method_channel_invoke_method(notificationChannel,
        "onWindowClose",
        value, NULL, NULL, NULL);
    return TRUE;
}

...

g_signal_connect(G_OBJECT(window), "delete_event",
    G_CALLBACK(main_window_close),
    NULL);

Using flutter_window_close Plugin

Using flutter_window_close is easy. The setWindowShouldCloseHandler will receive the close signal from window’s exit button. This can share the confirmation logic with the user trigger one mentioned above session.

@override
void initState(){
    super.initState();
    /// add listener for close window action
    FlutterWindowClose.setWindowShouldCloseHandler(() async {
    return await showDialog(
        context: context,
        builder: (dialogContext) {
          return AlertDialog(
          title: const Text('Are you sure to leave?'),
          actions: [
            ElevatedButton(
            onPressed: () => Navigator.pop(dialogContext, true),
            child: const Text('Confirm')),
            ElevatedButton(
            onPressed: () => Navigator.pop(dialogContext, false),
            child: const Text('Cancel')),
          ]);
      });
    });
}

Aware MacOS Multi-window

From flutter_window_close readme, MacOS allows multi window for the same app and have difficulties on identifying which window is closing. Default flutter desktop app is single window, this should have no problem. However, developing multi-window may aware on the close window handling.

Reference

Method Channel Implementation on Mobile and Desktop

flutter_window_close