Skip to content

Repository files navigation

Codelessly

Codelessly SDK

Server-driven UI for Flutter. Design screens in the Codelessly Editor, publish, and show them in your app with one widget. The SDK renders a published layout natively, straight into Flutter render objects, and runs it: variables, conditions, actions, navigation, data, APIs and scripts. A new publish reaches running apps without an app release.

A Codelessly created by slug and a CodelesslyWidget, with the three ways to load a project: slug, project token, bundle

One widget, three ways to load a project.

final codelessly = Codelessly(config: const CodelesslyConfig(slug: 'my-project'));

runApp(MaterialApp(home: CodelesslyWidget(codelessly: codelessly)));

Install

flutter pub add codelessly_sdk

Requires Dart 3.12 and Flutter 3.35 or newer. Runs on Android, iOS, web, macOS, Windows and Linux.

import 'package:codelessly_sdk/codelessly_sdk.dart';

Quickstart

A Codelessly is one published project. A CodelesslyWidget shows one of its screens. Pick how the project loads:

You need Network Live updates
Slug The project's public slug One HTTP request On the next start
Project token The project's SDK token Firebase (set up by the SDK) While the app runs
Bundle A bundle file in your assets None for the project With an app release

1. A published slug (HTTP)

The slug quickstart code: one HTTP request, no Firebase, new publishes on the next start

Load by slug.

A project with a public slug (the name in site.codelessly.com/<slug>) publishes one bundle file. The SDK fetches it over HTTP; there is nothing else to set up.

import 'package:codelessly_sdk/codelessly_sdk.dart';
import 'package:flutter/material.dart';

void main() {
  final codelessly = Codelessly(config: const CodelesslyConfig(slug: 'my-project'));

  runApp(MaterialApp(home: CodelesslyWidget(codelessly: codelessly)));
}

2. A project token (Firebase, live updates)

The token quickstart code with codelesslyWithToken: Firebase set up for you, live updates, not on Linux

Load by project token.

A project's SDK token gives an app access to its publish in Codelessly's backend. Each new publish reaches the running app. The token is all you need: codelesslyWithToken opens Codelessly's Firebase project as a secondary Firebase app, in Dart. No google-services.json, no flutterfire configure, and your own Firebase project (if you have one) is not touched.

import 'package:codelessly_sdk/codelessly_sdk.dart';
import 'package:codelessly_sdk/firebase.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final codelessly = await codelesslyWithToken('YOUR_PROJECT_TOKEN');

  runApp(MaterialApp(home: CodelesslyWidget(codelessly: codelessly)));
}

Firebase has no Linux support, so a token can't be used on Linux desktop: load by slug or from a bundle there. If the project uses Codelessly cloud data, accounts or file uploads, pass cloudServices: true (Cloud data and services).

3. A bundle shipped with the app (offline)

A pubspec asset entry and Codelessly.asset reading the bundle offline

Load a bundle offline.

A bundle is a publish in one JSON file (gzipped or not). Ship it as an asset and the project loads with no network.

# pubspec.yaml
flutter:
  assets:
    - assets/my_project.json.gz
void main() {
  final codelessly = Codelessly.asset('assets/my_project.json.gz');

  runApp(MaterialApp(home: CodelesslyWidget(codelessly: codelessly)));
}

A bundle already in memory works the same way: Codelessly.fromBundle(PublishBundle.fromJson(json)). Loading modes and caching says where a bundle file comes from.

CodelesslyWidget essentials

CodelesslyWidget with every essential parameter; layoutId picks the screen, params are read as route, data as data

The essentials, annotated.

CodelesslyWidget(
  codelessly: codelessly,
  controller: controller,
  // A layout id, a custom id, or the id of one of its canvases.
  // Without it: the project's entry screen.
  layoutId: 'profile-card',
  // `${route.userId}` in the layout.
  params: const {'userId': 'u_42'},
  // `${data.user.name}` in the layout.
  data: const {
    'user': {'name': 'Ada', 'plan': 'pro'},
  },
  sizing: CanvasSizing.fill,
  loadingBuilder: (context) => const Center(child: CircularProgressIndicator()),
  errorBuilder: (context, error) => Center(child: Text('Could not load: $error')),
  layoutBuilder: (context, layout) => SafeArea(child: layout),
  onNavigate: (context, request) => false,
)

Which screen. layoutId names a layout by its id, its custom id, or the id of one of its canvases. A layout can hold one canvas per breakpoint (mobile, tablet, desktop); the widget shows the one for the width it is given. canvasId forces one. With no layoutId, initialScreen opens a screen by its URL path ('/guide/42'), id or name ('record-form' opens "Record form"); with neither, the project's entry screen shows.

Which project. Pass codelessly, or put a CodelesslyScope(codelessly: ..., child: ...) above the widgets, or call Codelessly.setDefault(codelessly) once.

Data. data is read in the layout as ${data.<key>}. Rebuild the widget with new data and the layout updates in place. Codelessly(data: ...) and codelessly.setData(key, value) set data for every screen. params are read as ${route.<name>}.

Sizing. CanvasSizing.fill (the default) makes the canvas fill the space it is given: use it for screens. CanvasSizing.design keeps the canvas's design size (a card, a dialog). CanvasSizing.fillWidth fills the width and keeps the design height (a sheet). In a scroll view, where the height is unbounded, a canvas takes its own height.

Loading and errors. loadingBuilder and errorBuilder replace the default spinner and error text. layoutBuilder wraps the loaded layout.

Functions. A "call function" action calls a function of your app, by name:

final codelessly = Codelessly(
  config: CodelesslyConfig(
    slug: 'my-project',
    functions: {
      'addToCart': (context, params) async {
        final id = params['productId'] as String?;
        debugPrint('add $id to the cart');
        // Change a variable of the screen that called.
        context.setVariable('cartCount', 3);
        // The result is `${result}` for the next action of the chain.
        return {'ok': true};
      },
    },
  ),
);

Navigation. Navigation actions push the project's screens on the nearest Navigator. A project that is the whole app gives its screens to MaterialApp, so each has its URL path:

MaterialApp(onGenerateRoute: CodelesslyWidget.onGenerateRoute(codelessly))

To route some destinations yourself, return true from onNavigate:

CodelesslyWidget(
  codelessly: codelessly,
  layoutId: 'home',
  onNavigate: (context, request) {
    if (request.type == NavigationType.push && request.destinationId == 'settings') {
      Navigator.of(context).pushNamed('/settings', arguments: request.params);
      return true; // Handled: the SDK does nothing more.
    }
    return false; // The SDK navigates.
  },
)

Controller. A CodelesslyWidgetController reads and changes the shown screen from outside:

final controller = CodelesslyWidgetController(layoutId: 'cart');

controller.read('count');                 // a variable, `route.id`, `data.user.name`
controller.setVariable('count', 0);
controller.trigger('checkout-button', 'click');
controller.layoutId = 'checkout';         // show another layout

Developer guide

The developer guide covers each topic with working code:

Introduction and concepts Editor, publish, SDK; the model and the renderer
Quickstart Five minutes to a running screen
Embedding and sizing Layout ids, canvases, sizing, builders, the controller
Loading modes and caching Slug, token, bundles, self-hosting, offline
Data and variables data, route, storage, screen and app variables
Actions and functions Triggers, the action list, your functions
Navigation Routes, deep links, onNavigate
Conditions and breakpoints Conditions, ${screen}, responsive canvases
Cloud data and services Firebase, auth, APIs, storage, analytics
Scripting JavaScript in a sandbox; the QuickJS engine
Theming and fonts Project theme, host theme, fonts, text scale
Custom widgets Your widgets inside a layout; your own services
Publishing and environments Publish, preview, templates, app identity
Testing Widget tests without a network
Troubleshooting and FAQ Errors and what they mean

The example app runs offline and shows an embedded layout with data, a function and a controller, and a whole project as an app.

Libraries

Import What
package:codelessly_sdk/codelessly_sdk.dart Codelessly, CodelesslyWidget, loaders, services. Never imports Firebase.
package:codelessly_sdk/firebase.dart codelesslyWithToken, and Codelessly's services on Firebase (publish, accounts, cloud data, files, paywalls).
package:codelessly_sdk/store.dart In-app purchases through the App Store and Google Play.

Scripts need an engine on devices: add codelessly_quickjs to apps whose projects use scripts. The data model is its own package, codelessly_api.

License

BSD 3-Clause. See LICENSE.

Native paywall placements

Configure Services.paywalls with your PaywallRuntime, trusted backend and store-adapter factory. A published action opens a placement from any screen:

{"type":"paywall","operation":"openPaywall","placement":"onboarding"}

CodelesslyWidget resolves the placement, loads its store products and layout, and presents a route whose native actions share one PaywallSession. Supply paywallAppVersion for server targeting. Missing configuration/products fail before selling controls open; onActionError lets the host show its recovery UI. The session and its owned adapter are disposed when the route closes or the account changes. A custom host can use PaywallRuntime.open(present: ...).

Bindings read products.annual.price, products.monthly.price, the selected product, purchase progress in paywall, and live verified entitlements. Prices come from the store. Purchase/restore actions wait for a verified per-attempt outcome; cancellation and restore-nothing stop the success chain. openPaywall returns result.outcome (purchased, restored, closed) and result.entitlementId to its originating chain. A UI must check that outcome before displaying paid-access success. Account/profile bindings read live auth; do not declare app variables with service namespace names.