Skip to content

Commit 7d239e8

Browse files
authored
Set up engine benchmark app with Jetpack Compose (#2817)
1 parent 9440c54 commit 7d239e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1444
-5
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ buildscript {
1313
classpath(Plugins.kotlinGradlePlugin)
1414
classpath(Plugins.navSafeArgsGradlePlugin)
1515
classpath(Plugins.rulerGradlePlugin)
16+
classpath(Plugins.kotlinComposePlugin)
17+
classpath(Plugins.kotlinSerializationPlugin)
1618
}
1719
}
1820

buildSrc/src/main/kotlin/Plugins.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ object Plugins {
2323
const val dokka = "org.jetbrains.dokka"
2424
const val kotlin = "kotlin"
2525
const val kotlinAndroid = "kotlin-android"
26+
const val kotlinCompose = "org.jetbrains.kotlin.plugin.compose"
2627
const val kotlinKsp = "com.google.devtools.ksp"
28+
const val kotlinSerialization = "org.jetbrains.kotlin.plugin.serialization"
2729
const val mavenPublish = "maven-publish"
2830
const val fladle = "com.osacky.fladle"
2931
const val navSafeArgs = "androidx.navigation.safeargs.kotlin"
@@ -36,6 +38,10 @@ object Plugins {
3638
const val benchmarkGradlePlugin =
3739
"androidx.benchmark:benchmark-gradle-plugin:${Versions.benchmarkPlugin}"
3840
const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
41+
const val kotlinComposePlugin =
42+
"org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${Versions.kotlin}"
43+
const val kotlinSerializationPlugin =
44+
"org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:${Versions.kotlin}"
3945
const val navSafeArgsGradlePlugin = "androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0"
4046
const val rulerGradlePlugin = "com.spotify.ruler:ruler-gradle-plugin:1.2.1"
4147
const val flankGradlePlugin = "com.osacky.flank.gradle:fladle:0.17.4"

demo/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ dependencies {
5050
implementation(libs.androidx.datastore.preferences)
5151
implementation(libs.androidx.fragment)
5252
implementation(libs.androidx.lifecycle.livedata)
53-
implementation(libs.androidx.lifecycle.runtime)
53+
implementation(libs.androidx.lifecycle.runtime.ktx)
5454
implementation(libs.androidx.lifecycle.viewmodel)
5555
implementation(libs.androidx.navigation.fragment)
5656
implementation(libs.androidx.navigation.ui)

engine/benchmarks/app/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build
2+
/src/main/assets/bulk_data/*
3+
!*.keep
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
plugins {
2+
id(Plugins.BuildPlugins.application)
3+
id(Plugins.BuildPlugins.kotlinAndroid)
4+
id(Plugins.BuildPlugins.kotlinCompose)
5+
id(Plugins.BuildPlugins.kotlinSerialization)
6+
}
7+
8+
android {
9+
namespace = "com.google.android.fhir.engine.benchmarks.app"
10+
compileSdk = Sdk.COMPILE_SDK
11+
12+
defaultConfig {
13+
applicationId = "com.google.android.fhir.engine.benchmarks.app"
14+
minSdk = Sdk.MIN_SDK
15+
targetSdk = Sdk.TARGET_SDK
16+
versionCode = 1
17+
versionName = "1.0"
18+
19+
testInstrumentationRunner = Dependencies.androidJunitRunner
20+
}
21+
22+
buildTypes {
23+
release {
24+
isMinifyEnabled = false
25+
proguardFiles(
26+
getDefaultProguardFile("proguard-android-optimize.txt"),
27+
"proguard-rules.pro",
28+
)
29+
}
30+
31+
create("benchmark") {
32+
initWith(buildTypes.getByName("release"))
33+
signingConfig = signingConfigs.getByName("debug")
34+
matchingFallbacks += listOf("release")
35+
isDebuggable = false
36+
}
37+
}
38+
compileOptions {
39+
isCoreLibraryDesugaringEnabled = true
40+
sourceCompatibility = JavaVersion.VERSION_11
41+
targetCompatibility = JavaVersion.VERSION_11
42+
}
43+
kotlinOptions { jvmTarget = "11" }
44+
buildFeatures { compose = true }
45+
46+
packaging { resources.excludes.addAll(listOf("META-INF/ASL-2.0.txt", "META-INF/LGPL-3.0.txt")) }
47+
48+
kotlin { jvmToolchain(11) }
49+
}
50+
51+
dependencies {
52+
coreLibraryDesugaring(Dependencies.desugarJdkLibs)
53+
implementation(project(":engine"))
54+
55+
implementation(libs.androidx.core)
56+
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
57+
implementation(libs.androidx.activity.compose)
58+
implementation(platform(libs.androidx.compose.bom))
59+
implementation(libs.androidx.compose.ui)
60+
implementation(libs.androidx.compose.ui.graphics)
61+
implementation(libs.androidx.compose.ui.tooling.preview)
62+
implementation(libs.androidx.compose.material3)
63+
implementation(libs.androidx.navigation.compose)
64+
implementation(libs.bundles.androidx.tracing)
65+
implementation(libs.kotlinx.serialization.json)
66+
67+
testImplementation(libs.junit)
68+
androidTestImplementation(libs.androidx.test.ext.junit)
69+
androidTestImplementation(libs.androidx.test.espresso.core)
70+
androidTestImplementation(platform(libs.androidx.compose.bom))
71+
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
72+
debugImplementation(libs.androidx.compose.ui.tooling)
73+
debugImplementation(libs.androidx.compose.ui.test.manifest)
74+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<manifest
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
>
6+
7+
<uses-permission android:name="android.permission.INTERNET" />
8+
9+
<application
10+
android:name=".MainApplication"
11+
android:allowBackup="true"
12+
android:icon="@mipmap/ic_launcher"
13+
android:label="@string/app_name"
14+
android:networkSecurityConfig="@xml/network_security_config"
15+
android:roundIcon="@mipmap/ic_launcher_round"
16+
android:supportsRtl="true"
17+
android:theme="@style/Theme.Androidfhir"
18+
>
19+
20+
<profileable android:shell="true" tools:targetApi="29" />
21+
22+
<activity
23+
android:name=".MainActivity"
24+
android:exported="true"
25+
android:theme="@style/Theme.Androidfhir"
26+
>
27+
<intent-filter>
28+
<action android:name="android.intent.action.MAIN" />
29+
30+
<category android:name="android.intent.category.LAUNCHER" />
31+
</intent-filter>
32+
</activity>
33+
</application>
34+
35+
</manifest>

engine/benchmarks/app/src/main/assets/bulk_data/.keep

Whitespace-only changes.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.engine.benchmarks.app
18+
19+
import androidx.lifecycle.ViewModel
20+
import androidx.lifecycle.viewModelScope
21+
import com.google.android.fhir.FhirEngine
22+
import com.google.android.fhir.engine.benchmarks.app.data.ResourcesDataProvider
23+
import java.time.LocalDateTime
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
26+
import kotlinx.coroutines.flow.asStateFlow
27+
import kotlinx.coroutines.launch
28+
29+
@Suppress("unused")
30+
class CrudApiViewModel(
31+
private val resourcesDataProvider: ResourcesDataProvider,
32+
private val fhirEngine: FhirEngine,
33+
) : ViewModel() {
34+
private val _detailMutableStateFlow = MutableStateFlow("")
35+
val detailStateFlow: StateFlow<String> = _detailMutableStateFlow.asStateFlow()
36+
37+
init {
38+
viewModelScope.launch(benchmarkingViewModelWorkDispatcher) {
39+
_detailMutableStateFlow.value = "CRUD: ${LocalDateTime.now()}"
40+
}
41+
}
42+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.engine.benchmarks.app
18+
19+
import android.os.Bundle
20+
import androidx.activity.ComponentActivity
21+
import androidx.activity.compose.setContent
22+
import androidx.activity.enableEdgeToEdge
23+
import androidx.activity.viewModels
24+
import androidx.compose.runtime.Composable
25+
import androidx.lifecycle.ViewModel
26+
import androidx.lifecycle.ViewModelProvider
27+
import androidx.lifecycle.viewmodel.compose.viewModel
28+
import androidx.navigation.compose.NavHost
29+
import androidx.navigation.compose.composable
30+
import androidx.navigation.compose.rememberNavController
31+
import ca.uhn.fhir.context.FhirContext
32+
import com.google.android.fhir.FhirEngine
33+
import com.google.android.fhir.engine.benchmarks.app.data.ResourcesDataProvider
34+
import com.google.android.fhir.engine.benchmarks.app.ui.CrudDetail
35+
import com.google.android.fhir.engine.benchmarks.app.ui.Home
36+
import com.google.android.fhir.engine.benchmarks.app.ui.Screen
37+
import com.google.android.fhir.engine.benchmarks.app.ui.SearchApiDetail
38+
import com.google.android.fhir.engine.benchmarks.app.ui.SyncApiDetail
39+
import com.google.android.fhir.engine.benchmarks.app.ui.theme.AndroidfhirTheme
40+
import kotlin.getValue
41+
42+
class MainActivity : ComponentActivity() {
43+
44+
@Suppress("unused") private val viewModel: MainViewModel by viewModels()
45+
46+
private val resourcesDataProvider by lazy {
47+
val assetManager = this@MainActivity.assets
48+
val fhirR4JsonParser = FhirContext.forR4Cached().newJsonParser()
49+
ResourcesDataProvider(assetManager, fhirR4JsonParser)
50+
}
51+
52+
private val fhirEngine by lazy { MainApplication.fhirEngine(this@MainActivity) }
53+
54+
override fun onCreate(savedInstanceState: Bundle?) {
55+
super.onCreate(savedInstanceState)
56+
enableEdgeToEdge()
57+
setContent { AndroidfhirTheme { AndroidfhirApp(resourcesDataProvider, fhirEngine) } }
58+
}
59+
}
60+
61+
@Composable
62+
fun AndroidfhirApp(resourcesDataProvider: ResourcesDataProvider, fhirEngine: FhirEngine) {
63+
val navController = rememberNavController()
64+
NavHost(navController, startDestination = Screen.HomeScreen) {
65+
composable<Screen.HomeScreen> {
66+
Home(
67+
{ navController.navigate(Screen.CRUDDetailScreen) },
68+
{ navController.navigate(Screen.SearchDetailScreen) },
69+
{
70+
navController.navigate(
71+
Screen.SyncDetailScreen,
72+
)
73+
},
74+
)
75+
}
76+
77+
composable<Screen.CRUDDetailScreen> {
78+
val viewModel: CrudApiViewModel =
79+
viewModel(
80+
it,
81+
factory =
82+
object : ViewModelProvider.Factory {
83+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
84+
@Suppress("UNCHECKED_CAST")
85+
return CrudApiViewModel(resourcesDataProvider, fhirEngine) as T
86+
}
87+
},
88+
)
89+
CrudDetail(viewModel) { navController.popBackStack() }
90+
}
91+
92+
composable<Screen.SearchDetailScreen> {
93+
val viewModel: SearchApiViewModel =
94+
viewModel(
95+
it,
96+
factory =
97+
object : ViewModelProvider.Factory {
98+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
99+
@Suppress("UNCHECKED_CAST")
100+
return SearchApiViewModel(resourcesDataProvider, fhirEngine) as T
101+
}
102+
},
103+
)
104+
SearchApiDetail(viewModel) { navController.popBackStack() }
105+
}
106+
107+
composable<Screen.SyncDetailScreen> {
108+
val viewModel: SyncApiViewModel =
109+
viewModel(
110+
it,
111+
factory =
112+
object : ViewModelProvider.Factory {
113+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
114+
@Suppress("UNCHECKED_CAST")
115+
return SyncApiViewModel(resourcesDataProvider, fhirEngine) as T
116+
}
117+
},
118+
)
119+
SyncApiDetail(viewModel) { navController.popBackStack() }
120+
}
121+
}
122+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.android.fhir.engine.benchmarks.app
18+
19+
import android.app.Application
20+
import android.content.Context
21+
import android.util.Log
22+
import com.google.android.fhir.DatabaseErrorStrategy.RECREATE_AT_OPEN
23+
import com.google.android.fhir.FhirEngine
24+
import com.google.android.fhir.FhirEngineConfiguration
25+
import com.google.android.fhir.FhirEngineProvider
26+
import com.google.android.fhir.NetworkConfiguration
27+
import com.google.android.fhir.ServerConfiguration
28+
import com.google.android.fhir.sync.remote.HttpLogger
29+
30+
class MainApplication : Application() {
31+
32+
override fun onCreate() {
33+
super.onCreate()
34+
FhirEngineProvider.init(
35+
FhirEngineConfiguration(
36+
enableEncryptionIfSupported = true,
37+
RECREATE_AT_OPEN,
38+
ServerConfiguration(
39+
"https://hapi.fhir.org/baseR4/",
40+
httpLogger =
41+
HttpLogger(
42+
HttpLogger.Configuration(
43+
HttpLogger.Level.BODY,
44+
),
45+
) {
46+
Log.i(TAG, "App-HttpLog")
47+
Log.i(TAG, it)
48+
},
49+
networkConfiguration = NetworkConfiguration(uploadWithGzip = false),
50+
),
51+
),
52+
)
53+
}
54+
55+
private fun constructFhirEngine(): FhirEngine {
56+
return FhirEngineProvider.getInstance(this)
57+
}
58+
59+
// Only initiate the FhirEngine when used for the first time, not when the app is created.
60+
private val fhirEngine: FhirEngine by lazy { constructFhirEngine() }
61+
62+
companion object {
63+
private val TAG = MainApplication::class.java.simpleName
64+
65+
fun fhirEngine(context: Context) = (context.applicationContext as MainApplication).fhirEngine
66+
}
67+
}

0 commit comments

Comments
 (0)