Skip to content

Commit

Permalink
Add android app example with kotlin multiplatform (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
slinkydeveloper authored Oct 15, 2024
1 parent caef472 commit e0f8fda
Show file tree
Hide file tree
Showing 51 changed files with 1,639 additions and 6 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ challenges.

### Kotlin

| Type | Name / Link |
|------------|-------------------------------------------------------------------------|
| Templates | [Template using Gradle](templates/kotlin-gradle) |
| Basics | [Durable Execution, Event-processing, Virtual Objects](basics/basics-kotlin) |
| Use Cases | [Sagas](patterns-use-cases/sagas/sagas-kotlin) |
| End-to-End | [Food Ordering App](end-to-end-applications/kotlin/food-ordering) |
| Type | Name / Link |
|------------|-------------------------------------------------------------------------------------------------|
| Templates | [Template using Gradle](templates/kotlin-gradle) |
| Basics | [Durable Execution, Event-processing, Virtual Objects](basics/basics-kotlin) |
| Use Cases | [Sagas](patterns-use-cases/sagas/sagas-kotlin) |
| End-to-End | [Food Ordering App](end-to-end-applications/kotlin/food-ordering) |
| End-to-End | [Todos Kotlin Multiplatform + Android app](end-to-end-applications/kotlin/kmp-android-todo-app) |


### Python
Expand Down
3 changes: 3 additions & 0 deletions end-to-end-applications/kotlin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ manages orders, restaurants, payments, and delivery drivers.
The example mixes workflows (ordering) and stateful microservices (driver management),
and uses Kafka as an event source for updates from delivery drivers.

### Todos Kotlin Multiplatform example with Android App

The [Todos Kotlin multiplatform example](./kmp-android-todo-app) shows how to use Restate as the backend of your app, built using Kotlin Multiplatform.
19 changes: 19 additions & 0 deletions end-to-end-applications/kotlin/kmp-android-todo-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
*.iml
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
restate-data
15 changes: 15 additions & 0 deletions end-to-end-applications/kotlin/kmp-android-todo-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Kotlin Multiplatform example Android app + Restate backend

Kotlin multiplatform example for a Todo app using Restate as backend:

![](screenshot.png)

This project contains:

* The [shared](./shared) code
* The [Todos Virtual Object](./server/src/main/kotlin/dev/restate/examples/noteapp/Application.kt), to store the todos, built using the Kotlin Restate SDK
* The [Android app](./composeApp)

For more details on how to use it and run the Android app, check: https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html

To run the Restate server, just follow the same instructions as https://docs.restate.dev/get_started/quickstart?sdk=kotlin
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsCompose) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.kotlinxSerialization) apply false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}

kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}

sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.okhttp)
implementation(libs.kotlinx.coroutines.android)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(projects.shared)
implementation(libs.ktor.client.core)
implementation(libs.ktor.serialization.json)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.kotlinx.coroutines.core)
}
}
}

android {
namespace = "dev.restate.examples.noteapp"
compileSdk = libs.versions.android.compileSdk.get().toInt()

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
applicationId = "dev.restate.examples.noteapp"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
dependencies {
debugImplementation(compose.uiTooling)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:usesCleartextTraffic="true">
<activity
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.restate.examples.noteapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.restate.examples.noteapp.composables.TodoItemsContainer
import dev.restate.examples.noteapp.composables.TodoInputBar
import dev.restate.examples.noteapp.ui.constants.OverlappingHeight
import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers

// Thanks to https://medium.com/deuk/intermediate-android-compose-todo-app-ui-1d808ef7882d for the Compose UI related code.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mainViewModel = MainViewModel(TodosClient(Url("http://localhost:8080")), dispatcher = Dispatchers.IO)
setContent {
Box(
modifier = Modifier.fillMaxSize()
) {
TodoItemsContainer(
todoItemsFlow = mainViewModel.todos,
onItemClick = mainViewModel::toggleTodo,
onItemDelete = mainViewModel::removeTodo,
overlappingElementsHeight = OverlappingHeight
)
TodoInputBar(
modifier = Modifier.align(Alignment.BottomStart),
onAddButtonClick = mainViewModel::addTodo
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dev.restate.examples.noteapp

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

class MainViewModel(
private val client: TodosClient,
private val dispatcher: CoroutineDispatcher
) : ViewModel() {

private val updateSignal = Channel<Unit>()

val todos: Flow<List<TodoItem>> = updateSignal.receiveAsFlow().map {
getTodos().await()
}

init {
// Trigger first reload
viewModelScope.launch(dispatcher) {
triggerTodosReload()
}
}

fun addTodo(todo: String) =
viewModelScope.launch(dispatcher) {
client.add(TodoItem(content = todo))
triggerTodosReload()
}

fun toggleTodo(todoItem: TodoItem) =
viewModelScope.launch(dispatcher) {
client.markCompleted(todoItem.id)
triggerTodosReload()
}

fun removeTodo(todoItem: TodoItem) =
viewModelScope.launch(dispatcher) {
client.remove(todoItem.id)
triggerTodosReload()
}

private fun getTodos() =
viewModelScope.async(dispatcher) { this@MainViewModel.client.readAll() }

private suspend fun triggerTodosReload() {
updateSignal.send(Unit)
}

}
Loading

0 comments on commit e0f8fda

Please sign in to comment.