Skip to content

Commit 613fa32

Browse files
authored
Merge pull request #212 from joreilly/circuit
Initial integration of Circuit
2 parents fb1265d + 64d2dfc commit 613fa32

File tree

13 files changed

+350
-247
lines changed

13 files changed

+350
-247
lines changed

androidApp/src/main/java/dev/johnoreilly/bikeshare/MainActivity.kt

+5-43
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,7 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
7-
import androidx.compose.foundation.layout.Row
8-
import androidx.compose.foundation.layout.padding
9-
import androidx.compose.material3.Scaffold
10-
import androidx.compose.runtime.Composable
11-
import androidx.compose.ui.Modifier
12-
import androidx.navigation.compose.NavHost
13-
import androidx.navigation.compose.composable
14-
import androidx.navigation.compose.rememberNavController
157
import dev.johnoreilly.bikeshare.ui.theme.BikeShareTheme
16-
import dev.johnoreilly.common.di.AndroidApplicationComponent
178

189

1910
class MainActivity : ComponentActivity() {
@@ -26,50 +17,21 @@ class MainActivity : ComponentActivity() {
2617

2718
setContent {
2819
BikeShareTheme {
29-
BikeShareApp(applicationComponent)
20+
applicationComponent.bikeShareContent()
3021
}
3122
}
3223
}
3324
}
3425

35-
sealed class Screen(val title: String) {
36-
data object CountryListScreen : Screen("CountryList")
37-
data object NetworkListScreen : Screen("NetworkList")
38-
data object StationsScreen : Screen("Stations")
39-
}
4026

27+
/*
4128
42-
@Composable
43-
fun BikeShareApp(applicationComponent: AndroidApplicationComponent) {
44-
val navController = rememberNavController()
29+
TODO: support following using Circuit
4530
46-
Scaffold { innerPadding ->
47-
Row(Modifier.padding(innerPadding)) {
4831
val bikeNetwork = BuildConfig.BIKE_NETWORK
4932
if (bikeNetwork.isNotEmpty()) {
5033
val stationsScreen = applicationComponent.stationsScreen
5134
stationsScreen(bikeNetwork, null)
52-
} else {
53-
NavHost(navController, startDestination = Screen.CountryListScreen.title) {
54-
val countryListScreen = applicationComponent.countryListScreen
55-
composable(Screen.CountryListScreen.title) {
56-
countryListScreen {
57-
navController.navigate(Screen.NetworkListScreen.title + "/${it.code}")
58-
}
59-
}
60-
composable(Screen.NetworkListScreen.title + "/{countryCode}") { backStackEntry ->
61-
val networkListScreen = applicationComponent.networkListScreen
62-
networkListScreen(backStackEntry.arguments?.getString("countryCode") as String,
63-
{ navController.navigate(Screen.StationsScreen.title + "/$it") },
64-
{ navController.popBackStack() })
65-
}
66-
composable(Screen.StationsScreen.title + "/{networkId}") { backStackEntry ->
67-
val stationsScreen = applicationComponent.stationsScreen
68-
stationsScreen(backStackEntry.arguments?.getString("networkId") as String,
69-
{ navController.popBackStack() })
70-
}
71-
}
7235
}
73-
}
74-
}
75-
}
36+
37+
*/

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
alias(libs.plugins.jetbrainsCompose) apply false
77
alias(libs.plugins.kotlinMultiplatform) apply false
88
alias(libs.plugins.kmpNativeCoroutines) apply false
9+
alias(libs.plugins.kotlin.parcelize) apply false
910
}
1011

1112
// Explicitly adding the plugin to the classpath as it makes it easier to control the version

common/build.gradle.kts

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
2+
13
plugins {
24
alias(libs.plugins.kotlinMultiplatform)
35
alias(libs.plugins.android.library)
46
alias(libs.plugins.kotlinx.serialization)
7+
alias(libs.plugins.kotlin.parcelize)
58
alias(libs.plugins.ksp)
69
alias(libs.plugins.kmpNativeCoroutines)
710
alias(libs.plugins.jetbrainsCompose)
@@ -33,7 +36,7 @@ kotlin {
3336
jvm()
3437

3538
listOf(
36-
iosArm64(), iosX64(), iosSimulatorArm64(), macosArm64()
39+
iosArm64(), iosX64(), iosSimulatorArm64()
3740
).forEach {
3841
it.binaries.framework {
3942
baseName = "BikeShareKit"
@@ -46,6 +49,7 @@ kotlin {
4649
implementation(libs.kotlinx.serialization)
4750

4851
api(libs.kotlininject.runtime)
52+
api(libs.circuit.foundation)
4953

5054
implementation(libs.bundles.ktor.common)
5155
implementation(libs.realm)
@@ -80,14 +84,29 @@ kotlin {
8084
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
8185
compilations.get("main").kotlinOptions.freeCompilerArgs += "-Xexport-kdoc"
8286
}
87+
88+
targets.configureEach {
89+
val isAndroidTarget = platformType == KotlinPlatformType.androidJvm
90+
compilations.configureEach {
91+
compileTaskProvider.configure {
92+
compilerOptions {
93+
if (isAndroidTarget) {
94+
freeCompilerArgs.addAll(
95+
"-P",
96+
"plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=dev.johnoreilly.common.screens.Parcelize",
97+
)
98+
}
99+
}
100+
}
101+
}
102+
}
83103
}
84104

85105
multiplatformSwiftPackage {
86106
packageName("BikeShareKit")
87107
swiftToolsVersion("5.9")
88108
targetPlatforms {
89109
iOS { v("14") }
90-
macOS { v("12")}
91110
}
92111
}
93112

@@ -112,3 +131,5 @@ dependencies {
112131
add("kspIosSimulatorArm64", libs.kotlininject.compiler)
113132
add("kspJvm", libs.kotlininject.compiler)
114133
}
134+
135+

common/src/androidMain/kotlin/dev/johnoreilly/common/di/AndroidApplicationComponent.kt

+23-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
package dev.johnoreilly.common.di
22

3-
import dev.johnoreilly.common.ui.CountryListScreen
4-
import dev.johnoreilly.common.ui.NetworkListScreen
5-
import dev.johnoreilly.common.ui.StationsScreen
3+
import com.slack.circuit.foundation.Circuit
4+
import dev.johnoreilly.common.screens.CountryListPresenter
5+
import dev.johnoreilly.common.screens.CountryListScreen
6+
import dev.johnoreilly.common.screens.NetworkListPresenter
7+
import dev.johnoreilly.common.screens.NetworkListScreen
8+
import dev.johnoreilly.common.screens.StationListPresenter
9+
import dev.johnoreilly.common.screens.StationListScreen
10+
import dev.johnoreilly.common.ui.BikeShareContent
11+
import dev.johnoreilly.common.ui.CountryListUi
12+
import dev.johnoreilly.common.ui.NetworkListUi
13+
import dev.johnoreilly.common.ui.StationListUI
614
import io.ktor.client.engine.android.Android
715
import me.tatarka.inject.annotations.Component
8-
9-
16+
import me.tatarka.inject.annotations.Provides
1017

1118

1219
@Component
1320
@Singleton
1421
abstract class AndroidApplicationComponent: SharedApplicationComponent {
1522

16-
abstract val countryListScreen: CountryListScreen
17-
abstract val networkListScreen: NetworkListScreen
18-
abstract val stationsScreen: StationsScreen
23+
abstract val bikeShareContent: BikeShareContent
24+
25+
@Provides
26+
fun provideCircuit(): Circuit = Circuit.Builder()
27+
.addPresenterFactory(CountryListPresenter.Factory(repository))
28+
.addPresenterFactory(NetworkListPresenter.Factory(repository))
29+
.addPresenterFactory(StationListPresenter.Factory(repository))
30+
.addUi<CountryListScreen, CountryListScreen.State> { state, modifier -> CountryListUi(state, modifier) }
31+
.addUi<NetworkListScreen, NetworkListScreen.State> { state, modifier -> NetworkListUi(state, modifier) }
32+
.addUi<StationListScreen, StationListScreen.State> { state, modifier -> StationListUI(state, modifier) }
33+
.build()
1934

2035
override fun getHttpClientEngine() = Android.create()
2136

common/src/commonMain/kotlin/dev/johnoreilly/common/di/SharedApplicationComponent.kt

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import io.ktor.serialization.kotlinx.json.json
1717
import io.realm.kotlin.Realm
1818
import io.realm.kotlin.RealmConfiguration
1919
import kotlinx.serialization.json.Json
20-
import me.tatarka.inject.annotations.Component
2120
import me.tatarka.inject.annotations.Provides
2221
import me.tatarka.inject.annotations.Scope
2322
import kotlin.annotation.AnnotationTarget.CLASS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package dev.johnoreilly.common.screens
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.collectAsState
5+
import androidx.compose.runtime.getValue
6+
import com.slack.circuit.runtime.CircuitContext
7+
import com.slack.circuit.runtime.CircuitUiEvent
8+
import com.slack.circuit.runtime.CircuitUiState
9+
import com.slack.circuit.runtime.Navigator
10+
import com.slack.circuit.runtime.presenter.Presenter
11+
import com.slack.circuit.runtime.screen.Screen
12+
import dev.johnoreilly.common.getCountryName
13+
import dev.johnoreilly.common.model.Network
14+
import dev.johnoreilly.common.remote.Station
15+
import dev.johnoreilly.common.repository.CityBikesRepository
16+
import dev.johnoreilly.common.viewmodel.Country
17+
18+
19+
@Target(AnnotationTarget.CLASS)
20+
@Retention(AnnotationRetention.BINARY)
21+
annotation class Parcelize
22+
23+
24+
@Parcelize
25+
data object CountryListScreen : Screen {
26+
data class State(
27+
val countryList: List<Country>,
28+
val eventSink: (Event) -> Unit
29+
) : CircuitUiState
30+
31+
sealed class Event : CircuitUiEvent {
32+
data class CountryClicked(val countryCode: String) : Event()
33+
}
34+
}
35+
36+
@Parcelize
37+
data class NetworkListScreen(val countryCode: String) : Screen {
38+
data class State(
39+
val countryCode: String,
40+
val countryName: String,
41+
val networkList: List<Network>,
42+
val eventSink: (Event) -> Unit
43+
) : CircuitUiState
44+
45+
sealed class Event : CircuitUiEvent {
46+
data class NetworkClicked(val networkId: String) : Event()
47+
data object BackClicked : Event()
48+
}
49+
}
50+
51+
@Parcelize
52+
data class StationListScreen(val networkId: String) : Screen {
53+
data class State(
54+
val networkId: String,
55+
val stationList: List<Station>,
56+
val eventSink: (Event) -> Unit
57+
) : CircuitUiState
58+
59+
sealed class Event : CircuitUiEvent {
60+
data object BackClicked : Event()
61+
}
62+
}
63+
64+
65+
// Presenters (TODO: where should we put these?)
66+
67+
class CountryListPresenter(
68+
private val navigator: Navigator,
69+
private val cityBikesRepository: CityBikesRepository
70+
) : Presenter<CountryListScreen.State> {
71+
@Composable
72+
override fun present(): CountryListScreen.State {
73+
val groupedNetworkList by cityBikesRepository.groupedNetworkList.collectAsState()
74+
val countryCodeList = groupedNetworkList.keys.toList()
75+
val countryList = countryCodeList.map { countryCode -> Country(countryCode, getCountryName(countryCode)) }
76+
.sortedBy { it.displayName }
77+
return CountryListScreen.State(countryList) { event ->
78+
when (event) {
79+
is CountryListScreen.Event.CountryClicked -> navigator.goTo(NetworkListScreen(event.countryCode))
80+
}
81+
}
82+
}
83+
84+
class Factory(private val repository: CityBikesRepository) : Presenter.Factory {
85+
override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {
86+
return when (screen) {
87+
CountryListScreen -> return CountryListPresenter(navigator, repository)
88+
else -> null
89+
}
90+
}
91+
}
92+
}
93+
94+
95+
class NetworkListPresenter(
96+
private val screen: NetworkListScreen,
97+
private val navigator: Navigator,
98+
private val cityBikesRepository: CityBikesRepository
99+
) : Presenter<NetworkListScreen.State> {
100+
@Composable
101+
override fun present(): NetworkListScreen.State {
102+
val groupedNetworkList by cityBikesRepository.groupedNetworkList.collectAsState()
103+
val countryList = groupedNetworkList[screen.countryCode]?.sortedBy { it.city } ?: emptyList()
104+
val oountryName = getCountryName(screen.countryCode)
105+
return NetworkListScreen.State(screen.countryCode, oountryName, countryList) { event ->
106+
when (event) {
107+
is NetworkListScreen.Event.NetworkClicked -> navigator.goTo(StationListScreen(event.networkId))
108+
NetworkListScreen.Event.BackClicked -> navigator.pop()
109+
}
110+
}
111+
}
112+
113+
class Factory(private val repository: CityBikesRepository) : Presenter.Factory {
114+
override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {
115+
return when (screen) {
116+
is NetworkListScreen -> return NetworkListPresenter(screen, navigator, repository)
117+
else -> null
118+
}
119+
}
120+
}
121+
}
122+
123+
124+
class StationListPresenter(
125+
private val screen: StationListScreen,
126+
private val navigator: Navigator,
127+
private val cityBikesRepository: CityBikesRepository
128+
) : Presenter<StationListScreen.State> {
129+
@Composable
130+
override fun present(): StationListScreen.State {
131+
val stationList by cityBikesRepository.pollNetworkUpdates(screen.networkId).collectAsState(emptyList())
132+
return StationListScreen.State(screen.networkId, stationList) { event ->
133+
when (event) {
134+
StationListScreen.Event.BackClicked -> navigator.pop()
135+
}
136+
}
137+
}
138+
139+
class Factory(private val repository: CityBikesRepository) : Presenter.Factory {
140+
override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {
141+
return when (screen) {
142+
is StationListScreen -> return StationListPresenter(screen, navigator, repository)
143+
else -> null
144+
}
145+
}
146+
}
147+
}
148+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.johnoreilly.common.ui
2+
3+
import androidx.compose.runtime.Composable
4+
import com.slack.circuit.backstack.rememberSaveableBackStack
5+
import com.slack.circuit.foundation.Circuit
6+
import com.slack.circuit.foundation.CircuitCompositionLocals
7+
import com.slack.circuit.foundation.NavigableCircuitContent
8+
import com.slack.circuit.foundation.rememberCircuitNavigator
9+
import dev.johnoreilly.common.screens.CountryListScreen
10+
import me.tatarka.inject.annotations.Inject
11+
12+
13+
14+
typealias BikeShareContent = @Composable () -> Unit
15+
16+
17+
@Inject
18+
@Composable
19+
fun BikeShareContent(circuit: Circuit) {
20+
21+
val backStack = rememberSaveableBackStack(root = CountryListScreen)
22+
val navigator = rememberCircuitNavigator(backStack) {
23+
24+
}
25+
CircuitCompositionLocals(circuit) {
26+
NavigableCircuitContent(navigator = navigator, backStack = backStack)
27+
}
28+
}

0 commit comments

Comments
 (0)