Created
August 17, 2022 10:07
-
-
Save NinoDLC/6ddf728d84dbb096cb8701fe0aa3065a to your computer and use it in GitHub Desktop.
OC - P7: Go4Lunch - MVVM Example for MapFragment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + | |
'}'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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