A news application built with Kotlin Multiplatform (KMP) that fetches business news from the News API and displays them on both Android and iOS platforms.
- Cross-platform: Single codebase for Android and iOS
- News API integration: Fetches latest business news
- Offline caching: SQLDelight database for local storage
- Pull-to-refresh: Native refresh on both platforms
- Modern UI: Jetpack Compose (Android) and SwiftUI (iOS)
- Kotlin Multiplatform (KMP) - Cross-platform development
- Kotlin Coroutines & Flow - Async operations and reactive streams
- Koin - Dependency injection
- Ktor - HTTP client
- SQLDelight - Type-safe database
- BuildKonfig - API key management
- SKIE - Kotlin to Swift interop without Native Coroutines plugin
DailyPulse/
├── shared/ # Shared KMP code
│ ├── articles/ # News feature
│ │ ├── di/ # Feature dependency injection
│ │ ├── presentation/ # ViewModels and UI state
│ │ ├── services/ # Network, persistence, repository
│ │ └── use_cases/ # Business logic
│ ├── di/ # Shared dependency injection modules
│ ├── db/ # Database layer (SQLDelight)
│ └── utils/ # Cross-platform utilities
├── androidApp/ # Android-specific code
│ ├── screens/ # Compose UI screens
│ ├── di/ # Android DI modules
│ └── MainActivity.kt # Android entry point
└── iosApp/ # iOS-specific code
├── Screens/ # SwiftUI screens
├── Navigation/ # Navigation coordinators
└── iOSApp.swift # iOS entry point
- Android Studio with Kotlin Multiplatform plugin (Hedgehog or newer recommended)
- Xcode (version 14+ should work, 15+ recommended for iOS development)
- Kotlin (project uses 2.0.20/2.1.20, newer versions should be compatible)
- JDK 17+ (required for Kotlin 2.0+)
- Open Android Studio
- Go to File → Settings (Windows/Linux) or Android Studio → Preferences (macOS)
- Navigate to Plugins
- Search for "Kotlin Multiplatform"
- Click Install and restart Android Studio
If you encounter issues with KMP setup, you can use the KMP doctor tool:
# Install Kotlin (includes KMP doctor)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install kotlin
# Run KMP doctor to diagnose issues
kotlin doctorNote: This is only needed if you experience setup problems - Android Studio handles KMP setup automatically in most cases.
git clone <repository-url>
cd DailyPulseCreate local.properties in the project root:
NEWS_API_KEY=your_news_api_key_hereGet your free API key at: NewsAPI.org
- Open the project in Android Studio
- Wait for project sync
- Run the app
- Open in Xcode:
open iosApp/iosApp.xcodeproj
- Wait for indexing
- Run the app
- Jetpack Compose: UI with Material Design 3
- Navigation Compose: Simple navigation with enum-based routes
- BaseViewModel: Cross-platform ViewModel with
viewModelScopefor lifecycle management - Koin: Dependency injection with
koinViewModel() - Pull-to-refresh: Material 3
pullToRefreshcomponents
- SwiftUI: Native iOS UI with TabView
- NavigationStack: Coordinator pattern with NavigationPath
- Async/Await: Structured concurrency with Task
- @MainActor: Main thread safety for UI updates
- ObservableObject: Reactive state management
- SKIE Integration: Seamless Kotlin Flow to Swift async streams
- Pull-to-refresh: SwiftUI
.refreshablemodifier
val articlesModule = module {
single<ArticlesRemoteDataService> { ArticlesRemoteDataService(get()) }
single<ArticlesDataSource> { ArticlesDataSource(get()) }
single<ArticlesRepository> { ArticlesRepository(get(), get()) }
single<ListArticleUseCase> { ListArticleUseCase(get()) }
single<ArticlesViewModel> { ArticlesViewModel(get()) }
}class ArticlesRepository(
private val dataSource: ArticlesDataSource,
private val remoteDataService: ArticlesRemoteDataService
) {
suspend fun getArticles(forceFetch: Boolean): List<ArticleModel> {
// Implements caching strategy
}
}// Cross-platform BaseViewModel
expect open class BaseViewModel() {
val scope: CoroutineScope
}
// Android implementation
actual open class BaseViewModel: ViewModel() {
actual val scope: CoroutineScope = viewModelScope
}
// iOS implementation
actual open class BaseViewModel {
actual val scope: CoroutineScope = CoroutineScope(Dispatchers.IO)
fun clear() { scope.cancel() }
}
// Shared ViewModel
class ArticlesViewModel(
private val listArticleUseCase: ListArticleUseCase
) : BaseViewModel() {
private val internalContentState: MutableStateFlow<ArticlesState> =
MutableStateFlow(ArticlesState(loading = true))
val contentState: StateFlow<ArticlesState> = internalContentState
}// iOS Wrapper
@MainActor
class ArticlesViewModelWrapper: ObservableObject {
@Published private(set) var contentState: ArticlesState
func startObservingChanges() async {
contentStateTask = Task {
for await contentState in articlesViewModel.contentState {
self.contentState = contentState
}
}
}
}sqldelight {
databases {
create(name = "DailyPulseDatabase") {
packageName.set("com.eli.examples.dailypulse.db")
}
}
}class ArticlesRemoteDataService(
private val httpClient: HttpClient,
private val configuration: ArticlesConfiguration = ArticlesConfiguration.DEFAULT_CONFIG
) {
suspend fun fetchArticles(): List<ArticleRemoteItem> {
val response: ArticlesResponse = httpClient.get(allArticlesURL).body()
return response.articles
}
}- Kotlin: 2.0.20 / 2.1.20 (serialization)
- Kotlinx Coroutines: 1.7.3
- Ktor: 2.3.5
- Koin: 4.0.4
- SQLDelight: 2.0.2
- SKIE: 0.9.0
- Jetpack Compose: 1.5.4
- Material Design 3: 1.3.0
- Navigation Compose: 2.8.9
- Coil: 2.5.0
Built with ❤️ using Kotlin Multiplatform