Skip to content

Commit f93c0a0

Browse files
authored
dataconnect: demo: add "Delete" button an "List Items" screen. (#6585)
1 parent b164139 commit f93c0a0

File tree

11 files changed

+709
-386
lines changed

11 files changed

+709
-386
lines changed

firebase-dataconnect/demo/firebase/dataconnect/connector/operations.gql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@ query GetItemByKey(
4949
}
5050
}
5151

52+
query GetAllItems @auth(level: PUBLIC) {
53+
items: zwda6x9zyys {
54+
id
55+
string
56+
int
57+
int64
58+
float
59+
boolean
60+
date
61+
timestamp
62+
any
63+
}
64+
}
65+
66+
mutation DeleteItemByKey(
67+
$key: zwda6x9zyy_Key!
68+
) @auth(level: PUBLIC) {
69+
zwda6x9zyy_delete(key: $key)
70+
}
71+
5272
# This mutation exists only as a workaround for b/382688278 where the following
5373
# compiler error will otherwise result when compiling the generated code:
5474
# Serializer has not been found for type 'java.util.UUID'. To use context

firebase-dataconnect/demo/src/main/AndroidManifest.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ See the License for the specific language governing permissions and
1616
limitations under the License.
1717
-->
1818

19-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
20-
xmlns:tools="http://schemas.android.com/tools">
19+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2120

2221
<application
2322
android:name=".MyApplication"
2423
android:label="Data Connect Minimal Demo"
25-
android:theme="@style/Theme.AppCompat.NoActionBar"
24+
android:theme="@style/Theme.AppCompat"
2625
>
2726

2827
<activity android:name=".MainActivity" android:exported="true">
@@ -32,6 +31,11 @@ limitations under the License.
3231
</intent-filter>
3332
</activity>
3433

34+
<activity
35+
android:name=".ListItemsActivity"
36+
android:label="List Items"
37+
/>
38+
3539
</application>
3640

3741
</manifest>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2024 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+
package com.google.firebase.dataconnect.minimaldemo
17+
18+
import android.os.Bundle
19+
import android.view.LayoutInflater
20+
import android.view.View
21+
import android.view.ViewGroup
22+
import androidx.activity.viewModels
23+
import androidx.appcompat.app.AppCompatActivity
24+
import androidx.lifecycle.flowWithLifecycle
25+
import androidx.lifecycle.lifecycleScope
26+
import androidx.recyclerview.widget.DividerItemDecoration
27+
import androidx.recyclerview.widget.LinearLayoutManager
28+
import androidx.recyclerview.widget.RecyclerView
29+
import com.google.firebase.dataconnect.minimaldemo.connector.GetAllItemsQuery
30+
import com.google.firebase.dataconnect.minimaldemo.databinding.ActivityListItemsBinding
31+
import com.google.firebase.dataconnect.minimaldemo.databinding.ListItemBinding
32+
import kotlinx.coroutines.flow.collectLatest
33+
import kotlinx.coroutines.launch
34+
35+
class ListItemsActivity : AppCompatActivity() {
36+
37+
private lateinit var myApplication: MyApplication
38+
private lateinit var viewBinding: ActivityListItemsBinding
39+
private val viewModel: ListItemsViewModel by viewModels { ListItemsViewModel.Factory }
40+
41+
override fun onCreate(savedInstanceState: Bundle?) {
42+
super.onCreate(savedInstanceState)
43+
myApplication = application as MyApplication
44+
45+
viewBinding = ActivityListItemsBinding.inflate(layoutInflater)
46+
viewBinding.recyclerView.also {
47+
val linearLayoutManager = LinearLayoutManager(this)
48+
it.layoutManager = linearLayoutManager
49+
val dividerItemDecoration = DividerItemDecoration(this, linearLayoutManager.layoutDirection)
50+
it.addItemDecoration(dividerItemDecoration)
51+
}
52+
setContentView(viewBinding.root)
53+
54+
lifecycleScope.launch {
55+
if (viewModel.loadingState == ListItemsViewModel.LoadingState.NotStarted) {
56+
viewModel.getItems()
57+
}
58+
viewModel.stateSequenceNumber.flowWithLifecycle(lifecycle).collectLatest {
59+
onViewModelStateChange()
60+
}
61+
}
62+
}
63+
64+
private fun onViewModelStateChange() {
65+
val items = viewModel.result?.getOrNull()
66+
val exception = viewModel.result?.exceptionOrNull()
67+
val loadingState = viewModel.loadingState
68+
69+
if (loadingState == ListItemsViewModel.LoadingState.InProgress) {
70+
viewBinding.statusText.text = "Loading Items..."
71+
viewBinding.statusText.visibility = View.VISIBLE
72+
viewBinding.recyclerView.visibility = View.GONE
73+
viewBinding.recyclerView.adapter = null
74+
} else if (items !== null) {
75+
viewBinding.statusText.text = null
76+
viewBinding.statusText.visibility = View.GONE
77+
viewBinding.recyclerView.visibility = View.VISIBLE
78+
val oldAdapter = viewBinding.recyclerView.adapter as? RecyclerViewAdapterImpl
79+
if (oldAdapter === null || oldAdapter.items !== items) {
80+
viewBinding.recyclerView.adapter = RecyclerViewAdapterImpl(items)
81+
}
82+
} else if (exception !== null) {
83+
viewBinding.statusText.text = "Loading items FAILED: $exception"
84+
viewBinding.statusText.visibility = View.VISIBLE
85+
viewBinding.recyclerView.visibility = View.GONE
86+
viewBinding.recyclerView.adapter = null
87+
} else {
88+
viewBinding.statusText.text = null
89+
viewBinding.statusText.visibility = View.GONE
90+
viewBinding.recyclerView.visibility = View.GONE
91+
}
92+
}
93+
94+
private class RecyclerViewAdapterImpl(val items: List<GetAllItemsQuery.Data.ItemsItem>) :
95+
RecyclerView.Adapter<RecyclerViewViewHolderImpl>() {
96+
97+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewViewHolderImpl {
98+
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
99+
return RecyclerViewViewHolderImpl(binding)
100+
}
101+
102+
override fun getItemCount() = items.size
103+
104+
override fun onBindViewHolder(holder: RecyclerViewViewHolderImpl, position: Int) {
105+
holder.bindTo(items[position])
106+
}
107+
}
108+
109+
private class RecyclerViewViewHolderImpl(private val binding: ListItemBinding) :
110+
RecyclerView.ViewHolder(binding.root) {
111+
112+
fun bindTo(item: GetAllItemsQuery.Data.ItemsItem) {
113+
binding.id.text = item.id.toString()
114+
binding.name.text = item.string
115+
}
116+
}
117+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2024 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+
package com.google.firebase.dataconnect.minimaldemo
17+
18+
import android.util.Log
19+
import androidx.annotation.MainThread
20+
import androidx.lifecycle.ViewModel
21+
import androidx.lifecycle.ViewModelProvider
22+
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
23+
import androidx.lifecycle.viewModelScope
24+
import androidx.lifecycle.viewmodel.initializer
25+
import androidx.lifecycle.viewmodel.viewModelFactory
26+
import com.google.firebase.dataconnect.minimaldemo.connector.GetAllItemsQuery
27+
import com.google.firebase.dataconnect.minimaldemo.connector.execute
28+
import kotlinx.coroutines.CancellationException
29+
import kotlinx.coroutines.Deferred
30+
import kotlinx.coroutines.ExperimentalCoroutinesApi
31+
import kotlinx.coroutines.Job
32+
import kotlinx.coroutines.async
33+
import kotlinx.coroutines.flow.MutableStateFlow
34+
import kotlinx.coroutines.flow.StateFlow
35+
import kotlinx.coroutines.flow.asStateFlow
36+
import kotlinx.coroutines.launch
37+
38+
class ListItemsViewModel(private val app: MyApplication) : ViewModel() {
39+
40+
// Threading Note: _state and the variables below it may ONLY be accessed (read from and/or
41+
// written to) by the main thread; otherwise a race condition and undefined behavior will result.
42+
private val _stateSequenceNumber = MutableStateFlow(111999L)
43+
val stateSequenceNumber: StateFlow<Long> = _stateSequenceNumber.asStateFlow()
44+
45+
var result: Result<List<GetAllItemsQuery.Data.ItemsItem>>? = null
46+
private set
47+
48+
private var job: Job? = null
49+
val loadingState: LoadingState =
50+
job.let {
51+
if (it === null) {
52+
LoadingState.NotStarted
53+
} else if (it.isCancelled || it.isCompleted) {
54+
LoadingState.Completed
55+
} else {
56+
LoadingState.InProgress
57+
}
58+
}
59+
60+
enum class LoadingState {
61+
NotStarted,
62+
InProgress,
63+
Completed,
64+
}
65+
66+
@OptIn(ExperimentalCoroutinesApi::class)
67+
@MainThread
68+
fun getItems() {
69+
// If there is already a "get items" operation in progress, then just return and let the
70+
// in-progress operation finish.
71+
if (loadingState == LoadingState.InProgress) {
72+
return
73+
}
74+
75+
// Start a new coroutine to perform the "get items" operation.
76+
val job: Deferred<List<GetAllItemsQuery.Data.ItemsItem>> =
77+
viewModelScope.async { app.getConnector().getAllItems.execute().data.items }
78+
79+
this.result = null
80+
this.job = job
81+
_stateSequenceNumber.value++
82+
83+
// Update the internal state once the "get items" operation has completed.
84+
job.invokeOnCompletion { exception ->
85+
// Don't log CancellationException, as documented by invokeOnCompletion().
86+
if (exception is CancellationException) {
87+
return@invokeOnCompletion
88+
}
89+
90+
val result =
91+
if (exception !== null) {
92+
Log.w(TAG, "WARNING: Getting all items FAILED: $exception", exception)
93+
Result.failure(exception)
94+
} else {
95+
val items = job.getCompleted()
96+
Log.i(TAG, "Retrieved all items ${items.size} items")
97+
Result.success(items)
98+
}
99+
100+
viewModelScope.launch {
101+
if (this@ListItemsViewModel.job === job) {
102+
this@ListItemsViewModel.result = result
103+
this@ListItemsViewModel.job = null
104+
_stateSequenceNumber.value++
105+
}
106+
}
107+
}
108+
}
109+
110+
companion object {
111+
private const val TAG = "ListItemsViewModel"
112+
113+
val Factory: ViewModelProvider.Factory = viewModelFactory {
114+
initializer { ListItemsViewModel(this[APPLICATION_KEY] as MyApplication) }
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)