Extensions

Extension Catalog

Extension Catalog

Extensions are optional Flutter packages compiled into a Ruflet client. They add controls, widgets, or device integrations that are not part of the core client. This is separate from a service: a service is the Ruby API used at runtime, while an extension is client build configuration.

Enable extensions in ruflet.yaml, then rebuild the client:

extensions:
  - charts
  - code_editor
  - qrcode_scanner

Use an extension Ruflet does not bundle

Ruflet can build a third-party or private extension even when it is not in the built-in catalog. Use the Dart package name as the extension key and tell Ruflet where the package lives:

extensions:
  - charts
  - acme_widgets:
      git:
        url: [email protected]:acme/acme_widgets.git
        ref: 4f6d93f0b5a8d92d9b7a1af83dd752984e951eb2

During the build Ruflet:

  1. adds acme_widgets to the generated Flutter client's dependencies;
  2. imports package:acme_widgets/acme_widgets.dart; and
  3. registers acme_widgets.Extension() with Flet.

The package name must be a valid Dart identifier and match all three places: the extension key, name: in the package's pubspec.yaml, and its public lib/acme_widgets.dart library. That library must expose an Extension class implementing Flet's extension contract.

ref: is preferred for reproducible builds and accepts a commit, branch, or tag understood by Git. Ruflet also accepts branch: or tag: as aliases.

Private Git authentication

Ruflet does not copy or store repository credentials. Flutter's package tool invokes Git, so the machine running ruflet build must already be able to clone the private repository.

  • For SSH URLs, load a read-only deploy key or developer key into the SSH

agent and configure the Git host in known_hosts.

  • For HTTPS URLs, use the operating system's Git credential helper or a CI

secret supplied to Git at build time.

  • Do not put a personal access token in ruflet.yaml, a Rails initializer, or

the repository URL. Those values can appear in source control and build logs.

  • Give build credentials read-only access and pin production builds to a

commit SHA or immutable tag.

Verify authentication before building:

git ls-remote [email protected]:acme/acme_widgets.git
ruflet build ios --self

Rails configuration

Rails applications declare the same source in config/initializers/ruflet.rb. The Rails build task serializes it to the extension configuration consumed by the Ruflet CLI:

Ruflet::Rails.configure do |config|
  config.extensions = [
    "charts",
    {
      "acme_widgets" => {
        "git" => {
          "url" => "[email protected]:acme/acme_widgets.git",
          "ref" => "4f6d93f0b5a8d92d9b7a1af83dd752984e951eb2"
        }
      }
    }
  ]
end

Build normally after the build machine can access the repository:

rake ruflet:build[ios]

In an ERB-to-native view, use the extension's wire type as a normal tag. No Ruflet catalog entry is required:

<acme-rating value="4" maximum="5"></acme-rating>

The equivalent generic helper is useful when values come from Ruby:

<%= widget "acme-rating", value: @review.rating, maximum: 5 %>

If the private extension also ships a Ruby gem that registers a typed helper, require that gem and use its helper normally. ERB-to-native rendering does not require a typed Ruby helper for a plain wire tag.

Monorepos and local development

Use path: inside git: when the Dart package is in a repository subdirectory:

extensions:
  - acme_widgets:
      git:
        url: [email protected]:acme/mobile_extensions.git
        ref: v2.1.0
        path: packages/acme_widgets

Use a direct path while developing against a local checkout:

extensions:
  - acme_widgets:
      path: /Users/developer/code/acme_widgets

Absolute local paths are safest because the generated Flutter client lives in the build directory. Do not use a developer-specific local path in CI or a release configuration.

Available extensions

| Key | Ruby API provided | Kind | |---|---|---| | audio | audio(...) | Media control | | audio_recorder | page.audio_recorder(...) | Device service; documented under Services | | camera | page.camera(...) | Device service; documented under Services | | charts | Bar, line, pie, scatter, candlestick, and radar chart helpers | Control family | | code_editor | code_editor(...) | Editing control | | color_pickers | control("ColorPicker", ...) and related wire controls | Control family | | datatable2 | control("DataTable2", ...) | Data control | | flashlight | page.flashlight | Device service; documented under Services | | geolocator | page.geolocator(...) | Device service; documented under Services | | lottie | lottie(...) | Animation control | | map | map(...) and map-layer helpers | Control family | | permission_handler | page.permission_handler(...) | Device service; documented under Services | | qrcode_scanner | qrcode_scanner(...) | Scanner control | | rive | rive(...) | Animation control | | secure_storage | page.secure_storage | Storage service; documented under Services | | video | video(...) | Media control | | webview | web_view(...) | Web content control |

Widgets remain documented under Widgets, even when an extension supplies their Flutter implementation. Runtime service APIs remain under Services.

Service-backed extensions do not have duplicate extension reference pages. Their catalog links open the authoritative service page, which includes both the complete Ruby API and the required client build setup.

Permissions

Protected native access belongs in services.yaml:

services:
  - camera:
      description: Scan product QR codes.
  - microphone:
      description: Record voice notes.

Ruflet uses service declarations to configure platform permissions and include required packages. qrcode_scanner also adds its camera declarations when it is selected directly. Request access only when the user starts the related action.

First-party QR scanner

The first Ruflet-owned extension is ruflet_qrcode_scanner. It follows Flet's extension contract and uses the normal Ruby DSL:

scanner = qrcode_scanner(
  formats: [:qr_code],
  on_detect: ->(event) {
    page.update(result, value: event.value.to_s)
  }
)

To build an extension package, see Extension Authoring.