This page describes the architecture of the Immich Flutter mobile application, located under mobile/. It covers the app bootstrap sequence, state management, navigation, local database layers, the asset model, background processing, and iOS-specific native integrations.
For the mobile-facing feature set (backup, timeline display, locked folder, share extension), see 4.5 For the server-side incremental sync protocol that the mobile client consumes, see 3.6 For how the Dart SDK used by the mobile app is generated, see 2.5
The mobile application is a Flutter project targeting iOS and Android. The core technology choices are:
| Concern | Technology / Package |
|---|---|
| UI Framework | Flutter 3.35.7 |
| State Management | hooks_riverpod + riverpod_annotation |
| Navigation | auto_route 9.3.0 |
| New local database | drift (Immich fork of drift 2.26.0) |
| Legacy local database | isar_community |
| Localization | easy_localization |
| Device media access | photo_manager |
| Biometric auth | local_auth_darwin (iOS) |
| Maps | maplibre_gl |
| HTTP (iOS) | cupertino_http |
| HTTP (Android) | cronet_http |
| Background downloads | background_downloader |
| Secure storage | flutter_secure_storage |
| OAuth web flow | flutter_web_auth_2 |
Sources: mobile/pubspec.lock60-750 mobile/.vscode/settings.json1-15
The main() function in mobile/lib/main.dart51-72 initializes the application in a specific order before handing control to Flutter's widget tree.
Bootstrap sequence:
App Architecture Overview
Sources: mobile/lib/main.dart51-72 mobile/lib/main.dart126-295
Bootstrap.initDB() returns a tuple of three database handles: isar (legacy Isar instance), drift (new Drift database), and logDb. All three are injected into the Riverpod ProviderScope as overrides:
dbProvider and isarProvider → isardriftProvider → drift (via driftOverride())The ImmichApp widget uses WidgetsBindingObserver to relay AppLifecycleState changes to appStateProvider.notifier, which coordinates background service start/stop.
Sources: mobile/lib/main.dart62-71 mobile/lib/main.dart133-158
The app uses hooks_riverpod throughout. Providers are defined using both the Provider / NotifierProvider constructors and the riverpod_annotation code-generator.
Key top-level providers:
| Provider | Type | Purpose |
|---|---|---|
appRouterProvider | Provider<AppRouter> | Singleton router instance |
dbProvider / isarProvider | value override | Isar database |
driftProvider | value override | Drift database |
currentUserProvider | state | Authenticated user |
multiSelectProvider | NotifierProvider | Timeline multi-selection state |
actionProvider | NotifierProvider<ActionNotifier> | Asset action execution |
backgroundServiceProvider | Provider | Legacy background backup service |
backgroundWorkerFgServiceProvider | Provider | New foreground background worker |
timelineServiceProvider | Provider | Active timeline service |
Sources: mobile/lib/main.dart62-71 mobile/lib/providers/infrastructure/action.provider.dart26-30 mobile/lib/main.dart215-229
Route Tree
Sources: mobile/lib/routing/router.dart164-300
AppRouter wires up route guards at construction time:
| Guard | Class | Effect |
|---|---|---|
| Auth check | AuthGuard | Redirects unauthenticated users to LoginRoute |
| Duplicate prevention | DuplicateGuard | Prevents pushing the same route twice |
| Gallery permission | GalleryGuard | Checks photo library permission before backup |
| Backup permission | BackupPermissionGuard | Requires gallery permission for backup pages |
| Locked folder | LockedGuard | Requires biometric authentication |
Sources: mobile/lib/routing/router.dart141-159
ImmichApp._deepLinkBuilder() handles two deep-link schemes:
immich:// — internal navigation (e.g., SplashScreenRoute cold-start detection)https://my.immich.app — external share linksBoth are dispatched through deepLinkServiceProvider.
Sources: mobile/lib/main.dart179-198
All *Route classes in mobile/lib/routing/router.gr.dart are generated by auto_route_generator. Each class wraps a corresponding *Page widget and carries typed argument classes (e.g., AssetViewerRouteArgs, AlbumViewerRouteArgs). These are regenerated with build_runner.
The mobile app maintains two local SQLite databases simultaneously during a migration period.
Local Database Architecture
Sources: mobile/lib/main.dart54-68 mobile/lib/infrastructure/entities/remote_asset.entity.dart1-40 mobile/lib/infrastructure/entities/local_asset.entity.dart1-30 mobile/lib/infrastructure/entities/merged_asset.drift1-70
Drift is used for all new "beta timeline" features. The schema is defined in .dart entity files and .drift query files:
remote_asset_entity — server-synced assets (mobile/lib/infrastructure/entities/remote_asset.entity.dart)local_asset_entity — on-device assets (mobile/lib/infrastructure/entities/local_asset.entity.dart)merged_asset query — a UNION ALL query joining remote and local assets for timeline display (mobile/lib/infrastructure/entities/merged_asset.drift)The merged_asset query selects remote assets (filtered to visibility = 0 / timeline, excluding non-primary stack items), unioned with local assets that do not already have a matching remote asset (by checksum). This is the core data source for the main timeline.
The fork of Drift used (https://github.com/immich-app/drift, ref 53ef7e9f) contains Immich-specific patches.
Sources: mobile/pubspec.lock444-468 mobile/lib/infrastructure/entities/merged_asset.drift1-80
Isar (isar_community_flutter_libs) is injected via isarProvider / dbProvider. It holds legacy entity types for Asset, Album, Store, and others that have not yet been migrated to Drift. migrateDatabaseIfNeeded() handles schema migration between the two systems.
Sources: mobile/lib/main.dart54-60
Assets in the domain layer are represented by three classes in base_asset.model.dart:
Asset Model Hierarchy
AssetState (from storage getter) indicates whether an asset is localOnly, remoteOnly, or merged (exists on both device and server, matched by checksum).
AssetVisibility on RemoteAsset tracks: timeline, hidden, archive, locked.
Sources: mobile/lib/domain/models/asset/base_asset.model.dart1-50 mobile/lib/domain/models/asset/remote_asset.model.dart1-80
The TimelineService class manages a paginated, buffered view of assets for display in a scrollable timeline.
Timeline Service Architecture
TimelineFactory creates TimelineService instances for different contexts (main, localAlbum, remoteAlbum, favorite, trash, archive, lockedFolder, video, place, person, map).
TimelineService maintains an internal _buffer: List<BaseAsset> and _bufferOffset. When the UI requests loadAssets(index, count), the service either serves from the buffer or fetches a new batch from the database, biased toward the scroll direction to reduce DB round-trips. Bucket changes (from the Stream<List<Bucket>>) trigger a full buffer refresh and emit a TimelineReloadEvent.
TimelineOrigin enumerates all possible timeline contexts:
main, localAlbum, remoteAlbum, remoteAssets, favorite, trash, archive,
lockedFolder, video, place, person, map, search, deepLink, albumActivities
Sources: mobile/lib/domain/services/timeline.service.dart1-195 mobile/lib/infrastructure/repositories/timeline.repository.dart1-180
Asset actions (favorite, archive, trash, download, delete, move to locked folder, etc.) flow through two layers:
ActionService — stateless service that calls API repositories and local DB repositories directly.ActionNotifier — a Riverpod Notifier that reads selection state from multiSelectProvider or the current asset viewer, then delegates to ActionService and DownloadService.Action Flow
ActionSource is an enum with two values: timeline (operates on multiSelectProvider.selectedAssets) and viewer (operates on currentAssetNotifier).
Sources: mobile/lib/providers/infrastructure/action.provider.dart26-132 mobile/lib/services/action.service.dart28-130
The app has two background service implementations:
| Service | Provider | Notes |
|---|---|---|
| Legacy backup service | backgroundServiceProvider | Used when beta timeline is disabled |
| New foreground worker | backgroundWorkerFgServiceProvider | Used when Store.isBetaTimelineEnabled |
On startup, ImmichAppState.initState() reads Store.isBetaTimelineEnabled and enables the appropriate service. On Android, the new service sets notification text via saveNotificationMessage().
Sources: mobile/lib/main.dart213-233
The iOS Info.plist registers four background task identifiers with BGTaskSchedulerPermittedIdentifiers:
| Identifier | Purpose |
|---|---|
app.alextran.immich.background.refreshUpload | Periodic upload refresh |
app.alextran.immich.background.processingUpload | Upload processing |
app.alextran.immich.backgroundFetch | Background fetch |
app.alextran.immich.backgroundProcessing | Background processing |
UIBackgroundModes includes fetch and processing.
AppDelegate.swift registers these tasks with BGTaskScheduler and bridges them to Flutter via platform channels.
Sources: mobile/ios/Runner/Info.plist7-13 mobile/ios/Runner/Info.plist159-163 mobile/ios/Runner/AppDelegate.swift1-40
The Xcode project contains three build targets and several native Swift modules:
iOS Project Target Structure
Sources: mobile/ios/Runner.xcodeproj/project.pbxproj230-340 mobile/ios/Runner.xcodeproj/project.pbxproj342-412
Files named *.g.swift (e.g., BackgroundWorker.g.swift, Connectivity.g.swift, LocalImages.g.swift, RemoteImages.g.swift) are generated by Flutter's Pigeon tool. They define typed platform channel APIs between Dart and Swift. Their Dart counterparts are the *.g.dart files under mobile/lib/platform/.
ShareExtension is a separate app extension target with ShareViewController.swift. It receives files shared from other iOS apps and routes them back to the main app via a URL scheme (ShareMedia-<bundle-id>), which triggers the share-intent upload flow.
Sources: mobile/ios/Runner/Info.plist87-107 mobile/ios/Runner.xcodeproj/project.pbxproj316-326
WidgetExtension uses WidgetKit.framework and SwiftUI.framework to render a home-screen widget. It reads asset data from a shared app group ($(CUSTOM_GROUP_ID)) populated by the main app. The home_widget Flutter package bridges configuration from Flutter to the widget.
Sources: mobile/ios/Runner.xcodeproj/project.pbxproj374-411 mobile/ios/Podfile.lock34-35
| Permission Key | Reason |
|---|---|
NSPhotoLibraryUsageDescription | Backup photos |
NSPhotoLibraryAddUsageDescription | Save downloaded assets |
NSCameraUsageDescription | In-app camera |
NSMicrophoneUsageDescription | Video recording |
NSLocationWhenInUseUsageDescription | WiFi SSID for background upload |
NSFaceIDUsageDescription | Locked folder biometrics |
NSLocalNetworkUsageDescription | Local server access and casting |
Sources: mobile/ios/Runner/Info.plist135-152
The app uses easy_localization with CodegenLoader. Translation strings are loaded from assets/i18n/ (the shared i18n/en.json at the repo root is the source of truth). Key generation for type-safe access is done via build_runner. The supported locale list is declared in locales.dart and mirrored in Info.plist's CFBundleLocalizations.
For the full i18n system documentation, see 7.4
Sources: mobile/lib/main.dart75-76 mobile/lib/main.dart281-295 mobile/ios/Runner/Info.plist44-77
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.