Flutter, building a Home Widget on the iOS platform

Hi all! My name is Konstantin, I’m a Flutter developer at Nord Clan.

In this article, my colleague Anna and I would like to share our experience of combining Flutter and home widgets on the iOS platform.

Each of us, using a smartphone on any of the OS on the main (or not so) screens, came across widgets. Widgets are small applications that perform simple actions, are of some informational nature, or simply decorate your screen, given that this article is about Flutter in which “it’s all a widget”, it’s important not to get confused, so it’s best to immediately indicate that Widget we will refer to Flutter, and Home Widget (HW – for short) refer just to the auxiliary application / widget of the native system.

It is also important to understand what problem HW solves, in our case it is a reduction in user actions (opening a magnetic door lock through an endpoint), but as mentioned above, you may have a different task.

It is important to clarify that to write HW, we need native code, by default, any HW refers to the system we work with, Flutter in this case can only receive or transmit certain data.

All business logic was written in Flutter, it remains to deal exclusively with native code. To do this, I had to use my colleague, since at the moment I don’t know Swift 🙂

Next, I hand over the floor to Anna, our amazing iOS developer 🙂


To begin with, I would like to go through some of the nuances of iOS.

When adding WidgetKit (iOS 14, *) to an app, there are platform limitations that needed to be addressed.

Firstly, HWs are written exclusively on the new SwiftUI declarative framework. It is impossible to write it on UIKit, which is a rather limiting condition if you need to add HW to your application.

Secondly, HWs are read-only, which means that they only display information from the application, but are not something independent. This HW behavior deprives us of an interactive experience that the user can interact with. In fact, HW in iOS is just a picture with information, by taping on which the application will open. The only way to somehow interact with the application through HW is to hook WidgetURL() to it and pass a link to a specific screen in the application there, but that’s it.

A big problem: HW will always open the app. This limitation does not allow us to send a request to the application by tapping on HW and not open the application in full screen.

From here we understand that HWs of different sizes have different tap areas, that a small widget behaves like a single button, and on medium and large sized HWs, you can already set targets to open different screens in the application, if this is necessary to implement. You can already work with this.

As a result, these platform limitations give us the following:

  1. HW will always open the app.

  2. HW is not interactive, you cannot change something that is in the application in it, you can only change the data in the application and display them on the HW.

  3. Different implementation of url processing for all HW sizes.

  4. Cherry on the cake: all HW customization is only possible on SwiftUI, no UIKit.

The conclusions that were made:

It is necessary to create a method that will return whether the application is open from HW and from which one. This is necessary so that we, on the part of Flutter, can set the condition that if the application is open with HW, then there is a request to open the door. If the application is simply opened by tapping on it, nothing should happen.

Next, we move on to implementation.

A task: by clicking on HW, launch the application, call the method for opening the door in it, display that the request has worked by showing the banner and smoothly hide the application.

Problem: Using Home Widget on a native platform and business logic written in Flutter.

How did you decide: From the side of Flutter we created a communication channel (FlutterMethodChannel) whose name must match the one we create from the iOS side, it is important not to make a mistake (as always):

  static const platformChannel = MethodChannel('widgetChannel');

In the AppDelegate, we set up a connection with the created channel, in the didFinishLaunchingWithOptions method:

 override func application(
  _
   application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let channelName = "widgetChannel"
        let methodChannel = FlutterMethodChannel(name: channelName,
                                                 binaryMessenger: controller.binaryMessenger)

We create a controller, a channel and set it all up in the methodChannel, so we have a connection with the application, but we still need to set up something through which we will communicate. Here it happens via message passing, in our case we pass a Bool:

Future<void> resultFromIosWidget(BuildContext context) async {
    try {
      final bool result =
          await platformChannel.invokeMethod('openedFromWidget');
      
      if (result == true) {
        await MainRequests().openDoor(UrlConsts.doorUrlUl);} 
    } on PlatformException catch (e) {
      '${e.message}';
    } 
}

You need to create a condition in Flutter, under the criteria of which the HW opening method on iOS will fit.

On the iOS side, we create a variable:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
    var isTappedOnWidget: Bool = false

Initially, we set the value to false so that this condition does not work every time the application is opened. We only need this script to fire when the app is opened from the HW.

In the AppDelegate extension, we create a method that will catch a click and understand that the application is open with HW:

extension AppDelegate {
   private func openedFromWidget(url: URL, isTapped: Bool) {
        var tapped = isTappedOnWidget
        if url.scheme == "widget", url.host == "widgetFamily", tapped == true {
            let widgetFamily = url.lastPathComponent
            tapped = isTapped
        }
    }
}

We create a link and form it when setting up HW:

import WidgetKit
import SwiftUI

struct DoorOpenerWidgetEntryView: View {
    @Environment(\.widgetFamily) var widgetFamily
    var entry: Provider.Entry
 
    @ViewBuilder
    var body: some View {

        switch widgetFamily {
        case .systemSmall, .systemMedium:
            SystemWidgetView()
                .widgetURL(widgetLink)
        case .accessoryRectangular, .accessoryCircular:
          AccessoryWidgetView()
                .widgetURL(widgetLink)
        default:
            Text(Constants.errorWidget)
        }
    }
    private var widgetLink: URL {
        URL(string: "widget://widgetFamily/\(widgetFamily)")!
    }
}

Thus, we will know that the application was opened from HW, and even find out from which one.

We return to the AppDelegate, create a handler for the channel that will accept our condition and compare it with what condition is needed from the Flutter side in order to trigger the openDoor () request.

In recieveResult() we get the actual value of isTappedOnWidget and pass it to prepareMethodHandler(), then we call this method in didFinishLaunchingWithOptions before returning true:

private func prepareMethodHandler(channel: FlutterMethodChannel) {
        channel.setMethodCallHandler({
            (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if call.method == "openedFromWidget" {
                self.recieveResult(result)
            } else {
                result(FlutterMethodNotImplemented)
                return
            }
        })
    }

 private func recieveResult(_ result: FlutterResult) {
        let answer = isTappedOnWidget
        result(answer)
    }

But if we collect everything now, nothing will work, because we don’t change our Bool condition from false to true anywhere, and we don’t call the openFromWidget method with the boolean we need.

Therefore, we override the application (app, open, options) method in the AppDelegate, in which we will change the condition. This method will work only when the application is opened from the HW, because it keeps track of the urls we created earlier. And if we entered the application using one of the widget urls and our flag isTappedOnWidget = true, then we finally fall under all the conditions that we have designated for ourselves from Flutter:

override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    
        isTappedOnWidget = true

        if isTappedOnWidget == true {
            openedFromWidget(url: url, isTapped: isTappedOnWidget)
        }

        return true
    }

What has already been done: We open an application with HW and there is a request, we receive a response and open the door. But the application is still on the screen, you need to smoothly hide it. We do this through DispatchQueue.main.asyncAfter() in a separate function. We also register it in the AppDelegate extension and call it immediately after changing the flag in application(app, open, options):

private func closeApp () {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.isTappedOnWidget = false
            UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
        }
}

Most importantly, in order for this method to work correctly every time the application is closed to the widget, we need to change the flag from true to false in order to return everything to its original position:

override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    
        isTappedOnWidget = true

        if isTappedOnWidget == true {
            openedFromWidget(url: url, isTapped: isTappedOnWidget)
        }
        closeApp()

        return true
    }

Thus, we get a bunch of HW on the iOS platform with a Flutter application.


Thanks for reading the article! Have you had any experience with Home Widget? Share in the comments! 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *