Skip to content

Commit

Permalink
added javascript integration through dart ffi
Browse files Browse the repository at this point in the history
  • Loading branch information
abner committed Aug 31, 2020
1 parent 7377d8c commit 9cb20c6
Show file tree
Hide file tree
Showing 40 changed files with 5,440 additions and 264 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.1.0+0

- Changed to use Dart FFI to call the Javascript Runtimes: QuickJS by Default in Android and JavascriptCore in iOS

# 0.0.3+1

- Updated to use a new version of oasis-jsbridge-android which brings *quickjs* (js engine for Android)
Expand Down
79 changes: 46 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,36 @@
A Javascript engine to use with flutter. It uses quickjs on Android and JavascriptCore on IOS


In this very early stage version we only get the result of evaluated expressions as String.
Now the evaluation is executed through dart ffi, which makes the javascript a native citzen inside Flutter Mobile Apps.

But it is good enough to take advantage of great javascript libraries such as ajv (json schema validation), moment (DateTime parser and operations) in Flutter applications running on mobile devices, both Android and iOS.

On IOS this library relies on the native JavascriptCore provided by iOS SDK. In Android it uses the amazing and small Javascript Engine QuickJS [https://bellard.org/quickjs/](https://bellard.org/quickjs/) (A spetacular work of the Fabrice Bellard and Charlie Gordon).
It was ported to be used in Android through jni in this project i recently found on Github: [https://github.com/seven332/quickjs-android](https://github.com/seven332/quickjs-android).
We used seven332/quickjs-android in the very first versions of flutter_js. Thanks to [seven332](https://github.com/seven332)
In the previous versions we only get the result of evaluated expressions as String.

Recently we found the [oasis-jsbridge-android](https://github.com/p7s1digital/oasis-jsbridge-android) repository which brings quickjs integration to Android to a new level (Close to what JavascriptCore offers in iOS). So,
since version 0.0.2+1 we are using oasis-jsbridge-android quickjs library as our javascript engine under the hood. So thanks to the guys of [p7s1digital](https://github.com/p7s1digital/) team to theirs
amazing work.
BUT NOW we can do more with flutter_js like run xhr and fetch http calls through Dart http library. We are supporting Promises as well.

We can take advantage of great javascript libraries such as ajv (json schema validation), moment (DateTime parser and operations) in Flutter applications running on mobile devices, both Android and iOS.

~~On IOS this library relies on the native JavascriptCore provided by iOS SDK. In Android it uses the amazing and small Javascript Engine QuickJS [https://bellard.org/quickjs/](https://bellard.org/quickjs/) (A spetacular work of the Fabrice Bellard and Charlie Gordon).~~
~~It was ported to be used in Android through jni in this project i recently found on Github: [https://github.com/seven332/quickjs-android](https://github.com/seven332/quickjs-android).~~
~~We used seven332/quickjs-android in the very first versions of flutter_js. Thanks to [seven332](https://github.com/seven332)~~

~~Recently we found the [oasis-jsbridge-android](https://github.com/p7s1digital/oasis-jsbridge-android) repository which brings quickjs integration to Android to a new level (Close to what JavascriptCore offers in iOS). So,
since version 0.0.2+1 we are using oasis-jsbridge-android quickjs library as our javascript engine under the hood. So thanks to the guys of [p7s1digital](https://github.com/p7s1digital/) team to theirs amazing work.~~


On Android it uses the amazing and small Javascript Engine QuickJS [https://bellard.org/quickjs/](https://bellard.org/quickjs/). On IOS, it uses the Javascript Core. In both platforms we rely on dart ffi to make calls to the js runtime engines, which make javascript code evaluation an first class citzen inside Flutter Mobile Apps. We could use it
to execute validations logic of TextFormField, also we can execute rule engines or redux logic shared from our web applications. The opportunities are huge.


The project is open source under MIT license.

The bindings for use to communicate with JavascriptCore through dart:ffi we took it from the package [flutter_jscore](https://pub.dev/packages/flutter_jscore).

Flutter JS provided the implementation to the QuickJS dart ffi bindings ourselves and also constructed a wrapper API to Dart which
provides a unified API to evaluate javascript and communicate between Dart and Javascript.

This library also allows to call xhr and fetch on Javascript through Dart Http calls. We also provide the implementation
which allows to evaluate promises returns.


![](doc/flutter_js.png)
Expand All @@ -23,6 +42,11 @@ amazing work.

## Instalation

```yaml
dependencies:
flutter_js: 0.1.0+0
```
### iOS
Since flutter_js uses the native JavascriptCore, no action is needed.
Expand Down Expand Up @@ -58,29 +82,12 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
String _jsResult = '';
int _idJsEngine = -1;
JavascriptRuntime flutterJs;
@override
void initState() {
super.initState();
initJsEngine();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initJsEngine() async {
try {
_idJsEngine = await FlutterJs.initEngine();
} on PlatformException catch (e) {
print('Failed to init js engine: ${e.details}');
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
flutterJs = getJavascriptRuntime();
}
@override
Expand Down Expand Up @@ -109,10 +116,10 @@ class _MyAppState extends State<MyApp> {
child: Image.asset('assets/js.ico'),
onPressed: () async {
try {
String result = await FlutterJs.evaluate(
"Math.trunc(Math.random() * 100).toString();", _idJsEngine);
JsEvalResult jsResult = flutterJs.evaluate(
"Math.trunc(Math.random() * 100).toString();");
setState(() {
_jsResult = result;
_jsResult = jsResult.stringResult;
});
} on PlatformException catch (e) {
print('ERRO: ${e.details}');
Expand All @@ -127,7 +134,7 @@ class _MyAppState extends State<MyApp> {
```


## Alternatives
## Alternatives (and also why we think our library is better)

There were another packages which provides alternatives to evaluate javascript in flutter projects:

Expand All @@ -146,6 +153,12 @@ Allows to evaluate javascript in a hidden webview. Does not add weight to size o

Based on jerryscript which is slower than quickjs. The jsengine package does not have implementation to iOS.

### https://pub.dev/packages/flutter_jscore

Uses Javascript Core in Android and IOS. We got the JavascriptCore bindings from this amazing package. But, by
default we provides QuickJS as the javascript runtime on Android because it provides a smaller footprint. Also
our library adds support to ConsoleLog, SetTimeout, Xhr, Fetch and Promises.




Expand All @@ -171,7 +184,7 @@ Bellow you can see the apk sizes of the `example app` generated with *flutter_js
## Ajv

We just added an example of use of the amazing js library [Ajv](https://ajv.js.org/) which allow to bring state of the art json schema validation features
to the Flutter world.
to the Flutter world. We can see the Ajv examples here: https://github.com/abner/flutter_js/blob/master/example/lib/ajv_example.dart


See bellow the screens we added to the example app:
Expand Down
10 changes: 8 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion 28
compileSdkVersion 29

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
main.jniLibs.srcDirs = ['jniLibs']
}
defaultConfig {
minSdkVersion 21
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand All @@ -43,5 +47,7 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//implementation 'com.github.seven332:quickjs-android:89ba8b6b8f'
implementation 'com.github.abner.oasis-jsbridge-android:oasis-jsbridge-quickjs:0.9.68'
//implementation 'com.github.abner.oasis-jsbridge-android:oasis-jsbridge-quickjs:0.9.68'
implementation "com.github.fast-development.android-js-runtimes:fastdev-jsruntimes-quickjs:0.1.0"
//implementation "com.github.fast-development.android-js-runtimes:fastdev-jsruntimes-jsc:0.1.0"
}
56 changes: 56 additions & 0 deletions assets/js/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function fetch(url, options) {
options = options || {};
return new Promise( (resolve, reject) => {
const request = new XMLHttpRequest();
const keys = [];
const all = [];
const headers = {};

const response = () => ({
ok: (request.status/100|0) == 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
text: () => Promise.resolve(request.responseText),
json: () => {
// TODO: review this handle because it may discard \n from json attributes
try {
console.log('RESPONSE TEXT IN FETCH: ' + request.responseText);
return Promise.resolve(JSON.parse(request.responseText));
} catch (e) {
console.log('ERROR on fetch parsing JSON: ' + e.message);
return Promise.resolve(request.responseText);
}
},
blob: () => Promise.resolve(new Blob([request.response])),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => headers[n.toLowerCase()],
has: n => n.toLowerCase() in headers
}
});

request.open(options.method || 'get', url, true);

request.onload = () => {
request.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
keys.push(key = key.toLowerCase());
all.push([key, value]);
headers[key] = headers[key] ? `${headers[key]},${value}` : value;
});
resolve(response());
};

request.onerror = reject;

request.withCredentials = options.credentials=='include';

for (const i in options.headers) {
request.setRequestHeader(i, options.headers[i]);
}

request.send(options.body || null);
});
}
Binary file added doc/ios_capture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 15 additions & 64 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,81 +10,32 @@ project 'Runner', {
'Release' => :release,
}

def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
generated_key_values
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!
use_modular_headers!

# Flutter Pod

copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.

generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];

unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
end

# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'

# Plugin Pods

# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
install! 'cocoapods', :disable_input_output_paths => true

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
flutter_additional_ios_build_settings(target)
end
end
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- Flutter (1.0.0)
- flutter_js (0.0.1):
- flutter_js (0.1.0):
- Flutter

DEPENDENCIES:
Expand All @@ -15,8 +15,8 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
flutter_js: eb1d2d30ead7b5e597f7e7bfed642589ca15efe2
flutter_js: 95929d4e146e8ceb1c8e1889d8c2065c5d840076

PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c

COCOAPODS: 1.7.5
COCOAPODS: 1.9.1
Loading

0 comments on commit 9cb20c6

Please sign in to comment.