Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ViewModel is now access based on taskId using Bundle #2748

Closed
wants to merge 14 commits into from
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class DataCollectionFragment : AbstractFragment(), BackPressListener {
@Inject lateinit var navigator: Navigator
@Inject lateinit var viewPagerAdapterFactory: DataCollectionViewPagerAdapterFactory

private val viewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection)
val viewModel: DataCollectionViewModel by hiltNavGraphViewModels(R.id.data_collection)

private lateinit var binding: DataCollectionFragBinding
private lateinit var progressBar: ProgressBar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,10 @@ class CaptureLocationTaskFragment : AbstractTaskFragment<CaptureLocationTaskView
// NOTE(#2493): Multiplying by a random prime to allow for some mathematical uniqueness.
// Otherwise, the sequentially generated ID might conflict with an ID produced by Google Maps.
val rowLayout = LinearLayout(requireContext()).apply { id = View.generateViewId() * 11149 }
val fragment = CaptureLocationTaskMapFragment.newInstance(map, super.taskId)
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
parentFragmentManager
.beginTransaction()
.add(
rowLayout.id,
CaptureLocationTaskMapFragment.newInstance(viewModel, map),
CaptureLocationTaskMapFragment::class.java.simpleName,
)
.add(rowLayout.id, fragment, CaptureLocationTaskMapFragment::class.java.simpleName)
.commit()
return rowLayout
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,33 @@
import androidx.lifecycle.lifecycleScope
import com.google.android.ground.ui.common.AbstractMapFragmentWithControls
import com.google.android.ground.ui.common.BaseMapViewModel
import com.google.android.ground.ui.datacollection.DataCollectionFragment
import com.google.android.ground.ui.map.MapFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber

@AndroidEntryPoint
class CaptureLocationTaskMapFragment(private val viewModel: CaptureLocationTaskViewModel) :
AbstractMapFragmentWithControls() {
class CaptureLocationTaskMapFragment : AbstractMapFragmentWithControls() {

private lateinit var mapViewModel: CaptureLocationTaskMapViewModel
private lateinit var viewModel: CaptureLocationTaskViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapViewModel = getViewModel(CaptureLocationTaskMapViewModel::class.java)
arguments?.let {
try {
val taskId = it.getString("taskId")
taskId?.let {
viewModel =
(requireParentFragment() as DataCollectionFragment).viewModel.getTaskViewModel(taskId)

Check warning on line 45 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt#L45

Added line #L45 was not covered by tests
as CaptureLocationTaskViewModel
}

Check warning on line 47 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt#L47

Added line #L47 was not covered by tests
} catch (e: Exception) {
Timber.e("CaptureLocationTaskMapFragment - $e")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please cleanup the debug logs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need this, so that we can get in production, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add the new method, FirebaseCrashLogger().logException(t) here if it's safe to do so, and in the other log areas?

And one more thing, by catching the error without rethrowing it, the app will be in a weird state where no viewModel is set and we skip everything that is in the isInitialized check. But these are critical for the functionality of the app, like updating map pins or capturing the location.

My question is, if the app finds itself in an impossible to recover state like this, should we log the exception and try to continue, or should we go back to the home screen and show and error to the user "An unexpected error has occurred. Please try again or contact your survey organizer"? I'm not sure how to do that off the top of my head, so maybe just navigate to the home screen after logging. Also true for below.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better that the app crashes than remains in an invalid state. But if we know that that can happen, we should try to fix the root cause rather than eating the exception.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, @anandwana001 messages logged with Timber.e() aren't captured anywhere in prod. If you need to communicate critical debug info via Crashlytics, the method @sufyanAbbasi mentioned should work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it a regular error handling where we pass arguments to a new fragment and use it?
Not in a weird app state because this is something required to do.
yes, we can use FirebaseCrashLogger().logException(t)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better that the app crashes than remains in an invalid state. But if we know that that can happen, we should try to fix the root cause rather than eating the exception.

This is more of a workaround we can do and user can just do a back and forth and keep continuing the data collection, no more crashes.

Later, as a core solution, we can work on the abstract layer and the ViewModel and support the orientation change to work things better
@gino-m

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it a regular error handling where we pass arguments to a new fragment and use it?

Not sure what you mean about passing arguments to a new fragment is a regular error handling.. Can you rephrase?

Not in a weird app state because this is something required to do.

It's generally considered bad practice to catch Throwable without managing potential consequences. The app will be in an invalid state if the view model couldn't be set since the view requires it to function, no?

}
}

override fun getMapViewModel(): BaseMapViewModel = mapViewModel
Expand All @@ -46,18 +60,28 @@
): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
getMapViewModel().getLocationUpdates().collect { viewModel.updateLocation(it) }
if (this@CaptureLocationTaskMapFragment::viewModel.isInitialized) {
getMapViewModel().getLocationUpdates().collect { viewModel.updateLocation(it) }
}
}
return root
}

override fun onMapReady(map: MapFragment) {
binding.locationLockBtn.isClickable = false
viewLifecycleOwner.lifecycleScope.launch { viewModel.onMapReady(mapViewModel) }
viewLifecycleOwner.lifecycleScope.launch {

Check warning on line 72 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt#L72

Added line #L72 was not covered by tests
if (this@CaptureLocationTaskMapFragment::viewModel.isInitialized) {
viewModel.onMapReady(mapViewModel)
}
}

Check warning on line 76 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt#L76

Added line #L76 was not covered by tests
}

companion object {
fun newInstance(viewModel: CaptureLocationTaskViewModel, map: MapFragment) =
CaptureLocationTaskMapFragment(viewModel).apply { this.map = map }
fun newInstance(map: MapFragment, taskId: String): CaptureLocationTaskMapFragment {
val fragment = CaptureLocationTaskMapFragment().apply { this.map = map }
val args = Bundle().apply { putString("taskId", taskId) }
fragment.arguments = args
return fragment
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ class DropPinTaskFragment : AbstractTaskFragment<DropPinTaskViewModel>() {
// NOTE(#2493): Multiplying by a random prime to allow for some mathematical "uniqueness".
// Otherwise, the sequentially generated ID might conflict with an ID produced by Google Maps.
val rowLayout = LinearLayout(requireContext()).apply { id = View.generateViewId() * 11617 }
val fragment = DropPinTaskMapFragment.newInstance(map, super.taskId)
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
parentFragmentManager
.beginTransaction()
.add(rowLayout.id, DropPinTaskMapFragment.newInstance(viewModel, map), "Drop a pin fragment")
.add(rowLayout.id, fragment, "Drop a pin fragment")
.commit()
return rowLayout
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,60 @@
import android.os.Bundle
import com.google.android.ground.ui.common.AbstractMapFragmentWithControls
import com.google.android.ground.ui.common.BaseMapViewModel
import com.google.android.ground.ui.datacollection.DataCollectionFragment
import com.google.android.ground.ui.map.CameraPosition
import com.google.android.ground.ui.map.MapFragment
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber

@AndroidEntryPoint
class DropPinTaskMapFragment(private val viewModel: DropPinTaskViewModel) :
AbstractMapFragmentWithControls() {
class DropPinTaskMapFragment : AbstractMapFragmentWithControls() {

private lateinit var mapViewModel: BaseMapViewModel
private lateinit var viewModel: DropPinTaskViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapViewModel = getViewModel(BaseMapViewModel::class.java)
arguments?.let {
try {
val taskId = it.getString("taskId")
taskId?.let {
viewModel =
(requireParentFragment() as DataCollectionFragment).viewModel.getTaskViewModel(taskId)

Check warning on line 41 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt#L41

Added line #L41 was not covered by tests
as DropPinTaskViewModel
}

Check warning on line 43 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt#L43

Added line #L43 was not covered by tests
} catch (e: Exception) {
Timber.e("DropPinTaskMapFragment - $e")
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
}
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
}
}

override fun getMapViewModel(): BaseMapViewModel = mapViewModel

override fun onMapReady(map: MapFragment) {
viewModel.features.observe(this) { map.setFeatures(it) }
if (this@DropPinTaskMapFragment::viewModel.isInitialized) {
viewModel.features.observe(this) { map.setFeatures(it) }
}
}

override fun onMapCameraMoved(position: CameraPosition) {
super.onMapCameraMoved(position)
viewModel.updateCameraPosition(position)
if (this@DropPinTaskMapFragment::viewModel.isInitialized) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swallowing the invalid state here as well will make the issue impossible to identify in prod and even difficult to find locally.

viewModel.updateCameraPosition(position)
}
}

Check warning on line 63 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt#L63

Added line #L63 was not covered by tests

fun setViewModel(viewModel: DropPinTaskViewModel) {
this.viewModel = viewModel

Check warning on line 66 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/point/DropPinTaskMapFragment.kt#L66

Added line #L66 was not covered by tests
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
}

companion object {
fun newInstance(viewModel: DropPinTaskViewModel, map: MapFragment) =
DropPinTaskMapFragment(viewModel).apply { this.map = map }
fun newInstance(map: MapFragment, taskId: String): DropPinTaskMapFragment {
val fragment = DropPinTaskMapFragment().apply { this.map = map }
val args = Bundle().apply { putString("taskId", taskId) }
fragment.arguments = args
return fragment
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class DrawAreaTaskFragment : AbstractTaskFragment<DrawAreaTaskViewModel>() {
// NOTE(#2493): Multiplying by a random prime to allow for some mathematical "uniqueness".
// Otherwise, the sequentially generated ID might conflict with an ID produced by Google Maps.
val rowLayout = LinearLayout(requireContext()).apply { id = View.generateViewId() * 11411 }
drawAreaTaskMapFragment = DrawAreaTaskMapFragment.newInstance(viewModel, map)
drawAreaTaskMapFragment = DrawAreaTaskMapFragment.newInstance(map, super.taskId)
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
parentFragmentManager
.beginTransaction()
.add(rowLayout.id, drawAreaTaskMapFragment, DrawAreaTaskMapFragment::class.java.simpleName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,52 @@
import androidx.lifecycle.lifecycleScope
import com.google.android.ground.ui.common.AbstractMapFragmentWithControls
import com.google.android.ground.ui.common.BaseMapViewModel
import com.google.android.ground.ui.datacollection.DataCollectionFragment
import com.google.android.ground.ui.map.CameraPosition
import com.google.android.ground.ui.map.Feature
import com.google.android.ground.ui.map.MapFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber

@AndroidEntryPoint
class DrawAreaTaskMapFragment(private val viewModel: DrawAreaTaskViewModel) :
AbstractMapFragmentWithControls() {
class DrawAreaTaskMapFragment : AbstractMapFragmentWithControls() {

private lateinit var mapViewModel: BaseMapViewModel
private lateinit var viewModel: DrawAreaTaskViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapViewModel = getViewModel(BaseMapViewModel::class.java)
arguments?.let {
try {
val taskId = it.getString("taskId")
taskId?.let {
viewModel =
(requireParentFragment() as DataCollectionFragment).viewModel.getTaskViewModel(taskId)

Check warning on line 44 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt#L44

Added line #L44 was not covered by tests
as DrawAreaTaskViewModel
}

Check warning on line 46 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt#L46

Added line #L46 was not covered by tests
} catch (e: Exception) {
Timber.e("DrawAreaTaskMapFragment - $e")
}
anandwana001 marked this conversation as resolved.
Show resolved Hide resolved
}
}

override fun getMapViewModel(): BaseMapViewModel = mapViewModel

override fun onMapReady(map: MapFragment) {
viewLifecycleOwner.lifecycleScope.launch {
viewModel.draftArea.collect { feature: Feature? ->
map.setFeatures(if (feature == null) setOf() else setOf(feature))
if (this@DrawAreaTaskMapFragment::viewModel.isInitialized) {
viewModel.draftArea.collect { feature: Feature? ->
map.setFeatures(if (feature == null) setOf() else setOf(feature))
}

Check warning on line 60 in ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/polygon/DrawAreaTaskMapFragment.kt#L60

Added line #L60 was not covered by tests
}
}
}

override fun onMapCameraMoved(position: CameraPosition) {
super.onMapCameraMoved(position)
if (!viewModel.isMarkedComplete()) {
if (this@DrawAreaTaskMapFragment::viewModel.isInitialized && !viewModel.isMarkedComplete()) {
val mapCenter = position.coordinates
viewModel.updateLastVertexAndMaybeCompletePolygon(mapCenter) { c1, c2 ->
map.getDistanceInPixels(c1, c2)
Expand All @@ -57,7 +73,11 @@
}

companion object {
fun newInstance(viewModel: DrawAreaTaskViewModel, map: MapFragment) =
DrawAreaTaskMapFragment(viewModel).apply { this.map = map }
fun newInstance(map: MapFragment, taskId: String): DrawAreaTaskMapFragment {
val fragment = DrawAreaTaskMapFragment().apply { this.map = map }
val args = Bundle().apply { putString("taskId", taskId) }
fragment.arguments = args
return fragment
}
}
}