This project is a proof of concept under heavy development with the support of models focused on efficiency like Google's Gemini Flash family. The initial goal is to evaluate the models and the support in tooling (Android Studio, Antigravity, Gemini CLI, and so on). The file structure and guiding through markdown files (AGENTS.md and so on) is also under heavy development looking for an ideal that balances output quality and cost. The ultimate goal is to learn more about agentic development for Kotlin Multiplatform projects.
A modern, fluid, and high-performance Kotlin Multiplatform (KMP) application utilizing the public Unsplash API to deliver a rich, responsive photo discovery experience on Android, iOS, and macOS.
The Unsplash Image Feed App demonstrates clean, scalable multiplatform engineering by uniting a shared Kotlin Multiplatform business and presentation core with native platform UI toolkits:
- Android: Jetpack Compose powered by modern Material 3 design tokens, dynamic color tonal palettes, and Jetpack Navigation 3.
- iOS: SwiftUI (iOS 17+) featuring a frosted glass design system (
GlassTheme), translucent chrome, interactive detented sheets, and sensory haptic feedback. - macOS: Native desktop target compiling via XcodeGen with a multi-column
NavigationSplitViewsidebar, responsive grid scaling, and desktop keyboard shortcuts.
The application adheres strictly to the Unsplash API developer guidelines (hotlinking, attribution, ixid retention, download tracking) and implements a rich photo discovery experience across mobile, tablet, foldable, and desktop form factors.
The application delivers a rich set of capabilities across six major screen destinations:
- Editorial & Topic Feeds: Toggle between general editorial curation and specialized topic categories (e.g. Wallpapers, Nature, 3D Renders, Travel, Architecture) via an interactive horizontal pill selector.
- Responsive Multi-Column Grids: Dynamic column distribution that seamlessly adapts across mobile phones, foldable postures, tablets, and widescreen desktop windows.
- BlurHash Placeholders: Instant image decode placeholders computed from Unsplash BlurHash strings for seamless, zero-flash image load transitions.
- Dynamic CDN Optimization: Requests images appended with calculated container widths (
&w=,&q=80,&auto=format), minimizing bandwidth and device RAM usage. - Shake-to-Shuffle: Shake physical devices (accelerometer detector) or trigger the shuffle shortcut (βS / toolbar button) to discover a curated random photo.
- Infinite Pagination & Pull-to-Refresh: Seamless pre-fetching before reaching the bottom of the feed, paired with native pull-to-refresh mechanics on both platforms.
- Real-Time Debounced Queries: Responsive search input with debounced typing to prevent excessive API calls.
- Tri-Facet Category Tabs: Instant segmentation across Photos, Collections, and Users.
- Filter Sheets: Fine-tune queries with configurable sheet controls for image orientation (All, Landscape, Portrait, Squarish), primary color palette, and sort order (Relevant, Latest).
- Clickable Tag Navigation: Tapping any keyword tag on a photo detail view launches search with the selected query pre-filled.
- Full-Canvas Image View & Zoom: Pinch-to-zoom (1.0x to 4.0x) with pan gestures and double-tap zoom toggle.
-
Interactive Frosted Glass Inspector Sheet (iOS): Non-modal interactive detented bottom sheet (
.presentationDetents([.fraction(0.35), .fraction(0.70), .large])) with background interaction enabling simultaneous photo inspection and metadata browsing. - Responsive Dual-Pane Layout (Android): Side-by-side photo canvas and metadata inspector deck on wide screens, tablets, and book-posture foldables (with hinge crease avoidance).
- Inline Geolocation with Google Maps (Android): Visualizes image capture coordinates on an inline Google Map with custom dark-mode styling.
-
Comprehensive EXIF Specs: Detailed camera data including Make, Model, Lens, Aperture (
$f$ -stop), Shutter Speed, ISO, and focal dimensions. - Popularity Metrics & Tracking: Photo view counts, download numbers, like counts, and compliant Unsplash download tracking triggers.
- Mosaic Composite Cards: Rich 3-photo preview mosaics displaying collection cover imagery, titles, photo counts, and curator attribution.
- Collection Detail View: Dedicated infinite photo stream for any selected collection.
- Related Collections Carousel: Discover contextually related photo albums at the bottom of the collection detail view.
- Public Photographer Portfolio: Profile header with avatar, bio, location, and social links.
- Segmented Feeds: Browse a creator's work partitioned into Photos (uploads), Likes, and curated Collections.
- Interactive Trend Charts: Dual timeline charts tracking historical Views and Downloads with touch-drag scrubbing tooltips.
- Android Material 3: Full M3 tonal palettes, dynamic color support (Android 12+), semantic surface containers (
surfaceContainerLowestthroughsurfaceContainerHighest),M3 SearchBar,PullToRefreshBox, and predictive back navigation. - iOS Glassmorphism:
GlassThemematerial system with.ultraThinMaterial, floating glass category capsules, floating glass attribution chips, iOS 17.scrollTransitionscale/opacity dynamics, and declarative.sensoryFeedback. - Fluid Motion & Shared Transitions: Hero transitions via
matchedGeometryEffect(iOS) andSharedTransitionLayout(Android), staggered grid entrance animations, and springy press-scale feedback.
The app uses a unified, cross-linked navigation model where users can navigate smoothly from any entity (photo, collection, curator, tag) to its associated screens.
flowchart TD
%% Root Navigation Shells
subgraph RootShells["Root Navigation Shells"]
direction TB
AndroidShell["Android: Jetpack Navigation 3\n(NavigationRail β₯ 600dp | NavigationBar < 600dp)"]
iOSShell["iOS: Translucent TabView\n(Photos | Collections)"]
MacOSShell["macOS: NavigationSplitView\n(Sidebar Selection)"]
end
%% Primary Screens
subgraph PrimaryScreens["Primary Screen Destinations"]
FeedScreen["πΈ Photos Feed Screen\n- Editorial & Topic Feeds\n- Floating Glass Category Strip\n- Shake to Randomize"]
CollectionsScreen["π Collections Feed Screen\n- Curated Mosaic Cards\n- Infinite Collection Grid"]
SearchScreen["π Search Screen\n- Tri-Facet: Photos | Collections | Users\n- Filter Sheets & Live Debounce"]
end
%% Detail Screens
subgraph DetailScreens["Detail Screen Destinations"]
PhotoDetails["πΌοΈ Photo Details Screen\n- Full Canvas Pinch Zoom (1x-4x)\n- Frosted Glass Detented Sheet (iOS)\n- Dual-Pane & Inline Google Maps (Android)\n- EXIF, Stats & Download Tracking"]
CollectionDetails["π Collection Details Screen\n- Collection Cover & Curator Header\n- Infinite Photo Grid\n- Related Collections Carousel"]
UserProfile["π€ User Profile Screen\n- Bio & Social Links\n- Drag-Scrubbing Views & Downloads Charts\n- Segmented: Photos | Likes | Collections"]
end
%% Root Navigation Links
RootShells -->|"Tab / Rail / Sidebar"| FeedScreen
RootShells -->|"Tab / Rail / Sidebar"| CollectionsScreen
RootShells -->|"Tab / Rail (Android)"| SearchScreen
%% Photos Feed Transitions
FeedScreen -->|"Tap Photo Card\n(Hero Transition)"| PhotoDetails
FeedScreen -->|"Tap Photographer\nAttribution Capsule"| UserProfile
FeedScreen -->|"Tap Search Icon\n(or βF)"| SearchScreen
FeedScreen -->|"Shake Device / Shuffle\n(or βS)"| PhotoDetails
%% Collections Feed Transitions
CollectionsScreen -->|"Tap Collection Card"| CollectionDetails
CollectionsScreen -->|"Tap Search Icon"| SearchScreen
%% Photo Details Transitions
PhotoDetails -->|"Tap Photographer\nProfile Header"| UserProfile
PhotoDetails -->|"Tap Keyword / Tag Chip\n(Pre-populates Query)"| SearchScreen
%% Collection Details Transitions
CollectionDetails -->|"Tap Photo Card"| PhotoDetails
CollectionDetails -->|"Tap Related Collection"| CollectionDetails
%% User Profile Transitions
UserProfile -->|"Tap Photo\n(Photos / Likes Tab)"| PhotoDetails
UserProfile -->|"Tap Collection\n(Collections Tab)"| CollectionDetails
%% Search Transitions
SearchScreen -->|"Tap Photo Result"| PhotoDetails
SearchScreen -->|"Tap Collection Result"| CollectionDetails
SearchScreen -->|"Tap User Result"| UserProfile
-
Android Navigation 3 (
NavBackStack+NavDisplay):- Type-safe, sealed route hierarchy:
AppRoute.Feed,AppRoute.Collections,AppRoute.CollectionDetails(collectionId),AppRoute.Search(query),AppRoute.PhotoDetails(photoId),AppRoute.UserProfile(username). - Responsive navigation container: Start-docked
NavigationRailwith quick-shuffle access on Medium/Expanded displays ($\ge 600\text{dp}$ ), switching to a bottomNavigationBaron compact screens (< 600dp). - Smooth entry transitions wrapped in
SharedTransitionLayoutfor hero photo expansion.
- Type-safe, sealed route hierarchy:
-
iOS & macOS SwiftUI Navigation (
NavigationStack):- Typed path back stacks using
FeedPathItemandCollectionPathItemstructs (type: photo, user, collection, search). - Translucent glass
TabViewon iOS with.toolbarBackground(.ultraThinMaterial, for: .tabBar). - Native
NavigationSplitViewmaster-detail sidebar layout on macOS. - Interactive hero card zoom transitions powered by
matchedGeometryEffect.
- Typed path back stacks using
The codebase follows the Shared MVI / Unidirectional Data Flow (UDF) Pattern, maximizing cross-platform code reuse while retaining completely native rendering and scrolling performance.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β App View Shells β
β β
β βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β androidApp (Jetpack Compose)β β iosApp (SwiftUI) β β
β β - Material 3 Design Tokens β β - Glassmorphic Design β β
β β - Jetpack Navigation 3 β β - NavigationStack β β
β β - Adaptive Rail & Dual-Pane β β - Detented Sheets β β
β β - Google Maps Integration β β - iOS & macOS Targets β β
β βββββββββββββββββ¬ββββββββββββββββ βββββββββββββββ¬ββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββ
β (Collects StateFlow) β (Observes CommonFlow)
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β shared Module (Kotlin Multiplatform) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Shared MVI ViewModels (UDF Pattern) β β
β β (FeedViewModel, UnifiedSearchViewModel, PhotoDetailsViewModel, β β
β β CollectionDetailViewModel, CollectionsFeedViewModel, β β
β β RandomPhotoViewModel, UserProfileViewModel) β β
β β * MviViewModel (UiState, UiIntent, UiEffect) & viewModelScope β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Metro Compile-Time Dependency Injection β β
β β (@DependencyGraph, @Inject, @AssistedFactory) β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UnsplashRepository β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UnsplashApiClient β β
β β (Ktor 3.6 HTTP Engine, ContentNegotiation, Json) β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β BuildKonfig (Compile-Time Config Injection) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
shared(commonMain,appleMain,androidMain):- Compile-Time DI: Fast, reflection-free dependency injection with Metro (
dev.zacsweers.metro). - Shared MVI / UDF Architecture: Presentation logic is standardized on
MviViewModel,UiState,UiIntent, andUiEffectscoped toviewModelScope, providing structured concurrency, predictable state transitions, and deterministic teardown. - Networking: High-performance HTTP client using Ktor 3.6 with Darwin engine for Apple targets and OkHttp engine for Android.
- Compile-Time DI: Fast, reflection-free dependency injection with Metro (
androidApp: Pure Jetpack Compose UI shell with Navigation 3, Coil 3.6 image pipeline, Google Maps Compose, and Material 3 design tokens.iosApp: Pure SwiftUI shell observing shared Kotlin flows into Swift 5.9+@Observableview models, supporting iOS 17+ and macOS 14+ targets.
The codebase is executing a structured modularization migration from a monolithic :shared module to Vertical Feature Modules with an Umbrella Framework (Option B: Vertical Feature Ownership), as recommended in the architectural research evaluation (specs/researches/modularization_evaluation.md):
graph TD
subgraph Platform UI Shells
Android["androidApp (Compose + Nav 3)"]
Apple["iosApp / macosApp (SwiftUI)"]
end
subgraph Assembly & Umbrella
Umbrella[":shared (Umbrella Framework & Metro Graph)"]
end
subgraph Feature Modules [Vertical Feature Modules (VM + Repo + Client)]
FeatFeed[":feature:feed"]
FeatDetails[":feature:details"]
FeatSearch[":feature:search"]
FeatCollections[":feature:collections"]
FeatProfile[":feature:profile"]
end
subgraph Core Modules [Foundational Core Modules]
CoreNetwork[":core:network (Ktor Engine, PhotoDownloadTracker, CollectionsExt)"]
CoreModel[":core:model (Entities & DTOs)"]
CoreMvi[":core:mvi (MviViewModel, UiState, UiIntent, UiEffect)"]
end
Android --> Umbrella
Apple --> Umbrella
Umbrella --> FeatFeed
Umbrella --> FeatDetails
Umbrella --> FeatSearch
Umbrella --> FeatCollections
Umbrella --> FeatProfile
FeatFeed --> CoreNetwork
FeatFeed --> CoreMvi
FeatFeed --> CoreModel
FeatDetails --> CoreNetwork
FeatDetails --> CoreMvi
FeatDetails --> CoreModel
FeatSearch --> CoreNetwork
FeatSearch --> CoreMvi
FeatSearch --> CoreModel
FeatCollections --> CoreNetwork
FeatCollections --> CoreMvi
FeatCollections --> CoreModel
FeatProfile --> CoreNetwork
FeatProfile --> CoreMvi
FeatProfile --> CoreModel
CoreNetwork --> CoreModel
The migration is partitioned into four phased specifications:
- Phase 1: Foundation & Core Extraction (Spec 46): Extract
:core:model,:core:mvi, and:core:network(withPhotoDownloadTrackerandCollectionsExt), eliminating centralized:core:data. - Phase 2: Pilot Feature Migration (Spec 47): Migrate Photo Details (
:feature:details) to vertically ownPhotoDetailsRepository,PhotoDetailsApiClient, andPhotoStats, validating multi-module Metro assisted injection and Apple framework linking. - Phase 3: Remaining Feature Extractions (Spec 48): Extract
:feature:feed,:feature:search,:feature:collections, and:feature:profileto vertically own their repositories, clients, fakes, and models (UserStats), completely retiring the legacy monolithicUnsplashRepository. - Phase 4: Umbrella Framework & Linking Optimization (Spec 49): Configure selective binary export to minimize Objective-C bridging stubs, leverage
internalvisibility linking benefits, optimize Gradle build caching, and tune CI execution matrices.
To comply with security best practices, the application loads API credentials from local, git-ignored configuration files at compile time.
- Create a
local.propertiesfile in the root directory (if not already present):touch local.properties
- Add your Unsplash Access Key:
unsplash.api.key=YOUR_UNSPLASH_ACCESS_KEY_HEREBuildKonfigautomatically injects this property into the shared module during compilation (BuildKonfig.UNSPLASH_API_KEY).
To render inline Google Maps in Android's Photo Details screen:
- Append your Google Maps SDK key to
local.properties:Gradle injects this key into the Android Manifest placeholders at build time.google.maps.api.key=YOUR_GOOGLE_MAPS_API_KEY_HERE
This project adheres to a strict spec-driven engineering workflow:
- Specs & Planning: Every change is anchored in a specification document in
specs/. Checkspecs/steps.mdfor historical implementation notes. - Zero Warning Policy: Code changes must produce zero compiler warnings and clean lint reports.
- Dual-Platform Build Policy: Any change affecting
shared/or platform logic must be verified on both Android and iOS targets. While macOS is supported via the shared Xcode project, pre-commit CI and automated verification focus on the dual mobile targets (androidAppandiosApp). - Architecture Rules: Review
AGENTS.mdand the workspace skills in.agents/skills/for guidelines on MVI ViewModel lifecycles, cross-language interop, and UI styling.
- macOS (Required to compile the iOS and macOS native targets)
- Android Studio Ladybug / Koala+ with the Kotlin Multiplatform Mobile plugin
- Xcode 15+ / 16+
- JDK 17 or newer
- XcodeGen (
brew install xcodegen)
- Open the project folder in Android Studio.
- Allow Gradle to sync dependencies.
- Select
androidAppin the run configuration dropdown. - Launch on an Android Emulator (API 29+) or connected physical device.
Command-line build:
./gradlew :androidApp:assembleDebugThe shared KMP framework compiles for iOS device and simulator architectures (iosArm64, iosSimulatorArm64).
- Generate the Xcode project:
xcodegen generate --spec iosApp/project.yml
- Open
iosApp/iosApp.xcodeprojin Xcode. - Select the
iosAppscheme and an iOS Simulator (iPhone 15+ / iOS 17+). - Build and run with βR.
Command-line verification (XcodeGen + iOS Simulator build):
xcodegen generate --spec iosApp/project.yml && xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -destination 'generic/platform=iOS Simulator' build CODE_SIGNING_ALLOWED=NOTo compile only the shared KMP framework for Apple Silicon simulator:
./gradlew :shared:compileKotlinIosSimulatorArm64The macOS desktop app shares the same codebase and Xcode project:
- Open
iosApp/iosApp.xcodeprojin Xcode. - Select the
macosAppscheme in the top toolbar. - Build and run with βR.
Execute the shared multiplatform integration and package-level test suite:
./gradlew :shared:allTestsRun project linters before submitting changes:
# Kotlin linting & static analysis
./gradlew ktlintCheck detekt
# Automatically format Kotlin code
./gradlew ktlintFormat
# Swift linting & formatting checks
swiftformat --lint .
swiftlint lint iosApp/iosApp
# Automatically format Swift code
swiftformat .When modifying views or business logic, always follow the Unsplash API developer requirements:
- Direct Hotlinking: Never cache or host Unsplash image files on intermediate third-party servers. Display photos utilizing direct URLs returned by the Unsplash API.
- Parameter Preservation: Preserve all
ixidtracking parameters in image request query strings. - Photographer Attribution: Display photographer names and profile pictures prominently, including clickable links back to their Unsplash profile with UTM referral tracking parameters (
utm_source=ImageFeedApp&utm_medium=referral). - Download Tracking: Ensure that photo download and save actions trigger the designated
photo.links.download_locationendpoint. - Retina CDN Optimization: Multiply layout points by display scale (
UIScreen.main.scaleon iOS/macOS, display density on Android) when constructing&w=URL parameters to deliver sharp Retina imagery without requesting excessive raw file dimensions.