nimbus-native

module
v0.0.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: MIT

README

Nimbus Native

Native iOS and Android apps written in Go, and the same app on the web. Your app's Go code is compiled into the app and drives real native views — UIKit on iOS, Android Views on Android — laid out by a flexbox engine written in Go. Nimbus is the server, and serves the app's pages to browsers too. One language, one codebase, backend included.

Beta (0.0.10). Everything below runs on the iOS simulator, the Android emulator, in browsers and on a real iPhone; no app has shipped to a store yet, and APIs may still change. See CHANGELOG.md for what's in it and what isn't yet.

go install github.com/CodeSyncr/nimbus/cmd/nimbus@latest   # Nimbus v1.16.0 or later
nimbus native new myapp
cd myapp
nimbus native dev ios            # the app, plus the server and web app on :3333; reloads on save
type Counter struct{}

func (Counter) Render(c *ui.Ctx) ui.Component {
	n, setN := ui.State(c, 0)
	return ui.SafeArea(ui.Style{Padding: ui.All(16)},
		ui.Pressable(func() { setN(n + 1) }, ui.Style{Padding: ui.All(12)},
			ui.Text(fmt.Sprintf("Tapped %d times", n), ui.Style{FontWeight: 600}),
		),
	)
}

func init() { ui.Mount(Counter{}) }

func main() { ui.Main() } // runs only in dev mode; on a device the host owns the process

Requirements

  • Go 1.26+ for a project with a Nimbus server (Nimbus needs it; 1.23+ for the app alone), and the nimbus CLI v1.16.1+ from Nimbus
  • iOS: macOS with Xcode 16+. nimbus native run ios runs on a connected iPhone or iPad (USB, or Wi-Fi once paired in Xcode's Devices window, with Developer Mode on) and on the simulator when none is. With several, it asks which; the first phone run asks which Apple team signs it and saves that in the Xcode project. -sim forces the simulator, -udid picks a device, -team a team.
  • Android: the Android SDK with an NDK and build-tools, JDK 17+. The Kotlin compiler is downloaded on the first build.

nimbus native run android likewise prefers a connected phone, then a running emulator, then boots the first one. nimbus native doctor checks all of it. Release Android builds are signed with the debug key until you add yours to native/android/app/build.gradle.kts.

Layout

Path What
bridge/include/nn.h The C ABI: the only boundary between Go and a host
bridge/ Go side of the ABI (batched outbox, panic recovery, handle table, stats); jni_android.c binds it to the Kotlin Bridge
bridge/internal/abitest/ Desktop C host that checks the whole nn.h contract (go test ./bridge)
hosts/ios/ NimbusNativeKit (Swift)
hosts/android/ space.nimbusgo.nativekit (Kotlin): Bridge, wire runtime, renderer
wire/ Wire format runtime (Go); wire/schema reads schemas, wire/gen generates Go, Swift, Kotlin
hosts/*/…/Wire* Swift and Kotlin wire runtimes
layout/ Flexbox engine in pure Go; 62 fixtures match Chromium's layout; incremental
ui/ Components, hooks (State, Reducer, Effect, Memo, Ref, Viewport), elements, keyed reconciler, UI loop
ui/render/ The Go ↔ native renderer protocol (generated into both kits)
hosts/ios/…/Renderer.swift UIKit renderer and NimbusViewController
hosts/android/…/Renderer.kt, NimbusView.kt Android Views renderer and root view
dev/ Dev mode: frame (the socket protocol), client (the shell's proxy); the relay is in toolchain/dev.go
toolchain/ Builds apps — no gomobile; runs gen first. toolchain/template/ is what nimbus native new writes
cmd/nimbus-native/ The toolchain behind nimbus native … (never called directly)
examples/hello/ Bridge and wire checks
examples/gallery/ Every primitive plus a 1,000-row list, with a built-in benchmark
examples/showcase/ Every component on a screen of its own, the source of the guide's pictures
examples/baseline/ The same list in plain Android views, to calibrate emulator numbers
examples/fullstack/ A Nimbus server and app sharing one contract (its own module)
api/ Typed endpoints and a JSON client for Nimbus servers (standard library only)
router/ Path → page matching and router.Push; the table comes from pages/ (toolchain/routes.go)
plugin/ Calls native code: plugin.Call to a Swift/Kotlin plugin (hosts/*/Plugins.*)
web/ The app's pages for browsers: server rendering, SEO tags, sitemap, and host.js
plugins/ Built-in plugins: securestore, appinfo, linking, dialog, haptics, audio (spoken turns: the microphone and a player) and storage (a key-value file)

Start a project

nimbus native new myapp         # asks: a Nimbus server or your own backend; database, auth, plugins
cd myapp
nimbus native dev ios           # the app on a phone or simulator, plus the Nimbus
                                # server and web app on :3333, reloading on save
nimbus native dev android       # the same on Android
nimbus native run ios           # a normal build, installed and opened; or open
                                # native/ios/Runner.xcodeproj in Xcode

The generated app is a counter, ready to grow: one polished screen (a goal with a progress bar, steps, the count saved between launches, light and dark themes) on iOS, Android and the web.

new asks what the app talks to. A Nimbus server (the default) makes a whole Nimbus app, with the same questions and files as nimbus new (.env, config/, start/, controllers, migrations), and adds the native app to it; the server also serves the app's pages on the web. Your own backend (-server none) makes the phone app alone, for a Node, Go or any other server: the app calls it with api.New("https://…") over HTTP and JSON. Flags skip the questions: -y (defaults), -server, -db, -auth, -plugins.

A project with a Nimbus server is one Go module:

myapp/
  main.go, bin/            the Nimbus server's entry and boot
  start/routes.go          your routes and API; start/native.go serves the app's pages
  config/, .env            Nimbus configuration
  app/
    controllers/, models/  the server's code (never in the phone app)
    app.go                 the app's root component; installs the theme
    mobile/main.go         the phone app's package main
  core/                    constants, theme
  features/counter/
    data/                  the counter's state (a ui.Store), saved on the device
    presentation/          the screen
  pages/page.go            the page at "/" (folder routing: pages/settings/ is /settings)
  database/, public/       migrations; static files and fonts for the web
  native/
    ios/                   Xcode project (Runner + the NimbusNativeKit package)
    android/               Gradle project (app + the nimbusnativekit module)

Without a Nimbus server it is the same minus the server's files. As the app grows, add shared/ (widgets, services) and give features a domain/ package (types and api.Endpoints: the contract between app and server).

The server builds with CGO_ENABLED=0 (for a Linux container, say); only SQLite needs cgo, as in any Nimbus app.

Xcode and Gradle build the hosts as usual; a build step in each runs nimbus native lib, which builds app/mobile into the native library and writes the generated Swift or Kotlin. It also keeps the project's copy of the host kit at the version go.mod requires (after an upgrade, the first build updates it and asks you to build again), and refuses to build a phone app that imports Nimbus or any server package: that code stays on the server.

To move a project to a newer release:

nimbus native upgrade            # or: nimbus native upgrade v0.0.10

It updates go.mod, then the new release copies its kits into native/ and regenerates the Swift. Your own files under native/ are left alone; the changelog says when a release wants something changed there.

Icons, launch screens and store builds

nimbus native icon logo.png -background "#6d28d9"   # iOS + Android icons; -launch sets the launch color
nimbus native keystore                              # once: the Android release (upload) key, kept out of git
nimbus native build android -release -version 1.0.0 -build 1   # build/android/release/<App>.aab and .apk
nimbus native build ios -release -version 1.0.0 -build 1       # build/ios/release/<App>.ipa for App Store Connect

New projects start with an icon and launch screen in the app's colors. The icon is any square PNG or JPEG, 1024 px or more: iOS gets one flattened App Store icon, Android an adaptive icon for every density.

iOS releases sign automatically with your team (-team, or the one saved in the Xcode project). App Store export needs the Apple ID in Xcode to have the Admin or App Manager role; -export ad-hoc and -export development make an .ipa for registered devices. Upload with Xcode's Organizer or Transporter. Android release builds use the key from nimbus native keystore; without one they fall back to the debug key, which Google Play refuses (the build says so).

Folder routing

Optional: a project with a pages/ folder gets its URLs from it.

pages/orders/page.go             /orders
pages/orders/new/page.go         /orders/new    (beats :id)
pages/orders/_id/page.go         /orders/:id
pages/orders/_id/items/_item/    /orders/:id/items/:item

Each page.go declares a Page component; parameters fill fields tagged param (string, int or int64; a value that doesn't parse matches nothing), and an optional Title() names the screen:

package order // pages/orders/_id

type Page struct {
	ID int `param:"id"`
}

func (p Page) Title() string                  { return fmt.Sprintf("Order #%d", p.ID) }
func (p Page) Render(c *ui.Ctx) ui.Component { return presentation.OrderScreen{ID: p.ID} }

Every build regenerates pages/routes_gen.go, a static table: no reflection or scanning at run time, and a missing or mistyped parameter field fails the build with the folder named. Register it once, then navigate by path; the page is pushed onto the screen's native stack:

router.Register(pages.Routes)   // app/router.go
router.Push(c, "/orders/42")    // any screen

Go skips _-prefixed folders in ./... patterns, so go vet ./... passes over parameter pages; they are compiled through the table the app imports.

Shared state

ui.Store is state the whole app shares, such as a cart or who is signed in: one value per app on a phone, and one per browser tab on the web (where every visitor runs in the same server, so a package variable would be shared between them).

var Cart = ui.NewStore(func() Cart { return nil })

items := Cart.Use(c)                                          // re-renders c when it changes
Cart.Update(c, func(ct Cart) Cart { return ct.Add(p) })       // from handlers, effects, actions

Controls and dialogs

ui.Switch(on, setOn, ui.Style{})                                   // UISwitch / Switch, theme-tinted
ui.TextInput(note, setNote, box).Multiline()                       // or FieldOptions{Multiline: true}
ui.List(n, row, ui.Flex(1)).OnRefresh(q.Loading, q.Refetch)        // pull to refresh (Scroll too)

ok, _ := dialog.Confirm(ctx, "Delete order?", "This can't be undone.", "Delete", true)
i, _ := dialog.Choose(ctx, "Share via", "Mail", "Messages")        // action sheet; -1 if cancelled
dialog.Toast(ctx, "Saved")
day, ok, _ := dialog.PickDate(ctx, dialog.Date, time.Now())        // Date, Time or DateTime

More: ui.Slider(value, min, max, onChange, style) (UISlider, SeekBar, a range input on the web); .OnLongPress(fn) on a Pressable (a right click on the web); Style{Wrap: true} for rows that wrap onto new lines (chips, tags), with LineGap between lines; and Style{Transition: 250} to animate a view's changes of position, opacity and background over 250 ms.

SafeArea keeps clear of the on-screen keyboard: its bottom padding grows by however much of the keyboard reaches below it.

Fonts

nimbus native font add Inter.ttf bundles a font in the iOS and Android apps (and serves it to the web); use it as ui.Style{FontFamily: "Inter"}. The family is the file name; weights come from FontWeight.

Every project has a URL scheme, its module name: myapp://orders/42 opens the app on /orders/42, at launch or while it runs, pushed on the navigation stack on screen (or as a sheet where there is none). Set ui.HandleURL to decide yourself.

The web

A project's Nimbus server also serves its pages to browsers (main.go mounts web.Handler as the router's fallback):

  • Server rendering. A request runs the page's components on the server, waits for its UseQuery data (up to 3 s), and answers with plain HTML: content, links (router.Link, .Href), and the page's <title>, description, Open Graph tags and canonical URL from its Head() method. /sitemap.xml lists the pages without parameters; /robots.txt points to it.
  • Then live. host.js (12 KB, 4 KB gzipped) adopts that HTML. Taps and typing go to the page's UI session on the server, and what it renders comes back as DOM changes over Server-Sent Events. Layout is the browser's own flexbox.
  • Navigation. On the web the shell is app.WebLayout, a header with a link per tab, instead of tabs and stacks. router.Push changes the page and the address; Back and Forward work; every page has its own URL.
func (p Page) Head() router.Head {
	return router.Head{Title: fmt.Sprintf("Order #%d", p.ID), Description: "…", Image: "https://…/og.png"}
}

Each open tab holds a small UI session on the server, closed a minute after the tab goes away. Native plugins answer unavailable on the web.

Plugins: calling native code

Install a third-party plugin module

Plugins can be shared as Go modules and installed by module path, with an optional pinned version:

nimbus native plugin add github.com/acme/native-battery@v1.2.0
nimbus native plugin list
nimbus native plugin remove battery

The module root contains plugin.json, its Go API, and ios/*.swift and android/*.kt handlers. The manifest declares the plugin name, description, handler, and any iOS usage descriptions/Swift packages or Android permissions/ Gradle dependencies. The Swift handler is a type with the declared name; the Kotlin handler is an object with that name in space.nimbusgo.nativekit. Nimbus stages those native files under native/plugins/<name>, records enabled plugins in native/plugins.json, and adds the module to go.mod. Commit those files with go.mod and go.sum. Without @version, the CLI resolves the module's latest version; pin a version for repeatable builds. Set the optional go field in the manifest to a subpackage import path if the Go API is not in the module root. An Android plugin can also declare android.native libraries to compile C/C++ sources under android/ for each ABI. After removing a plugin, run go mod tidy if its Go API is no longer imported.

Go reaches Swift and Kotlin through plugins. Built in:

securestore.Set(ctx, "token", t)            // Keychain / Android Keystore
token, ok, err := securestore.Get(ctx, "token")
storage.Set(ctx, "theme", "dark")            // JSON in the app's data folder
info, _ := appinfo.Get(ctx)                  // version, locale, data and cache dirs
linking.Open(ctx, "https://example.com")     // browser, tel:, mailto:, maps

Your own plugin is a name and a handler in the host, and one call in Go. Arguments and answers are JSON, so neither side needs generated code:

// native/ios/Runner/AppDelegate.swift, before the NimbusViewController
Plugins.register("battery") { call in
    UIDevice.current.isBatteryMonitoringEnabled = true
    call.resolve(["level": UIDevice.current.batteryLevel])
}
// native/android/.../MainActivity.kt, before the NimbusView
Plugins.register("battery") { call ->
    val bm = getSystemService(BATTERY_SERVICE) as android.os.BatteryManager
    call.resolve(mapOf("level" to bm.getIntProperty(android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY) / 100.0))
}
type battery struct{ Level float64 `json:"level"` }
b, err := plugin.Call[battery](ctx, "battery", "level", nil)

Handlers run on the main thread and answer once with resolve or reject(code, message); codes (cancelled, denied, not_found, unavailable, failed) come back as plugin.Error, testable with plugin.IsCode. plugin.Call blocks its goroutine until the answer, so call it from ui.UseQuery, ui.UseAction or an effect, not in Render. Plugins work unchanged under nimbus native dev.

On Android, fmt.Println and log output goes to logcat (tag NimbusNativeApp), which nimbus native run android streams.

Try the examples

Everything goes through the nimbus CLI — there is no second tool:

nimbus native doctor
nimbus native dev ios ./examples/gallery       # hot reload: edit, save, see it
nimbus native run ios ./examples/gallery       # boots a simulator if needed
nimbus native run android ./examples/gallery   # boots the first AVD if needed
nimbus native build ios|android ./examples/gallery
nimbus native gen ./examples/hello             # every build does this too

nimbus native runs the toolchain (cmd/nimbus-native) at the version the app's go.mod requires, so the CLI always matches the app's runtime. Outside such a module it runs the latest release, or NIMBUS_NATIVE_BIN if set.

run streams the app's log until you press Ctrl+C.

Typed messages: the wire schema

An app describes what crosses the bridge as plain Go types in a package of its own (the hello app uses examples/hello/proto):

//wire:message
type Hello struct{ Name string }

//wire:enum
type Kind uint8

//wire:union
type Op interface{ isOp() }

//wire:variant Op
type Remove struct{ ID uint32 }

nimbus native gen (run by every build) writes wire_gen.go next to the schema (proto.Encode, proto.Decode, methods on each type) and the same types for the hosts: Proto.Message.hello(Proto.Hello(name: "Yash")) in Swift, Proto.Hello(name = "Yash") in Kotlin. Supported: bool, every int and uint size, float32/64, string, []byte, lists, maps with string or integer keys, optionals (*T), nested structs, recursion through lists ([]*Node), unions, open enums.

The encoding is compact binary (varints, zigzag, little-endian floats, sorted map keys; spec in wire/wire.go). Each schema has a hash; the host passes the combined hash to nn_start, which refuses a mismatch with NN_ERR_SCHEMA. go test ./wire/gen proves the three languages agree: Go encodes every kitchen-sink fixture plus corrupt variants, and compiled Swift and Kotlin programs must decode and re-encode them to identical bytes and reject the same bad inputs.

Android is Kotlin

The Android kit, generated wire types and example activities are Kotlin. nimbus native downloads a pinned Kotlin compiler (2.0.21, SHA-256 checked) into the user cache on the first Android build; NIMBUS_KOTLINC points it at another. Plain Java activities still build alongside.

How a build works

Projects build with xcodebuild and Gradle (the IDEs' own builds). The examples build with the platform tools directly:

  • iOS: go build -buildmode=c-archive (GOOS=ios, Xcode clang) → libapp.a, then swiftc links it with the host and app Swift code, writes Info.plist, ad-hoc signs the .app.
  • Android: go build -buildmode=c-shared (NDK clang, 16 KB pages) → libnimbusapp.so per ABI, then kotlinc + d8 (with the Kotlin standard library) → classes.dex, aapt2 → APK, native libs stored uncompressed, zipalign -P 16, apksigner.

How the bridge moves messages

  • Native → Go: nn_send (async, in order) or nn_call (sync, returns a reply). A Go panic in nn_call comes back as NN_ERR_PANIC with the panic text and Go stack — BridgeError on iOS, BridgeException on Android.
  • Go → native: bridge.Send appends to an outbox and never blocks. Only when the outbox goes from empty to non-empty does Go call the host's wake; the host then calls nn_drain on its UI thread and gets every waiting message in one buffer. A burst costs one thread hop, not one per message.
  • Hosts refer to Go values by integer Handle, never by pointer.

Dev mode

 laptop                                        simulator / emulator
 nimbus native dev ── relay :7357 ◄──TCP──►   dev shell: the real app, its Go
   │ your app, built for the laptop            library swapped for a proxy
   └ restarted on every save                   (dev/client); native code as shipped

nimbus native dev ios|android builds a dev shell once, then runs the app's Go code on the laptop (-tags nimbusdev: the same bridge API over a socket). Saving a .go file rebuilds and restarts it; the shell clears the old views and the new process renders. A compile error is printed and the last good build keeps running. A wire-schema change rebuilds the shell. The relay caches the device's text measurements, so a reload re-measures nothing. Android reaches the relay through adb reverse.

In a project, dev also runs the Nimbus server (go run .) and restarts it on every save, so the web app and your API stay current too; it takes the next free port when 3333 is busy, o opens it in the browser, and -no-server leaves it out. On Android the app reaches it through adb reverse, at the device's own 127.0.0.1.

While it runs, the terminal shows the app's output (app │ lines from the Go process, device │ lines from the simulator or emulator), panics and errors in red, a line when an app process stops, and how long each reload took to reach the screen. Single keys work as well as saving:

r  reload    R  restart (rebuild and reinstall)    o  open the web app    c  clear    q  quit    h  help

Measured on the gallery (1,000 rows): a save is on screen in 1.9–3.5 s on the iOS simulator and 2.7 s on the Android emulator, of which 0.6–2.3 s is compiling — this Mac runs the Intel build of Go under Rosetta; the Apple Silicon build compiles roughly twice as fast.

Like run, dev puts the shell on a connected phone when there is one (-sim for the simulator or emulator). An iPhone reaches the relay over Wi-Fi, so iOS asks once to let the app find devices on the local network; Android phones go through adb reverse.

In a project, nimbus native dev ios|android builds the shell with the project's own Xcode or Gradle build and watches the whole module (not native/ or build/), so a new page folder is live on the next save. Component state starts fresh on each reload, but the screen doesn't: the selected tabs and every screen pushed with router.Push come back. Each dev session starts on the first screen.

How a screen gets drawn

  1. A state change (or host event) is queued to the UI loop goroutine.
  2. Components whose props or state changed render again; the rest are kept.
  3. The keyed reconciler turns the new elements into ops — create, children order, style, text, image, input — only for what changed.
  4. The flexbox engine lays out only changed subtrees, asking the host to measure text (a synchronous host call, cached per text).
  5. Changed frames join the ops in one render.Commit, applied on the UI thread in a single pass.

Navigation

ui.Stack("Orders", OrdersScreen{}, ui.Flex(1))   // the app's root

nav := ui.Navigation(c)                          // in any component on a screen
nav.Push("Order #10012", OrderDetail{o})
nav.Pop()

The stack is native: UINavigationController on iOS (its bar, push animation and swipe-back), a Material-style bar with a back arrow, slide transitions and the system back button on Android (onBackPressed → renderer.back()). Go owns the screens; when the user goes back, the host tells Go (Popped) and the two stay in step. Each screen's content is Go-rendered at the size the host reports.

Tabs work the same way: ui.Tabs(style, ui.Tab{Title, Icon, Screen}...) is UITabBarController on iOS (SF Symbol icons) and a bottom bar on Android; every tab stays mounted; ui.TabsOf(c).Select(i) switches from Go and a user's tap is reported back. ui.SafeArea pads clear of the status bar, home indicator and tab bar.

Modals: ui.Modals(c).Present(title, screen) shows a page sheet on iOS (swipe down to close) and a full-screen layer on Android (back to close); each has its own stack and title bar, so Navigation works inside it. ui.Modals(c).Dismiss() closes it from Go, and a user's close is reported back.

Gallery: tap a row → detail iOS simulator Android emulator
Push, tap → committed Go 2 ms; UIKit push 57–123 ms 19 ms total
Native back → Go follows ✓ ✓

Talking to a Nimbus server

// shared/api.go — imported by the Nimbus server and the app
var ListOrders  = api.GET[ListOrdersReq, []Order]("/api/orders")
var CreateOrder = api.POST[NewOrder, Order]("/api/orders")

// in the app
client := api.New(api.ServerURL("https://orders.example.com"))
q := ui.UseQuery(c, "orders", func(ctx context.Context) ([]Order, error) {
	return api.Call(ctx, client, ListOrders, ListOrdersReq{})
})
place := ui.UseAction(c, func(ctx context.Context, o NewOrder) (Order, error) {
	created, err := api.Call(ctx, client, CreateOrder, o)
	form.SetErrors(err) // a Nimbus 422 lands on the form's fields
	return created, err
}, func(Order) { ui.RefetchQuery(c, "orders") })

The api package depends only on the standard library, so shared code that imports it builds into the app without pulling in the server. An endpoint's method, path and request/response types are declared once; {name} path parameters come from fields tagged path:"name", GET/DELETE requests become query parameters and the rest JSON bodies. A non-2xx response is an *api.Error; a Nimbus 422's field errors come with it. ui.UseQuery fetches off the UI goroutine, caches by key across components, refreshes stale data in the background and refetches on demand; ui.UseAction runs writes with their progress and result. See examples/fullstack for a Nimbus server and app sharing one contract.

api.ServerURL finds the server wherever the app runs. Under nimbus native dev it's the server running beside the app. A nimbus native run uses the project's server on this Mac at an address the target can reach: the simulator's 127.0.0.1, the emulator's 10.0.2.2, or this Mac's address on the local network for a phone (start it with nimbus serve). Any build takes -server https://your-server, built into the app; a release build warns when a project with a server has none. The argument to ServerURL is the fallback.

Theming and dark mode

ui.Theme holds an app's tokens (background, surface, text, muted, border, primary, danger, bar colors, radius, type and spacing scales); ui.LightTheme / ui.DarkTheme are built in and ui.SetTheme(light, dark) replaces them. The runtime follows the system's dark mode: ui.UseTheme(c) re-renders with the other theme, unset text, input and spinner colors take the theme's, and the Chrome op restyles what the platform draws — navigation and tab bars, screen and modal backgrounds, Android's status and navigation bars.

Forms

form := ui.UseForm(c, Signup{}, validation.ValidateStruct) // a Nimbus server's own rules
ui.TextField(form.Field("email", func(s *Signup) *string { return &s.Email }),
	ui.FieldOptions{Label: "Email", Keyboard: render.KeyboardEmail})
ui.Pressable(form.Submit(save), style, ui.Text("Sign up", style))

UseForm takes any func(T) error; field errors come from ui.FieldErrors, Nimbus's validation.ValidationErrors or anything with ToMap() map[string][]string — so one rule set validates both the server and the app, without Nimbus Native importing Nimbus. Errors show once a field is touched, and for every field on submit; submit runs only when the value is valid. ui.FieldErrorsOf(err) reads a server's 422 the same way.

Lists

ui.List(count, item, style) renders only the rows on screen and about one screen above and below; rows can differ in height (each is measured once shown). Scrolling renders no app components — only rows entering and leaving — and re-windows only when the screen nears the edge of what is rendered.

Gallery, 1,000 rows iOS simulator Android emulator
Native views at start 256 (was 8,014) 256
First render, Go + layout / apply 22 ms / 84 ms (was 143 / 457) 285 ms / 154 ms (was 1.8 s / 1 s)
Tap → screen median 2.2 ms 25 ms (was 52)
Fast scroll, 180 frames 60 fps, 0 dropped, 37 window updates 51.6 fps (was 38)
Native views in the list after scrolling 36 39

Phase 0d results

iOS simulator Android emulator*
First render, 1,000 rows (8,014 native views) Go 21 ms + layout 128 ms; UIKit 457 ms Go 422 ms + layout 1.4 s; apply 1 s
Tap → screen (20 taps) median 3.2 ms, worst 5 ms median 52 ms (frame-bound)
Tap commit 4 ops; Go 0.9 ms, layout 1.7 ms 1 op; Go 1.7 ms, layout 3.8 ms
Scroll through the list, 180 frames 60 fps, 0 dropped 38 fps — plain Android views: 37 fps
Typing → Go → greeting updates ✓ ✓
Layout of the 1,000-row screen, desktop full 4.7 ms; one change 0.08 ms

* The emulator is the limit here: the same list built with plain Android views scrolls at the same rate. First render is slow because all 1,000 rows exist as native views and measure their text; a virtualized list (Phase 1) creates only the visible rows.

Phase 0c results

Desktop iOS simulator Android emulator*
Swift / Kotlin byte-identical to Go (all fixtures + corrupt inputs) ✓ — —
Stale schema refused at start ✓ ✓ ✓
Typed sync call (encode + call + decode) — 0.67 µs ~17 µs
1,000-row list (strings, optional, enum, tags, map) Go → host — 1.66 µs/row ~15 µs/row
1,000-node tree, Go encode / decode 0.12 / 0.30 ms — —
100k typed messages, in order — 7 drains 2 drains
Leak check (2 soak rounds) — heap 871 → 871 KB heap 665 → 665 KB

* Emulator timings are not trustworthy (see below); a real device is next.

Phase 0b results

Desktop C host iOS simulator Android emulator*
100k-message burst from Go 362 msgs/drain (8 goroutines) 10 drains, 99 ns/msg 2 drains
Ordering kept ✓ (per goroutine) ✓ ✓
Sync call 0.48 µs 0.42 µs 4–10 µs
Ping-pong round trip 10 µs 73 µs 1.5–2.4 ms
Panic text + Go stack reach native ✓ ✓ ✓
Leak check: 2 soak rounds (100k calls + 100k msgs each) heap +40 KB heap 871 → 871 KB heap 980 → 981 KB

* The emulator was under heavy load (the 0a build measured 14–19 µs per sync call in the same session). Run back to back, 0b was no slower than 0a. Real Android latency needs a device.

Phase 0a results (M1, Go 1.23.3)

iOS simulator Android emulator (API 34, arm64)
Runtime start 0.1 ms 21.3 ms
Sync call native → Go 0.71 µs 1.83 µs
Async ping-pong, incl. main-thread hop 104 µs 979 µs
Go panic recovered, app keeps running ✓ ✓
Go goroutines → native ✓ ✓
16 KB page alignment — ✓ (zip + ELF)

Directories

Path Synopsis
Package api calls a JSON server — a Nimbus app — with typed endpoints.
Package api calls a JSON server — a Nimbus app — with typed endpoints.
Package bridge is the Go side of the Nimbus Native C ABI (include/nn.h).
Package bridge is the Go side of the Nimbus Native C ABI (include/nn.h).
internal/abitest/app command
The Go side of the ABI test: host.c drives it through nn.h exactly as an iOS or Android host would.
The Go side of the ABI test: host.c drives it through nn.h exactly as an iOS or Android host would.
cmd
nimbus-native command
Command nimbus-native is the Nimbus Native toolchain behind `nimbus native`: the nimbus CLI runs the version of this command that an app depends on, so users never call it directly.
Command nimbus-native is the Nimbus Native toolchain behind `nimbus native`: the nimbus CLI runs the version of this command that an app depends on, so users never call it directly.
dev
client
Package client is the Go side of a dev shell: built into the app on the device instead of the app's own Go code, it forwards bridge traffic to the relay in `nimbus native dev`, where the app runs on the laptop.
Package client is the Go side of a dev shell: built into the app on the device instead of the app's own Go code, it forwards bridge traffic to the relay in `nimbus native dev`, where the app runs on the laptop.
frame
Package frame is the dev-mode wire between the device proxy, the relay in `nimbus native dev` and the app process on the laptop: typed, length-prefixed frames over TCP.
Package frame is the dev-mode wire between the device proxy, the relay in `nimbus native dev` and the app process on the laptop: typed, length-prefixed frames over TCP.
examples
baseline command
baseline is the gallery's order list built with plain Android views and no Go UI, to tell emulator limits apart from renderer costs.
baseline is the gallery's order list built with plain Android views and no Go UI, to tell emulator limits apart from renderer costs.
gallery command
gallery shows the Phase 0d engine: components, state, every primitive, and a 1,000-row list, all rendered as native views.
gallery shows the Phase 0d engine: components, state, every primitive, and a 1,000-row list, all rendered as native views.
guide
Package guide holds the complete example on each plugin's guide page, compiled here so that they can't go stale.
Package guide holds the complete example on each plugin's guide page, compiled here so that they can't go stale.
guide/components
Package components holds the complete example on each component's guide page, compiled here so that they can't go stale.
Package components holds the complete example on each component's guide page, compiled here so that they can't go stale.
hello command
hello is the bridge and wire check app.
hello is the bridge and wire check app.
hello/proto
Package proto is the hello app's wire schema: every message between its Go code and its iOS / Android hosts.
Package proto is the hello app's wire schema: every message between its Go code and its iOS / Android hosts.
showcase command
showcase is every component on a screen of its own, the source of the guide's pictures: the app of a small cafe.
showcase is every component on a screen of its own, the source of the guide's pictures: the app of a small cafe.
Package layout is Nimbus Native's flexbox engine, in pure Go so the same layout runs on iOS, Android and (later) the web.
Package layout is Nimbus Native's flexbox engine, in pure Go so the same layout runs on iOS, Android and (later) the web.
Package plugin calls native code: a method of a plugin registered in the iOS or Android host, written in Swift or Kotlin.
Package plugin calls native code: a method of a plugin registered in the iOS or Android host, written in Swift or Kotlin.
plugins
appinfo
Package appinfo describes the running app and device, and where the app may keep files.
Package appinfo describes the running app and device, and where the app may keep files.
applemaps
Package applemaps shows Apple Maps inside your screen, on iOS.
Package applemaps shows Apple Maps inside your screen, on iOS.
appreview
Package appreview asks for a rating with the system's own prompt.
Package appreview asks for a rating with the system's own prompt.
appstate
Package appstate says when the app goes to the background or comes back: to pause something, or refresh what's on screen.
Package appstate says when the app goes to the background or comes back: to pause something, or refresh what's on screen.
audio
Package audio is the phone's microphone and speaker: one spoken turn at a time, for talking to an assistant.
Package audio is the phone's microphone and speaker: one spoken turn at a time, for talking to an assistant.
audioplayer
Package audioplayer plays audio from a URL or from bytes, with pause, seek, volume, speed and looping.
Package audioplayer plays audio from a URL or from bytes, with pause, seek, volume, speed and looping.
audiorecorder
Package audiorecorder records from the microphone to an AAC file, for a voice memo, say.
Package audiorecorder records from the microphone to an AAC file, for a voice memo, say.
backgroundlocation
Package backgroundlocation keeps following the phone after the person leaves your app: a delivery, a run, a trip.
Package backgroundlocation keeps following the phone after the person leaves your app: a delivery, a run, a trip.
backgroundtasks
Package backgroundtasks runs Go now and then, with the app closed.
Package backgroundtasks runs Go now and then, with the app closed.
badge
Package badge sets the number on the app's icon.
Package badge sets the number on the app's icon.
barcode
Package barcode scans QR codes and barcodes with the camera, on a screen of its own.
Package barcode scans QR codes and barcodes with the camera, on a screen of its own.
barometer
Package barometer reads the air pressure, and how far the phone has climbed.
Package barometer reads the air pressure, and how far the phone has climbed.
biometrics
Package biometrics confirms it's really them, with Face ID, Touch ID or a fingerprint.
Package biometrics confirms it's really them, with Face ID, Touch ID or a fingerprint.
bluetooth
Package bluetooth talks to Bluetooth LE devices: scales, printers, beacons and the like.
Package bluetooth talks to Bluetooth LE devices: scales, printers, beacons and the like.
brightness
Package brightness reads and sets how bright the screen is: up for a boarding pass or a QR code, down for reading in bed.
Package brightness reads and sets how bright the screen is: up for a boarding pass or a QR code, down for reading in bed.
browser
Package browser opens a web page inside the app, with the system browser's cookies and sign-ins, and a button back.
Package browser opens a web page inside the app, with the system browser's cookies and sign-ins, and a button back.
calendar
Package calendar adds an event with the system's own screen, so people see it and choose the calendar before it's saved.
Package calendar adds an event with the system's own screen, so people see it and choose the calendar before it's saved.
calendarevents
Package calendarevents reads and writes the phone's calendars directly.
Package calendarevents reads and writes the phone's calendars directly.
camera
Package camera takes a photo, or picks one from the library, using the system's own screens.
Package camera takes a photo, or picks one from the library, using the system's own screens.
cameraview
Package cameraview puts a live camera inside your screen, with your own buttons over it.
Package cameraview puts a live camera inside your screen, with your own buttons over it.
cellular
Package cellular describes the phone's mobile network.
Package cellular describes the phone's mobile network.
clipboard
Package clipboard copies and pastes text.
Package clipboard copies and pastes text.
contacts
Package contacts reads and changes the phone's contacts.
Package contacts reads and changes the phone's contacts.
device
Package device describes the phone the app is running on.
Package device describes the phone the app is running on.
dialog
Package dialog shows the platform's own dialogs: alerts, confirmations, action sheets, short toasts, and date and time pickers.
Package dialog shows the platform's own dialogs: alerts, confirmations, action sheets, short toasts, and date and time pickers.
filepicker
Package filepicker lets someone choose a file with the system's picker.
Package filepicker lets someone choose a file with the system's picker.
filesystem
Package filesystem keeps files for the app.
Package filesystem keeps files for the app.
geofencing
Package geofencing notices when the phone enters or leaves a place, even while the app is in the background.
Package geofencing notices when the phone enters or leaves a place, even while the app is in the background.
geolocation
Package geolocation says where the phone is.
Package geolocation says where the phone is.
googlemaps
Package googlemaps shows Google Maps inside your screen.
Package googlemaps shows Google Maps inside your screen.
haptics
Package haptics plays the phone's taps and buzzes: the system's own feedback (Impact, Notify, Selection) and patterns of your own (Play) — Core Haptics on iOS, the vibrator's effects on Android.
Package haptics plays the phone's taps and buzzes: the system's own feedback (Impact, Notify, Selection) and patterns of your own (Play) — Core Haptics on iOS, the vibrator's effects on Android.
hinge
Package hinge tells a screen about a foldable phone's fold: where it runs, and whether the phone is half open, like a laptop or a tent, so a layout can keep things off the crease.
Package hinge tells a screen about a foldable phone's fold: where it runs, and whether the phone is half open, like a laptop or a tent, so a layout can keep things off the crease.
imagemanipulator
Package imagemanipulator resizes, crops, rotates and flips pictures, and encodes them again.
Package imagemanipulator resizes, crops, rotates and flips pictures, and encodes them again.
intentlauncher
Package intentlauncher starts other screens on Android: its settings pages, and other apps' activities.
Package intentlauncher starts other screens on Android: its settings pages, and other apps' activities.
keepawake
Package keepawake keeps the screen on, for a till, a recipe or a map.
Package keepawake keeps the screen on, for a till, a recipe or a map.
keyboard
Package keyboard puts the on-screen keyboard away.
Package keyboard puts the on-screen keyboard away.
lightsensor
Package lightsensor reads how bright it is around the phone.
Package lightsensor reads how bright it is around the phone.
linking
Package linking opens URLs in other apps: https in the browser, tel:, mailto:, maps and app links in their apps.
Package linking opens URLs in other apps: https in the browser, tel:, mailto:, maps and app links in their apps.
livephoto
Package livephoto picks Live Photos and plays them inside your screen, on iOS.
Package livephoto picks Live Photos and plays them inside your screen, on iOS.
localization
Package localization says how the person likes things shown: their languages, region, currency, and date and number formats.
Package localization says how the person likes things shown: their languages, region, currency, and date and number formats.
localnotifications
Package localnotifications shows reminders the app schedules on the phone itself, with no server.
Package localnotifications shows reminders the app schedules on the phone itself, with no server.
magnetometer
Package magnetometer reads the magnetic field around the phone: for a metal detector, say, or a compass of your own.
Package magnetometer reads the magnetic field around the phone: for a metal detector, say, or a compass of your own.
mailcomposer
Package mailcomposer opens the system's screen for writing an email, filled in, for them to check and send.
Package mailcomposer opens the system's screen for writing an email, filled in, for them to check and send.
medialibrary
Package medialibrary saves to and reads from the phone's photos.
Package medialibrary saves to and reads from the phone's photos.
meshgradient
Package meshgradient fills a view with a mesh gradient: a grid of points, each with a color, blended smoothly between them.
Package meshgradient fills a view with a mesh gradient: a grid of points, each with a color, blended smoothly between them.
motion
Package motion reads the accelerometer and gyroscope.
Package motion reads the accelerometer and gyroscope.
navigationbar
Package navigationbar sets the look of the bar at the bottom of an Android phone, or hides it.
Package navigationbar sets the look of the bar at the bottom of an Android phone, or hides it.
network
Package network says whether the phone is online, and how.
Package network says whether the phone is online, and how.
nfc
Package nfc reads NFC tags.
Package nfc reads NFC tags.
oauth
Package oauth signs them in with any OAuth 2 provider: GitHub, Microsoft, Slack, your own.
Package oauth signs them in with any OAuth 2 provider: GitHub, Microsoft, Slack, your own.
orientation
Package orientation locks the screen to portrait or landscape.
Package orientation locks the screen to portrait or landscape.
pedometer
Package pedometer counts steps.
Package pedometer counts steps.
printing
Package printing prints HTML or a PDF with the system's print dialog.
Package printing prints HTML or a PDF with the system's print dialog.
purchases
Package purchases sells products and subscriptions through the App Store and Google Play.
Package purchases sells products and subscriptions through the App Store and Google Play.
push
Package push receives notifications from your server: APNs on iOS, Firebase Cloud Messaging on Android, and Web Push in a browser.
Package push receives notifications from your server: APNs on iOS, Firebase Cloud Messaging on Android, and Web Push in a browser.
receiveshare
Package receiveshare puts your app in the share sheet, so people can send it text, links, pictures and files from other apps.
Package receiveshare puts your app in the share sheet, so people can send it text, links, pictures and files from other apps.
scene
Package scene shows 3D inside your screen.
Package scene shows 3D inside your screen.
screencapture
Package screencapture keeps a screen out of screenshots and recordings: for a card number, say, or a one-time code.
Package screencapture keeps a screen out of screenshots and recordings: for a card number, say, or a one-time code.
screenreader
Package screenreader works with VoiceOver and TalkBack: whether one is on, and saying something that changed on its own.
Package screenreader works with VoiceOver and TalkBack: whether one is on, and saying something that changed on its own.
securestore
Package securestore keeps small secrets, such as auth tokens, in the platform's secure storage: the Keychain on iOS, an Android Keystore key on Android.
Package securestore keeps small secrets, such as auth tokens, in the platform's secure storage: the Keychain on iOS, an Android Keystore key on Android.
share
Package share opens the system's share sheet.
Package share opens the system's share sheet.
signin
Package signin signs them in with Apple or Google, using the phone's own sheet.
Package signin signs them in with Apple or Google, using the phone's own sheet.
sms
Package sms opens the system's screen for writing a text message, filled in, for them to check and send.
Package sms opens the system's screen for writing a text message, filled in, for them to check and send.
speech
Package speech reads text aloud with the phone's own voices.
Package speech reads text aloud with the phone's own voices.
speechrecognition
Package speechrecognition turns speech into text, with the phone's own recognizer.
Package speechrecognition turns speech into text, with the phone's own recognizer.
splashscreen
Package splashscreen keeps the launch screen's color up until your first screen has what it needs, so the app doesn't flash empty.
Package splashscreen keeps the launch screen's color up until your first screen has what it needs, so the app doesn't flash empty.
splitview
Package splitview shows a list beside what's open from it when there's room, as on a tablet, and one at a time on a phone: picking from the list pushes the detail onto the screen's Stack.
Package splitview shows a list beside what's open from it when there's room, as on a tablet, and one at a time on a phone: picking from the list pushes the detail onto the screen's Stack.
sqlite
Package sqlite is a SQLite database on the phone, through Go's own database/sql.
Package sqlite is a SQLite database on the phone, through Go's own database/sql.
statusbar
Package statusbar sets the look of the status bar, the strip with the clock.
Package statusbar sets the look of the status bar, the strip with the clock.
storage
Package storage is a small key-value store for app data that should survive restarts: settings, drafts, the last-seen screen.
Package storage is a small key-value store for app data that should survive restarts: settings, drafts, the last-seen screen.
symbols
Package symbols draws the platforms' own icon sets: SF Symbols on iOS, and Google's Material Symbols on Android.
Package symbols draws the platforms' own icon sets: SF Symbols on iOS, and Google's Material Symbols on Android.
trackingconsent
Package trackingconsent asks before the app tracks them across other apps and websites, as the App Store requires.
Package trackingconsent asks before the app tracks them across other apps and websites, as the App Store requires.
video
Package video plays video inside your screen, from a URL, with the system's own player.
Package video plays video inside your screen, from a URL, with the system's own player.
webview
Package webview embeds a web page in your screen.
Package webview embeds a web page in your screen.
Package router maps URL paths to screens.
Package router maps URL paths to screens.
Package toolchain builds Nimbus Native apps for iOS and Android.
Package toolchain builds Nimbus Native apps for iOS and Android.
tools
docref command
Command docref writes the API reference of the guide's plugin pages from the plugins themselves: their Go source and plugin.json.
Command docref writes the API reference of the guide's plugin pages from the plugins themselves: their Go source and plugin.json.
ui
Package ui is Nimbus Native's component layer: apps describe their screens as Go components, and the runtime keeps real native views (UIKit, Android Views) in step with them.
Package ui is Nimbus Native's component layer: apps describe their screens as Go components, and the runtime keeps real native views (UIKit, Android Views) in step with them.
render
Package render is the wire protocol between the ui runtime (Go) and a host's native renderer.
Package render is the wire protocol between the ui runtime (Go) and a host's native renderer.
Package web serves a Nimbus Native app's pages to browsers: the same components the phone apps run, rendered on the server.
Package web serves a Nimbus Native app's pages to browsers: the same components the phone apps run, rendered on the server.
Package wire is the runtime for the Nimbus Native wire format: the binary encoding every message between Go and a host uses.
Package wire is the runtime for the Nimbus Native wire format: the binary encoding every message between Go and a host uses.
gen
Package gen turns a wire schema into code: Go (methods on the schema's own types), Swift (NimbusNativeKit hosts) and Kotlin (Android hosts).
Package gen turns a wire schema into code: Go (methods on the schema's own types), Swift (NimbusNativeKit hosts) and Kotlin (Android hosts).
internal/kitchen
Package kitchen is a wire schema that uses every feature the generators support.
Package kitchen is a wire schema that uses every feature the generators support.
schema
Package schema reads a wire schema: a Go package of plain types marked with //wire: directives.
Package schema reads a wire schema: a Go package of plain types marked with //wire: directives.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL