A Kotlin Multiplatform (KMP) application for browsing XKCD comics, built with shared business logic and native UIs for both Android (Jetpack Compose) and iOS (SwiftUI).
This app demonstrates a clean architecture approach to building a cross-platform mobile application where:
- Business logic is shared between Android and iOS using Kotlin Multiplatform
- UI is native on each platform (Jetpack Compose for Android, SwiftUI for iOS)
- Data is cached locally using SQLDelight for offline support
- Network requests are handled via Ktor Client
- Dependency injection is managed with Koin
The app follows Clean Architecture principles with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (ViewModels, UI State, Event Managers) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Domain Layer │
│ (Use Cases, Repository Interfaces) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Data Layer │
│ (Repository Implementation, API Service, Database) │
└─────────────────────────────────────────────────────────┘
Key Components:
- Data Layer:
ComicRepositoryImpl,XkcdApiService, SQLDelight database (Flow-based, cache-first with stale-while-revalidate)- Uses
ComicDtofor API responses (internal to data layer) - Database as single source of truth with reactive Flow streams
- TTL-based cache refresh (1h for list, 24h for detail)
- Uses
- Domain Layer: Use cases (
ObserveComicsUseCase,ObserveFavoriteComicsUseCase,LoadInitialComicsUseCase,LoadMoreComicsUseCase,ToggleFavoriteUseCase, etc.)Observe*UseCase: Expose reactive Flow streamsLoad*UseCase: Trigger API fetches and cache updates
- Presentation Layer: ViewModels (
ComicsViewModel,ComicDetailViewModel,FavoritesViewModel) using extension functionsViewModelExtensions: Common error handling and Flow observation utilities- Composition over inheritance pattern
- Platform Layer: Platform-specific implementations (Android/iOS drivers, HTTP clients, logging)
-
Comics List Screen
- Infinite scroll pagination (auto-loads when scrolling near bottom)
- Error handling with retry mechanism
- Loading states (initial load, pagination, refresh)
- Stale-while-revalidate: Shows cached data while refreshing in background
- Smart initial loading: Shows loading indicator first, then data or error
-
Comic Detail Screen
- Displays full comic details
- Favorite toggle functionality
- Navigation from list screen
-
Favorites Screen
- Lists all favorited comics (Android - fully implemented, iOS - placeholder)
- Reactive updates when favorites change
-
Offline Support & Caching Strategy
- Cache-First Approach: Always shows cached data immediately
- Stale-While-Revalidate: Displays cached data even if stale, refreshes in background
- TTL-Based Refresh:
- Comics list: Refreshes if cache is older than 1 hour
- Comic detail: Refreshes if cache is older than 24 hours
- Automatic Background Refresh: Triggers API calls when cache is empty or stale
- Falls back to cached data when network fails
- Favorite status persists locally
-
Automatic Cache Management
- Smart cache cleanup that preserves favorites
- Time-based expiration: Removes comics older than 30 days (configurable)
- Size-based limiting: Keeps maximum 500 non-favorite comics (configurable)
- Automatic cleanup runs on app start in the background
- Favorites are always preserved regardless of age or cache size
- Database versioning: Uses
comics_v2.dbto handle schema changes
-
Reactive Architecture
- Flow-Based Repository: All data operations expose reactive Flow streams
- Database as Single Source of Truth: ViewModels observe database changes via Flow
- Automatic UI Updates: UI automatically reflects data changes without manual synchronization
- No Event Managers: Reactive Flow eliminates need for event dispatchers
- Cache-First with Background Refresh: Immediate data display with smart background updates
I chose KMP to maximize code sharing while maintaining native UI experiences. This approach:
- Reduces code duplication for business logic
- Allows platform-specific optimizations where needed
- Provides a single source of truth for data operations
- Clean Architecture: Separates concerns and makes the codebase testable and maintainable
- Reactive Flow-Based Architecture:
- Repository exposes Flow streams for all data operations
- Database is the single source of truth
- ViewModels observe Flows for automatic UI updates
- MVVM Pattern: ViewModels manage UI state and business logic, making UI reactive
- Use Cases: Encapsulate business rules and make the domain layer independent of data sources
Observe*UseCase: For reactive data observationLoad*UseCase: For triggering data fetches
- Repository Pattern: Abstracts data sources (API + Database) behind a single interface
- Cache-first strategy with stale-while-revalidate
- TTL-based automatic refresh
- Dependency Injection (Koin): Simplifies dependency management and testing
- Composition over Inheritance: ViewModels use extension functions instead of BaseViewModel
- Model Separation:
ComicDto: Data layer model (API responses)Comic: Domain model (includes business logic likeisFavorite)- Extension functions convert between layers
- Logging: Napier library for cross-platform logging (Android Log / iOS NSLog)
- Cache Management: Automatic cache cleanup prevents unbounded growth while preserving user favorites
The project currently includes the shared testing infrastructure (in-memory SQLDelight driver, coroutine test utilities) but only minimal unit tests. The priority has been stabilizing the reactive data flow; expanding end-to-end and ViewModel tests is tracked as a follow-up task.
- StateFlow Extensions: Helper functions to observe Kotlin
StateFlowfrom Swift - ViewModel Wrappers:
ObservableObjectwrappers that bridge Kotlin ViewModels to SwiftUI - Koin iOS: Platform-specific Koin initialization for iOS
- Napier Logging: Logs visible in Xcode Console (View > Debug Area > Activate Console)
- iOS Deployment Target: 16.0 (required for NavigationStack)
- Error Tracking: OSLog integration for error logging in Xcode
comics/
├── shared/ # Shared Kotlin Multiplatform module
│ └── src/
│ ├── commonMain/ # Shared code
│ │ ├── data/ # Data layer (API, Repository impl, models)
│ │ ├── domain/ # Domain layer (Use cases, repository interfaces)
│ │ ├── presentation/ # Presentation layer (ViewModels, UI state)
│ │ ├── di/ # Dependency injection modules
│ │ ├── platform/ # Platform abstractions
│ │ └── util/ # Utilities (CacheConfig, ErrorType, etc.)
│ ├── androidMain/ # Android-specific implementations
│ ├── iosMain/ # iOS-specific implementations
│ └── androidUnitTest/ # JVM-only unit tests (MockK, SQLDelight)
├── androidApp/ # Android application
│ └── src/main/
│ ├── kotlin/ # Android UI (Jetpack Compose)
│ └── res/ # Android resources
└── iosApp/ # iOS application
└── iosApp/
├── ui/ # SwiftUI screens
└── viewmodel/ # ViewModel wrappers
Test coverage is intentionally lean at the moment. The shared module ships with coroutine test utilities and in-memory SQLDelight drivers so that repository/use-case tests can be added quickly. Expanding the androidUnitTest source set with MockK-powered tests is on the roadmap.
Cache Management:
- Automatic cache cleanup with time-based (30 days) and size-based (500 comics) limits
PerformCacheCleanupUseCase: Handles cache cleanup logicCacheConfig: Centralized cache configuration constants- Platform-agnostic
TimeProvider(in platform folder) for timestamp handling
-
iOS Favorites Screen: Currently shows "Not implemented" placeholder
- The ViewModel logic exists in shared code
- Needs SwiftUI implementation and ViewModel wrapper
-
Comprehensive Test Coverage:
- More ViewModel tests (error scenarios, edge cases)
- Use case tests for all use cases
- Integration tests for full flows
-
Error Handling Enhancements:
- More specific error messages
- Retry strategies for different error types
- Better offline state indication
-
UI/UX Improvements:
- Image caching and optimization
- Skeleton loaders instead of simple progress indicators
- Better empty states
- Pull-to-refresh (currently only refresh button)
-
Code Quality:
- ✅ Napier logging integrated
- ✅ Comprehensive KDoc documentation added
- Code formatting/linting automation
-
Performance Optimizations:
- Image loading optimization
- Database query optimization
- Pagination improvements
-
Cache Management UI:
- Settings screen to manually trigger cache cleanup
- Display cache size information
- Allow users to configure cache limits
-
Testing:
- More comprehensive unit test coverage
- Integration tests for end-to-end flows
- UI tests for critical user journeys
-
Error Handling:
- More granular error types
- Better error recovery strategies
- User-friendly error messages with actionable suggestions
-
Code Organization:
- Extract more reusable components
- Better separation of concerns in some ViewModels
- More consistent error handling patterns
-
Documentation:
- ✅ Comprehensive KDoc comments added
- Architecture decision records (ADRs)
- Setup and contribution guidelines
-
Performance:
- Image caching strategy
- Database query optimization
- Pagination improvements (virtual scrolling for large lists)
-
Platform Parity:
- Complete iOS favorites screen
- Ensure feature parity between Android and iOS
- Platform-specific optimizations
-
Developer Experience:
- CI/CD pipeline
- Automated testing
- Code formatting/linting automation
- Better build scripts
- Android Studio (for Android development)
- Xcode (for iOS development)
- JDK 11+
- Gradle 8.0+
-
Clone the repository
git clone <repository-url> cd comics
-
Run Android app
- Open project in Android Studio
- Run
androidAppconfiguration
-
Run iOS app
- Open
iosApp/iosApp.xcodeprojin Xcode - Select a simulator or device
- Build and run (Cmd+R)
- Open
./gradlew :shared:test- Kotlin Multiplatform: Shared business logic
- Jetpack Compose: Android UI
- SwiftUI: iOS UI
- Koin: Dependency injection
- SQLDelight: Local database with automatic cache management
- Ktor: HTTP client
- Kotlin Coroutines: Asynchronous programming
- Kotlin Flow: Reactive streams for data observation
- StateFlow: Reactive state management
- Napier: Cross-platform logging library (KMM-friendly)
- MockK: Testing/mocking
- JUnit: Testing framework
The app implements a sophisticated caching strategy with automatic cleanup and smart refresh:
- Immediate Data Display: Always shows cached data immediately (if available)
- Background Refresh: Automatically refreshes stale data in the background
- TTL-Based Refresh:
- Comics List: Refreshes if latest comic is older than 1 hour (
CACHE_FRESHNESS_TTL_SECONDS) - Comic Detail: Refreshes if comic is older than 24 hours (
COMIC_DETAIL_CACHE_TTL_SECONDS)
- Comics List: Refreshes if latest comic is older than 1 hour (
- Smart Refresh Logic: Only triggers API calls when cache is actually stale
- Error Resilience: On API failure, cached data remains available
Configuration:
- Max Cache Age: 30 days (configurable via
CacheConfig.MAX_CACHE_AGE_DAYS) - Max Non-Favorite Comics: 500 comics (configurable via
CacheConfig.MAX_NON_FAVORITE_COMICS)
How It Works:
- Automatic Cleanup: Runs automatically when the app starts (in
ComicsViewModel.init) - Time-Based Expiration: Removes comics older than the configured age limit (favorites preserved)
- Size-Based Limiting: When non-favorite comics exceed the limit, oldest ones are removed (favorites preserved)
- Background Execution: Cleanup runs in the background without blocking the UI
PerformCacheCleanupUseCase: Encapsulates cache cleanup logicComicRepository.performAutomaticCacheCleanup(): Combines time-based and size-based clearinglast_refreshed_atcolumn tracks cache freshness for TTL-based refresh- Platform abstraction
TimeProvider(expect/actual) ensures consistent timestamp handling across platforms - Database versioning: Uses
comics_v2.dbto handle schema changes (replaces old database)
The app uses Napier for cross-platform logging:
- Android: Logs appear in Logcat with tag "ComicsApp"
- iOS: Logs appear in Xcode Console (View > Debug Area > Activate Console)
- Error Logging: All errors are logged with full stack traces
- Usage:
Logger.e(tag, message, throwable)for errors,Logger.d/i/wfor other levels
- Unknown Errors: Enhanced error messages guide users to check console
- OSLog Integration: iOS uses OSLog for structured error logging
- Automatic Error Logging: ViewModels automatically log all errors with full details
- Xcode Console: Filter by "ComicsApp" or search for specific tags to find errors