Ensemble

repository·main·Indexed 41 days ago

https://github.com/ensembleui/ensemble

A platform for building native iOS, Android, and web apps using a declarative configuration language. It leverages Flutter for mobile and React for web to enable instant app updates. The platform includes various modules such as ensemble_adobe_analytics for tracking and identity management, ensemble_auth for OAuth and sign-in widgets, ensemble_bracket for tournament brackets, and ensemble_camera for photo and video capture.

Tokens
65.5K
Snippets
202
Records
312
Agent score
97%

What's inside Ensemble

  1. Overview of ensemble_auth

    main

    The ensemble_auth module provides concrete implementations for authentication services and sign-in widgets within Ensemble applications. It integrates with the core ensemble package by registering implementations for auth context, tokens, OAuth, and various sign-in methods via GetIt.

    Key capabilities include:

    • Sign-in Methods: Google, Apple, Microsoft, Anonymous, Custom-token, and Verification-code sign-in.
    • Widgets: Pre-built UI components like SignInWithGoogle, SignInWithApple, ConnectWithGoogle, and ConnectWithMicrosoft.
    • Management: Token management and OAuth flow handling.
  2. Available Icon Libraries

    main

    The package provides access to three main icon sets:

    • Material Design Icons: Google's icon set. Accessed via MaterialIcons.iconMap['icon_name'].
    • Remix Icons: An open-source library with 2,000+ icons (v4.2.0). Accessed via Remix.iconMap['icon_name'].
    • Font Awesome Icons: A professional toolkit with 1,600+ free icons (v6.5.1). Accessed via FontAwesome.iconMap['icon_name'].
  3. How the ensemble_ts_interpreter execution pipeline works

    main

    The ensemble_ts_interpreter executes JavaScript code within Flutter applications by following a three-stage pipeline:

    1. Parsing: JavaScript source code (as a String) is passed to the parsejs_null_safety parser.
    2. AST Generation: The parser produces an Abstract Syntax Tree (AST).
    3. Interpretation: The JSInterpreter (defined in newjs_interpreter.dart) performs a recursive traversal of the AST to execute logic, calculations, and UI state bindings, resulting in Dart objects.

    This model allows you to run JavaScript logic directly inside your Dart/Flutter environment without a separate JS engine.

  4. What are Reusable Actions in Ensemble

    main

    Reusable Actions are named, parameterized workflows that can be invoked from any screen using the executeAction command. They act as self-contained units of logic that can include their own imports, global JavaScript functions, and API definitions.

    There are two types of Actions:

    1. App-level Actions: Defined in separate YAML files under an actions/ directory (for local apps) or within the Studio resources artifact Actions map. These are globally available.
    2. Page-level Actions: Defined inline within a screen's YAML using an Action: block. Page-level actions override app-level actions if they share the same name.

    Each execution of an action creates a child scope, ensuring that its inputs, imports, APIs, and Global code do not leak into the caller's scope unless explicitly communicated via events.

  5. How variable scoping and resolution works

    main

    The interpreter uses two distinct scope models to resolve variable names:

    1. Dynamic Scope (Context)

    Used for variables declared with var and function declarations. These are hoisted to the enclosing function or global context and are stored in simple key-value maps. Resolution happens dynamically during runtime.

    2. Lexical Scope (_LexicalContext)

    Used for block-level variables (let and const). These are managed on a stack of _LexicalContext structures:

    • Scope Entry: A new _LexicalContext is pushed onto the stack every time a block is entered (visitBlock).
    • Temporal Dead Zone (TDZ): Lexical variables are hoisted as uninitialized bindings. Attempting to access them before initialization throws a JSException.
    • Const Protection: const bindings are protected from reassignment; attempting to reassign them throws a runtime exception.
    • Per-Iteration Scopes: Loops push a new _LexicalContext for every iteration, ensuring closures created inside loops capture the specific state of that iteration.
  6. Configure file source and extension behavior

    main

    The behavior of the file picker changes based on the source and allowedExtensions provided:

    • source: files (or omitted): Uses FileType.custom if allowedExtensions is set; otherwise, it allows any file type.
    • source: gallery:
      • Maps image extensions (jpg, jpeg, png, gif, bmp) to the image picker.
      • Maps video extensions (mp4, avi, mkv, flv, wmv) to the video picker.
      • If no recognized extension is present, it picks mixed media.
    • allowCompression: Defaults to true.
    • allowMultiple: Defaults to false.
    • Cancellation: Cancelling the picker returns no files and triggers the onError callback if provided.
  7. Use the Interval class

    main

    The Interval class represents a time span between two DateTime objects. It provides methods for checking containment, intersection, and union of time ranges.

    import 'package:ensemble_date/ensemble_date.dart';
    
    final start = DateTime(2023, 1, 1);
    final end = DateTime(2023, 1, 31);
    final interval = Interval(start, end);
    
    final dateToCheck = DateTime(2023, 1, 15);
    final isIncluded = interval.includes(dateToCheck);
    
    final otherInterval = Interval(DateTime(2023, 1, 15), DateTime(2023, 2, 1));
    final intersection = interval.intersection(otherInterval);
  8. Handle route exit reasons and callbacks

    main

    Ensemble uses EnsembleRouteExitReason to distinguish why a route was removed. This ensures that programmatic navigation is not confused with user-initiated back actions.

    Exit Reasons:

    • back: A real user-initiated pop.
    • replaced: The route was replaced (e.g., via Navigator.pushReplacement).
    • historyReconciled: The route was removed during declarative reconciliation.
    • rootCleared: The route was removed during a clear-all operation (e.g., Navigator.pushAndRemoveUntil).

    Callback Behavior:

    • The onNavigateBack callback is attached to the destination route's popped future.
    • Crucially, onNavigateBack only executes when the recorded reason is back. It will not be invoked during reconciliation, replacement, or clear-all operations, even though those operations dispose of the route widgets normally.
  9. Handle Asynchronous JavaScript (Promises and await)

    main

    The interpreter supports asynchronous execution using Dart's native Future system, ensuring that await calls do not block the Flutter UI thread.

    Execution Logic

    • Synchronous Path: If the interpreter detects no await keywords in a statement (via _containsAwait), it executes the code synchronously with zero overhead.
    • Asynchronous Path: If await is present, the interpreter uses _executeAsyncStatement to yield control back to the Dart event loop.

    Promise Support

    • JSPromise: JavaScript Promise objects are modeled by JSPromise, which wraps a Dart Completer.
    • Polyfills: Standard Promise methods like all, allSettled, race, any, and finally are mapped to Dart Future helpers (e.g., Future.wait).
    • Interoperability: Use toFuture() to convert a JSPromise instance into a standard Dart Future when working at the Dart-to-JS integration boundary.
  10. Manage asset expiry and cleanup

    main

    The cache automatically deletes assets that have not been used for 30 days. The expiry calculation uses the following logic:

    ageTimestamp = lastUsedAt ?? downloadedAt

    Implications:

    • Used assets: Expire 30 days after their last real use (lastUsedAt).
    • Prefetched but unused assets: Expire 30 days after they were downloaded (downloadedAt).
    • Cleanup process: Occurs in the background after the cache state is loaded or a new manifest is saved. It removes files from the local asset_cache/assets/ directory and removes metadata from asset-cache.json without blocking the main thread.
  11. Understand how screen tracking works with lazy routes

    main

    Screen tracking in Ensemble represents actual visibility, not just structural membership in the Navigator.

    Lazy historical routes (routes reconstructed during reconciliation) behave differently than standard routes:

    • Their structural didPush is ignored by the screen tracker.
    • Reconciliation removal silently purges them from tracking history.
    • Popping onto a lazy route removes the outgoing tracked route without restoring a stale screen.
    • A Screen widget only publishes itself to the tracker when it is actually materialized and revealed to the user.

    Important: Do not attempt to make screen tracking respond to every lazy route push, as this will corrupt analytics by reporting screens the user has never actually seen.

  12. Understand the Asset Cache and Prefetcher architecture

    main

    Ensemble uses two primary components for managing remote assets: CdnAssetCache and CdnAssetPrefetcher.

    • CdnAssetCache: Manages the local CDN asset manifest, cached files, metadata, and download de-duplication. It resolves asset sources (CDN URLs or manifest file names) to local files. When an asset is used in the foreground, the cache updates its lastUsedAt timestamp.
    • CdnAssetPrefetcher: A background helper that warms up assets for upcoming screens. It scans screen definitions for static navigation targets (navigateScreen and navigateModalScreen) and prefetches assets tagged for those screens.

    Key distinction: Prefetching does not mark assets as used. Prefetched assets are stored with lastUsedAt: null, ensuring they don't count as 'active' usage until the user actually navigates to them. This prevents prefetched-only assets from being treated as recently used during expiry checks.