Bram  Vanbilsen Bram Vanbilsen - 27 days ago 9
Dart Question

How to connect a StreamHandler with an unreachable object's listener?

I'm writing a

GeoFire
plugin and I'm trying to make the listeners for the queries available in the Dart code, but with no luck. I can't seem to figure out how to set the
StreamHandler
for an
EventChannel
. To understand my struggle, you need to take a look at how I create queries.

Here is the code for that:

if (call.method.equals("addGeoQueryEventListener")) {
String refPath = call.argument("refPath");
DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
ref = refPath != null ? ref.child(refPath) : ref;
GeoFire geofire = new GeoFire(ref);
List<Double> center = call.argument("center");
double radius = call.argument("radius");
GeoQuery query = geofire.queryAtLocation(new GeoLocation(center.get(0), center.get(1)), radius);
query.addGeoQueryEventListener(new GeoQueryEventListener() {
@Override
public void onKeyEntered(String key, GeoLocation location) {
System.out.println("entered");
}

@Override
public void onKeyExited(String key) {
System.out.println("exited");
}

@Override
public void onKeyMoved(String key, GeoLocation location) {
System.out.println("moved");
}

@Override
public void onGeoQueryReady() {

}

@Override
public void onGeoQueryError(DatabaseError error) {
System.out.println(error.getMessage());
}
});
}


Every time the
addGeoQueryEventListener
gets called, I create a completely new
DatabaseReference
as done in the
FlutterFire
plugins. I do the same thing creating the queries. This means that there is no reference of the query outside this
if statement
.

So I've created a EventChannel to get data back from the
onKeyEntered
event.

final EventChannel geoQueryKeyEnteredEventChannel = new EventChannel(registrar.messenger(), "com.bram.vanbilsen.geofire.keyEnteredChannel");


After that I try setting the
StreamHandler
for that channel:

geoQueryKeyEnteredEventChannel.setStreamHandler(new StreamHandler() {
@Override
public void onListen(Object o, EventSink eventSink) {

}

@Override
public void onCancel(Object o) {

}
});


How do I know connect this
StreamHandler
with the listener of my query created in my
addGeoQueryEventListener
?

---- EDIT ----

This is my
registerWith
method all setup with one
MethodChannel
:

public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "com.bram.vanbilsen.geofire.GeoFire");
final GeofirePlugin instance = new GeofirePlugin(registrar.activity(), channel);
channel.setMethodCallHandler(instance);
}


Here is an updated piece of code that calls the appropriate methods:

if (call.method.equals("addGeoQueryEventListener")) {
String refPath = call.argument("refPath");
DatabaseReference ref = FirebaseDatabase.getInstance().getReference();
ref = refPath != null ? ref.child(refPath) : ref;
final GeoFire geofire = new GeoFire(ref);
List<Double> center = call.argument("center");
double radius = call.argument("radius");
GeoQuery query = geofire.queryAtLocation(new GeoLocation(center.get(0), center.get(1)), radius);
query.addGeoQueryEventListener(new GeoQueryEventListener() {
@Override
public void onKeyEntered(String key, GeoLocation location) {
HashMap<String, HashMap<String, ArrayList<Double>>> arguments = new HashMap<>();
HashMap<String, ArrayList<Double>> result = new HashMap<>();
ArrayList<Double> geoLocation = convertGeoLocation(location);
result.put(key, geoLocation);
arguments.put("result", result);
channel.invokeMethod("geoQueryEventKeyEntered", arguments);
}

@Override
public void onKeyExited(String key) {
HashMap<String, String> arguments = new HashMap<>();
arguments.put("result", key);
channel.invokeMethod("geoQueryEventKeyExited", arguments);
}

@Override
public void onKeyMoved(String key, GeoLocation location) {
HashMap<String, HashMap<String, ArrayList<Double>>> arguments = new HashMap<>();
HashMap<String, ArrayList<Double>> result = new HashMap<>();
ArrayList<Double> geoLocation = convertGeoLocation(location);
result.put(key, geoLocation);
arguments.put("result", result);
channel.invokeMethod("geoQueryEventKeyMoved", arguments);
}

@Override
public void onGeoQueryReady() {
HashMap<String, String> arguments = new HashMap<>();
arguments.put("result", "success");
channel.invokeMethod("geoQueryEventReady", arguments);
}

@Override
public void onGeoQueryError(DatabaseError error) {
HashMap<String, HashMap> arguments = new HashMap<>();
HashMap<String, String> databaseError = new HashMap<>();
databaseError.put("code", Integer.toString(error.getCode()));
databaseError.put("details", error.getMessage());
databaseError.put("message", error.getMessage());
arguments.put("result", databaseError);
channel.invokeMethod("geoQueryEventError", arguments);
}
});
} else if ( (call.method.equals("geoQueryEventKeyEntered")) || (call.method.equals("geoQueryEventKeyExited")) ||
(call.method.equals("geoQueryEventKeyMoved")) || (call.method.equals("geoQueryEventReady")) || (call.method.equals("geoQueryEventError")) ) {
result.success(call.argument("result"));
}


How do I now listen for the function results in my Dart code? Do I use
setMethodCallHandler
on my lonely
MethodChannel
? If so, wouldn't that give problems when I have to use that channel for invoking a method that has nothing to do with my listener?

Answer Source

You should either do everything with method calls on the MethodChannel, issuing method calls in the Java to Dart direction on each GeoFire event, or everything with an EventChannel, basically moving the code of your if statement into the listen method of the StreamHandler.

With MethodChannel:

On Java side:

MethodCallChannel myChannel = new MethodCallChannel(messenger, "geofire");
myChannel.setMethodCallHandler(new MethodCallHandler() {
  @Override
  public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("addGeoQueryEventListener")) {
      // ... setup query using call.arguments
      query.addGeoQueryEventListener(new GeoQueryEventListener() {
        @Override
        public void onKeyEntered(String key, GeoLocation location) {
          Map<String, Object> event = // ... extract info from key,location
          myChannel.invokeMethod("onKeyEntered", event);
        }

        @Override
        public void onKeyExited(String key) {
          myChannel.invokeMethod("onKeyExited", key);
        }
        // ... other GeoQueryEventListener handlers.
      });
      result.success(null); 
    } else {
      result.notImplemented();
    }
  }
});

On Dart side:

const myChannel = const MethodChannel('geofire');

myChannel.setMethodCallHandler((MethodCall call) {
  switch (call.method) {
    case 'onKeyEntered':
      // ... extract event data from call.arguments
      return null;
    case 'onKeyExited':
      // ... extract event data from call.arguments
      return null;
    // ... other methods
    default: throw new MissingPluginException();
  }
});
myChannel.invokeMethod('addGeoQueryEventListener', someArguments);

The code above uses the same channel for communicating in both directions. Calling invokeMethod from Dart invokes the Java MethodCallHandler, while calling invokeMethod from Java invokes the Dart handler.

Another option is to use two different channels, one for Dart->Java communication to request setting up the GeoFire listener, and one for Java->Dart communication to send events.

The best choice probably depends on whether you have other uses for the channel. I'd use different channels for different purposes, so that all communication on a single channel is somehow related. Channels are extremely light-weight; they only carry the weight of their name.

With EventChannel:

On Java side:

final EventChannel myChannel = new EventChannel(messenger, "geofire");
myChannel.setStreamHandler(new StreamHandler() {
  private GeoQuery query;
  private GeoQueryEventListener listener;

  @Override
  public void onListen(Object arguments, EventSink eventSink) {
    query = // ... setup query using arguments
    listener = new GeoQueryEventListener() {
      @Override
      public void onKeyEntered(String key, GeoLocation location) {
        Map<String, Object> event = // ... create key entered event
        eventSink.success(event);
      }

      @Override
      public void onKeyExited(String key) {
        Map<String, Object> event = // ... create key exited event
        eventSink.success(event);
      }
      // ... other GeoQueryEventListener handlers.
    }
    query.addGeoQueryEventListener(listener);
  }

  @Override
  public void onCancel(Object args) {
    query.removeGeoQueryEventListener(listener);
  }
});

On Dart side:

const myChannel = const EventChannel('geofire');
myChannel.receiveBroadcastStream(someArguments).listen((event) {
  // handle event
});