The code in this repository has been constructed to be built as a Flutter Plugin. It is not yet constructed as a federated plugin but this is in our backlog as issue 118.
After checking out this repository, run the following command:
flutter pub get
You may also find it insightful to run the following command, as it can reveal issues with your development environment:
flutter doctor
If using Android Studio, delete the .packages
file. It is a deprecated autogenerated file which confuses Android Studio into thinking all files are build outputs instead of source files.
The Realtime
'interface' is an abstract class
which seems to be the correct way to do this in Dart - every class has an
'implicit interface' and then you make the
class abstract to present it as a more traditional interface as understood by the perhaps a Java programmer.
Dart libraries don't extend the Exception class, they implement its implicit interface. (see this comment).
For the moment it would not seem appropriate for our plugin to throw instances conforming to the implicit interface of the Error class, as this is perhaps more appropriate for programmer's failures on the Dart side of things. But time will tell.
The platform SDKs (ably-android, ably-cocoa) provide ways to check if device activation, deactivation or registration update fails. On Android, these errors are sent in Intents which you should register for at runtime. In Cocoa, your errors are provided through ARTPushRegistererDelegate
methods. However, this error does not get returned immediately/quickly in all cases. For example, if there was no internet connection, then Push.activate()
will not throw an error, it will just block forever, because errors are not provided by the SDKs. Once an internet connection is made, the intent will be sent / delegate methods will be called.
Implementation wise, we do this by passing a reference to the FlutterResult
we can use to pass back the result, when. This makes it convenient for users: they can await push.activate()
. However, they should not rely on this Future completing, in the case of network issues.
Android's firebase-messaging / FCM library allows users to select the Intent action used when the automatically generated notification is tapped by the user. You can do this by setting fcm.notification.click_action
in Ably's push payload. However, for this to work, users would need to declare the intent action within their AndroidManifest.xml
. Therefore, we don't really tell users they can modify click_action
and configure it. However, they can do so if they wish.
iOS allows users to show the notification even if the app is in the foreground, by calling a delegate method (userNotificationCenter(_:willPresent:withCompletionHandler:)
) which the user can choose to show the message to the user, based on the notification content. FCM / Android does not provide this functionality. Users can only configure this behaviour for iOS, by using PushNotificationEvents#setOnShowNotificationInForeground
.
Background processing in Flutter is a complicated subject that has not been explained very much in public. It involves creating Flutter Engines/isolates manually (for Android), passing messages back and forth, etc.
Differences between ably-Flutter and Firebase Messaging implementation (Android only):
- Isolate code: In Firebase Messaging, they explicitly define a callback which is launched in it's own isolate. We do not launch a custom entrypoint/ dart code, but instead re-use the user's default entrypoint (their app code), by using
DartExecutor.DartEntrypoint.createDefault()
. Therefore, we provide the same environment for message handling on both Android and iOS: their application is running when we handle the message. - Resource consumption tradeoffs: Firebase launches an isolate capable of only handling messages at app launch, even if they don't need it. They keep this isolate launching throughout the app, and have a queue process to queue messages. To do this, they maintain an Android Service. This allows them 10 minutes of execution time, where as on iOS the same library only has 30 seconds of execution time. Instead, we launch a new isolate on every message if the application is not yet running. Instead of creating a service and queueing work for it for each message. FCM will hold the messages until we are ready anyway (they are our queue). A new message will spawn a new engine.
- Execution / wall clock time: We have an execution time of 30 seconds on both Android and iOS for each message. On Android, Firebase messaging launches a Service which has approximately 10 minutes of execution time from it's launch to handle all messages received before Android stops the service. It's unclear if the Service will be automatically launched by iOS immediately, or if it will only be launched in the future. On iOS, each message has 30 seconds of execution time. This seems to be a bug in the design of firebase_messaging. If users want more execution time, we recommend package:workmanager.
Because of this architectural simplicity, we do not need to use PluginUtilies
, pass references of 2 methods between Dart and platform side, or save / load these methods in SharedPreferences
. We avoid conflicts between the default FlutterEngine
launched in an Activity
and by ourselves, by using separate method channels (unique channel names).
The Android project does use AndroidX, which appears to be the default specified when Flutter created the plugin project, however Flutter's Java API for plugins (e.g. MethodChannel) appears to still use deprecated platform APIs like the UiThread annotation.
Once changes have been made to the platform code in the ios folder, especially if those changes involve changing the pod spec to add a dependency, then it may be necessary to force that build up stream with:
- Bump
s.version
in the pod spec - From example/ios run
pod install
- Open Runner.xcworkspace in Xcode, clean and build
Otherwise, after making simple code changes to the platform code it will not get seen with a hot restart "R". Therefore if there's a current emulation running then use "q" to quit it and then re-run the emulator - e.g. with this if you've got both iOS and Android emulators open:
flutter run -d all
To debug both platform and Dart code simultaneously:
- In Android: in the Flutter project window, launch the application in debug mode in Android Studio. Then, in the Android project window, attach the debugger to the Android process.
- In iOS: To debug iOS code, you must use/ set breakpoints in Xcode. In Android Studio or command line, run the flutter run --dart-define
command you would usually run. This ensures when you build with Xcode, the environment variables are available to the app. Then, re-run the application using Xcode. Then, in Android Studio, click
Run>
Flutter Attach, or click the
Flutter Attach` button.
As features are developed, ensure documentation (both in the public API/ interface) and in relevant markdown files are updated. When referencing images in markdown files, using a local path such as images/android.png
, for example ![An android device running on API level 30](images/android.png)
will result in the image missing on pub.dev README preview. Therefore, we currently reference images through the github.com URL path (https://github.com/ably/ably-flutter/raw/
), for example to reference images/android.png
, we would use ![An android device running on API level 30](https://github.com/ably/ably-flutter/raw/main/images/android.png)
. A suggestion has been made to automatically replace this relative image path to the github URL path.
- Flutter plug-in package development, being a specialized package that includes platform-specific implementation code for Android and/or iOS.
- Flutter documentation, offering tutorials, samples, guidance on mobile development, and a full API reference.
Some files in the project are generated to maintain sync between
platform constants on both native and dart side.
Generated file paths are configured as values in bin/codegen.dart
for toGenerate
Map
Read about generation of platform specific constant files
- Add new type along with value in
_types
list at bin/codegen_context.dart - Add an object definition with object name and its properties to
objects
list at bin/codegen_context.dart This will createTx<ObjectName>
under which all properties are accessible.
Generate platform constants and continue
- update
getCodecType
in Codec.dart so new codec type is returned based on runtime type - update
codecPair
in Codec.dart so new encoder/decoder is assigned for new type - update
writeValue
in android.src.main.java.io.ably.flutter.plugin.AblyMessageCodec so new codec type is obtained from runtime type - update
codecMap
in android.src.main.java.io.ably.flutter.plugin.AblyMessageCodec so new encoder/decoder is assigned for new type - add new codec encoder method in ios.Classes.codec.AblyFlutterWriter
and update
getType
andgetEncoder
so that new codec encoder is called - add new codec encoder method in ios.classes.codec.AblyFlutterReader
and update
getDecoder
so that new codec decoder is called
- Add new method name in
_platformMethods
list at bin/codegen_context.dart
Generate platform constants and use wherever required
The new flutter analyzer does a great job at analyzing complete flutter package.
Running flutter analyze
in project root will analyze dart files in complete project,
i.e., plugin code and example code
Or, use the good old dart analyzer
dartanalyzer --fatal-warnings lib/**/*.dart
dartanalyzer --fatal-warnings example/lib/**/*.dart
With just the Flutter tools installed, the following is observed:
ably-flutter % which dartdoc
dartdoc not found
?1 ably-flutter % which flutter
/Users/quintinwillison/flutter/bin/flutter
The dartdoc
tool can be activated via the flutter
command like this:
ably-flutter % flutter pub global activate dartdoc
Resolving dependencies...
Downloading...
Precompiling executables...
Precompiled dartdoc:dartdoc.
Installed executable dartdoc.
Warning: Pub installs executables into $HOME/flutter/.pub-cache/bin, which is not on your path.
You can fix that by adding this to your shell's config file (.bashrc, .bash_profile, etc.):
export PATH="$PATH":"$HOME/flutter/.pub-cache/bin"
Activated dartdoc 0.39.0.
And, indeed, on inspecting my path I could confirm that it wasn't present:
ably-flutter % echo $PATH
/Users/quintinwillison/.asdf/shims:/Users/quintinwillison/.asdf/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/MacGPG2/bin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Apple/usr/bin:/Users/quintinwillison/Library/Android/sdk/platform-tools:/Users/quintinwillison/flutter/bin
So I edited my configuration to add the PATH
export suggested:
ably-flutter % vi ~/.zshrc
ably-flutter % source ~/.zshrc
ably-flutter % echo $PATH
/Users/quintinwillison/.asdf/shims:/Users/quintinwillison/.asdf/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/MacGPG2/bin:/usr/local/share/dotnet:~/.dotnet/tools:/Library/Apple/usr/bin:/Users/quintinwillison/Library/Android/sdk/platform-tools:/Users/quintinwillison/flutter/bin:/Users/quintinwillison/Library/Android/sdk/platform-tools:/Users/quintinwillison/flutter/bin:/Users/quintinwillison/flutter/.pub-cache/bin
And I was then able to find the dartdoc
tool:
ably-flutter % which dartdoc
/Users/quintinwillison/flutter/.pub-cache/bin/dartdoc
And see that it had been installed globally in the Flutter context:
ably-flutter % flutter pub global list
dartdoc 0.39.0
ably-flutter % dartdoc
Documenting ably_flutter...
Initialized dartdoc with 195 libraries in 180.4 seconds
Generating docs for library ably_flutter from package:ably_flutter/ably_flutter.dart...
Validating docs...
warning: dartdoc generated a broken link to: DeveloperNotes.md, linked to from package-ably_flutter: file:///Users/quintinwillison/code/ably/ably-flutter
found 1 warning and 0 errors
Documented 1 public library in 5.6 seconds
Success! Docs generated into /Users/quintinwillison/code/ably/ably-flutter/doc/api
Releases should always be made through a release pull request (PR), which needs to bump the version number and add to the change log. For an example of a previous release PR, see #89.
The release process must include the following steps:
- Ensure that all work intended for this release has landed to
main
- Create a release branch named like
release/1.2.3
- Add a commit to bump the version number
- Add a commit to update the change log.
- Autogenerate the changelog contents by running
github_changelog_generator -u ably -p ably-flutter --since-tag 1.2.0 --output delta.md
and manually copying the relevant contents fromdelta.md
intoCHANGELOG.md
- Make sure to replace
HEAD
in the autogenerated URL's with the version tag you will create (e.g.v1.2.1
).
- Push the release branch to GitHub
- Open a PR for the release against the release branch you just pushed
- Gain approval(s) for the release PR from maintainer(s)
- Land the release PR to
main
- Execute
flutter pub publish
from the root of this repository - Create a tag named like
v1.2.3
, usinggit tag v1.2.3
- Push the newly created tag to GitHub:
git push origin v1.2.3
- Create a release on GitHub following the previous releases as examples.
To check that everything is looking sensible to the Flutter tools, without publishing, you can use:
flutter pub publish --dry-run
We tend to use github_changelog_generator to collate the information required for a change log update.
Your mileage may vary, but it seems the most reliable method to invoke the generator is something like:
github_changelog_generator -u ably -p ably-flutter --since-tag v1.2.0 --output delta.md
and then manually merge the delta contents in to the main change log (where v1.2.0
in this case is the tag for the previous release).