Created
April 11, 2016 04:41
-
-
Save emanuelet/b8c0049fc29702bc2fdbd493b385e7b2 to your computer and use it in GitHub Desktop.
Populating ListView (or RecyclerView) based on Firebase cross-referenced results with a Geofire node
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 ExampleFragment extends Fragment implements GoogleApiClient.ConnectionCallbacks, | |
GoogleApiClient.OnConnectionFailedListener, | |
ResultCallback<LocationSettingsResult>, | |
SharedPreferences.OnSharedPreferenceChangeListener { | |
private Callbacks mCallbacks; | |
private Snackbar snackbar; | |
public interface Callbacks { | |
/** | |
* Callback for when an item has been selected. | |
*/ | |
public void onItemSelected(Item deal, View view); | |
} | |
/** | |
* A dummy implementation of the {@link Callbacks} interface that does | |
* nothing. Used only when this fragment is not attached to an activity. | |
*/ | |
private static Callbacks sDummyCallbacks = new Callbacks() { | |
@Override | |
public void onItemSelected(Item deal, View view) { | |
} | |
}; | |
/** | |
* The desired interval for location updates. Inexact. Updates may be more or less frequent. | |
*/ | |
public static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; | |
/** | |
* The fastest rate for active location updates. Exact. Updates will never be more frequent | |
* than this value. | |
*/ | |
public static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = | |
UPDATE_INTERVAL_IN_MILLISECONDS / 2; | |
public static final String GEOFIRE_CHILD = "geofire"; | |
public static final String DEALS_CHILD = "items"; | |
protected static final int REQUEST_CHECK_SETTINGS = 0x1; | |
private static final String SELECTED_KEY = "selected_position"; | |
private final String LOG_TAG = ExampleFragment.class.getSimpleName(); | |
public Firebase mFirebaseRef; | |
/** | |
* Stores parameters for requests to the FusedLocationProviderApi. | |
*/ | |
protected LocationRequest mLocationRequest; | |
/** | |
* Stores the types of location services the client is interested in using. Used for checking | |
* settings to determine if the device has optimal location settings. | |
*/ | |
protected LocationSettingsRequest mLocationSettingsRequest; | |
//Main Screen Views | |
ListView mListView; | |
@InjectView(R.id.MainLayout) | |
LinearLayout mMainLayout; | |
@InjectView(R.id.list_container) | |
MultiStateView mListContainer; | |
@InjectView(R.id.header_msg) | |
TextView mHeader; | |
SharedPreferences settings; | |
// global variables | |
@Icicle | |
int mPosition = ListView.INVALID_POSITION; | |
GeoLocation center; | |
@Icicle | |
boolean mActiveGeoQuery = false; | |
private GeoFire geoFire; | |
private GeoQuery query; | |
private View rootView; | |
private ItemListAdapter mItemListAdapter; | |
private int counter = 0; | |
/* Data from the authenticated user */ | |
private AuthData mAuthData; | |
/* Client used to interact with Google APIs. */ | |
private GoogleApiClient mGoogleApiClient; | |
private Location mLastLocation; | |
private double mLatitudeText; | |
private double mLongitudeText; | |
private Context mContext; | |
public ExampleFragment() { | |
} | |
@Override | |
public void onActivityCreated(Bundle savedInstanceState) { | |
super.onActivityCreated(savedInstanceState); | |
} | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
Log.d(LOG_TAG, "onCreate"); | |
super.onCreate(savedInstanceState); | |
mContext = getActivity(); | |
settings = PreferenceManager.getDefaultSharedPreferences(mContext); | |
setHasOptionsMenu(true); | |
mFirebaseRef = new Firebase(getString(R.string.firebase_url)).child(DEALS_CHILD); | |
//I initialize the object that will be used to retrieve the location | |
buildGoogleApiClient(); | |
buildLocationSettingsRequest(); | |
checkLocationSettings(); | |
geoFire = new GeoFire(new Firebase(getString(R.string.firebase_url)).child(GEOFIRE_CHILD)); | |
mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo(GEOFIRE_CHILD), getActivity(), R.layout.list_item_items); | |
} | |
protected synchronized void buildGoogleApiClient() { | |
Log.d(LOG_TAG, "buildGoogleApiClient"); | |
mGoogleApiClient = new GoogleApiClient.Builder(mContext) | |
.addConnectionCallbacks(this) | |
.addOnConnectionFailedListener(this) | |
.addApi(LocationServices.API) | |
.build(); | |
} | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
settings.unregisterOnSharedPreferenceChangeListener(this); | |
} | |
@Override | |
public void onStop() { | |
super.onStop(); | |
if (mGoogleApiClient.isConnected()) { | |
mGoogleApiClient.disconnect(); | |
} | |
} | |
@Override | |
public void onStart() { | |
super.onStart(); | |
Log.d(LOG_TAG, "onStart"); | |
mGoogleApiClient.connect(); | |
//TODO: check if location is activated | |
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext)); | |
if (center.latitude != 0 && center.longitude != 0 && !mActiveGeoQuery) { | |
startGeoQuery(); | |
} else if (mActiveGeoQuery) { | |
Log.d(LOG_TAG, "geoquery already active"); | |
} else { | |
Log.d(LOG_TAG, "center not setted"); | |
//I first try to set the center at the Last Known Location if retrieved | |
if (Double.isNaN(mLatitudeText) && mLatitudeText != 0) { | |
Log.d(LOG_TAG, "center setted from fused location"); | |
center = new GeoLocation(mLatitudeText, mLongitudeText); | |
startGeoQuery(); | |
} | |
} | |
checkForItems(); | |
} | |
@Override | |
public void onResume() { | |
super.onResume(); | |
Log.d(LOG_TAG, "onResume"); | |
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext)); | |
if (center.latitude != 0 && center.longitude != 0) { | |
Log.d(LOG_TAG, "onResume center setted"); | |
mItemListAdapter.notifyDataSetChanged(); | |
} else { | |
Log.d(LOG_TAG, "onResume center not setted"); | |
} | |
} | |
@Override | |
public View onCreateView(LayoutInflater inflater, ViewGroup container, | |
Bundle savedInstanceState) { | |
rootView = inflater.inflate(R.layout.fragment_main, container, false); | |
ButterKnife.inject(this, rootView); | |
mListView = (ListView) mListContainer.getContentView(); | |
mListContainer.setState(MultiStateView.ContentState.LOADING); | |
mListContainer.setEmptyString(getString(R.string.empty_list_description)); | |
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { | |
@Override | |
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { | |
Item cursor = (Item) mListView.getItemAtPosition(i); | |
if (cursor != null) { | |
mCallbacks.onItemSelected(cursor, view); | |
} | |
mPosition = i; | |
} | |
}); | |
return rootView; | |
} | |
private void bindListView() { | |
mListView.setAdapter(mItemListAdapter); | |
} | |
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { | |
Log.d(LOG_TAG, "preference " + key + " change"); | |
if (query != null) { | |
if (key.equals(getString(R.string.search_radius_key))) { | |
String newRadius = PrefsUtils.getSearchadius(mContext); | |
Log.d(LOG_TAG, "new radius " + newRadius); | |
query.setRadius(Double.parseDouble(newRadius)); | |
} | |
// when the location will change it will trigger the change on the latitude and longitude preference | |
// I hook up the change of the center in the query to longitude that will be fired last | |
if (key.equals(getString(R.string.long_pref_key))) { | |
GeoLocation newCenter = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext)); | |
Log.d(LOG_TAG, "new center " + newCenter.toString()); | |
query.setCenter(newCenter); | |
} | |
if (key.equals(getString(R.string.category_pref_key))) { | |
String filter = PrefsUtils.getCategoryFilter(mContext); | |
Log.d(LOG_TAG, "filter: " + filter); | |
mItemListAdapter.getFilter().filter(filter, new Filter.FilterListener() { | |
@Override | |
public void onFilterComplete(int count) { | |
//wait until the the filtering is completed to change the state of the main view | |
isListEmpty(); | |
} | |
}); | |
// I update the header view | |
if (!filter.isEmpty()) { | |
mHeader.setVisibility(View.VISIBLE); | |
mHeader.setText(filter); | |
} else { | |
mHeader.setVisibility(View.GONE); | |
} | |
} | |
} | |
} | |
private void startGeoQuery() { | |
double radius = Double.parseDouble(PrefsUtils.getSearchadius(mContext)); | |
// safety net in case that for some reasons the returned radius is 0 | |
if (radius < 1.0) { | |
radius = 2.0; | |
} | |
query = geoFire.queryAtLocation(center, radius); | |
Log.d(LOG_TAG, "center: " + center.toString() + ", radius: " + radius); | |
query.addGeoQueryEventListener(new GeoQueryEventListener() { | |
@Override | |
public void onKeyEntered(String key, GeoLocation location) { | |
Log.d(LOG_TAG, "Key " + key + " entered the search area at [" + location.latitude + "," + location.longitude + "]"); | |
Firebase tempRef = mFirebaseRef.child(key); | |
tempRef.addValueEventListener(new ValueEventListener() { | |
@Override | |
public void onDataChange(DataSnapshot snapshot) { | |
// I add the deal only if it doesn't exist already in the adapter | |
String key = snapshot.getKey(); | |
if (!mItemListAdapter.exists(key)) { | |
Log.d(LOG_TAG, "item added " + key); | |
mItemListAdapter.addSingle(snapshot); | |
mItemListAdapter.notifyDataSetChanged(); | |
mListContainer.setState(MultiStateView.ContentState.CONTENT); | |
} else { | |
//...otherwise I will update the record | |
Log.d(LOG_TAG, "item updated: " + key); | |
mItemListAdapter.update(snapshot, key); | |
mItemListAdapter.notifyDataSetChanged(); | |
} | |
} | |
@Override | |
public void onCancelled(FirebaseError firebaseError) { | |
Log.d(LOG_TAG, "cancelled with error:" + firebaseError.getMessage()); | |
} | |
}); | |
} | |
@Override | |
public void onKeyExited(String key) { | |
Log.d(LOG_TAG, "deal " + key + " is no longer in the search area"); | |
mItemListAdapter.remove(key); | |
isListEmpty(); | |
} | |
@Override | |
public void onKeyMoved(String key, GeoLocation location) { | |
Log.d(LOG_TAG, String.format("Key " + key + " moved within the search area to [%f,%f]", location.latitude, location.longitude)); | |
} | |
@Override | |
public void onGeoQueryReady() { | |
Log.d(LOG_TAG, "All initial data has been loaded and events have been fired!"); | |
isListEmpty(); | |
mActiveGeoQuery = true; | |
} | |
@Override | |
public void onGeoQueryError(FirebaseError error) { | |
Log.e(LOG_TAG, "There was an error with this query: " + error); | |
mListContainer.setState(MultiStateView.ContentState.ERROR_GENERAL); | |
mListContainer.setCustomErrorString(mContext.getString(R.string.list_error_message)); | |
mActiveGeoQuery = false; | |
} | |
}); | |
bindListView(); | |
} | |
private void isListEmpty() { | |
if (mItemListAdapter.getCount() == 0) { | |
mListContainer.setState(MultiStateView.ContentState.EMPTY); | |
if (mListView.hasFocus()) { | |
mListContainer.setEmptyString(getString(R.string.empty_list_description)); | |
} | |
} else { | |
mListContainer.setState(MultiStateView.ContentState.CONTENT); | |
} | |
} | |
private void checkForItems() { | |
Handler handler = new Handler(); | |
handler.postDelayed(new Runnable() { | |
public void run() { | |
if (mListView.hasFocus()) { | |
if (mItemListAdapter.getCount() == 0) { | |
mListContainer.setState(MultiStateView.ContentState.EMPTY); | |
mListContainer.setEmptyString(getString(R.string.empty_list_description)); | |
} | |
} | |
} | |
}, 8000); | |
} | |
public void filterList(String query) { | |
if (query.equals("")) { | |
mItemListAdapter = new ItemListAdapter(mFirebaseRef.limitToFirst(20), getActivity(), R.layout.list_item_items); | |
} else { | |
mItemListAdapter = new ItemListAdapter(mFirebaseRef.orderByValue().startAt(query), getActivity(), R.layout.list_item_items); | |
} | |
bindListView(); | |
} | |
@Override | |
public void onConnected(Bundle bundle) { | |
Log.d(LOG_TAG, "connected"); | |
mLastLocation = LocationServices.FusedLocationApi.getLastLocation( | |
mGoogleApiClient); | |
if (mLastLocation != null) { | |
mLatitudeText = mLastLocation.getLatitude(); | |
mLongitudeText = mLastLocation.getLongitude(); | |
Log.d(LOG_TAG, "lat: " + mLatitudeText + ", long: " + mLongitudeText); | |
center = new GeoLocation(PrefsUtils.getPreferredLat(mContext), PrefsUtils.getPreferredLong(mContext)); | |
if (!mActiveGeoQuery) { | |
PrefsUtils.setPreferredLatLong(mContext, mLatitudeText, mLongitudeText); | |
center = new GeoLocation(mLatitudeText, mLongitudeText); | |
startGeoQuery(); | |
mItemListAdapter.notifyDataSetChanged(); | |
} | |
} | |
} | |
@Override | |
public void onConnectionSuspended(int i) { | |
// The connection to Google Play services was lost for some reason. We call connect() to | |
// attempt to re-establish the connection. | |
Log.d(LOG_TAG, "Connection suspended"); | |
mGoogleApiClient.connect(); | |
} | |
@Override | |
public void onConnectionFailed(ConnectionResult connectionResult) { | |
Log.d(LOG_TAG, "connection failed" + connectionResult.getErrorCode()); | |
} | |
protected void createLocationRequest() { | |
mLocationRequest = new LocationRequest(); | |
// Sets the desired interval for active location updates. This interval is | |
// inexact. You may not receive updates at all if no location sources are available, or | |
// you may receive them slower than requested. You may also receive updates faster than | |
// requested if other applications are requesting location at a faster interval. | |
mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS); | |
// Sets the fastest rate for active location updates. This interval is exact, and your | |
// application will never receive updates faster than this value. | |
mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS); | |
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); | |
} | |
protected void buildLocationSettingsRequest() { | |
LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); | |
builder.addLocationRequest(mLocationRequest); | |
mLocationSettingsRequest = builder.build(); | |
} | |
protected void checkLocationSettings() { | |
PendingResult<LocationSettingsResult> result = | |
LocationServices.SettingsApi.checkLocationSettings( | |
mGoogleApiClient, | |
mLocationSettingsRequest | |
); | |
result.setResultCallback(this); | |
} | |
@Override | |
public void onResult(LocationSettingsResult locationSettingsResult) { | |
final Status status = locationSettingsResult.getStatus(); | |
switch (status.getStatusCode()) { | |
case LocationSettingsStatusCodes.SUCCESS: | |
Log.i(LOG_TAG, "All location settings are satisfied."); | |
break; | |
case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: | |
Log.i(LOG_TAG, "Location settings are not satisfied. Show the user a dialog to" + | |
"upgrade location settings "); | |
try { | |
// Show the dialog by calling startResolutionForResult(), and check the result | |
// in onActivityResult(). | |
status.startResolutionForResult(getActivity(), REQUEST_CHECK_SETTINGS); | |
} catch (IntentSender.SendIntentException e) { | |
Log.i(LOG_TAG, "PendingIntent unable to execute request."); | |
} | |
break; | |
case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: | |
Log.i(LOG_TAG, "Location settings are inadequate, and cannot be fixed here. Dialog " + | |
"not created."); | |
break; | |
default: | |
Log.i(LOG_TAG, "Result not recognized"); | |
} | |
} | |
} |
I am having an issue with ItemListAdapter is abstract and cannot be instantiated.
mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo("GeoFire"), getActivity(), R.layout.activity_post_detail);
Here is my ItemListAdapter code.
public abstract class ItemListAdapter<T> extends BaseAdapter implements Filterable {
private static final String LOG_TAG = "FirebaseListAdapter";
private Query mRef;
private Class<T> mModelClass;
private int mLayout;
private LayoutInflater mInflater;
private List<T> mModels;
private List<T> mFilteredModels;
private Map<String, T> mModelKeys;
private Map<String, T> mFilteredKeys;
private ChildEventListener mListener;
private Context mContext;
private ValueFilter valueFilter;
/**
* @param mRef The Firebase location to watch for data changes. Can also be a slice of a location, using some
* combination of <code>limit()</code>, <code>startAt()</code>, and <code>endAt()</code>,
* @param mModelClass Firebase will marshall the data at a location into an instance of a class that you provide
* @param mLayout This is the mLayout used to represent a single list item. You will be responsible for populating an
* instance of the corresponding view with the data from an instance of mModelClass.
* @param activity The activity containing the ListView
*/
public ItemListAdapter(Query mRef, Class<T> mModelClass, int mLayout, Activity activity) {
this.mRef = mRef;
this.mModelClass = mModelClass;
this.mLayout = mLayout;
mInflater = activity.getLayoutInflater();
mModels = new ArrayList<>();
mModelKeys = new HashMap<>();
// Look for all child events. We will then map them to our own internal ArrayList, which backs ListView
mListener = this.mRef.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
T model = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
mModelKeys.put(dataSnapshot.getKey(), model);
// Insert into the correct location, based on previousChildName
if (previousChildName == null) {
mModels.add(0, model);
} else {
T previousModel = mModelKeys.get(previousChildName);
int previousIndex = mModels.indexOf(previousModel);
int nextIndex = previousIndex + 1;
if (nextIndex == mModels.size()) {
mModels.add(model);
} else {
mModels.add(nextIndex, model);
}
}
notifyDataSetChanged();
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
Log.d(LOG_TAG, "onChildChanged");
// One of the mModels changed. Replace it in our list and name mapping
String modelName = dataSnapshot.getKey();
T oldModel = mModelKeys.get(modelName);
T newModel = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
int index = mModels.indexOf(oldModel);
mModels.set(index, newModel);
mModelKeys.put(modelName, newModel);
notifyDataSetChanged();
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.d(LOG_TAG, "onChildRemoved");
// A model was removed from the list. Remove it from our list and the name mapping
String modelName = dataSnapshot.getKey();
T oldModel = mModelKeys.get(modelName);
mModels.remove(oldModel);
mModelKeys.remove(modelName);
notifyDataSetChanged();
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(LOG_TAG, "onChildMoved");
// A model changed position in the list. Update our list accordingly
String modelName = dataSnapshot.getKey();
T oldModel = mModelKeys.get(modelName);
T newModel = dataSnapshot.getValue(FireBaseListAdapter.this.mModelClass);
int index = mModels.indexOf(oldModel);
mModels.remove(index);
if (previousChildName == null) {
mModels.add(0, newModel);
} else {
T previousModel = mModelKeys.get(previousChildName);
int previousIndex = mModels.indexOf(previousModel);
int nextIndex = previousIndex + 1;
if (nextIndex == mModels.size()) {
mModels.add(newModel);
} else {
mModels.add(nextIndex, newModel);
}
}
notifyDataSetChanged();
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.e("FirebaseListAdapter", "Listen was cancelled, no more updates will occur");
}
});
}
What is to be entered for the Model Class in the ItemListAdapter?
I am getting NullPointerException atif (!mFireAdapter.exists(key))
mFireAdapter is FireBaseListAdapter.
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.android.pokechat.FireBaseListAdapter.update(com.google.firebase.database.DataSnapshot, java.lang.String)' on a null object reference
at com.android.pokechat.fragment.GeoPostsFragment$3$1.onDataChange(GeoPostsFragment.java:236)
Here it shows the information correctly in my logcat.
07-19 01:49:26.640 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMsopZitLFzcxFuPOCn entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMu2IcjIyMx5Qpb5Kfh entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMz3knxKsAAixlf4ijY entered the search area at [40.7853889,-122.4056973]
07-19 01:49:26.642 7703-7703/com.android.pokechat D/GeoPostsFragment: Key -KMzXh2Kf7nLdOPqnMpY entered the search area at [40.7853889,-122.4056973]
Is this is where I could be possibly wrong I am using your ExampleAdapter as mItemListAdapter
mItemListAdapter = new ItemListAdapter(mFirebaseRef.equalTo("GeoFire"),getActivity(), R.layout.activity_post_detail);
Please help me resolve the issue I am quite ahead with my project finishing.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To be used in conjunction with this Adapter