docs.sheetjs.com/docz/docs/03-demos/17-mobile/06-flutter.md

16 KiB

title sidebar_label description pagination_prev pagination_next sidebar_position sidebar_custom_props
Let Data Fly with Flutter Dart + Flutter Build data-intensive mobile apps with Dart + Flutter. Seamlessly integrate spreadsheets into your app using SheetJS. Securely process and generate Excel files on the go. demos/static/index demos/desktop/index 6
summary
Dart + JS Interop

import current from '/version.js'; import CodeBlock from '@theme/CodeBlock';

Dart1 + Flutter2 is a popular cross-platform app framework. JavaScript code can be run through embedded engines.

SheetJS is a JavaScript library for reading and writing data from spreadsheets.

This demo uses Dart + Flutter and SheetJS to process spreadsheets. We'll explore how to use the flutter_js package to run JavaScript code and how to pass data between Dart code and the platform-specific JS engines.

The "Demo" creates an app that looks like the screenshots below:

iOS Android

iOS screenshot

Android screenshot

:::note Tested Deployments

This demo was tested in the following environments:

Simulators

OS Device Dart Flutter Dev Platform Date
Android 34 Pixel 3a 3.2.2 3.16.2 darwin-x64 2023-12-04
iOS 17.0.1 iPhone 15 Pro Max 3.2.2 3.16.2 darwin-x64 2023-12-04

Real Devices

OS Device Dart Flutter Date
Android 29 NVIDIA Shield 3.2.2 3.16.2 2023-12-04
iOS 15.1 iPad Pro 3.2.2 3.16.2 2023-12-04

:::

:::danger Telemetry

Before starting this demo, manually disable telemetry. On MacOS:

dart --disable-telemetry
dart --disable-analytics
flutter config --no-analytics
flutter config --disable-telemetry

:::

Integration Details

:::note pass

This demo assumes familiarity with Dart and Flutter.

:::

For the iOS and Android targets, the flutter_js package3 wraps JavaScriptCore4 and QuickJS5 engines respectively.

The SheetJS Standalone scripts can be parsed and evaluated in the wrapped engines.

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:

flutter:
  assets:
    - scripts/xlsx.full.min.js
    - scripts/shim.min.js

Once loaded, the contents can be loaded with rootBundle.loadString:

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:

import 'package:flutter_js/flutter_js.dart';

class SheetJSFlutterState extends State<SheetJSFlutter> {
  // highlight-next-line
  late JavascriptRuntime _engine;

  @override void initState() {
    // highlight-next-line
    _engine = getJavascriptRuntime();
  }
}

Running SheetJS Scripts

Since fetching assets is asynchronous, it is recommended to create a wrapper async function and sequentially await each script:

class SheetJSFlutterState extends State<SheetJSFlutter> {
  String _version = '0.0.0';
  late JavascriptRuntime _engine;

  @override void initState() {
    _engine = getJavascriptRuntime();
    _initEngine(); // note: this is not `await`-ed
  }

  Future<void> _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 following diagram depicts the workbook waltz:

flowchart LR
  file[(file data\nUint8List)]
  subgraph SheetJS operations
    base64(Base64\nstring)
    wb((SheetJS\nWorkbook))
    csv(CSV\nstring)
  end
  lld(List of\nLists)
  tbl{{Data\nTable}}
  file --> |`base64Encode`\nDart| base64
  base64 --> |`XLSX.read`\nParse Bytes| wb
  wb --> |`sheet_to_csv`\nExtract Data| csv
  csv --> |`CsvToListConverter`\nDart| lld
  lld --> |`Table`\nFlutter| tbl

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 using base64Encode before parsing.

Once passed into the JS engine, the SheetJS read function6 can read the Base64-encoded string and the sheet_to_csv utility function7 can generate a CSV string from a worksheet. This string can be pulled back into Dart code.

The csv package provides a special CsvToListConverter converter to generate List<List<dynamic>> (Dart's spiritual equivalent of the array of arrays).

The following snippet generates List<List<dynamic>> from a Dart Uint8List:

import 'dart:convert';
import 'package:csv/csv.dart';

class SheetJSFlutterState extends State<SheetJSFlutter> {
  List<List<dynamic>> _data = [];
  late JavascriptRuntime _engine;

  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

  1. Follow the official "Install" instructions for Flutter8.

Run flutter doctor and confirm the following items are checked:

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.0.1)
[✓] Android Studio (version 2022.3)

(the actual version numbers may differ)

Installation Notes (click to hide)

:::note pass

On first run, there may be a warning with "Android toolchain":

[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor
      --android-licenses

As stated, the fix is to run the command:

flutter doctor --android-licenses

:::

:::note pass

On first run, there may be a warning with "Xcode":

[!] Xcode - develop for iOS and macOS (Xcode 15.0.1)
    ✗ Unable to get list of installed Simulator runtimes.

Open "Settings" panel in Xcode. Under "Platforms", click "Get" next to "iOS".

:::

:::caution pass

In local testing, there were issues with the Android toolchain:

error: Android sdkmanager not found. Update to the latest Android SDK and ensure that the cmdline-tools are installed to resolve this.

This was fixed by switching to Java 20, installing Android SDK 33, and rolling back to Android SDK Command-Line Tools (revision: 10.0)

:::

:::note pass

If Google Chrome is not installed, flutter doctor will show an issue:

[✗] Chrome - develop for the web (Cannot find Chrome executable at
    /Applications/Google Chrome.app/Contents/MacOS/Google Chrome)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

If Chromium is installed, the environment variable should be manually assigned:

export CHROME_EXECUTABLE=/Applications/Chromium.app/Contents/MacOS/Chromium

:::

Run flutter emulators and check for both ios and android emulators:

apple_ios_simulator • iOS Simulator  • Apple  • ios
Pixel_3a_API_34     • Pixel 3a API 34 • Google • android
  1. Disable telemetry.
dart --disable-telemetry
dart --disable-analytics
flutter config --no-analytics
flutter config --disable-telemetry

Base Project

  1. Create a new Flutter project:
flutter create sheetjs_flutter
cd sheetjs_flutter
  1. Start the Android emulator.
Details (click to hide)

Android Studio

In Android Studio, click "More actions" > "Virtual Device Manager". Look for the emulated device in the list and click the ▶ button to play.

Command Line

List the available emulators with flutter emulators:

% flutter emulators
2 available emulators:

apple_ios_simulator • iOS Simulator  • Apple  • ios
Pixel_3a_API_34     • Pixel 3a API 34 • Google • android
^^^^^^^^^^^^^^^--- the first column is the name

The first column shows the name that should be passed to emulator -avd. In a previous test, the name was Pixel_3a_API_34 and the launch command was:

emulator -avd Pixel_3a_API_34

:::note pass

On macOS, ~/Library/Android/sdk/emulator/ is the typical location for the emulator binary. If it cannot be found, add the folder to PATH:

export PATH="$PATH":~/Library/Android/sdk/emulator
emulator -avd Pixel_3a_API_34

:::

  1. While the Android emulator is open, start the application:
flutter run
If emulator is not detected (click to show)

In some test runs, flutter run did not automatically detect the emulator.

Run flutter -v -d sheetjs run and the command will fail. Inspect the output:

// highlight-next-line
[   +6 ms] No supported devices found with name or id matching 'sheetjs'.
[        ] The following devices were found:
...
// highlight-next-line
[  +26 ms] sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 13 (API 33) (emulator)
[        ] macOS (desktop)             • macos         • darwin-arm64   • macOS 13.5.1 22G90 darwin-arm64
...

Search the output for sheetjs. After that line, search for the emulator list. One of the lines will correspond to the running emulator:

[  +26 ms] sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 13 (API 33) (emulator)
                                         ^^^^^^^^^^^^^--- the second column is the name

The second column is the device name. Assuming the name is emulator-5554, run:

flutter -v -d emulator-5554 run

Once the app loads, stop the terminal process and close the simulator.

  1. Install Flutter / Dart dependencies:
flutter pub add http csv flutter_js
  1. Open pubspec.yaml with a text editor. Search for the line that starts with flutter: (no whitespace) and add the highlighted lines:
# The following section is specific to Flutter packages.
flutter:
// highlight-start
  assets:
    - scripts/xlsx.full.min.js
    - scripts/shim.min.js
// highlight-end
  1. 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 ..}

  1. Download main.dart to lib/main.dart:
curl -L -o lib/main.dart https://docs.sheetjs.com/flutter/main.dart

Android

  1. Start the Android emulator using the same instructions as Step 3.

  2. Launch the app:

flutter run

The app fetches https://docs.sheetjs.com/pres.numbers, parses, converts data to an array of arrays, and presents the data in a Flutter Table widget.

:::caution pass

When the demo was last run, there was a build error:

│ The plugin flutter_js requires a higher Android SDK version.                      │
│ Fix this issue by adding the following to the file /.../android/app/build.gradle: │
│ android {                                                                         │
│   defaultConfig {                                                                 │
│     minSdkVersion 21                                                              │
│   }                                                                               │
│ }                                                                                 │

This was fixed by editing android/app/build.gradle.

Searching for minSdkVersion should reveal the following line:

        minSdkVersion flutter.minSdkVersion

flutter.minSdkVersion should be replaced with 21:

        minSdkVersion 21

:::

  1. Close the Android emulator.

iOS

  1. Start the iOS simulator.

  2. Launch the app:

flutter run

The app fetches https://docs.sheetjs.com/pres.numbers, parses, converts data to an array of arrays, and presents the data in a Flutter Table widget.

Android Device

  1. Connect an Android device using a USB cable.

If the device asks to allow USB debugging, tap "Allow".

  1. Verify that flutter can find the device:
flutter devices

The list should include the device:

  SheetJS (mobile)           • 1234567890                           •  android-arm64  • Android 10 (API 29)
  ^^^^^^^--- the first column is the name
  1. Build an APK:
flutter build apk --release
  1. Install on the Android device:
flutter install

The script will ask for a device:

[1]: SheetJS (1234567890)
[2]: iPhone 15 Pro Max (12345678-9ABC-DEF0-1234-567890ABCDEF)
[3]: macOS (macos)
[4]: Chrome (chrome)
Please choose one (or "q" to quit):

Select the number corresponding to the device.

  1. Launch the installed sheetjs_flutter app on the device

:::caution pass

The app may take 30 seconds to load the content.

There are known bugs in the Dart HTTP client in Android 129.

:::

iOS Device

  1. Follow the official "Deploy to physical iOS devices" instructions10

  2. Connect the iOS device and verify that flutter can find the device:

flutter devices

The list should include the device:

  SheetPad (mobile)          • 00000000-0000000000000000            •  ios            • iOS 15.1 19B74
  ^^^^^^^^--- the first column is the name
  1. Run the program on the device:
flutter run -d SheetPad

  1. https://dart.dev/ is the official site for the Dart Programming Language. ↩︎

  2. https://flutter.dev/ is the official site for the Flutter Framework. ↩︎

  3. The flutter_js package is hosted on the Dart package repository. ↩︎

  4. See the dedicated "Swift + JavaScriptCore" demo for more details. ↩︎

  5. See the dedicated "C + QuickJS" demo for more details. ↩︎

  6. See read in "Reading Files" ↩︎

  7. See sheet_to_csv in "CSV and Text" ↩︎

  8. See the Flutter Installation Instructions ↩︎

  9. For example, Issue 836 in the http repository mentions that API calls may take 10+ seconds. This is an issue in Dart + Flutter. ↩︎

  10. See "Deploy to physical iOS devices" in the Flutter documentation ↩︎