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

feat: Use AndroidView overload which re-uses MapView to improve performance in lazy layouts #436

Open
wants to merge 117 commits into
base: main
Choose a base branch
from

Conversation

philip-segerfast
Copy link

@philip-segerfast philip-segerfast commented Oct 13, 2023

This PR uses an AndroidView overload which allows re-using of the underlying view. This improves performance substantially when using GoogleMap in a Lazy layout, like LazyColumn.

This PR also incorporates @bubenheimer's changes from #522.


Fixes #285, #492 🦕

This will make sure that the underlying MapView is re-used in a LazyColumn (and elsewhere) which will significantly improve scrolling performance in LazyColumn.

If this overload isn't used the MapView will be destroyed every time it leaves composition.

The MapView will still be destroyed when the parent node leaves the composition.
@google-cla
Copy link

google-cla bot commented Oct 13, 2023

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@philip-segerfast philip-segerfast changed the title Android view overload re use map view feat: Android view overload re use map view Oct 13, 2023
@philip-segerfast philip-segerfast changed the title feat: Android view overload re use map view feat: Use AndroidView overload which re-uses MapView to improve performance Oct 13, 2023
@philip-segerfast philip-segerfast changed the title feat: Use AndroidView overload which re-uses MapView to improve performance feat: Use AndroidView overload which re-uses MapView to improve performance in lazy layouts Oct 13, 2023
@philip-segerfast philip-segerfast marked this pull request as draft October 24, 2023 18:35
@kikoso
Copy link
Collaborator

kikoso commented Oct 27, 2023

Hi @philip-segerfast. As far as I read in the documentation, we need to provide a non-null value in the onReset method. This PR does not seem to include it.

@philip-segerfast
Copy link
Author

philip-segerfast commented Oct 27, 2023

Hi @kikoso, it is enough to make onReset non-null. You can verify by commenting that line out in my PR and you will see that the views are re-created when scrolling.
It still needs some more work though, mainly the GoogleMap state should be reset (for example animations). That's why I made this PR into a draft.
I'll try to make another commit or get back to you regarding this issue during the weekend.

Cheers

@philip-segerfast
Copy link
Author

Upon further examination, it appears that executing a reset within onReset may not be necessary since dispositions already clear markers and halt animations, although the underlying GoogleMap's camera position state appears to be untouched.

Some other notes:

  • The issue concerning the zoom controls is unrelated to this PR, and appears to stem from a different source. You can reproduce on the main branch by putting a bunch of GoogleMap composables in a LazyColumn.
  • Additionally, version 1.6.0-alpha08 of androidx.compose.ui:ui harbors a bug leading to a crash, hence it is advisable to avoid using it.

The PR is quite ready except for the androidx.compose.ui:ui version being in alpha.

@philip-segerfast philip-segerfast marked this pull request as ready for review October 29, 2023 13:19
@wangela wangela added the status: blocked Resolving the issue is dependent on other work. label Nov 17, 2023
@wangela
Copy link
Member

wangela commented Nov 17, 2023

We'll wait until this feature moves out of alpha and doesn't cause crashes before we merge.

@philip-segerfast
Copy link
Author

philip-segerfast commented Apr 24, 2024

Thanks for the review and everything in between, Uli @bubenheimer! It was an honor.
Don't hesitate to request more changes if there is anything.

@bubenheimer
Copy link
Contributor

Looks great! LGT @kikoso

Copy link

@ivannarino ivannarino left a comment

Choose a reason for hiding this comment

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

LGTM

@kikoso kikoso removed the status: blocked Resolving the issue is dependent on other work. label May 1, 2024
@el-qq
Copy link
Contributor

el-qq commented May 8, 2024

@dkhawk Would you look at the PR and include it in the plans please?

@dkhawk
Copy link
Contributor

dkhawk commented May 8, 2024

@dkhawk Would you look at the PR and include it in the plans please?

This is a great change! I spent some time with the demo activity and it works pretty well, except I can't scrolling the maps vertically. That might be nice to look into. Barring that we should be able to merge in this change into the 5.1.x release which should be soon.

Comment on lines 128 to 138
fun CoroutineScope.launchSubcomposition(mapView: MapView): Job {
val mapCompositionContent: @Composable () -> Unit = {
MapUpdater(
mergeDescendants = mergeDescendants,
contentDescription = currentContentDescription,
cameraPositionState = currentCameraPositionState,
contentPadding = currentContentPadding,
locationSource = currentLocationSource,
mapProperties = currentMapProperties,
mapUiSettings = currentUiSettings,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be useful to move launchSubcomposition() to the top level, to help avoid direct references to GoogleMap() parameters from the long-running lambda closure. There have been subtle bugs in this project caused by such references, they're easy to miss.

This should incorporate the changes from recently approved #522, grouping together all MapUpdater() parameters.

Copy link
Author

Choose a reason for hiding this comment

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

Alright 👍

Copy link
Author

Choose a reason for hiding this comment

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

I've incorporated the changes from #522. Please tell me if there's anything you'd like to change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks great, thank you! The MapUpdater() changes are excellent.

return launch(start = CoroutineStart.UNDISPATCHED) {
val map = mapView.awaitMap()
val composition = Composition(
applier = MapApplier(map, mapView, mapClickListeners),
Copy link
Contributor

Choose a reason for hiding this comment

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

mapClickListeners should be moved out of MapApplier, and passed as a parameter to MapClickListenerUpdater(). @philip-segerfast determined earlier in the review process that MapClickListeners does not actually have to be a singleton. (I verified the finding.)

It's a trivial change that will also prepare MapApplier for potential future use in ReusableComposition.

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented this. Please tell me what you think.

Copy link
Contributor

Choose a reason for hiding this comment

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

@philip-segerfast I've realized that the MapClickListeners change that I proposed is not valid, sadly. It will work in this case, because MapClickListeners actually is a singleton within the scope of a non-reused composition anyway. However, from the perspective of MapClickListenerUpdater(MapClickListeners) as a standalone Composable function it is not correct, as passing a different MapClickListener on recomposition will not do the right thing. The initial listener is too baked into the lambdas there.

Let's undo this part of the commit and leave it for the future, it does not look trivial. My sincere apologies!

Copy link
Author

Choose a reason for hiding this comment

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

No worries. This has been reverted.

@philip-segerfast
Copy link
Author

Hi, @dkhawk! I made the maps pannable in the demo if you first pan them horizontally so that you still can easily scroll the list. Is this enough for you or would you like the maps to completely intercept the touch events from the LazyColumn like in MapInColumnActivity?

…operty from MapApplier [see desc.]

- Wrap MapUpdater parameters inside a MapUpdaterState data class
- Remove MapClickListeners property from MapApplier
- Pass MapClickListeners directly to MapClickListenerUpdater
Comment on lines 202 to 206
mapUpdaterState: MapUpdaterState,
parentComposition: CompositionContext,
clickListeners: MapClickListeners,
content: (@Composable @GoogleMapComposable () -> Unit)?,
mapView: MapView
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the parameter order is non-standard. The content lambda should be the last parameter.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, content is now a regular non-nullable lambda after the latest changes from main. This will also slightly simplify its usage below.

Copy link
Author

Choose a reason for hiding this comment

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

Good point. fixed.

Comment on lines 202 to 225
event.targetState
when (event) {
Lifecycle.Event.ON_CREATE -> {
// Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in
// this case the GoogleMap composable also doesn't leave the composition. So,
// recreating the map does not restore state properly which must be avoided.
if (previousState.value != Lifecycle.Event.ON_STOP) {
this.onCreate(Bundle())
CompositionLocalProvider(
LocalCameraPositionState provides currentCameraPositionState
) {
content?.invoke()
Copy link
Contributor

Choose a reason for hiding this comment

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

currentCameraPositionState is not valid here. After the content type change, this whole expression can change to simply:

CompositionLocalProvider(
    LocalCameraPositionState provides mapUpdaterState.cameraPositionState,
    content
)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consider using the new AndroidView in 1.4.0-rc01
7 participants