17 KiB
App Boilerplate
A modular, offline-first Flutter boilerplate for apps that store, sync, and share media and metadata across centralized (Immich, Firebase) and decentralized (Nostr) systems.
Phase 8 - Navigation & UI Scaffold
- Complete navigation structure connecting all main modules
- Bottom navigation bar with 5 main screens: Home, Immich, Nostr Events, Session, Settings
- Route guards requiring authentication for protected screens
- Placeholder screens for all modules ready for custom UI implementation
- Modular navigation architecture with testable components
- Comprehensive UI tests for navigation and route guards
Quick Start
# Install dependencies
flutter pub get
# Run tests
flutter test
# Run app
flutter run
Local Storage & Caching
Service for local storage and caching operations. Provides CRUD operations for items stored in SQLite, image caching with automatic download/storage, and cache hit/miss handling. Modular design with no UI dependencies.
Files:
lib/data/local/local_storage_service.dart- Main service classlib/data/local/models/item.dart- Item data modeltest/data/local/local_storage_service_test.dart- Unit tests
Key Methods: initialize(), insertItem(), getItem(), getAllItems(), updateItem(), deleteItem(), getCachedImage(), clearImageCache(), close()
Immich Integration
Service for interacting with Immich API. Uploads images, fetches asset lists, and automatically stores metadata in local database. Offline-first design allows access to cached metadata without network.
Configuration: Edit lib/config/config_loader.dart to set immichBaseUrl and immichApiKey for dev/prod environments (lines 40-41 for dev, lines 47-48 for prod).
Files:
lib/data/immich/immich_service.dart- Main service classlib/data/immich/models/immich_asset.dart- Asset modellib/data/immich/models/upload_response.dart- Upload response modeltest/data/immich/immich_service_test.dart- Unit tests
Key Methods: uploadImage(), fetchAssets(), getCachedAsset(), getCachedAssets()
Nostr Integration
Service for decentralized metadata synchronization using Nostr protocol. Generates keypairs, publishes events, and syncs metadata across multiple relays. Modular design allows testing without real relay connections.
Files:
lib/data/nostr/nostr_service.dart- Main service classlib/data/nostr/models/nostr_keypair.dart- Keypair modellib/data/nostr/models/nostr_event.dart- Event modellib/data/nostr/models/nostr_relay.dart- Relay modeltest/data/nostr/nostr_service_test.dart- Unit tests
Key Methods: generateKeyPair(), addRelay(), connectRelay(), publishEvent(), syncMetadata(), dispose()
Sync Engine
Engine for coordinating data synchronization between local storage, Immich, and Nostr. Handles conflict resolution, offline queuing, and automatic retries. Processes operations by priority with configurable conflict resolution strategies.
Files:
lib/data/sync/sync_engine.dart- Main sync engine classlib/data/sync/models/sync_status.dart- Status and priority enumslib/data/sync/models/sync_operation.dart- Operation modeltest/data/sync/sync_engine_test.dart- Unit and integration tests
Key Methods: syncToImmich(), syncFromImmich(), syncToNostr(), syncAll(), queueOperation(), resolveConflict(), getPendingOperations()
Conflict Resolution: useLocal, useRemote, useLatest, merge - set via setConflictResolution()
Relay Management UI
User interface for managing Nostr relays. View configured relays, add/remove relays, monitor connection health, and trigger manual syncs. Modular design with controller-based state management for testability.
Files:
lib/ui/relay_management/relay_management_screen.dart- Main UI screenlib/ui/relay_management/relay_management_controller.dart- State management controllertest/ui/relay_management/relay_management_screen_test.dart- UI teststest/ui/relay_management/relay_management_controller_test.dart- Controller tests
Key Features: Add/remove relays, connect/disconnect, health monitoring, manual sync trigger, error handling
Usage: Navigate to "Manage Relays" from the main screen after initialization.
User Session Management
Service for managing user sessions, login, logout, and session isolation. Provides per-user data isolation with separate storage paths and cache directories. Clears cached data on logout and integrates with local storage and sync engine.
Files:
lib/data/session/session_service.dart- Main session management servicelib/data/session/models/user.dart- User modeltest/data/session/session_service_test.dart- Unit tests
Key Methods: login(), logout(), switchSession(), getCurrentUserDbPath(), getCurrentUserCacheDir()
Features: Per-user storage isolation, cache clearing on logout, session switching with data preservation, integration with local storage
Usage: Initialize SessionService with LocalStorageService and optional SyncEngine. Call login() to start a session, logout() to end it, and switchSession() to change users.
Firebase Layer
Optional Firebase integration providing cloud sync, storage, authentication, push notifications, and analytics. Fully modular - can be enabled or disabled without affecting offline-first functionality. Integrates with local storage and session management to maintain offline-first behavior.
Files:
lib/data/firebase/firebase_service.dart- Main Firebase servicelib/data/firebase/models/firebase_config.dart- Firebase configuration modeltest/data/firebase/firebase_service_test.dart- Unit tests
Key Methods: initialize(), loginWithEmailPassword(), logout(), syncItemsToFirestore(), syncItemsFromFirestore(), uploadFile(), getFcmToken(), logEvent()
Features: Firestore cloud sync, Firebase Storage for media, Firebase Auth for authentication, Firebase Cloud Messaging for push notifications, Firebase Analytics for analytics, all optional and modular
Usage: Create FirebaseService with FirebaseConfig (disabled by default). Pass to SessionService for automatic sync on login/logout. Initialize Firebase with initialize() before use. All services gracefully handle being disabled.
Note: Firebase requires actual Firebase project setup with google-services.json (Android) and GoogleService-Info.plist (iOS) configuration files. The service handles missing configuration gracefully and maintains offline-first behavior.
Navigation & UI Scaffold
Complete navigation structure connecting all main modules with bottom navigation bar. Includes route guards for protected screens and placeholder screens ready for custom UI implementation.
Files:
lib/ui/navigation/main_navigation_scaffold.dart- Main navigation scaffold with bottom navlib/ui/navigation/app_router.dart- Router with route guards and route generationlib/ui/home/home_screen.dart- Home screen (local storage items)lib/ui/immich/immich_screen.dart- Immich media screen (placeholder)lib/ui/nostr_events/nostr_events_screen.dart- Nostr events screen (placeholder)lib/ui/session/session_screen.dart- Session management (login/logout)lib/ui/settings/settings_screen.dart- Settings screentest/ui/navigation/main_navigation_scaffold_test.dart- Navigation tests
Navigation Structure:
- Home - Local storage and cached content (no auth required)
- Immich - Immich media integration (requires login)
- Nostr Events - Nostr events display (requires login)
- Session - User login/logout (no auth required)
- Settings - App settings and Relay Management access (no auth required)
Route Guards: Immich and Nostr Events screens require authentication. Unauthenticated users see a login prompt with option to navigate to Session screen.
Usage: The app automatically uses MainNavigationScaffold after initialization. All services are passed to the scaffold for dependency injection. Customize placeholder screens by editing the respective screen files in lib/ui/.
Running UI Tests:
flutter test test/ui/navigation/main_navigation_scaffold_test.dart
Configuration
Configuration uses .env files for sensitive values (API keys, URLs) with fallback defaults in lib/config/config_loader.dart.
Setup .env File
-
Create
.env.examplefile in the project root with the following template:# Application Configuration APP_NAME=app_boilerplate # Immich Configuration IMMICH_BASE_URL=https://photos.satoshinakamoto.win IMMICH_API_KEY_DEV=your-dev-api-key-here IMMICH_API_KEY_PROD=your-prod-api-key-here # Nostr Relays (comma-separated list) NOSTR_RELAYS_DEV=wss://nostrum.satoshinakamoto.win,wss://nos.lol NOSTR_RELAYS_PROD=wss://relay.damus.io # API Configuration API_BASE_URL_DEV=https://api-dev.example.com API_BASE_URL_PROD=https://api.example.com # Logging ENABLE_LOGGING_DEV=true ENABLE_LOGGING_PROD=false # Firebase Configuration (optional) FIREBASE_ENABLED=false FIREBASE_FIRESTORE_ENABLED=true FIREBASE_STORAGE_ENABLED=true FIREBASE_AUTH_ENABLED=true FIREBASE_MESSAGING_ENABLED=true FIREBASE_ANALYTICS_ENABLED=true -
Copy
.env.exampleto.envand fill in your actual values:cp .env.example .env -
Edit
.envwith your actual configuration values.
Important: The .env file is in .gitignore and should never be committed to version control. Only commit .env.example as a template.
Environment Variables
The app uses:
.envfile (recommended) - Loaded at runtime, falls back to defaults if not found--dart-define- For environment selection:
# Set environment at runtime
flutter run --dart-define=ENV=prod
If .env file is not found or variables are missing, the app uses default values from lib/config/config_loader.dart.
Available Environments
dev- Development (default): Logging enabled, dev API URLprod- Production: Logging disabled, production API URL
App Name Configuration
1. Set APP_NAME in your .env file
2. Run the script
./scripts/set_app_name.sh
To make this easier, you can use the provided script that reads APP_NAME from .env and automatically updates all platform files:
# Make sure APP_NAME is set in your .env file first
./scripts/set_app_name.sh
This script will:
- Read
APP_NAMEfrom your.envfile - Update
android/app/src/main/AndroidManifest.xml - Update
macos/Runner/Configs/AppInfo.xcconfig
Manual Setup: If you prefer to set it manually, just update the files listed above directly.
App Icon Configuration
App icons are platform-specific and must be placed in the correct directories with the correct formats.
Android Icons
Location: android/app/src/main/res/mipmap-*/
Format: PNG files
Required Sizes:
mipmap-mdpi/ic_launcher.png- 48x48 pxmipmap-hdpi/ic_launcher.png- 72x72 pxmipmap-xhdpi/ic_launcher.png- 96x96 pxmipmap-xxhdpi/ic_launcher.png- 144x144 pxmipmap-xxxhdpi/ic_launcher.png- 192x192 px
Instructions:
- Create your app icon as a square image (recommended: 1024x1024 px source)
- Generate all required sizes using an icon generator tool (e.g., App Icon Generator)
- Replace the existing
ic_launcher.pngfiles in eachmipmap-*directory - The icon is referenced in
AndroidManifest.xmlas@mipmap/ic_launcher
Best Practices:
- Use PNG format (no transparency for launcher icons on some Android versions)
- Keep icon simple and recognizable at small sizes
- Follow Material Design guidelines for Android icons
- Ensure icon works on both light and dark backgrounds
macOS Icons
Location: macos/Runner/Assets.xcassets/AppIcon.appiconset/
Format: PNG files
Required Sizes:
app_icon_16.png- 16x16 pxapp_icon_32.png- 32x32 pxapp_icon_64.png- 64x64 pxapp_icon_128.png- 128x128 pxapp_icon_256.png- 256x256 pxapp_icon_512.png- 512x512 pxapp_icon_1024.png- 1024x1024 px
Instructions:
- Create your app icon as a square image (recommended: 1024x1024 px source)
- Generate all required sizes
- Replace the existing PNG files in
AppIcon.appiconset/directory - The
Contents.jsonfile defines which sizes map to which files - update if needed
Best Practices:
- Use PNG format
- macOS icons can have transparency
- Follow macOS Human Interface Guidelines
- Icon should be recognizable at 16x16 size
iOS Icons (if adding iOS support)
Location: ios/Runner/Assets.xcassets/AppIcon.appiconset/
Format: PNG files (no transparency for some sizes)
Required Sizes: iOS requires many sizes. Use Xcode's App Icon set or a tool like App Icon Generator to generate all required sizes automatically.
Best Practices:
- Use PNG format
- Some sizes require no transparency (check iOS guidelines)
- Follow iOS Human Interface Guidelines
- Generate all sizes from a 1024x1024 px source
Icon Generation Tools
Recommended tools for generating all required icon sizes:
- AppIcon.co - Online tool, supports multiple platforms
- IconKitchen - Google's icon generator
- MakeAppIcon - Generates all sizes from one image
- Xcode (for macOS/iOS) - Built-in asset catalog editor
Quick Setup
- Prepare your icon: Create a 1024x1024 px square PNG image
- Generate sizes: Use one of the tools above to generate all required sizes
- Replace files: Copy generated icons to the appropriate directories
- Test: Run the app and verify icons appear correctly
Running the App
Android Emulator
Important: Wait for emulator to fully boot (30-60 seconds on cold boot).
- Start emulator from Android Studio AVD Manager
- Wait for boot animation to complete and home screen appears
- Run:
flutter run
iOS Simulator (macOS)
open -a Simulator
flutter run
Running Tests
# Run all tests
flutter test
# Run with coverage
flutter test --coverage
Important: Tests must be run separately - flutter run does not execute tests.
Project Structure
lib/
├── config/
│ ├── app_config.dart
│ └── config_loader.dart
├── data/
│ ├── local/
│ │ ├── local_storage_service.dart
│ │ └── models/
│ │ └── item.dart
│ ├── immich/
│ │ ├── immich_service.dart
│ │ └── models/
│ │ ├── immich_asset.dart
│ │ └── upload_response.dart
│ ├── nostr/
│ │ ├── nostr_service.dart
│ │ └── models/
│ │ ├── nostr_keypair.dart
│ │ ├── nostr_event.dart
│ │ └── nostr_relay.dart
│ ├── session/
│ │ ├── session_service.dart
│ │ └── models/
│ │ └── user.dart
│ ├── firebase/
│ │ ├── firebase_service.dart
│ │ └── models/
│ │ └── firebase_config.dart
│ └── sync/
│ ├── sync_engine.dart
│ └── models/
│ ├── sync_status.dart
│ └── sync_operation.dart
├── ui/
│ ├── navigation/
│ │ ├── main_navigation_scaffold.dart
│ │ └── app_router.dart
│ ├── home/
│ │ └── home_screen.dart
│ ├── immich/
│ │ └── immich_screen.dart
│ ├── nostr_events/
│ │ └── nostr_events_screen.dart
│ ├── session/
│ │ └── session_screen.dart
│ ├── settings/
│ │ └── settings_screen.dart
│ └── relay_management/
│ ├── relay_management_screen.dart
│ └── relay_management_controller.dart
└── main.dart
test/
├── config/
│ └── config_loader_test.dart
└── data/
├── local/
│ └── local_storage_service_test.dart
├── immich/
│ └── immich_service_test.dart
├── nostr/
│ └── nostr_service_test.dart
├── session/
│ └── session_service_test.dart
├── firebase/
│ └── firebase_service_test.dart
├── sync/
│ └── sync_engine_test.dart
└── ui/
├── navigation/
│ └── main_navigation_scaffold_test.dart
└── relay_management/
├── relay_management_screen_test.dart
└── relay_management_controller_test.dart