I have a flutter app that has uses SQFLite to persist data. I'm interested in trying to do an iOS home screen widget, which from what I've been able to gather, has to be built in swiftui. I'm wondering is it possible to access my flutter sqlite database from the native widget in Swiftui?

Update: Trying to use the below suggestion but running into issues. Since the widget extension seems to cause xcode to fail building if I add the appdelegate module as a target to share....I'm attempting to hop it to a swift file call and then sharing that. But I'm running into errors.

My Flutter helper (it's currently just trying to send an integer over from a list count more or less as proof of concept)

import 'package:flutter/services.dart';
import 'dart:async';

import 'db.dart';


final channelName = 'widgetDataCheckItOff';

Future<void> _handleMethod(MethodCall call) async {
  final String utterance = call.arguments;
  switch(call.method) {
    case "getDataFromFlutter":
      var db = new DB();
      dynamic result = await db.widgetQuery();
      return result;
  }
  final methodChannel = MethodChannel(channelName);
  methodChannel.setMethodCallHandler(_handleMethod);

}

AppDelegate

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    var controller : FlutterViewController!;
    var widgetDataCheckItOff : FlutterMethodChannel!;
    class func shared() -> AppDelegate
    {
        return UIApplication.shared.delegate as! AppDelegate
    }
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        controller = window?.rootViewController as? FlutterViewController;
        widgetDataCheckItOff = FlutterMethodChannel (
            name : "widgetDataCheckItOff",
            binaryMessenger : controller as! FlutterBinaryMessenger
        )
        
        GeneratedPluginRegistrant.register(with: self)
        if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    @available(iOS 9.0, *)
    override func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
        let controller = window.rootViewController as? FlutterViewController
        
        let channel = FlutterMethodChannel(name: "plugins.flutter.io/quick_actions", binaryMessenger: controller! as! FlutterBinaryMessenger)
        channel.invokeMethod("launch", arguments: shortcutItem.type)
    }
    public func getTasks ( args: String, completion:@escaping (Int) -> Int) {
        var x = -1
        widgetDataCheckItOff.setMethodCallHandler { (call : FlutterMethodCall, result: @escaping FlutterResult) in
            //        print("set method call handler")
            if(call.method == "getDataFromFlutter") {
                self.widgetDataCheckItOff.invokeMethod ("getDataFromFlutter", arguments: args, result: {(r:Any?) -> () in
                    x=r as! Int
                });
            }
            else{
                result(FlutterMethodNotImplemented)
                x = -1// to handle undeclared methods
            }
            let result = x
            completion(result)
        }
    }
    
}

Hop File I dubbed widgetSwift

import Foundation

@objc class WidgetData: NSObject{
    public func getWidgetData(result: Int)-> Int {
        var x = -1
        let a = UIApplication.shared.delegate as! AppDelegate
        a.getTasks(args: "getDataFromFlutter",  completion: {(result) -> Int in
            x = result
        })
        return x
//        let x = AppDelegate.getTasks(<#T##self: AppDelegate##AppDelegate#>);
        
    }
}

and my widget extension section calling it

struct widget_extension_widgetEntryView : View {
    var entry: Provider.Entry

       var body: some View {
        let w = WidgetData()
        let x: Int =         w.getWidgetData(result: <#Int#>)
        
        let str = "Today's Tasks:  \(x)"
        Text("", tableName: str)
       }
}

The extension gives me the error on the function call: Editor placeholder in source file.

The widgetSwift gives me errors on the getTasks call: Cannot find 'AppDelegate' in scope Declared closure result '()' is incompatible with contextual type 'Int'

1

There are 1 answers

3
yusufpats On

Flutter provides a way to communicate with the native platform code using MethodChannel.

From the flutter MethodChannel documentation (here):

Flutter uses a flexible system that allows you to call platform-specific APIs whether available in Kotlin or Java code on Android, or in Swift or Objective-C code on iOS.

Note: If desired, method calls can also be sent in the reverse direction, with the platform acting as client to methods implemented in Dart. A concrete example of this is the quick_actions plugin.

MethodChannel usage to invoke a function on native platform-code from flutter:

We create a method channel object in Flutter, iOS, and Android with the same name. The Android and iOS objects will set a method call handler to receive calls from Flutter. The Flutter code can then call invokeMethod to call the handlers on the native objects

Flutter:

static final channelName = 'com.example.widget/database';
final methodChannel = MethodChannel(channelName);

await this.methodChannel.invokeMethod("getDataFromDatabase");

Android:

import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "com.example.widget/database";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
  super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            if (call.method.equals("getDataFromDatabase")) {
              //TODO: Perform method to query database here
              String data;

              if (data != null) {
                result.success(data);
              } else {
                result.error("UNAVAILABLE", "Data not available", null);
              }
            } else {
              result.notImplemented();
            }
          }
        );
  }
}

iOS:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "com.example.widget/database",
                                              binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      guard call.method == "getDataFromDatabase" else {
         result(FlutterMethodNotImplemented)
         return
      }

      //TODO: Perform method to query database here
      let data;

      if data == nil {
         result(FlutterError(code: "UNAVAILABLE",
                        message: "Data not available",
                        details: nil))
      } else {
         result(data)
      }     
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Reverse Example – Calling Flutter from Native If you need to call a method defined in Flutter from native code then you call the same methods from earlier, but this time you call setMethodCallHandler on the Flutter side and invokeMethod on the native side.

Flutter:

final channelName = 'dataFromFlutterChannel';

final methodChannel = MethodChannel(channelName);
methodChannel.setMethodCallHandler(this._handleMethod);

Future<void> _handleMethod(MethodCall call) async {
   final String utterance = call.arguments; 
   switch(call.method) {
     case "getDataFromFlutter":
        //TODO: Query database here and return the data to native side
   }
}

Android:

val channelName = 'dataFromFlutterChannel'
val methodChannel = MethodChannel(flutterView, channelName)
    
methodChannel.invokeMethod("getDataFromFlutter", utterance)

iOS:

let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
let channelName = "dataFromFlutterChannel"
let methodChannel = FlutterEventChannel(name: channelName, binaryMessenger: rootViewController)
    
methodChannel.invokeMethod("getDataFromFlutter", utterance)

This article can help you with a more detailed explanation.