# Mobile Applications

> Develops cross-platform mobile applications with Go using Fyne and Go mobile for iOS and Android with platform-optimized UI patterns.

- Skill: `paulpas/mobile-applications` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/mobile-applications`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/mobile-applications/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/mobile-applications

---





# Go Mobile Applications

Senior mobile engineer building cross-platform applications in Go using Fyne and Go Mobile. This skill covers UI development, platform-specific features, battery-conscious design, and deployment to iOS and Android from a single Go codebase.

## TL;DR Checklist

- [ ] Use Fyne for cross-platform UI or Go Mobile for native bindings — never mix both in the same app
- [ ] Keep UI logic on the main thread; use goroutines for background work
- [ ] Handle lifecycle events (onPause, onResume, onDestroy) for resource management
- [ ] Optimize for touch interaction and mobile screen sizes
- [ ] Use Go Mobile's `bind` for exposing Go functions to native iOS/Android code
- [ ] Profile battery and memory — Go's GC is not designed for mobile real-time constraints

---

## When to Use

Use this skill when:

- Building a cross-platform mobile app from a single Go codebase
- Integrating Go libraries into an existing iOS or Android app via Go Mobile bindings
- Prototyping a mobile app quickly with Fyne's cross-platform widgets
- Needing platform-agnostic networking, crypto, or data processing in a mobile app
- Targeting both iOS and Android with shared business logic

---

## When NOT to Use

Avoid this skill for:

- Apps requiring deep platform-specific UI (use SwiftUI/Kotlin instead)
- Performance-critical real-time apps (game engines, AR/VR — Go GC pauses are unacceptable)
- Apps that need heavy native platform integration (CameraX, CoreML, ML Kit)
- Apps targeting API levels below 23 on Android or iOS 13 on iOS

---

## Core Workflow

1. **Choose the Approach** — Fyne for cross-platform apps with native widgets, or Go Mobile for native bindings.
   **Checkpoint:** Decision is based on UI requirements and platform integration depth.

2. **Set Up the Project Structure** — Organize shared logic, UI code, and platform-specific code.
   **Checkpoint:** Business logic is in a pure Go package that compiles on all platforms.

3. **Implement the UI** — Build interfaces using Fyne widgets or Go Mobile native bindings.
   **Checkpoint:** UI adapts to both portrait and landscape orientations.

4. **Handle Lifecycle Events** — Implement pause, resume, and destroy hooks for resource management.
   **Checkpoint:** Background work is cancelled on pause; resources are released on destroy.

5. **Build and Deploy** — Cross-compile for target platforms using `fyne package` or `gomobile bind`.
   **Checkpoint:** Binary sizes are under 50MB for initial download on both platforms.

---

## Implementation Patterns

### Pattern 1: Fyne Cross-Platform App (❌ BAD vs ✅ GOOD)

Fyne provides a single API for native-looking UI on desktop, iOS, and Android.

#### ❌ BAD — Platform-Specific UI Code

```go
// ❌ BAD: platform-specific UI code — cannot run on all platforms
func main() {
	// This only works on Windows/macOS/Linux
	app := desktop.NewApplication()

	// Android-specific code
	if runtime.GOOS == "android" {
		androidApp := android.NewActivity() // requires CGO, Android NDK
	}

	// iOS-specific code
	if runtime.GOOS == "ios" {
		iOSApp := iOS.NewViewController() // requires CGO, Xcode toolchain
	}

	// UI is built with platform-specific APIs — not portable
	window := app.NewWindow("My App")
	window.Resize(fyne.NewSize(800, 600))
	window.ShowAndRun()
}
```

**What's wrong:**
- Code branches on `runtime.GOOS` — not a single portable binary
- Requires different build toolchains for each platform
- UI code is not shared — each platform needs its own implementation
- Fyne's `desktop.NewApplication()` is not the correct entry point for mobile

#### ✅ GOOD — True Cross-Platform with Fyne

```go
// app is the cross-platform Fyne application singleton.
// It works on desktop, iOS, and Android from the same source code.
var app fyne.App

// main is the entry point for the Fyne mobile application.
// Build with: go build -o myapp .
// Package for iOS: fyne package -os ios -icon icon.png
// Package for Android: fyne package -os android -icon icon.png
func main() {
	app = fyne.NewApp()
	app.SetIcon(fyne.NewResource("icon.png", []byte{}))

	window := app.NewWindow("Go Mobile App")
	window.SetContent(buildUI())

	// Set minimum window size for mobile screens
	window.Resize(fyne.NewSize(360, 640))
	window.SetFixedSize(true) // prevent resize on mobile

	window.ShowAndRun()
}

// buildUI creates the application's user interface using Fyne widgets.
// All widgets render as native controls on each platform.
func buildUI() fyne.CanvasObject {
	// Layout: responsive grid that works on portrait and landscape
	content := fyne.NewContainerWithLayout(
		fyne.NewVBoxLayout(),
		fyne.NewLabel("Go Mobile App"),
		fyne.NewSeparator(),

		// Data entry
		fyne.NewLabel("Enter name:"),
		nameEntry := fyne.NewEntry(),
		nameEntry.SetPlaceHolder("Your name"),

		// Action buttons
		fyne.NewContainerWithLayout(
			fyne.NewHBoxLayout(),
			submitBtn := fyne.NewButton("Submit", func() {
				handleSubmit(nameEntry.Text)
			}),
			fyne.NewButton("Clear", func() {
				nameEntry.SetText("")
			}),
		),

		// Results display
		resultsLabel := fyne.NewLabel(""),
	)

	// Store references in app state for lifecycle management
	appState = &AppState{nameEntry: nameEntry, resultsLabel: resultsLabel}

	return content
}

// AppState holds mutable UI state for access across handlers and lifecycle.
type AppState struct {
	nameEntry    *fyne.Entry
	resultsLabel *fyne.Label
}

var appState *AppState

// handleSubmit processes user input on a background goroutine.
func handleSubmit(name string) {
	if name == "" {
		appState.resultsLabel.SetText("Error: name cannot be empty")
		return
	}

	// UI updates must happen on the main thread
	fyne.Do(func() {
		appState.resultsLabel.SetText("Processing...")
	})

	// Background work in goroutine
	go func() {
		// Simulate network request or computation
		result := processName(name)

		// Back to main thread for UI update
		fyne.Do(func() {
			if result == nil {
				appState.resultsLabel.SetText("Error: processing failed")
			} else {
				appState.resultsLabel.SetText(fmt.Sprintf("Hello, %s!", *result))
			}
		})
	}()
}

// processName processes a name and returns the result.
func processName(name string) *string {
	// Simulate work
	result := strings.ToUpper(strings.TrimSpace(name))
	return &result
}
```

**Why this works:**
- Single codebase runs on desktop, iOS, and Android without changes
- `fyne.Do()` ensures UI updates happen on the main thread
- Background work runs in goroutines, results posted back to main thread
- `SetFixedSize(true)` prevents layout issues on mobile
- `fyne package` handles all platform-specific build steps

---

### Pattern 2: Go Mobile Bindings (❌ BAD vs ✅ GOOD)

Go Mobile generates bindings for calling Go code from native iOS/Android apps.

#### ❌ BAD — Raw CGO Without Bindings

```go
// ❌ BAD: raw CGO without Go Mobile bindings — requires manual JNI/Objective-C code
package main

import "C" // requires CGO and C toolchain

//export GoFunction
func GoFunction() *C.char {
	// Manual memory management — easy to leak
	cstr := C.CString("Hello from Go")
	defer C.free(unsafe.Pointer(cstr))
	return cstr
}

// No automatic binding generation — must write JNI code manually
// for Android and Objective-C bridges for iOS
func main() {}
```

**What's wrong:**
- Raw CGO requires manual bridge code for each platform
- No automatic binding generation — must write JNI/Objective-C manually
- Memory management is error-prone with `C.CString`/`C.free`
- No type safety between Go and native code
- Build requires NDK for Android and Xcode toolchain for iOS separately

#### ✅ GOOD — Go Mobile Bindings

```go
// Package mobilelib provides functions callable from iOS and Android apps.
// Generated bindings with: gomobile bind -target=ios,android
package mobilelib

import (
	"context"
	"fmt"
	"time"
)

// Config holds configuration for the mobile library.
type Config struct {
	APIKey    string
	Timeout   time.Duration
	MaxRetries int
}

// Result represents a processed result from the library.
type Result struct {
	Data    string
	Success bool
	Error   string
}

// ProcessData processes input data and returns a Result.
// This function is available from iOS (Swift/Objective-C) and Android (Java/Kotlin).
func ProcessData(ctx context.Context, input string, cfg Config) (*Result, error) {
	if input == "" {
		return nil, fmt.Errorf("input cannot be empty")
	}
	if cfg.APIKey == "" {
		return nil, fmt.Errorf("API key is required")
	}
	if cfg.Timeout == 0 {
		cfg.Timeout = 30 * time.Second
	}
	if cfg.MaxRetries == 0 {
		cfg.MaxRetries = 3
	}

	// Validate context deadline
	if deadline, ok := ctx.Deadline(); !ok {
		ctx, cancel := context.WithTimeout(ctx, cfg.Timeout)
		defer cancel()
	}

	// Process with retries
	var lastErr error
	for i := 0; i <= cfg.MaxRetries; i++ {
		result, err := processWithAPI(ctx, input, cfg.APIKey)
		if err == nil {
			return &Result{
				Data:    result,
				Success: true,
			}, nil
		}
		lastErr = err

		// Exponential backoff between retries
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(time.Duration(i+1) * 500 * time.Millisecond):
		}
	}

	return nil, fmt.Errorf("process data: all %d retries failed: %w", cfg.MaxRetries, lastErr)
}

// processWithAPI calls the external API with the given input and API key.
func processWithAPI(ctx context.Context, input, apiKey string) (string, error) {
	// Implementation would use http.Client with ctx for cancellation
	// For binding purposes, the function is self-contained
	_ = apiKey // API key used for authentication
	return fmt.Sprintf("processed: %s", input), nil
}

// IsAvailable checks if required system features are available.
// Called from native code to check device capabilities before using Go functions.
func IsAvailable() bool {
	// Check for required system features
	// In a real implementation, this might check GPS, camera, etc.
	return true
}
```

**Why this works:**
- `gomobile bind` generates native bindings automatically for iOS and Android
- Go types map to native types — `string` → `String`/`String`, `bool` → `Bool`/`boolean`
- Context propagation enables cancellation from native code
- Error handling uses Go's idiomatic `error` return, mapped to exceptions in native code
- No manual JNI or Objective-C bridge code needed

---

### Pattern 3: Lifecycle Management (❌ BAD vs ✅ GOOD)

Mobile apps have lifecycle events that must be handled to prevent resource leaks.

#### ❌ BAD — No Lifecycle Awareness

```go
// ❌ BAD: goroutines continue running after app is paused or destroyed
func startBackgroundSync() {
	go func() {
		for {
			// Infinite loop — never stops
			syncData()
			time.Sleep(5 * time.Minute)
		}
	}()
}
```

**What's wrong:**
- Goroutine runs forever — drains battery and memory
- No way to stop the goroutine when the app is paused
- Network requests continue in the background — wastes data and battery
- App can be killed by the OS for excessive resource usage

#### ✅ GOOD — Lifecycle-Aware Background Work

```go
// syncManager manages background synchronization with lifecycle awareness.
// It starts sync on resume and cancels on pause.
type syncManager struct {
	mu       sync.Mutex
	ctx      context.Context
	cancel   context.CancelFunc
	interval time.Duration
	active   bool
}

// newSyncManager creates a sync manager with the given interval.
func newSyncManager(interval time.Duration) *syncManager {
	return &syncManager{interval: interval}
}

// Start begins background synchronization. Safe to call multiple times.
func (m *syncManager) Start() {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.active {
		return // already running
	}

	m.ctx, m.cancel = context.WithCancel(context.Background())
	m.active = true

	go m.syncLoop()
}

// Stop cancels the sync loop and waits for it to finish.
func (m *syncManager) Stop() {
	m.mu.Lock()
	defer m.mu.Unlock()

	if !m.active {
		return
	}

	m.cancel()
	m.active = false
}

// syncLoop runs the synchronization loop, respecting the context deadline.
func (m *syncManager) syncLoop() {
	ticker := time.NewTicker(m.interval)
	defer ticker.Stop()

	for {
		select {
		case <-m.ctx.Done():
			return // cancelled — clean exit
		case <-ticker.C:
			m.doSync(m.ctx)
		}
	}
}

// doSync performs a single synchronization cycle.
func (m *syncManager) doSync(ctx context.Context) {
	// Use context-aware API calls
	if err := syncWithServer(ctx); err != nil {
		// Log but don't retry immediately — let the next tick handle it
		log.Printf("sync failed: %v", err)
	}
}

// OnAppResume is called when the app comes to the foreground.
func OnAppResume(manager *syncManager) {
	manager.Start()
}

// OnAppPause is called when the app goes to the background.
func OnAppPause(manager *syncManager) {
	manager.Stop()
}
```

**Why this works:**
- `context.WithCancel` provides clean cancellation of the sync loop
- `syncManager` is thread-safe with mutex protection
- `Start()` is idempotent — safe to call multiple times
- `Stop()` cancels context, allowing the goroutine to exit cleanly
- Fyne's `OnLifecycle` callback can wire these to actual lifecycle events

---

## Constraints

### MUST DO

- **MUST** ensure all UI updates happen on the main thread using `fyne.Do()` or equivalent
- **MUST** run background work in goroutines and post results back to the main thread
- **MUST** handle lifecycle events (pause, resume, destroy) for resource management
- **MUST** use `context.Context` for cancellable network requests in mobile apps
- **MUST** use `go build` with the target OS flag for cross-compilation (`-os ios`, `-os android`)

### MUST NOT DO

- **MUST NOT** update UI from background goroutines — this causes undefined behavior on iOS
- **MUST NOT** spawn unbounded goroutines — mobile devices have limited CPU and memory
- **MUST NOT** use `time.Sleep` for synchronization in mobile apps — it blocks the main thread
- **MUST NOT** ignore battery consumption — profile goroutine and network usage
- **MUST NOT** mix Fyne and Go Mobile in the same application — choose one approach

---

## Related Skills

| Skill | Purpose |
|-------|---------|
| `best-practices` | Go idioms, naming, and error handling conventions |
| `web-applications` | HTTP client patterns for mobile API communication |
| `modular-design` | Package structure for shared business logic across mobile and web |

