The bridge between the Dart/Flutter side and the native (Kotlin/Swift) side is handled by a library called Pigeon.
The docs for Pigeon can be found here:
https://docs.flutter.dev/platform-integration/platform-channels#pigeon
Pigeon is mentioned in the official Flutter docs as a type safe alternative to platform channels, which communicate only using strings.
You write a Dart file (in this case it's pigeons/core_api.dart), let's call
it the Pigeon file, for clarity, where you define the data structures that
will be used to communicate between the Flutter and Native parts.
Pigeon will generate 2 files (or 3 when iOS/Swift support will be implemented)
(in this case they are core.g.dart and Core.g.kt).
These generated files will have all the structures that were defined in the
Pigeon file, as native classes for the respective programming languages.
Also, in the Pigeon file, we have the CoreHostApi class, that defines some methods.
Each method of this class is used to communicate between Flutter and the
Native part, which means that in TalkjsCoreFlutterPlugin.kt, we have the
class PigeonApiImplementation that implements all the methods defined in CoreHostApi.
We also have the CoreFlutterApi class, that is used when the Native part
wants to communicate with Flutter, and lib/src/api.dart has the class
CoreFlutterApiImplementation that implements all those methods.
To generate the core.g.dart and Core.g.kt files, run the command:
dart run pigeon --input pigeons/core_api.dartThe biggest limitation of Pigeon is that you cannot import other files from the Pigeon file, which means that all the data structures passed between Flutter and Native must be declared in a sigle file. I would have liked to make the Pigeon file more modular, but unfortunately it's not possible.
Another limitation is that the members of the classes that you define in the
Pigeon file cannot have default values. To mitigate this limitation, it's
still possible to have nullable members, and not mark those members as
required in the constructor.
All the functions from Flutter to Native are async.
Let's first take a look at TalkjsCoreFlutterPlugin.kt:
The auto generated classes might have the same members and look identical to
the classes already defined in the TalkJS Core library, but in Kotlin they are
treated as completely different and incompatible classes.
For this reason, I decided to completely bypass the whole Pigeon conversions
for most of the classes, and use JSON when passing them to/from Native.
This has the tradeoff that I needed to implement the JSON (de)serialization
for those classes on the Dart side, but on the Native side I get JSON
(de)serialization and mapping to the correct classes for free.
The only classes that still go through Pigeon are TalkSessionOptions and the
metadata classes used for file uploads, because they are not JSON-serializable
on the native side.
Each class that we expose from TalkJS Core (sessions, users, conversations, etc...)
live in a MutableMap<Long, *class*>. For example, users live in a MutableMap<Long, UserRef>.
I call the Long that indexes each map a handle, and the idea is that each
function that creates a new object (for example session.user) return a Long (that
will be an int in Dart), that is the handle of the newly created object.
Also, each function has the handle as the first parameter, for example
session.user passes the handle to the session as the first parameter.
Each class family lives in its own file (for class family I'm referring, for
example in user_ref.dart lives not only the UserRef class, but also
UserSubscription and UserOnlineSubscription).
Each class has a private constructor (UserRef._) that is called by the
function that provides it (for example session.user).
Each method of the class is a thin wrapper to the native API.
The snapshots.dart and entity_tree.dart files contain snapshots and entity
tree stuff, that are JSON-serializable, and they also implement the ==
operator and hashCode functions, so that they can be compared structurally.
Structural comparison is needed, so that when building the UI on top of this
library, the UI needs to be re-rendered only if the snapshots change.
The params.dart file contains the classes for the params parameters of the
various Refs. They only need to implement the toJson method, because they
only get passed from Dart to Native and never on the other way.
Considering that subscription snapshots arrive from the Native part, in
api.dart we have a Map<int, Function> for each subscription, in the same
spirit as the Native part.