16
16
package com.google.firebase.dataconnect.minimaldemo
17
17
18
18
import android.util.Log
19
+ import androidx.annotation.MainThread
19
20
import androidx.lifecycle.ViewModel
20
21
import androidx.lifecycle.ViewModelProvider
21
22
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -30,18 +31,21 @@ import io.kotest.property.Arb
30
31
import io.kotest.property.RandomSource
31
32
import io.kotest.property.arbitrary.next
32
33
import java.util.Objects
33
- import kotlinx.coroutines.CoroutineStart
34
+ import kotlinx.coroutines.CancellationException
34
35
import kotlinx.coroutines.Deferred
35
36
import kotlinx.coroutines.ExperimentalCoroutinesApi
36
- import kotlinx.coroutines.Job
37
37
import kotlinx.coroutines.async
38
38
import kotlinx.coroutines.flow.MutableStateFlow
39
39
import kotlinx.coroutines.flow.StateFlow
40
40
import kotlinx.coroutines.flow.asStateFlow
41
+ import kotlinx.coroutines.launch
41
42
import kotlinx.serialization.Serializable
42
43
43
44
class MainActivityViewModel (private val app : MyApplication ) : ViewModel() {
44
45
46
+ // Threading Note: _state may be _read_ by any thread, but _MUST ONLY_ be written to by the
47
+ // main thread. To support writing on other threads, special concurrency controls must be put
48
+ // in place to address the resulting race condition.
45
49
private val _state =
46
50
MutableStateFlow (
47
51
State (
@@ -56,62 +60,37 @@ class MainActivityViewModel(private val app: MyApplication) : ViewModel() {
56
60
57
61
private val rs = RandomSource .default()
58
62
63
+ @OptIn(ExperimentalCoroutinesApi ::class )
64
+ @MainThread
59
65
fun insertItem () {
60
- while (true ) {
61
- if (tryInsertItem()) {
62
- break
63
- }
64
- }
65
- }
66
-
67
- private fun tryInsertItem (): Boolean {
68
66
val arb = Arb .insertItemVariables()
69
67
val variables = if (rs.random.nextFloat() < 0.333f ) arb.edgecase(rs)!! else arb.next(rs)
70
68
71
- val oldState = _state .value
69
+ val originalState = _state .value
72
70
73
71
// If there is already an "insert" in progress, then just return and let the in-progress
74
72
// operation finish.
75
- when (oldState .getItem) {
76
- is State .OperationState .InProgress -> return true
73
+ when (originalState .getItem) {
74
+ is State .OperationState .InProgress -> return
77
75
is State .OperationState .New ,
78
76
is State .OperationState .Completed -> Unit
79
77
}
80
78
81
- // Create a new coroutine to perform the "insert" operation, but don't start it yet by
82
- // specifying start=CoroutineStart.LAZY because we won't start it until the state is
83
- // successfully set.
84
- val newInsertJob: Deferred <Zwda6x9zyyKey > =
85
- viewModelScope.async(start = CoroutineStart .LAZY ) {
86
- app.getConnector().insertItem.ref(variables).execute().data.key
87
- }
88
-
89
- // Update the state and start the coroutine if it is successfully set.
90
- val insertItemOperationInProgressState =
91
- State .OperationState .InProgress (oldState.nextSequenceNumber, variables, newInsertJob)
92
- val newState = oldState.withInsertInProgress(insertItemOperationInProgressState)
93
- if (! _state .compareAndSet(oldState, newState)) {
94
- return false
95
- }
96
-
97
- // Actually start the coroutine now that the state has been set.
79
+ // Start a new coroutine to perform the "insert" operation.
98
80
Log .i(TAG , " Inserting item: $variables " )
99
- newState.startInsert(insertItemOperationInProgressState)
100
- return true
101
- }
102
-
103
- @OptIn(ExperimentalCoroutinesApi ::class )
104
- private fun State.startInsert (
105
- insertItemOperationInProgressState :
106
- State .OperationState .InProgress <InsertItemMutation .Variables , Zwda6x9zyyKey >
107
- ) {
108
- require(insertItemOperationInProgressState == = insertItem)
109
- val job: Deferred <Zwda6x9zyyKey > = insertItemOperationInProgressState.job
110
- val variables: InsertItemMutation .Variables = insertItemOperationInProgressState.variables
111
-
112
- job.start()
81
+ val job: Deferred <Zwda6x9zyyKey > =
82
+ viewModelScope.async { app.getConnector().insertItem.ref(variables).execute().data.key }
83
+ val inProgressOperationState =
84
+ State .OperationState .InProgress (originalState.nextSequenceNumber, variables, job)
85
+ _state .value = originalState.withInsertInProgress(inProgressOperationState)
113
86
87
+ // Update the internal state once the "insert" operation has completed.
114
88
job.invokeOnCompletion { exception ->
89
+ // Don't log CancellationException, as document by invokeOnCompletion().
90
+ if (exception is CancellationException ) {
91
+ return @invokeOnCompletion
92
+ }
93
+
115
94
val result =
116
95
if (exception != = null ) {
117
96
Log .w(TAG , " WARNING: Inserting item FAILED: $exception (variables=$variables )" , exception)
@@ -122,187 +101,117 @@ class MainActivityViewModel(private val app: MyApplication) : ViewModel() {
122
101
Result .success(key)
123
102
}
124
103
125
- while ( true ) {
104
+ viewModelScope.launch {
126
105
val oldState = _state .value
127
- if (oldState.insertItem != = insertItemOperationInProgressState) {
128
- break
129
- }
130
-
131
- val insertItemOperationCompletedState =
132
- State .OperationState .Completed (oldState.nextSequenceNumber, variables, result)
133
- val newState = oldState.withInsertCompleted(insertItemOperationCompletedState)
134
- if (_state .compareAndSet(oldState, newState)) {
135
- break
106
+ if (oldState.insertItem == = inProgressOperationState) {
107
+ _state .value =
108
+ oldState.withInsertCompleted(
109
+ State .OperationState .Completed (oldState.nextSequenceNumber, variables, result)
110
+ )
136
111
}
137
112
}
138
113
}
139
114
}
140
115
116
+ @OptIn(ExperimentalCoroutinesApi ::class )
141
117
fun getItem () {
142
- while (true ) {
143
- if (tryGetItem()) {
144
- break
145
- }
146
- }
147
- }
148
-
149
- private fun tryGetItem (): Boolean {
150
- val oldState = _state .value
118
+ val originalState = _state .value
151
119
152
120
// If there is no previous successful "insert" operation, then we don't know any ID's to get,
153
121
// so just do nothing.
154
- val key: Zwda6x9zyyKey = oldState .lastInsertedKey ? : return true
122
+ val key: Zwda6x9zyyKey = originalState .lastInsertedKey ? : return
155
123
156
124
// If there is already a "get" in progress, then just return and let the in-progress operation
157
125
// finish.
158
- when (oldState .getItem) {
159
- is State .OperationState .InProgress -> return true
126
+ when (originalState .getItem) {
127
+ is State .OperationState .InProgress -> return
160
128
is State .OperationState .New ,
161
129
is State .OperationState .Completed -> Unit
162
130
}
163
131
164
- // Create a new coroutine to perform the "get" operation, but don't start it yet by specifying
165
- // start=CoroutineStart.LAZY because we won't start it until the state is successfully set.
166
- val newGetJob: Deferred <GetItemByKeyQuery .Data .Item ?> =
167
- viewModelScope.async(start = CoroutineStart .LAZY ) {
168
- app.getConnector().getItemByKey.execute(key).data.item
169
- }
170
-
171
- // Update the state and start the coroutine if it is successfully set.
172
- val getItemOperationInProgressState =
173
- State .OperationState .InProgress (oldState.nextSequenceNumber, key, newGetJob)
174
- val newState = oldState.withGetInProgress(getItemOperationInProgressState)
175
- if (! _state .compareAndSet(oldState, newState)) {
176
- return false
177
- }
178
-
179
- // Actually start the coroutine now that the state has been set.
180
- Log .i(TAG , " Getting item with key: $key " )
181
- newState.startGet(getItemOperationInProgressState)
182
- return true
183
- }
184
-
185
- @OptIn(ExperimentalCoroutinesApi ::class )
186
- private fun State.startGet (
187
- getItemOperationInProgressState :
188
- State .OperationState .InProgress <Zwda6x9zyyKey , GetItemByKeyQuery .Data .Item ?>
189
- ) {
190
- require(getItemOperationInProgressState == = getItem)
191
- val job: Deferred <GetItemByKeyQuery .Data .Item ?> = getItemOperationInProgressState.job
192
- val key: Zwda6x9zyyKey = getItemOperationInProgressState.variables
193
-
194
- job.start()
132
+ // Start a new coroutine to perform the "get" operation.
133
+ Log .i(TAG , " Retrieving item with key: $key " )
134
+ val job: Deferred <GetItemByKeyQuery .Data .Item ?> =
135
+ viewModelScope.async { app.getConnector().getItemByKey.execute(key).data.item }
136
+ val inProgressOperationState =
137
+ State .OperationState .InProgress (originalState.nextSequenceNumber, key, job)
138
+ _state .value = originalState.withGetInProgress(inProgressOperationState)
195
139
140
+ // Update the internal state once the "get" operation has completed.
196
141
job.invokeOnCompletion { exception ->
142
+ // Don't log CancellationException, as document by invokeOnCompletion().
143
+ if (exception is CancellationException ) {
144
+ return @invokeOnCompletion
145
+ }
146
+
197
147
val result =
198
148
if (exception != = null ) {
199
- Log .w(TAG , " WARNING: Getting item with key $key FAILED: $exception " , exception)
149
+ Log .w(TAG , " WARNING: Retrieving item with key= $key FAILED: $exception " , exception)
200
150
Result .failure(exception)
201
151
} else {
202
152
val item = job.getCompleted()
203
- Log .i(TAG , " Got item with key $key : $ item" )
153
+ Log .i(TAG , " Retrieved item with key: $key (item= ${ item} ) " )
204
154
Result .success(item)
205
155
}
206
156
207
- while ( true ) {
157
+ viewModelScope.launch {
208
158
val oldState = _state .value
209
- if (oldState.getItem != = getItemOperationInProgressState) {
210
- break
211
- }
212
-
213
- val getItemOperationCompletedState =
214
- State .OperationState .Completed (
215
- oldState.nextSequenceNumber,
216
- getItemOperationInProgressState.variables,
217
- result,
218
- )
219
- val newState = oldState.withGetCompleted(getItemOperationCompletedState)
220
- if (_state .compareAndSet(oldState, newState)) {
221
- break
159
+ if (oldState.getItem == = inProgressOperationState) {
160
+ _state .value =
161
+ oldState.withGetCompleted(
162
+ State .OperationState .Completed (oldState.nextSequenceNumber, key, result)
163
+ )
222
164
}
223
165
}
224
166
}
225
167
}
226
168
227
169
fun deleteItem () {
228
- while (true ) {
229
- if (tryDeleteItem()) {
230
- break
231
- }
232
- }
233
- }
234
-
235
- private fun tryDeleteItem (): Boolean {
236
- val oldState = _state .value
170
+ val originalState = _state .value
237
171
238
172
// If there is no previous successful "insert" operation, then we don't know any ID's to delete,
239
173
// so just do nothing.
240
- val key: Zwda6x9zyyKey = oldState .lastInsertedKey ? : return true
174
+ val key: Zwda6x9zyyKey = originalState .lastInsertedKey ? : return
241
175
242
176
// If there is already a "delete" in progress, then just return and let the in-progress
243
177
// operation finish.
244
- when (oldState.deleteItem ) {
245
- is State .OperationState .InProgress -> return true
178
+ when (originalState.getItem ) {
179
+ is State .OperationState .InProgress -> return
246
180
is State .OperationState .New ,
247
181
is State .OperationState .Completed -> Unit
248
182
}
249
183
250
- // Create a new coroutine to perform the "delete" operation, but don't start it yet by
251
- // specifying start=CoroutineStart.LAZY because we won't start it until the state is
252
- // successfully set.
253
- val newDeleteJob: Deferred <Unit > =
254
- viewModelScope.async(start = CoroutineStart .LAZY ) {
255
- app.getConnector().deleteItemByKey.execute(key)
256
- }
257
-
258
- // Update the state and start the coroutine if it is successfully set.
259
- val deleteItemOperationInProgressState =
260
- State .OperationState .InProgress (oldState.nextSequenceNumber, key, newDeleteJob)
261
- val newState = oldState.withDeleteInProgress(deleteItemOperationInProgressState)
262
- if (! _state .compareAndSet(oldState, newState)) {
263
- return false
264
- }
265
-
266
- // Actually start the coroutine now that the state has been set.
184
+ // Start a new coroutine to perform the "delete" operation.
267
185
Log .i(TAG , " Deleting item with key: $key " )
268
- newState.startDelete(deleteItemOperationInProgressState)
269
- return true
270
- }
271
-
272
- private fun State.startDelete (
273
- deleteItemOperationInProgressState : State .OperationState .InProgress <Zwda6x9zyyKey , Unit >
274
- ) {
275
- require(deleteItemOperationInProgressState == = deleteItem)
276
- val job: Job = deleteItemOperationInProgressState.job
277
- val key: Zwda6x9zyyKey = deleteItemOperationInProgressState.variables
278
-
279
- job.start()
186
+ val job: Deferred <Unit > =
187
+ viewModelScope.async { app.getConnector().deleteItemByKey.execute(key) }
188
+ val inProgressOperationState =
189
+ State .OperationState .InProgress (originalState.nextSequenceNumber, key, job)
190
+ _state .value = originalState.withDeleteInProgress(inProgressOperationState)
280
191
192
+ // Update the internal state once the "delete" operation has completed.
281
193
job.invokeOnCompletion { exception ->
194
+ // Don't log CancellationException, as document by invokeOnCompletion().
195
+ if (exception is CancellationException ) {
196
+ return @invokeOnCompletion
197
+ }
198
+
282
199
val result =
283
200
if (exception != = null ) {
284
- Log .w(TAG , " WARNING: Deleting item with key $key FAILED: $exception " , exception)
201
+ Log .w(TAG , " WARNING: Deleting item with key= $key FAILED: $exception " , exception)
285
202
Result .failure(exception)
286
203
} else {
287
- Log .i(TAG , " Deleted item with key $key " )
204
+ Log .i(TAG , " Deleted item with key: $key " )
288
205
Result .success(Unit )
289
206
}
290
207
291
- while ( true ) {
208
+ viewModelScope.launch {
292
209
val oldState = _state .value
293
- if (oldState.deleteItem != = deleteItemOperationInProgressState) {
294
- break
295
- }
296
-
297
- val deleteItemOperationCompletedState =
298
- State .OperationState .Completed (
299
- oldState.nextSequenceNumber,
300
- deleteItemOperationInProgressState.variables,
301
- result,
302
- )
303
- val newState = oldState.withDeleteCompleted(deleteItemOperationCompletedState)
304
- if (_state .compareAndSet(oldState, newState)) {
305
- break
210
+ if (oldState.deleteItem == = inProgressOperationState) {
211
+ _state .value =
212
+ oldState.withDeleteCompleted(
213
+ State .OperationState .Completed (oldState.nextSequenceNumber, key, result)
214
+ )
306
215
}
307
216
}
308
217
}
0 commit comments