Skip to content

Commit 6aefeda

Browse files
committed
refactor: Make GoogleMapsInitializer suspendable
This change refactors the `GoogleMapsInitializer` to use suspend functions for initialization and reset operations, improving its testability and adherence to structured concurrency. Key changes: - Converted `GoogleMapsInitializer.initialize()` and `GoogleMapsInitializer.reset()` to `suspend` functions. - Removed the internal `CoroutineScope` and job management from `GoogleMapsInitializer`. - Updated unit and instrumentation tests to use `runTest` from `kotlinx-coroutines-test`, removing the need for `Thread.sleep()`. - Added the `kotlinx-coroutines-test` dependency to the `maps-app` module.
1 parent cdac9aa commit 6aefeda

4 files changed

Lines changed: 33 additions & 59 deletions

File tree

maps-app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ dependencies {
8787
testImplementation(libs.robolectric)
8888
testImplementation(libs.androidx.test.core)
8989
testImplementation(libs.truth)
90+
testImplementation(libs.kotlinx.coroutines.test)
9091

9192
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
9293

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
1-
// Copyright 2025 Google LLC
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
14-
151
package com.google.maps.android.compose
162

173
import android.content.Context
@@ -20,29 +6,29 @@ import androidx.test.platform.app.InstrumentationRegistry
206
import com.google.common.truth.Truth.assertThat
217
import com.google.maps.android.compose.internal.GoogleMapsInitializer
228
import com.google.maps.android.compose.internal.InitializationState
9+
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.test.runTest
2311
import org.junit.After
2412
import org.junit.Test
2513
import org.junit.runner.RunWith
2614

15+
@OptIn(ExperimentalCoroutinesApi::class)
2716
@RunWith(AndroidJUnit4::class)
2817
class GoogleMapsInitializerTest {
2918

3019
@After
31-
fun tearDown() {
20+
fun tearDown() = runTest {
3221
GoogleMapsInitializer.reset()
3322
}
3423

3524
@Test
36-
fun testInitializationSuccess() {
25+
fun testInitializationSuccess() = runTest {
3726
// In an instrumentation test environment, Google Play services are available.
3827
// Therefore, we expect the initialization to succeed.
3928
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
4029

4130
GoogleMapsInitializer.initialize(context)
4231

43-
// Wait for the initialization to complete
44-
Thread.sleep(1000)
45-
4632
assertThat(GoogleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
4733
}
4834
}
Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,35 @@
1-
// Copyright 2025 Google LLC
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
14-
151
package com.google.maps.android.compose
162

173
import android.content.Context
184
import androidx.test.core.app.ApplicationProvider
195
import com.google.common.truth.Truth.assertThat
206
import com.google.maps.android.compose.internal.GoogleMapsInitializer
217
import com.google.maps.android.compose.internal.InitializationState
8+
import kotlinx.coroutines.ExperimentalCoroutinesApi
9+
import kotlinx.coroutines.test.runTest
2210
import org.junit.After
2311
import org.junit.Test
2412
import org.junit.runner.RunWith
2513
import org.robolectric.RobolectricTestRunner
2614

15+
@OptIn(ExperimentalCoroutinesApi::class)
2716
@RunWith(RobolectricTestRunner::class)
2817
class GoogleMapsInitializerTest {
2918

3019
@After
31-
fun tearDown() {
20+
fun tearDown() = runTest {
3221
GoogleMapsInitializer.reset()
3322
}
3423

3524
@Test
36-
fun testInitializationFailure() {
25+
fun testInitializationFailure() = runTest {
3726
// In a unit test environment, Google Play services are not available.
3827
// Therefore, we expect the initialization to fail.
3928
val context: Context = ApplicationProvider.getApplicationContext()
4029

4130
GoogleMapsInitializer.initialize(context)
4231

43-
// Wait for the initialization to complete
44-
Thread.sleep(1000)
45-
32+
// The initialization is now synchronous within the test scope, so we don't need to wait.
4633
assertThat(GoogleMapsInitializer.state.value).isEqualTo(InitializationState.FAILURE)
4734
}
4835
}

maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import com.google.android.gms.maps.MapsApiSettings
2323
import com.google.maps.android.compose.meta.AttributionId
2424
import kotlinx.coroutines.CoroutineScope
2525
import kotlinx.coroutines.Dispatchers
26-
import kotlinx.coroutines.Job
26+
import kotlinx.coroutines.coroutineScope
2727
import kotlinx.coroutines.launch
2828
import kotlinx.coroutines.sync.Mutex
2929
import kotlinx.coroutines.sync.withLock
30+
import kotlinx.coroutines.withContext
3031

3132
/**
3233
* Enum representing the initialization state of the Google Maps SDK.
@@ -72,7 +73,6 @@ public object GoogleMapsInitializer {
7273
private val _state = mutableStateOf(InitializationState.UNINITIALIZED)
7374
public val state: State<InitializationState> = _state
7475

75-
private var initializationJob: Job? = null
7676
private val mutex = Mutex()
7777

7878
/**
@@ -93,24 +93,28 @@ public object GoogleMapsInitializer {
9393
*
9494
* @param context The context to use for initialization.
9595
*/
96-
public fun initialize(context: Context) {
97-
CoroutineScope(Dispatchers.IO).launch {
96+
public suspend fun initialize(context: Context) {
97+
if (_state.value != InitializationState.UNINITIALIZED) {
98+
return
99+
}
100+
coroutineScope {
98101
mutex.withLock {
102+
// Re-check state to prevent re-initialization even when calling this function in parallel
99103
if (_state.value != InitializationState.UNINITIALIZED) {
100104
return@withLock
101105
}
102-
103106
_state.value = InitializationState.INITIALIZING
104-
initializationJob = launch {
105-
try {
106-
if (MapsInitializer.initialize(context) == ConnectionResult.SUCCESS) {
107-
MapsApiSettings.addInternalUsageAttributionId(context, attributionId)
108-
_state.value = InitializationState.SUCCESS
109-
}
110-
} catch (e: Exception) {
111-
// In tests where the map is mocked, this can fail.
112-
_state.value = InitializationState.FAILURE
107+
}
108+
109+
launch(Dispatchers.IO) {
110+
try {
111+
if (MapsInitializer.initialize(context) == ConnectionResult.SUCCESS) {
112+
MapsApiSettings.addInternalUsageAttributionId(context, attributionId)
113+
_state.value = InitializationState.SUCCESS
113114
}
115+
} catch (e: Exception) {
116+
// In tests where the map is mocked, this can fail.
117+
_state.value = InitializationState.FAILURE
114118
}
115119
}
116120
}
@@ -123,13 +127,9 @@ public object GoogleMapsInitializer {
123127
* This is useful in test environments where you might need to re-initialize the SDK
124128
* multiple times.
125129
*/
126-
public fun reset() {
127-
CoroutineScope(Dispatchers.IO).launch {
128-
mutex.withLock {
129-
initializationJob?.cancel()
130-
initializationJob = null
131-
_state.value = InitializationState.UNINITIALIZED
132-
}
130+
public suspend fun reset() {
131+
mutex.withLock {
132+
_state.value = InitializationState.UNINITIALIZED
133133
}
134134
}
135135
}

0 commit comments

Comments
 (0)