Geo-fencing with MVP Part 2

In the previous post, we talked about the idea behind geo-fencing, why is it so useful and what is the reason behind choosing a particular design pattern like MVP. It was most like an introductory step before getting our hands dirty with some actual source code. Thus, in this part of the series, I will try to go through the different layers of the MVP pattern and will try to explain as much as possible the things that are happening there.

The GeofenceExample application

As said earlier, the application which will be used as an example will rely on the usage of predefined geo-fences with some radius. The main task is to display these geo-fences on a map and fire events each time we enter or exit the predefined radius of such a location. Lets see the main classes separated by layers based on the MVP design pattern:

overall_mvp
Overall MVP design

Model Layer

This is the most simple layer of all of them. It consists of a single class called CompanyLocation. This is a simple POJO which contains a LatLng and Geofence fields. It represents a single company which has particular GPS coordinates and contains a geofence in it. That’s all!

/**
* Just a dummy container class in order to
* collect {@link com.google.android.gms.location.Geofence}
* and {@link com.google.android.gms.maps.model.LatLng} in one entity
*/
public class CompanyLocation {
private LatLng coordinates;
private Geofence geofence;
public CompanyLocation(LatLng coordinates, Geofence geofence) {
this.coordinates = coordinates;
this.geofence = geofence;
}
public LatLng getCoordinates() {
return coordinates;
}
public void setCoordinates(LatLng coordinates) {
this.coordinates = coordinates;
}
public Geofence getGeofence() {
return geofence;
}
public void setGeofence(Geofence geofence) {
this.geofence = geofence;
}
}
view raw CompanyLocation.java hosted with ❤ by GitHub

View Layer

The View layer consists of MapsActivity and MapsView (interface). In other words, the single activity we have will be our actual view. It’s responsibilities will be only to show / hide information and controls from the screen and to initialize the entities from the Presenter layer.

public interface MapsView {
void generateMap();
void updateLocationOnMap(Location location);
void showGeofences(List<CompanyLocation> companyLocationList);
}
view raw MapsView.java hosted with ❤ by GitHub
public class MapsActivity extends FragmentActivity implements OnMapReadyCallback, MapsView {
private GoogleMap mMap;
private MapsPresenter presenter;
private Marker currentPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_maps);
presenter = new MapsPresenterImpl(this, this, this);
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
// Add a marker in Sydney and move the camera
LatLng sydney = new LatLng(-34, 151);
currentPosition = mMap.addMarker(new MarkerOptions().position(sydney).title("Marker in Sydney"));
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney));
presenter.onMapReady();
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override
public void generateMap() {
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
}
@Override
public void updateLocationOnMap(Location location) {
currentPosition.remove();
LatLng myLocation = new LatLng(location.getLatitude(), location.getLongitude());
currentPosition = mMap.addMarker(new MarkerOptions().position(myLocation).title("My Location"));
// mMap.moveCamera(CameraUpdateFactory.newLatLng(myLocation));
}
@Override
public void showGeofences(List<CompanyLocation> companyLocationList) {
for(CompanyLocation companyLocation : companyLocationList) {
MarkerOptions markerOptions = new MarkerOptions()
.position(companyLocation.getCoordinates())
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE));
CircleOptions circleOptions = new CircleOptions()
.center(companyLocation.getCoordinates())
.strokeColor(Color.argb(50, 70,70,70))
.fillColor( Color.argb(100, 150,150,150) )
.radius( 100.0f );
mMap.addCircle( circleOptions );
mMap.addMarker(markerOptions);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
presenter.disconnectFromLocationService();
}
}
view raw MapsActivity.java hosted with ❤ by GitHub
The MapsView is a simple interface which describes the major actions which the MapsActivity is responsible for.

Presenter Layer

The number of the entities in the Presenter Layer is no different than the number in the View Layer. Again we have just two items in here – a simple interface and a class which implements it. Now, the MapsPresenterImpl class, which implements MapsPresenter, is a bit more complicated. That’s due to the fact that this class is playing the role of a hub. It contains instances of all of the components in the application and is distributing responsibilities and tasks to the right places.

public interface MapsPresenter {
void connectToLocationService();
void disconnectFromLocationService();
void fetchCompanyLocations();
void onMapReady();
}
view raw MapsPresenter.java hosted with ❤ by GitHub
public class MapsPresenterImpl implements MapsPresenter, LocationCallback, GeofenceCallback {
private static final String TAG = MapsPresenterImpl.class.getSimpleName();
private MapsView view;
private GoogleLocationApiManager googleLocationApiManager;
private GeofencingManager geofencingManager;
private List<CompanyLocation> companyLocationList = new ArrayList<>();
public MapsPresenterImpl(MapsView view, FragmentActivity fragmentActivity, Context context) {
if(view == null) throw new NullPointerException("view can not be NULL");
if(fragmentActivity == null) throw new NullPointerException("fragmentActivity can not be NULL");
if(context == null) throw new NullPointerException("context can not be NULL");
this.view = view;
this.googleLocationApiManager = new GoogleLocationApiManager(fragmentActivity, context);
this.googleLocationApiManager.setLocationCallback(this);
this.geofencingManager = new GeofencingManager(this.googleLocationApiManager, context);
this.geofencingManager.setmGeofenceCallback(this);
this.view.generateMap();
}
@Override
public void onLocationApiManagerConnected() {
fetchCompanyLocations();
List<Geofence> geofenceList = new ArrayList<>();
for(CompanyLocation companyLocation : companyLocationList) {
geofenceList.add(companyLocation.getGeofence());
}
geofencingManager.addGeofences(geofenceList);
}
@Override
public void onLocationChanged(Location location) {
view.updateLocationOnMap(location);
}
@Override
public void connectToLocationService() {
Log.d(TAG, "connectToLocationService: hit");
googleLocationApiManager.connect();
}
@Override
public void disconnectFromLocationService() {
Log.d(TAG, "disconnectFromLocationService: hit");
googleLocationApiManager.disconnect();
geofencingManager.removeGeofences();
}
@Override
public void fetchCompanyLocations() {
LatLng interExpoLatLng = new LatLng(42.64923011, 23.39556813);
LatLng metroLatLng = new LatLng(42.64685481, 23.39321852);
LatLng sirmaLatLng = new LatLng(42.65430002, 23.39087963);
Geofence interExpoGeofence = new Geofence.Builder()
.setRequestId("Inter Expo Center")
.setCircularRegion(interExpoLatLng.latitude, interExpoLatLng.longitude, 100.0f)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setExpirationDuration(60 * 60 * 1000)
.build();
Geofence metroGeofence = new Geofence.Builder()
.setRequestId("Metro")
.setCircularRegion(metroLatLng.latitude, metroLatLng.longitude, 100.0f)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setExpirationDuration(60 * 60 * 1000)
.build();
Geofence sirmaGeofence = new Geofence.Builder()
.setRequestId("Sirma")
.setCircularRegion(sirmaLatLng.latitude, sirmaLatLng.longitude, 100.0f)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.setExpirationDuration(60 * 60 * 1000)
.build();
CompanyLocation interExpoLocation = new CompanyLocation(interExpoLatLng, interExpoGeofence);
CompanyLocation metroLocation = new CompanyLocation(metroLatLng, metroGeofence);
CompanyLocation sirmaLocation = new CompanyLocation(sirmaLatLng, sirmaGeofence);
companyLocationList.add(interExpoLocation);
companyLocationList.add(metroLocation);
companyLocationList.add(sirmaLocation);
}
@Override
public void onGeofenceResultAvailable() {
view.showGeofences(companyLocationList);
}
@Override
public void onMapReady() {
connectToLocationService();
}
}
 

The MapsPresenterImpl class also contains a function for ‘fetching’ data from e remote server. But in order to keep the example as simple as possible, the actual communication with a server is replaced by manually creating some dummy CompanyLocation objects and including them in a List.

Despite acting like a hub, the MapsPresenterImpl class is also listening for several important events to occur. Such events are:
  • A change in the user’s location – when a new location is registered, the pointer of the user’s current location on the MapView must be updated.
public void onLocationChanged(Location location)
  • A successfully established connection to the Google Location Service
public void onLocationApiManagerConnected()
  • A successful result from the Google Geofencing API

public void onGeofenceResultAvailable()

Feel free to share, comment & give your opinion on the topic!

And only if you really REALLY liked the article, you can buy me a cup of coffee! Otherwise, don’t do it! Donate $1

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s