Home Features Docs Blog Support GitHub

Success with Flutter integration! (almost)


(Marco) #1

Hi guys,

I’m experimenting with MOE and Flutter together in order to have a real common source base for both Android and iOS.

I’ve got most of it working but I’m stuck on a minor(?) problem with the binding.

To give you a bit of background, what I did is the following.

  • In Android Studio I’ve installed both MOE and Flutter plugin.
  • I’ve created a new MOE project with 3 modules, android, common and ios.
  • I’ve added support for CocoaPod to the ios module.
  • I’ve created a new flutter module following https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps
  • I’ve created a binding for the Flutter framework
  • I’ve adapted the Objective-C code from the link above to the binding code in MOE.
  • I’ve disabled the storyboard in xcode and forced the AppDelegate to load the FlutterViewController

And, it works! :slight_smile: It was straightforward and it’s working really well, I’m really happy with this.

The next step is to call native code from Flutter, and here I’m stuck.

I’m following https://flutter.io/docs/development/platform-integration/platform-channels and at some point they call this:

FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                      methodChannelWithName:@"samples.flutter.io/battery"
                                      binaryMessenger:controller];
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // TODO
}];

The moeNatJGen gradlew task generated FlutterMethodChannel but not the setMethodCallHandler method.

I looked in the log and there are exactly 5 skipped methods in all the framework, including this one (how lucky! :sob: )

This is the log:

**Skipping:**  setMessageHandler: in FlutterBasicMessageChannel (Objective-C Instance Method) reason:  *unhadled callback at arg(handler@0) type [^(void)(@id, ^(void)(@id))]*
**Skipping:**  userNotificationCenter:willPresentNotification:withCompletionHandler: in FlutterPluginAppLifeCycleDelegate (Objective-C Instance Method) reason:  *bad argument(1) type [object type 'UNNotification' is not processed]*
**Skipping:**  setMethodCallHandler: in FlutterMethodChannel (Objective-C Instance Method) reason:  *unhadled callback at arg(handler@0) type [^(void)(FlutterMethodCall*, ^(void)(@id))]*
**Skipping:**  setMessageHandlerOnChannel:binaryMessageHandler: in FlutterBinaryMessenger (Objective-C Instance Method) reason:  *unhadled callback at arg(handler@1) type [^(void)(NSData*, ^(void)(NSData*))]*
**Skipping:**  userNotificationCenter:willPresentNotification:withCompletionHandler: in FlutterPlugin (Objective-C Instance Method) reason:  *bad argument(1) type [object type 'UNNotification' is not processed]*

I was thinking that maybe I can fix the binding code manually, it’s not the first method I bind myself, but in this particular case I don’t know how to proceed.

The relevant parts from the header file (FlutterChannels.h) are these:

typedef void (^FlutterResult)(id _Nullable result);
typedef void (^FlutterMethodCallHandler)(FlutterMethodCall* call, FlutterResult result);
- (void)setMethodCallHandler:(FlutterMethodCallHandler _Nullable)handler;

Any idea how can I bind the setMethodCallHandler method? I don’t mind about the other methods for now. This is really blocking me on the last step of this project.

Thanks for the help!


(Mira Stinktshir) #2

Flutter is cross-platform so why would you need MOE?


(Marco) #3

Up until now I always wrote my apps in java, with as much code as possible in a common module.
There are few components that access native functionalities like the camera or the GPS that are kept separated, but this is a minority of the code.
What biggest non-common code is the UI. I don’t like the interface builder of xcode, I prefer to code my layout. It works, I have full control and everything is as I want it, but having to maintain 2 searate UIs is boring and take time.

With Flutter as a module I can migrate the native UI to a Flutter one, one bit at a time. This will reduce the amount of non-shared code to almost 0.

I know I can start from scratch but I don’t want to loose my existing codebase, or handy stuff like retrofit.

I’ve started looking into Flutter only yesterday and I’m amazed I was able to take it so far in only a day. I can say I’ve met my goal.

For my original problem, I still have to find a good solution, but I was able to work around it making a wrapper method in Objective-C.

Of course I’m still interested if someone has a pure NatJ solution.


(Marco) #4

Going back to the original problem, I’ve tried this but it doesn’t work.

@Selector("setMethodCallHandler:")
public native void setMethodCallHandler(@ObjCBlock(name = "handle") FlutterMethodCallHandler callback);

public interface FlutterMethodCallHandler {
	void handle(
	        FlutterMethodCall call,
                    @ObjCBlock(name = "call") FlutterResult result
	);
}

public interface FlutterResult {
	void call(@Mapped(ObjCObjectMapper.class) Object result
    );
}

It doesn’t give me any error at compile time.

At runtime it doesn’t crash or anything, but if I use the debugger on setMethodCallHandler I see that the parameter is null. In java I’ve confirmed that it isn’t null and it’s valid.

Any idea?


#5

How about doing this?

@ObjCBlock(name = "handle")
to
@ObjCBlock(name = "call_setMethodCallHandler")

void handle(
to
void call_setMethodCallHandler(

In my case, the name mismatch affected the behavior.

@Generated
@Runtime(ObjCRuntime.class)
@ObjCProtocolName("Aaa")
public interface Aaa {
	@Generated
	@Selector("setBbb:")
	void setCcc(@ObjCBlock(name = "call_setBbb") Block_ddd value);

	@Runtime(ObjCRuntime.class)
	@Generated
	public interface Block_ddd {
		@Generated
		void call_setBbb(NSDictionary<?, ?> arg0);
	}
}

If rename call_setBbb to call_setBbb2 then

RuntimeException: Could not find Java method for native callback!
at ObjCCallbackMapper.java:542

(Marco) #6

Thank you @ark100 , I’ll give it a shot later, but I don’t think this is it.
Originally I was using a naming convention that I mimiked from a @Generated bind and it beginned with call_ .

The docs about ObjCBlock say that the “name” attribute must match the name in the interface, nothing else, but maybe that’s outdated. https://doc.multi-os-engine.org/multi-os-engine/6_natj/natj_interoperability/NatJ_How_to.html

The thing is that I don’t get a RuntimeException, the Object-C method is invoked, but the parameter that get passed seems to be null, while in java it’s not.

Maybe it’s better if I do a proof of concept of the problem, who knows, maybe I discovered a bug or I’m hitting a corner case.


(Marco) #7

Thank you @ark100, I’ve tested your idea. The call_ prefix doesn’t change the behaviour but the Block_ prefix in the interface name does change a lot of thing.
It doesn’t work yet but I’m doing various experiments. I think that this may be the right direction.


(Marco) #8

I’ve made a progress I think. This is the code I’m testing now:

@Selector("setMethodCallHandler:")
public native void setMethodCallHandler(@ObjCBlock(name = "call_A") Block_FlutterMethodCallHandler handler);

@Runtime(ObjCRuntime.class)
public interface Block_FlutterMethodCallHandler {
    void call_A(
            FlutterMethodCall call,
            @ObjCBlock(name = "call_B") Block_FlutterResult result
    );
}

@Runtime(ObjCRuntime.class)
public interface Block_FlutterResult {
    void call_B(@Mapped(ObjCObjectMapper.class) Object result
    );
}

If I name the interface FlutterMethodCallHandler instead of Block_FlutterMethodCallHandler then the handler argument is null in ObjC. With the right name the argument get the right value, and this is the first step forward.

Changing the call_* method names doesn’t seems to affect the behaviour in any way.

When the call_A method is invoked from ObjC I get this error:

Terminating app due to uncaught exception 'java.lang.NullPointerException', reason: 'java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.String org.moe.natj.objc.ann.ObjCBlock.name()' on a null object reference
at org.moe.natj.objc.map.ObjCCallbackMapper.objectToJava(ObjCCallbackMapper.java:539)
at org.moe.natj.objc.map.ObjCCallbackMapper.toJava(ObjCCallbackMapper.java:630)
at org.moe.natj.objc.map.ObjCObjectMapper.toJava(ObjCObjectMapper.java:504)
at org.moe.natj.general.NatJ.toJava(NatJ.java:1053)
at apple.uikit.c.UIKit.UIApplicationMain(Native Method)
at com.example.myapp.Main.main(Main.java:21)

The source for ObjCCallbackMapper is here https://github.com/multi-os-engine/moe-natj/blob/moe-master/src/main/java/org/moe/natj/objc/map/ObjCCallbackMapper.java

The problem is with the second argument of call_A. If I use anything that is not a block it works as expected.

In practice if the second argument is not a block the binding code can be automatically generated by NatJ, it’s only when it’s a block that NatJ skip it.

Ideas?


#9

For workarounds or narrow down the problem,
how about via the Objective-C implementation?

At that time, wrapping Blocks is one means.
java --(protocol/class)–> adapter --(blocks)–> Flutter --(blocks)–> adapter --(protocol/class)–> java

(code image)

Objective-C:

@interface FlutterResultWrap : NSObject
...

@protocol Handler<NSObject>
@required
- (void)handle:(FlutterMethodCall*)call result:(FlutterResultWrap*)result;
...

@interface Adapter : NSObject
...

@implementation Adapter
- (id)init:(Handler*)cb
{ ... }
- setMethodCallHandlerAdp()
{
	__weak typeof(self) weakSelf = self;
	[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResultWrap result) {
		[weakSelf->cb handle:call result: [[FlutterResultWrap alloc] init:result] ];
	}];
}

Java:

class A extends NSObject implements Handler {
	...
	void f(){
		adapter = Adapter.alloc().init(this);
		adapter.setMethodCallHandlerAdp();
	}
	
	@Selector("handle:result:")
	@Override
	public void handleResult(FlutterMethodCall call, FlutterResultWrap result){
	}
}

On another note.
If my memory is correct, if _Nullable(or _Nonnull) is included in the definition, auto generation at NatJ was skipped.


(Marco) #10

Thank you @ark100, quoting from my 2nd comment

The wrapper I was referring to is pretty much what you propose, and it works really well.

The reasons I’m still trying to find a solution that doesn’t involve a wrapper are that I want ti learn more about the internals of NatJ / MOE and that I’d like to see this fixed one day. If I understand it well enough to fix the problem maybe I can send a pull request. Sadly I don’t expect Migeran to work on this any time soon.

Days ago I’ve tested it without _Nullable, NatJ still skip the method with the same reason.


(Thomas Gallinari) #11

Hi @martinellimarco, I may be in the same situation than you in the next months, with a common module and two separate code bases for the UI that I wish I can migrate to Flutter.
Have you considered writing a piece of blog about your experience? I would be very interested!


(Marco) #12

Hi @t.gallinari, sorry for the late reply but I’ve got a flu.
I don’t have a blog but I’ll be more than happy to help you if you’ll need. Mention me in a message here and I’ll reply as soon as I can.