2019-12-22 20:31:15 +00:00
|
|
|
# PeopleInSpace
|
2019-12-22 20:25:01 +00:00
|
|
|
|
2020-04-17 19:05:04 +00:00
|
|
|
Minimal **Kotlin Multiplatform** project using Jetpack Compose and SwiftUI. Currently running on
|
|
|
|
* Android
|
|
|
|
* iOS
|
|
|
|
* watchOS
|
|
|
|
* macOS
|
2020-05-02 13:40:33 +00:00
|
|
|
* Web
|
2019-12-22 20:31:15 +00:00
|
|
|
|
|
|
|
It makes use of basic API (http://open-notify.org/Open-Notify-API/People-In-Space/) to show list of people currently in
|
|
|
|
space (inspired by https://kousenit.org/2019/12/19/a-few-astronomical-examples-in-kotlin/)! The list is shown on Android
|
|
|
|
using **Jetpack Compose** and on iOS using **SwiftUI**
|
2019-12-22 20:25:01 +00:00
|
|
|
|
2020-05-02 14:33:18 +00:00
|
|
|
Related posts:
|
|
|
|
* [Minimal Kotlin Multiplatform project using Compose and SwiftUI](https://johnoreilly.dev/posts/minimal-kotlin-platform-compose-swiftui/)
|
|
|
|
* [Adding some Storage (to) Space](https://johnoreilly.dev/posts/adding-sqldelight-to-peopleinspace/)
|
|
|
|
* [Kotlin Multiplatform running on macOS](https://johnoreilly.dev/posts/kotlinmultiplatform-macos/)
|
|
|
|
* [PeopleInSpace hits the web with Kotlin/JS and React](https://johnoreilly.dev/posts/peopleinspace-kotlinjs/)
|
2020-05-16 22:19:52 +00:00
|
|
|
* [Using Koin in a Kotlin Multiplatform Project](https://johnoreilly.dev/posts/kotlinmultiplatform-koin/)
|
2020-05-02 14:33:18 +00:00
|
|
|
|
|
|
|
|
2020-05-12 19:13:31 +00:00
|
|
|
Note that this repository very much errs on the side of mimimalism to help more clearly illustrate key moving parts of a Koltin
|
|
|
|
Multiplatform project and also to hopefully help someone just starting to explore KMP to get up and running for first time (and is of course
|
|
|
|
primarily focussed on use of Jetpack Compose and SwiftUI). If you're at stage of moving
|
2020-05-12 17:54:08 +00:00
|
|
|
beyond this then I'd definitely recommend checking out [KaMPKit](https://github.com/touchlab/KaMPKit)
|
|
|
|
|
|
|
|
|
2020-06-13 13:02:56 +00:00
|
|
|
**Note**: You need to use Android Studio v4.2 (currently preview/alpha version). Have tested on XCode v11.3. When opening
|
|
|
|
iOS/watchOS/macOS projects remember to open `.xcworkspace` file (and not `.xcodeproj` one)
|
2019-12-22 20:25:01 +00:00
|
|
|
|
2020-02-08 12:51:00 +00:00
|
|
|
|
2020-04-17 19:05:04 +00:00
|
|
|
**Update Jan 14th 2020**: This now also includes WatchOS version thanks to [Neal Sanche](https://github.com/nealsanche)
|
2019-12-22 21:34:15 +00:00
|
|
|
|
2019-12-23 13:53:01 +00:00
|
|
|
The following is pretty much all the code used (along with gradle files/resources etc). I did say it was *minimal*!!
|
2019-12-22 21:34:15 +00:00
|
|
|
|
2020-02-08 12:51:00 +00:00
|
|
|
**Update Jan 25th 2020**: Have added SQLDelight support for locally persisting data (across all the platforms).
|
|
|
|
I haven't updated code below yet as I think it still has value in demonstrating what a minimum Kotlin
|
|
|
|
Multiplatform project would be.
|
2019-12-22 21:34:15 +00:00
|
|
|
|
2020-04-17 19:05:04 +00:00
|
|
|
**Update April 15th 2020**: Added macOS support
|
|
|
|
|
2020-05-02 13:40:33 +00:00
|
|
|
**Update May 2nd 2020**: Added basic Kotlin/JS support
|
2020-04-17 19:05:04 +00:00
|
|
|
|
2020-06-10 20:29:04 +00:00
|
|
|
**Update May 15th 2020**: Using Koin in shared multiplatform code
|
|
|
|
|
2019-12-23 13:53:01 +00:00
|
|
|
### iOS SwiftUI Code
|
2019-12-22 21:34:15 +00:00
|
|
|
|
2019-12-22 21:42:24 +00:00
|
|
|
```swift
|
2019-12-22 21:34:15 +00:00
|
|
|
struct ContentView: View {
|
|
|
|
@ObservedObject var peopleInSpaceViewModel = PeopleInSpaceViewModel(repository: PeopleInSpaceRepository())
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
NavigationView {
|
|
|
|
List(peopleInSpaceViewModel.people, id: \.name) { person in
|
|
|
|
PersonView(person: person)
|
|
|
|
}
|
|
|
|
.navigationBarTitle(Text("PeopleInSpace"), displayMode: .large)
|
|
|
|
.onAppear(perform: {
|
|
|
|
self.peopleInSpaceViewModel.fetch()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct PersonView : View {
|
|
|
|
var person: Assignment
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
HStack {
|
|
|
|
VStack(alignment: .leading) {
|
|
|
|
Text(person.name).font(.headline)
|
|
|
|
Text(person.craft).font(.subheadline)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2020-01-14 07:47:16 +00:00
|
|
|
|
|
|
|
### WatchOS SwiftUI Code
|
|
|
|
|
|
|
|
```swift
|
|
|
|
struct ContentView: View {
|
|
|
|
@ObservedObject var peopleInSpaceViewModel = PeopleInSpaceViewModel(repository: PeopleInSpaceRepository())
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
VStack {
|
|
|
|
List(peopleInSpaceViewModel.people, id: \.name) { person in
|
|
|
|
PersonView(person: person)
|
|
|
|
}
|
|
|
|
.onAppear(perform: {
|
|
|
|
self.peopleInSpaceViewModel.fetch()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct PersonView : View {
|
|
|
|
var person: Assignment
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
NavigationLink(person.name, destination: Text(person.craft).font(.subheadline))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-12-23 13:53:01 +00:00
|
|
|
### iOS Swift ViewModel
|
|
|
|
|
|
|
|
```swift
|
|
|
|
class PeopleInSpaceViewModel: ObservableObject {
|
|
|
|
@Published var people = [Assignment]()
|
|
|
|
|
|
|
|
private let repository: PeopleInSpaceRepository
|
|
|
|
init(repository: PeopleInSpaceRepository) {
|
|
|
|
self.repository = repository
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetch() {
|
|
|
|
repository.fetchPeople(success: { data in
|
|
|
|
self.people = data
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### Android Jetpack Compose code
|
2019-12-22 21:34:15 +00:00
|
|
|
|
2019-12-22 21:42:24 +00:00
|
|
|
```kotlin
|
2019-12-22 21:34:15 +00:00
|
|
|
class MainActivity : AppCompatActivity() {
|
|
|
|
private val peopleInSpaceViewModel: PeopleInSpaceViewModel by viewModel()
|
|
|
|
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
2020-04-30 19:53:51 +00:00
|
|
|
|
2019-12-22 21:34:15 +00:00
|
|
|
setContent {
|
2020-04-30 19:53:51 +00:00
|
|
|
val peopleState = peopleInSpaceViewModel.peopleInSpace.observeAsState()
|
|
|
|
mainLayout(peopleState)
|
2019-12-22 21:34:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Composable
|
2020-04-30 19:53:51 +00:00
|
|
|
fun mainLayout(peopleState: State<List<Assignment>?>) {
|
2019-12-22 21:34:15 +00:00
|
|
|
MaterialTheme {
|
|
|
|
Column {
|
2020-04-30 19:53:51 +00:00
|
|
|
TopAppBar(
|
|
|
|
title = {
|
|
|
|
Text("People In Space")
|
|
|
|
}
|
|
|
|
)
|
|
|
|
AdapterList(data = peopleState.value!!) { person ->
|
2019-12-22 21:34:15 +00:00
|
|
|
Row(person)
|
|
|
|
}
|
2020-04-30 19:53:51 +00:00
|
|
|
|
2019-12-22 21:34:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
fun Row(person: Assignment) {
|
2020-04-30 19:53:51 +00:00
|
|
|
Text(
|
|
|
|
text = "${person.name} (${person.craft})",
|
|
|
|
modifier = Modifier.padding(16.dp)
|
|
|
|
)
|
2019-12-22 21:34:15 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2019-12-23 13:53:01 +00:00
|
|
|
### Android Kotlin ViewModel
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
class PeopleInSpaceViewModel(peopleInSpaceRepository: PeopleInSpaceRepository) : ViewModel() {
|
|
|
|
val peopleInSpace = MutableLiveData<List<Assignment>>(emptyList())
|
|
|
|
|
|
|
|
init {
|
2019-12-26 18:28:57 +00:00
|
|
|
viewModelScope.launch {
|
2019-12-23 13:53:01 +00:00
|
|
|
val people = peopleInSpaceRepository.fetchPeople()
|
2019-12-26 18:28:57 +00:00
|
|
|
peopleInSpace.value = people
|
2019-12-23 13:53:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2020-05-02 13:40:33 +00:00
|
|
|
### Kotlin/JS client
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
val App = functionalComponent<RProps> { _ ->
|
|
|
|
val scope = MainScope()
|
|
|
|
val api = PeopleInSpaceApi()
|
|
|
|
|
|
|
|
val (people, setPeople) = useState(emptyList<Assignment>())
|
|
|
|
|
|
|
|
useEffect(dependencies = listOf()) {
|
|
|
|
scope.launch {
|
|
|
|
setPeople(api.fetchPeople().people)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
+"People In Space"
|
|
|
|
}
|
|
|
|
ul {
|
|
|
|
people.forEach { item ->
|
|
|
|
li {
|
|
|
|
+"${item.name} (${item.craft})"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2019-12-23 13:53:01 +00:00
|
|
|
|
|
|
|
### Shared Kotlin Repository
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
class PeopleInSpaceRepository {
|
|
|
|
private val peopleInSpaceApi = PeopleInSpaceApi()
|
|
|
|
|
|
|
|
suspend fun fetchPeople() : List<Assignment> {
|
|
|
|
val result = peopleInSpaceApi.fetchPeople()
|
|
|
|
return result.people
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun fetchPeople(success: (List<Assignment>) -> Unit) {
|
2019-12-26 18:28:57 +00:00
|
|
|
GlobalScope.launch(Dispatchers.Main) {
|
2019-12-23 13:53:01 +00:00
|
|
|
success(fetchPeople())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Shared Kotlin API Client Code (using **Ktor** and **Kotlinx Serialization** library)
|
|
|
|
|
|
|
|
```kotlin
|
|
|
|
@Serializable
|
|
|
|
data class AstroResult(val message: String, val number: Int, val people: List<Assignment>)
|
|
|
|
|
|
|
|
@Serializable
|
|
|
|
data class Assignment(val craft: String, val name: String)
|
|
|
|
|
|
|
|
class PeopleInSpaceApi {
|
2020-04-17 19:05:04 +00:00
|
|
|
private val baseUrl = "http://api.open-notify.org"
|
2019-12-23 13:53:01 +00:00
|
|
|
|
|
|
|
private val client by lazy {
|
|
|
|
HttpClient() {
|
|
|
|
install(JsonFeature) {
|
|
|
|
serializer = KotlinxSerializer(Json(JsonConfiguration(strictMode = false)))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-17 19:05:04 +00:00
|
|
|
suspend fun fetchPeople() = client.get<AstroResult>("$baseUrl/astros.json")
|
2019-12-23 13:53:01 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
|
2019-12-22 21:34:15 +00:00
|
|
|
|
|
|
|
|
2019-12-22 20:39:14 +00:00
|
|
|
### Languages, libraries and tools used
|
|
|
|
|
|
|
|
* [Kotlin](https://kotlinlang.org/)
|
|
|
|
* [Kotlin Corooutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
|
|
|
|
* [Kotlin Serialization](https://github.com/Kotlin/kotlinx.serialization)
|
|
|
|
* [Ktor client library](https://github.com/ktorio/ktor)
|
|
|
|
* [Android Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html)
|
|
|
|
* [Koin](https://github.com/InsertKoinIO/koin)
|
|
|
|
* [Jetpack Compose](https://developer.android.com/jetpack/compose)
|
|
|
|
* [SwiftUI](https://developer.apple.com/documentation/swiftui)
|