diff --git a/docz/docs/03-demos/05-mobile/06-flutter.md b/docz/docs/03-demos/05-mobile/06-flutter.md new file mode 100644 index 0000000..7b7adcb --- /dev/null +++ b/docz/docs/03-demos/05-mobile/06-flutter.md @@ -0,0 +1,231 @@ +--- +title: Flutter +pagination_prev: demos/static/index +pagination_next: demos/desktop/index +sidebar_position: 5 +sidebar_custom_props: + summary: Dart + JS Interop +--- + +import current from '/version.js'; +import CodeBlock from '@theme/CodeBlock'; + +Dart + Flutter is a cross-platform alternative to [JS + React Native](/docs/demos/mobile/reactnative). + +For the iOS and Android targets, the `flutter_js` package wraps JavaScriptCore +and QuickJS engines respectively. + +The [Standalone scripts](/docs/getting-started/installation/standalone) can be +parsed and evaluated in the wrapped engines. + +The "Complete Example" creates an app that looks like the screenshots below: + + + +
iOS
+ +![iOS screenshot](pathname:///flutter/ios.png) + +
+ +:::warning Telemetry + +Before starting this demo, manually disable telemetry. On MacOS: + +```bash +dart --disable-telemetry +dart --disable-analytics +flutter config --no-analytics +flutter config --disable-telemetry +``` + +::: + +## Integration Details + +This demo assumes familiarity with Dart and Flutter. + +### Loading SheetJS + +#### Adding the scripts + +The `flutter.assets` property in `pubspec.yaml` specifies assets. Assuming the +standalone script and shim are placed in the `scripts` folder, the following +snippet loads the scripts as assets: + +```yaml title="pubspec.yaml" +flutter: + assets: + - scripts/xlsx.full.min.js + - scripts/shim.min.js +``` + +Once loaded, the contents can be loaded with `rootBundle.loadString`: + +```dart +import 'package:flutter/services.dart' show rootBundle; + +String shim = await rootBundle.loadString("scripts/shim.min.js"); +String sheetjs = await rootBundle.loadString("scripts/xlsx.full.min.js"); +``` + +#### Initialization + +It is strongly recommended to add the engine to the state of a `StatefulWidget`: + +```dart +import 'package:flutter_js/flutter_js.dart'; + +class SheetJSFlutterState extends State { + // highlight-next-line + late JavascriptRuntime _engine; + + @override void initState() { + // highlight-next-line + _engine = getJavascriptRuntime(); + } +} +``` + +#### Running SheetJS Scripts + +Since fetching assets is asychronous, it is recommended to create a wrapper +`async` function and sequentially await each script: + +```dart +class SheetJSFlutterState extends State { + String _version = '0.0.0'; + + @override void initState() { + _engine = getJavascriptRuntime(); + _initEngine(); // note: this is not `await`-ed + } + + Future _initEngine() async { + /* fetch and evaluate the shim */ + String shim = await rootBundle.loadString("scripts/shim.min.js"); + _engine.evaluate(shim); + // highlight-start + /* fetch and evaluate the main script */ + String sheetjs = await rootBundle.loadString("scripts/xlsx.full.min.js"); + _engine.evaluate(sheetjs); + // highlight-end + /* capture the version string */ + JsEvalResult vers = _engine.evaluate("XLSX.version"); + setState(() => _version = vers.stringResult); + } +} +``` + +### Reading data + +The most common binary data type in Dart is `Uint8List`. It is the data type +for `http.Response#bodyBytes` and the return type of `File#readAsBytes()`. + +The Flutter JS connector offers no simple interop for `Uint8List` data. The data +should be converted to Base64 before parsing. + +The `csv` package provides a special `CsvToListConverter` converter to generate +`List>` (Dart's spiritual equivalent of the array of arrays): + +```dart +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:csv/csv.dart'; + +class SheetJSFlutterState extends State { + List> _data = []; + + void _processBytes(Uint8List bytes) { + String base64 = base64Encode(bytes); + JsEvalResult func = _engine.evaluate(""" + var wb = XLSX.read('$base64'); + XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]); + """); + String csv = func.stringResult; + setState(() { _data = CsvToListConverter(eol: "\n").convert(csv); }); + } +``` + +## Demo + +:::note + +This demo was tested on an Intel Mac on 2023 May 31 with Flutter 3.10.2, +Dart 3.0.2, and `flutter_js` 0.7.0 + +The iOS simulator runs iOS 16.2 on an iPhone 14 Pro Max. + +::: + +### Base Project + +1) Disable telemetry. + +```bash +dart --disable-telemetry +dart --disable-analytics +flutter config --no-analytics +flutter config --disable-telemetry +``` + +2) Create a new Flutter project: + +```bash +flutter create sheetjs_flutter +cd sheetjs_flutter +``` + +3) Open the iOS simulator + +4) While the iOS simulator is open, start the application: + +```bash +flutter run +``` + +Once the app loads in the simulator, stop the terminal process. + +5) Install Flutter / Dart dependencies: + +```bash +flutter pub add http csv flutter_js +``` + +6) Open `pubspec.yaml` with a text editor. Search for the line that starts with +`flutter:` (no whitespace) and add the highlighted lines: + +```yaml title="pubspec.yaml" +# The following section is specific to Flutter packages. +flutter: +// highlight-start + assets: + - scripts/xlsx.full.min.js + - scripts/shim.min.js +// highlight-end +``` + +7) Download dependencies to the `scripts` folder: + +{`\ +mkdir -p scripts +cd scripts +curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js +curl -LO https://cdn.sheetjs.com/xlsx-${current}/package/dist/shim.min.js +cd ..`} + + +8) Download [`main.dart`](pathname:///flutter/main.dart) to `lib/main.dart`: + +```bash +curl -L -o lib/main.dart https://docs.sheetjs.com/flutter/main.dart +``` + +9) Launch the app: + +```bash +flutter run +``` + +The app fetches , parses, converts data to an +array of arrays, and presents the data in a Flutter `Table` widget. diff --git a/docz/docusaurus.config.js b/docz/docusaurus.config.js index b3e9df2..d651c23 100644 --- a/docz/docusaurus.config.js +++ b/docz/docusaurus.config.js @@ -145,7 +145,7 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, - additionalLanguages: [ "visual-basic", "swift", "java", "csharp", "perl", "ruby", "cpp", "applescript", "liquid", "rust" ], + additionalLanguages: [ "visual-basic", "swift", "java", "csharp", "perl", "ruby", "cpp", "applescript", "liquid", "rust", "dart" ], }, liveCodeBlock: { playgroundPosition: 'top' diff --git a/docz/static/flutter/ios.png b/docz/static/flutter/ios.png new file mode 100644 index 0000000..c5ba8b7 Binary files /dev/null and b/docz/static/flutter/ios.png differ diff --git a/docz/static/flutter/main.dart b/docz/static/flutter/main.dart new file mode 100644 index 0000000..5a961bd --- /dev/null +++ b/docz/static/flutter/main.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter_js/flutter_js.dart'; +import 'package:csv/csv.dart'; +import 'package:collection/collection.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'SheetJS x Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), + useMaterial3: true, + ), + home: const SheetJSFlutter(), + ); + } +} + +class SheetJSFlutter extends StatefulWidget { + const SheetJSFlutter({super.key}); + + @override + State createState() => SheetJSFlutterState(); +} + +class SheetJSFlutterState extends State { + String _version = '0.0.0'; + List> _data = []; + late JavascriptRuntime _engine; + + @override + void initState() { + super.initState(); + _async(); + } + + void _async() async { + await _initEngine(); + await _fetch(); + } + + Future _initEngine() async { + /* load scripts */ + _engine = getJavascriptRuntime(); + String shim = await rootBundle.loadString("scripts/shim.min.js"); + _engine.evaluate(shim); + String sheetjs = await rootBundle.loadString("scripts/xlsx.full.min.js"); + _engine.evaluate(sheetjs); + JsEvalResult vers = _engine.evaluate("XLSX.version"); + setState(() => _version = vers.stringResult); + } + + Future _fetch() async { + final res = await http.get(Uri.parse("https://sheetjs.com/pres.numbers")); + if (res.statusCode == 200) + _processBytes(res.bodyBytes); + else + throw Exception("Failed to fetch file"); + } + + void _processBytes(Uint8List bytes) { + String base64 = base64Encode(bytes); + JsEvalResult func = _engine.evaluate(""" + var wb = XLSX.read('$base64'); + XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]]); + """); + setState(() { + _data = CsvToListConverter(eol: "\n").convert(func.stringResult); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text('SheetJS x Flutter $_version'), + ), + body: Table( + children: _data.mapIndexed((R, row) => TableRow( + decoration: BoxDecoration(color: R == 0 ? Colors.blue[50] : null), + children: row.mapIndexed((C, cell) => Text(cell.toString())).toList() + )).toList() + ), + ); + } +} +