This project is a proof of concept written entirely with recent models focused on efficiency like Google's Gemini 3.5 Flash and MAI-Code-1-Flash. The main goal was to evaluate the models and different ways to work with them (Antigravity, Gemini CLI, GitHub Copilot and so on).
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:
Screen.Feed,Screen.Collections,Screen.CollectionDetails(collectionId),Screen.Search(query),Screen.PhotoDetails(photoId),Screen.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
FeedPathItemandCollectionPathItemenums. - 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 Presenter 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) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Presenters β β
β β (FeedPresenter, UnifiedSearchPresenter, PhotoDetailsPresenter, β β
β β CollectionDetailPresenter, UserProfilePresenter) β β
β β * PresenterScope Lifecycle Management & updateIfActive Guard β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Metro Compile-Time Dependency Injection β β
β β (@DependencyGraph, @Inject, @AssistedFactory) β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UnsplashRepository β β
β ββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β UnsplashApiClient β β
β β (Ktor 3.5 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). - Lifecycle-Safe Presenters: Presenters own managed
PresenterScopeinstances with cleanclear()lifecycles, guarded byupdateIfActive()helpers to prevent memory leaks or emissions after screen teardown. - Networking: High-performance HTTP client using Ktor 3.5 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.5 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.
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. - Architecture Rules: Review
AGENTS.mdand the rule sets inai-rules/for guidelines on presenter 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:
cd iosApp && xcodegen
- Open
iosApp/iosApp.xcodeprojin Xcode. - Select the
iosAppscheme and an iOS Simulator (iPhone 15+ / iOS 17+). - Build and run with βR.
Command-line simulator compilation:
./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
swiftlint lint iosApp/iosApp
swiftformat --lint .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.