🧩 Real-time London Underground status for Android & iOS — built with Kotlin Multiplatform and Compose Multiplatform.
A Kotlin Multiplatform library providing real-time London Underground tube line status information with ready-to-use UI components and shared business logic for Android and iOS applications.
- Real-time TFL Data: Fetches live tube line status from Transport for London API
- Kotlin Multiplatform: Shared business logic for Android and iOS
- Compose Multiplatform UI: Native-feeling UI components
- Two Integration Options: Core library only or complete UI solution
- Easy Integration: Drop into existing navigation patterns
- Offline Handling: Graceful error states and retry mechanisms
The project consists of three main modules:
- Business logic and data layer
- TFL API integration with Ktor
- ViewModels and use cases
- Koin dependency injection setup
- Custom UI implementation support
- Pre-built Compose Multiplatform UI components
- Authentic TFL branding and colors
- Ready-to-use screens and components
- Includes core library as dependency
- Demonstrates library integration
- Example theming and navigation
- Development and testing environment
- Android Studio Iguana+ with Kotlin Multiplatform plugin
- Xcode 15+ (for iOS development)
- Java 11+
- TFL API Credentials (free from TFL Developer Portal) - Not required to run the app, but recommended to avoid throttling and rate limitations from TFL servers
-
Clone the repository:
git clone https://github.com/IntSoftDev/LondonTubeStatus.git cd LondonTubeStatus -
Setup TFL API credentials: Create
local.propertiesfile:tfl.app.id=your_tfl_app_id tfl.app.key=your_tfl_app_key
-
Configure local development: In
gradle.properties, set:importLocalKmp=true -
Run the sample app:
./gradlew :composeApp:assembleDebug
# Android-only build
./gradlew assembleDebug
# Tests
./gradlew allTests
# Lint check
./gradlew ktlintCheck
# Full build
./gradlew build
# Clean build cache
./gradlew cleanLondonTubeStatus/
├── composeApp/ # Sample Android/iOS app
│ └── src/
│ ├── androidMain/ # Android-specific code
│ ├── commonMain/ # Shared Compose UI
│ └── iosMain/ # iOS-specific code
├── tflstatus-core/ # Core business logic library
│ └── src/
│ ├── commonMain/ # Shared business logic
│ ├── androidMain/ # Android HTTP client
│ └── iosMain/ # iOS HTTP client
├── tflstatus-ui/ # UI components library
│ └── src/
│ ├── commonMain/ # Compose Multiplatform UI
│ └── ...
├── iosApp/ # iOS app wrapper
└── PUBLISHING_GUIDE.md # Library publishing documentation
- Publishing Guide - How to publish updates to Maven Central
- TFL API Documentation - Official TFL API reference
- Android Integration - How to integrate the library in your Android app
- iOS Integration - How to integrate the library in your SwiftUI app
- Screenshots and usage in Live apps - See how it looks and where it's used
To use the pre-built artifacts:
In gradle.properties, set:
importLocalKmp=falseAdd to your build.gradle.kts:
dependencies {
// Option 1: Complete UI solution (recommended)
implementation("com.intsoftdev:tflstatus-core:<version>")
implementation("com.intsoftdev:tflstatus-ui:<version>")
// Option 2: Core library only (custom UI)
implementation("com.intsoftdev:tflstatus-core:<version>")
}The TFL SDK can be initialised in your Application class with various configuration options:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Option 1 - Basic initialisation without using API keys or Koin
initTflSDK(context = this)
// With custom API configuration
val apiConfig = TflApiConfig(
appId = "your-app-id",
appKey = "your-api-key"
)
// Option 2 - if App does not use Koin
initTflSDK(context = this, apiConfig = apiConfig)
// Option 3 - with Koin
val koinApp = startKoin { /* your modules */ }
initTflSDK(context = this, koinApplication = koinApp, apiConfig = apiConfig)
}
}@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(onNavigateToTFL = {
navController.navigate("tfl_status")
})
}
composable("tfl_status") {
tflStatusUI(onBackPressed = {
navController.popBackStack()
})
}
}
}The UI library uses authentic TFL branding but can be customised:
@Composable
fun CustomThemedTFL() {
MaterialTheme(
colorScheme = yourCustomColorScheme,
typography = yourCustomTypography
) {
tflStatusUI(onBackPressed = { /* handle back */ })
}
}Add the TFL Status library to your iOS project using CocoaPods:
Local Development
# Podfile
platform :ios, '17.0'
install! 'cocoapods', :deterministic_uuids => false
target 'YourApp' do
use_frameworks!
# Local paths - adjust to match your project structure
pod 'tflstatus_core', :path => '../tflstatus-core/'
pod 'tflstatus_ui', :path => '../tflstatus-ui/'
endThen run:
cd ios
pod install// From iOS Swift code:
let viewController = TFLStatusBridgeKt.createTFLStatusViewController(
showBackButton: true,
onBackPressed: {
// Handle back button press
},
enableLogging: true,
apiConfig: TflApiConfig()
)Add the framework to your iOS project and use in SwiftUI:
import SwiftUI
import tflstatus_ui
import tflstatus_core
struct TFLStatusComposeView: UIViewControllerRepresentable {
var showBackButton: Bool = true
var onBackPressed: (() -> Void)?
func makeUIViewController(context: Context) -> UIViewController {
return TFLStatusBridgeKt.createTFLStatusViewController(
showBackButton: showBackButton,
onBackPressed: onBackPressed ?? {
},
enableLogging: true,
apiConfig: TflApiConfig(
appId: "your-app-id",
appKey: "your-api-key"
)
)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// No-op: Compose content is self-updating
}
}
struct TFLStatusScreen: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
ZStack {
Color.black
.navigationBarTitleDisplayMode(.inline)
.edgesIgnoringSafeArea(.all)
// Pipe SwiftUI's dismiss into KMP's onBackPressed
TFLStatusComposeView(
showBackButton: true,
onBackPressed: { dismiss() }
)
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("TFL Status")
.foregroundColor(.white)
.font(.headline)
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
dismiss()
}) {
Image(systemName: "arrow.left")
.foregroundColor(.white)
})
}
.preferredColorScheme(.dark)
}
}
// Usage in your main ContentView
struct ContentView: View {
@State private var showTFLStatus = false
var body: some View {
NavigationView {
VStack {
Button("London Tube Status") {
showTFLStatus = true
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("Your App")
.sheet(isPresented: $showTFLStatus) {
TFLStatusScreen()
}
}
}
}@Composable
fun CustomTFLScreen(viewModel: TubeStatusViewModel = koinInject()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.getLineStatuses("victoria,central,northern")
}
when (val state = uiState) {
is TubeStatusUiState.Loading -> LoadingScreen()
is TubeStatusUiState.Success -> CustomLineList(state.tubeLines)
is TubeStatusUiState.Error -> ErrorScreen(state.message)
}
}| Android | iOS |
|---|---|
| Used in Play Store app | Used in Apple App Store |
![]() |
![]() |
Libraries are published to Maven Central:
- Core:
com.intsoftdev:tflstatus-core - UI:
com.intsoftdev:tflstatus-ui
See PUBLISHING_GUIDE.md for detailed publishing instructions.
- Fork the repository
- Create a feature branch (
git checkout -b feature/<feature_name>) - Commit your changes (
git commit -m 'Add <feature>') - Push to the branch (
git push origin feature/<feature_name>) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Transport for London for providing the public API
- JetBrains for Kotlin Multiplatform and Compose Multiplatform
- Koin for dependency injection framework
- Issues: GitHub Issues
- Documentation: ReadMe
- Email: [email protected]

