Skip to content

Instantly share code, notes, and snippets.

@NinoDLC
Created August 17, 2022 10:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NinoDLC/6ddf728d84dbb096cb8701fe0aa3065a to your computer and use it in GitHub Desktop.
Save NinoDLC/6ddf728d84dbb096cb8701fe0aa3065a to your computer and use it in GitHub Desktop.
OC - P7: Go4Lunch - MVVM Example for MapFragment
public class MapFragment extends SupportMapFragment {
public static MapFragment newInstance() {
return new MapFragment();
}
private MapViewModel viewModel;
private GoogleMap googleMap;
@SuppressLint("MissingPermission")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
viewModel = new ViewModelProvider(this, ViewModelFactory.getInstance()).get(MapViewModel.class);
viewModel.getViewStateLiveData().observe(getViewLifecycleOwner(), mapViewState -> {
// Markets "stack" on top of each other, even if they are the "same", causing performance issues...
googleMap.clear();
for (MapMarker mapMarker : mapViewState.getMapMarkers()) {
googleMap.addMarker(new MarkerOptions()
.position(
new LatLng(
mapMarker.getLatitude(),
mapMarker.getLongitude()
)
)
.title(mapMarker.getName())
.snippet(mapMarker.getAddress())
);
}
if (mapViewState.getCameraPosition() != null) {
googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(mapViewState.getCameraPosition()));
}
googleMap.setMyLocationEnabled(mapViewState.isMyLocationEnabled());
googleMap.getUiSettings().setMyLocationButtonEnabled(mapViewState.isMyLocationButtonEnabled());
});
getMapAsync(googleMapRef -> {
googleMap = googleMapRef;
initGoogleMap();
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewModel.isMapAvailable(false);
}
private void initGoogleMap() {
googleMap.getUiSettings().setCompassEnabled(true);
googleMap.getUiSettings().setIndoorLevelPickerEnabled(true);
googleMap.getUiSettings().setAllGesturesEnabled(true);
googleMap.setOnCameraIdleListener(() ->
viewModel.onMapCameraStabilized(googleMap.getProjection().getVisibleRegion().latLngBounds)
);
viewModel.isMapAvailable(true);
}
}
public class MapMarker {
@NonNull
private final String placeId;
@NonNull
private final String name;
@NonNull
private final String address;
private final double latitude;
private final double longitude;
public MapMarker(
@NonNull String placeId,
@NonNull String name,
@NonNull String address,
double latitude,
double longitude
) {
this.placeId = placeId;
this.name = name;
this.address = address;
this.latitude = latitude;
this.longitude = longitude;
}
@NonNull
public String getPlaceId() {
return placeId;
}
@NonNull
public String getName() {
return name;
}
@NonNull
public String getAddress() {
return address;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MapMarker mapMarker = (MapMarker) o;
return Double.compare(mapMarker.latitude, latitude) == 0 &&
Double.compare(mapMarker.longitude, longitude) == 0 &&
placeId.equals(mapMarker.placeId) &&
Objects.equals(name, mapMarker.name) &&
Objects.equals(address, mapMarker.address);
}
@Override
public int hashCode() {
return Objects.hash(placeId, name, address, latitude, longitude);
}
@NonNull
@Override
public String toString() {
return "MapMarker{" +
"placeId='" + placeId + '\'' +
", name='" + name + '\'' +
", address='" + address + '\'' +
", latitude=" + latitude +
", longitude=" + longitude +
'}';
}
}
public class MapViewModel extends ViewModel {
private static final float DEFAULT_ZOOM = 15f;
// A UseCase is a layer between ViewModel and Repository. If you're unfamiliar with it, consider it's a smart Repository for now.
@NonNull
private final GetNearbyRestaurantsUseCase getNearbyRestaurantsUseCase;
@NonNull
private final MediatorLiveData<MapViewState> mapViewStateMediatorLiveData = new MediatorLiveData<>();
@NonNull
private final MutableLiveData<Boolean> isMapAvailableLiveData = new MutableLiveData<>();
public MapViewModel(
@NonNull GetNearbyRestaurantsUseCase getNearbyRestaurantsUseCase,
@NonNull LocationRepository locationRepository
) {
this.getNearbyRestaurantsUseCase = getNearbyRestaurantsUseCase;
// Restaurants found around the user come from here
LiveData<Collection<Restaurant>> nearbyRestaurantsLiveData = getNearbyRestaurantsUseCase.get();
LiveData<Location> locationLiveData = locationRepository.getLocationLiveData();
mapViewStateMediatorLiveData.addSource(nearbyRestaurantsLiveData, nearbyRestaurants ->
combine(nearbyRestaurants, locationLiveData.getValue(), isMapAvailableLiveData.getValue())
);
mapViewStateMediatorLiveData.addSource(isMapAvailableLiveData, isMapAvailable ->
combine(nearbyRestaurantsLiveData.getValue(), locationLiveData.getValue(), isMapAvailable)
);
mapViewStateMediatorLiveData.addSource(locationLiveData, location ->
combine(nearbyRestaurantsLiveData.getValue(), location, isMapAvailableLiveData.getValue())
);
}
public void onMapCameraStabilized(@NonNull LatLngBounds bounds) {
double latitude = bounds.getCenter().latitude;
double longitude = bounds.getCenter().longitude;
// Convert the camera "zoom" to the actual distance represented on the map to show POI that user could see on its map
float[] distance = new float[1];
Location.distanceBetween(
bounds.northeast.latitude,
bounds.northeast.longitude,
bounds.southwest.latitude,
bounds.southwest.longitude,
distance
);
int radius = (int) ((distance[0] / 2) * 0.8); // Let's take only 80% of the screen diagonal
getNearbyRestaurantsUseCase.searchArea(latitude, longitude, radius);
}
public void isMapAvailable(boolean isMapAvailable) {
isMapAvailableLiveData.setValue(isMapAvailable);
}
@NonNull
public LiveData<MapViewState> getViewStateLiveData() {
return mapViewStateMediatorLiveData;
}
private void combine(
@Nullable Collection<Restaurant> restaurants,
@Nullable Location location,
@Nullable Boolean isMapAvailable
) {
if (isMapAvailable == null) {
// We don't know if Map (view) is ready or not, wait for its creation !
return;
}
if (isMapAvailable) {
MapViewState mapViewState = new MapViewState(
computeMapMarkers(restaurants),
computeCameraPosition(location),
location != null,
location != null
);
mapViewStateMediatorLiveData.setValue(mapViewState);
} else {
mapViewStateMediatorLiveData.setValue(null);
}
}
@NonNull
private List<MapMarker> computeMapMarkers(@Nullable Collection<Restaurant> restaurants) {
List<MapMarker> markers = new ArrayList<>();
if (restaurants != null) {
for (Restaurant restaurant : restaurants) {
markers.add(
new MapMarker(
restaurant.getPlaceId(),
restaurant.getName(),
restaurant.getAddress(),
restaurant.getLatitude(),
restaurant.getLongitude()
)
);
}
}
return markers;
}
@Nullable
private CameraPosition computeCameraPosition(@Nullable Location location) {
CameraPosition cameraPosition;
if (location == null) {
cameraPosition = null;
} else {
cameraPosition = CameraPosition.builder()
.target(
new LatLng(
location.getLatitude(),
location.getLongitude()
)
)
.zoom(DEFAULT_ZOOM)
.build();
}
return cameraPosition;
}
}
public class MapViewState {
@NonNull
private final List<MapMarker> mapMarkers;
@Nullable
private final CameraPosition cameraPosition;
private final boolean isMyLocationEnabled;
private final boolean isMyLocationButtonEnabled;
public MapViewState(
@NonNull List<MapMarker> mapMarkers,
@Nullable CameraPosition cameraPosition,
boolean isMyLocationEnabled,
boolean isMyLocationButtonEnabled
) {
this.mapMarkers = mapMarkers;
this.cameraPosition = cameraPosition;
this.isMyLocationEnabled = isMyLocationEnabled;
this.isMyLocationButtonEnabled = isMyLocationButtonEnabled;
}
@NonNull
public List<MapMarker> getMapMarkers() {
return mapMarkers;
}
@Nullable
public CameraPosition getCameraPosition() {
return cameraPosition;
}
public boolean isMyLocationEnabled() {
return isMyLocationEnabled;
}
public boolean isMyLocationButtonEnabled() {
return isMyLocationButtonEnabled;
}
@NonNull
@Override
public String toString() {
return "MapViewState{" +
"mapMarkers=" + mapMarkers +
", cameraPosition=" + cameraPosition +
", isMyLocationEnabled=" + isMyLocationEnabled +
", isMyLocationButtonEnabled=" + isMyLocationButtonEnabled +
'}';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment