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: Add Clustering composable, maps-compose-utils library #258

Merged
merged 12 commits into from
Feb 16, 2023

Conversation

DSteve595
Copy link
Contributor

@DSteve595 DSteve595 commented Jan 24, 2023

This adds a Compose-friendly API for marker clustering. I'd appreciate heavy scrutiny on it, since it's got some hacks and ugly bits!

MapClusteringActivity has been updated to show an example of how it can be used. Notably, the bug with Marker (and other) click handlers no longer working, as described in #140 (comment), is fixed! It's unfortunately done by working around some unwanted behavior of maps-utils, where it clobbers the click listeners that MapApplier previously set. To facilitate that, I made some changes to how MapApplier dispatches input, namely a new InputHandler composable that can be added to arbitrarily handle all unhandled input. This solves the problem for clustering since ClusterManager wants to receive input events directly, and the current implementation throws away the marker click events since we haven't added corresponding MarkerNodes for them.

To match the structure of maps-utils (which contains the non-Compose clustering utilities), this also adds a new artifact, maps-compose-utils, in which the Clustering composable lives.

To do before merging:

  • Update readme to mention Clustering instead of MapEffect
  • Update kdoc in MapEffect

Fixes #44

@DSteve595 DSteve595 changed the title Add Clustering composable, maps-compose-utils library feat: Add Clustering composable, maps-compose-utils library Jan 24, 2023
@DSteve595
Copy link
Contributor Author

Open question: Do we still need MapEffect? Clustering is the only use case I'm aware of for it (though there certainly could be more), and it introduces lots of footguns

@wangela wangela removed the request for review from arriolac January 25, 2023 00:54
@StephenVinouze
Copy link

Except if I've missed it, I don't see a way to customize the cluster icons. That's critical for branding and one of the pain points when using maps-compose with the former util library since you need to shape your marker and cluster icons with the View system and turn them into bitmaps (preventing us from using our Composables).

@DSteve595
Copy link
Contributor Author

Correct, that isn't accounted for in this PR. Will explore options.

@ColtonIdle
Copy link

Adding "true" clustering support with compose-maps would be great. I don't mind the MapEffect to be honest since it works, but something more "native" is def fine with me. But to @StephenVinouze point of customizing the marker icons and cluster icons would be like a 100% need.

Good work getting it this far @DSteve595

Copy link
Contributor

@arriolac arriolac left a comment

Choose a reason for hiding this comment

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

Have you considered implementing Clustering as a ComposeNode<ClusteringNode, MapApplier>? The advantage of implementing it this way is that you can update underlying state in the same frame that recomposition happens. With LaunchedEffect, properties won't be updated until the next frame which means you might lose the click events

@Composable
public fun <T : ClusterItem> Clustering(
items: Collection<T>,
cameraPositionState: CameraPositionState,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion/idea: how about making this a part of the MapApplier so that you can access it implicitly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was wondering if that'd be prudent. I think I'll go for it.

Copy link
Contributor Author

@DSteve595 DSteve595 Feb 2, 2023

Choose a reason for hiding this comment

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

Leaning towards a CompositionLocal (not publicly providable), to avoid giving MapApplier even more responsibilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, a public Composable property seems better, since then I can annotate it with @GoogleMapComposable.

@DSteve595
Copy link
Contributor Author

DSteve595 commented Jan 26, 2023

@arriolac

Have you considered implementing Clustering as a ComposeNode<ClusteringNode, MapApplier>? The advantage of implementing it this way is that you can update underlying state in the same frame that recomposition happens. With LaunchedEffect, properties won't be updated until the next frame which means you might lose the click events

I wanted to directly reference MapApplier for a few things, but since this is technically in another library (maps-compose-utils), I don't have access to it (it's internal). Forced to eat our own MapEffect dogfood.

Can you give an example of how a click event might be lost? I'm assuming you're referring to the LaunchedEffect that sets the click listeners. As I understand it, the user would have to click a marker within a single choreographer frame after an onClick lambda is changed. I don't think that's even possible, since clicks take multiple frames of input to process.

@arriolac
Copy link
Contributor

@arriolac

Have you considered implementing Clustering as a ComposeNode<ClusteringNode, MapApplier>? The advantage of implementing it this way is that you can update underlying state in the same frame that recomposition happens. With LaunchedEffect, properties won't be updated until the next frame which means you might lose the click events

I wanted to directly reference MapApplier for a few things, but since this is technically in another library (maps-compose-utils), I don't have access to it (it's internal). Forced to eat our own MapEffect dogfood.

Can you give an example of how a click event might be lost? I'm assuming you're referring to the LaunchedEffect that sets the click listeners. As I understand it, the user would have to click a marker within a single choreographer frame after an onClick lambda is changed. I don't think that's even possible, since clicks take multiple frames of input to process.

Ahh, that's right, this is a different module altogether.

I was thinking an event can be lost in this scenario:

  • state triggers recomposition and you update the click handler
  • click event occurs
  • new handler hasn't been applied yet since the block inside LaunchedEffect hasn't been executed yet

It's an unlikely edge case I thought worth calling out though it would definitely have to be validated if that's even possible. Using ComposeNode would rule this out entirely but I'm hesitant about changing the scope of MapApplier for this use case.

@DSteve595
Copy link
Contributor Author

DSteve595 commented Feb 2, 2023

I updated the PR with new args to Clustering:

  • clusterContent: (@Composable (Cluster) -> Unit))?
  • clusterItemContent: (@Composable (T) -> Unit))?

This lets you render custom cluster icons using composables.
This also contains two other PRs, #261 and #262, which enable some of the functionality needed.

@DSteve595
Copy link
Contributor Author

DSteve595 commented Feb 2, 2023

@arriolac I've been trying to come up with a test case that repros missing click handler events, but I've been coming up dry. I might be missing a case you're envisioning. If you find the time, would you be able to come up with one?
I'm not considering it a blocker currently because the API itself should be fine, we can always patch the implementation.

@arriolac
Copy link
Contributor

arriolac commented Feb 2, 2023

@arriolac I've been trying to come up with a test case that repros missing click handler events, but I've been coming up dry. I might be missing a case you're envisioning. If you find the time, would you be able to come up with one? I'm not considering it a blocker currently because the API itself should be fine, we can always patch the implementation.

Agreed that it shouldn't be blocking as the underlying implementation can change. An alternative to using LaunchedEffect in https://github.com/googlemaps/android-maps-compose/pull/258/files#diff-73d069b3c67a66f9bbaf70ba78d487309d39454a28940f02898176dabd6e6414R73 that doesn't have the downsides I mentioned is to use SideEffect as the lambda inside will be invoked in the same frame as the recomposition.

@DSteve595
Copy link
Contributor Author

Note: Exploring possibilities for removing the no-animation restriction, since that also would break async image loaders

@DSteve595 DSteve595 marked this pull request as draft February 14, 2023 17:15
@DSteve595
Copy link
Contributor Author

With some invalidation hacks, marker/cluster icons are now rendered continuously! This importantly fixes async image loaders (like Glide), and also allows for animations.

@DSteve595 DSteve595 marked this pull request as ready for review February 16, 2023 00:07
@wangela
Copy link
Member

wangela commented Feb 16, 2023

Noting here some decisions we have made in this PR:

  • Add a maps-compose-utils library even though it only contains the one utility for now, Marker Clustering.
  • Manage the code for the maps-compose-utils library in this repo but distribute it as a separate dependency through Maven, similar to maps-compose-widgets, to be close to the code for the related composables.
  • Keep documentation about the Experimental MapEffect, in case direct control of the map is needed for non-clustering functionality. Remove documentation only when MapEffect is removed (RFC: Remove experimental MapEffect composable #267).

@wangela wangela merged commit 7f00a72 into googlemaps:main Feb 16, 2023
googlemaps-bot pushed a commit that referenced this pull request Feb 16, 2023
# [2.11.0](v2.10.0...v2.11.0) (2023-02-16)

### Features

* Add Marker Clustering composable, maps-compose-utils library ([#258](#258)) ([7f00a72](7f00a72))
@googlemaps-bot
Copy link
Contributor

🎉 This PR is included in version 2.11.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@ColtonIdle
Copy link

Great job @DSteve595 Can't wait to use this!

@DSteve595 DSteve595 deleted the clustering branch February 18, 2023 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for clustering
6 participants