A simple mobile app example (iOS, Android) that uses a Flutter (Dart) UI and JUCE (C++) as backend.
-
Created a JUCE GUI project
JucyFlutteringwith Android and iOS exporters (well it does not really show a GUI, but has a message loop running, to provide timers, event handling etc.) -
Created a flutter plugin project (subfolder
fluttering) for platforms android and iOs, using Java for android, and ObjectiveC for iOSflutter create --template=plugin --platforms=android,ios -a java -i objc fluttering- the .dart source file are placed in
fluttering/lib
JUCE is already does a good job at setting up all the platform-dependend boilerplate code to instantiate and run iOs and Android apps. Some adjustments had to be done to do the Flutter instantiation part too, as documented here:
Made custom activity and application Java class files, placed in Sources/Android.
- The Actvity extends the FlutteringActivity class
- The Application initialises JUCE, but also and instantiates, pre-warms and caches the Flutter Engine, and sets the initial Flutter navigation route to '
/'
Some manual configuration for the Projucers Android exporter was necessary:
-
Java Source code folders:
Source/Android -
Custom Android Activity:
eu.selfhost.audiooffler.jucyfluttering.FlutteringActivity -
Custom Android Application:
eu.selfhost.audiooffler.jucyfluttering.FlutteringApplication -
Module Dependencies:
implementation project(':flutter') -
Extra module's build.gradle content:
defaultConfig { ndk { // filter for architectures supported by Flutter abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' } } compileOptions { // Flutter Android engine uses Java 8 features sourceCompatibility 1.8 targetCompatibility 1.8 } -
Custom settings.gradle content:
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile.parentFile, 'fluttering/.android/include_flutter.groovy' )) -
Android Theme:
@android:style/Theme.NoTitleBar
Projucer can't create / manipulate a gradle.properties file, but this is needed for Flutter
-
The file with the following content was manually placed at
Builds/Androidfolder of this repository, it won't get overwritten by the Projucre, just do not delete it manually:android.useAndroidX=true android.enableJetifier=true
Made a custom App Delegate class, placed at Sources/iOS. This is an UIApplicationDelegate for running JUCE and Flutter in an iOS app. It's .mm File also has the START_JUCE_APPLICATION_WITH_CUSTOM_DELEGATE call to automatically start the JUCEApplication with this Delegate, as documented in JUCE/modules/juce_events/messages/juce_Initialisation.h. The delegate takes care of:
- Juce Initialisation and handling
- Flutter Engine Initalisation and handling
- UIWindow (showing the Flutter View)
- FlutterViewController
- FlutterPluginAppLifeCycleDelegate
CocoaPods is needed for FLutter integration.
- If do not have it installed yet (check with
which pod), do so withsudo gem install cocoapod - A Podfile was made manually and placed in this repository (
Builds/iOS/Podfile), containing the following. The Projucer won't overwite it, just do not delete it manually.platform :ios, '9.0' flutter_application_path = '../../fluttering' load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') target 'Jucy Fluttering - App' do install_all_flutter_pods(flutter_application_path) end
In Projucer, some iOS export settings had to be configured:
- To make it work with CocoaPods (as found in adamski's thread in the JUCE forum), for all build target configuations (Release/Debug), set Binary Location:
$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) - Custom PList
<plist> <dict> <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>remote-notification</string> </array> </dict> </plist>
No extra steps needed building. The project can be opened with Android Studio from the Projucer or by opening the Builds/Android project folder in Android Studio.
If dart support is enabled for the project in Android Studio, it even attaches the flutter debugger.
Else, while running / debugging, one may call flutter attach from terminal (first go to cd ./fluttering).
After saving / exporting with Projucer, the XCode Project can't simply be build as usual for JUCE projects. Two more steps are necassary:
- from terminal run
pod install(first go tocd ./Builds/iOS) - then open the generated
.xcworkspaceproject and build that- the .xcodeproj would fail, due to the missing Flutter dependencies
While running / debugging from xCode, one may call flutter attach from terminal (first go to cd ./fluttering).
Check out the Flutter/dart:ffi documentation for the basic setup of accessing nativ C++ code from Flutter/dart.
Some examples can be found in Sources/JucyFlutteringInterop.h and the dart counterparts in fluttering/lib/juce_fluttering_interop.dart.
- Externalized JUCE/C++ functions can be called from Fluttter/Dart via dart:ffi NativeFunctions, since the dart isolate (thread) will execute it.
- Some boilercode / wrapping is still needed. See
fluttering/lib/juce_fluttering_interop.dart.
- For getting
juce::Strings, JUCE has to return UTF-8 char* pointers - If the String is local it has to be copied to memory or it will get lost on return.
- This can be done using
toRawUTF8,mallocandstrcpy, see the example inEXTERN_C const char *getAppName()inSource/JucyFlutteringInterop.h, using the helper functionconst char *copyStringToUTF8(String juceString)from the same file - The dart caller then can convert this to a dart String, using
fromUtf8and callfreeon the returnedPointer<Utf8>, utilizing importpackage:ffi/ffi.dart. See tthe example influttering/lib/juce_fluttering_interop.dart
Problem:
- Access to classes and values is only allowed from the same isolate!
- JUCE Message Loop (e.g.
juce::Timer) or AudioCallback Threads (e.g.juce::AudioProcessor) won't run in the same isolate as Flutter/Dart UI. - Therefore, just registering callback dart:ffi function pointers in your JUCE/C++ code will not be sufficient. (Except if the callback was called from some C++ function that was started from dart).
Solution:
- Different isolates can communicate by sending values through ports (see ReceivePort, SendPort).
- To access the dart native API from C++, files from GitHub: dart-lang/sdk/runtime/include where included in the JUCE project Source/DartApiDL (the folder was added to the header search paths for all Build/Debug targets). see https://github.com/mraleph/go_dart_ffi_example
- Init by calling the NativeAPI from dart (that will be the receiver) and tell JUCE/C++ the port.
- From then on JUCE/C++ may send messages containing objects with int, double or UTF-8 char* string pointers to the dart receiver port.