Skip to content

Implement ParseQueryPager #129

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

Merged
merged 1 commit into from
Feb 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.ListView;
import android.widget.Toast;

Expand All @@ -16,6 +17,8 @@

public class ListActivity extends AppCompatActivity {

private static final String TAG = "ListActivity";

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand All @@ -36,11 +39,12 @@ public ParseQuery<ParseObject> create() {
adapter.addOnQueryLoadListener(new ParseQueryAdapter.OnQueryLoadListener<ParseObject>() {
@Override
public void onLoading() {

Log.d(TAG, "loading");
}

@Override
public void onLoaded(List<ParseObject> objects, Exception e) {
Log.d(TAG, "loaded");
if (e != null
&& e instanceof ParseException
&& ((ParseException) e).getCode() != ParseException.CACHE_MISS) {
Expand Down
199 changes: 69 additions & 130 deletions ParseUI-Widget/src/main/java/com/parse/ParseQueryAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,14 @@
import android.widget.LinearLayout;
import android.widget.TextView;

import com.parse.ParseQuery.CachePolicy;
import com.parse.widget.util.ParseQueryPager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import bolts.Capture;
import bolts.CancellationTokenSource;

/**
* A {@code ParseQueryAdapter} handles the fetching of objects by page, and displaying objects as
Expand Down Expand Up @@ -112,15 +109,23 @@ public interface OnQueryLoadListener<T extends ParseObject> {
void onLoaded(List<T> objects, Exception e);
}

private final Object lock = new Object();
private ParseQueryPager<T> pager;
private CancellationTokenSource cts;

//region Backwards compatibility
private ParseQuery<T> query;
private int objectsPerPage = 25;
//endregion

private Integer itemResourceId;

// The key to use to display on the cell text label.
private String textKey;

// The key to use to fetch an image for display in the cell's image view.
private String imageKey;

// The number of objects to show per page (default: 25)
private int objectsPerPage = 25;

// Whether the table should use the built-in pagination feature (default:
// true)
private boolean paginationEnabled = true;
Expand All @@ -142,24 +147,6 @@ public interface OnQueryLoadListener<T extends ParseObject> {

private Context context;

private List<T> objects = new ArrayList<>();

private Set<ParseQuery> runningQueries =
Collections.newSetFromMap(new ConcurrentHashMap<ParseQuery, Boolean>());


// Used to keep track of the pages of objects when using CACHE_THEN_NETWORK. When using this,
// the data will be flattened and put into the objects list.
private List<List<T>> objectPages = new ArrayList<>();

private int currentPage = 0;

private Integer itemResourceId;

private boolean hasNextPage = true;

private QueryFactory<T> queryFactory;

private List<OnQueryLoadListener<T>> onQueryLoadListeners =
new ArrayList<>();

Expand Down Expand Up @@ -277,7 +264,7 @@ public ParseQueryAdapter(Context context, QueryFactory<T> queryFactory, int item
private ParseQueryAdapter(Context context, QueryFactory<T> queryFactory, Integer itemViewResource) {
super();
this.context = context;
this.queryFactory = queryFactory;
query = queryFactory.create();
itemResourceId = itemViewResource;
}

Expand All @@ -290,13 +277,38 @@ public Context getContext() {
return context;
}

private ParseQueryPager<T> getPager() {
synchronized (lock) {
if (pager == null) {
pager = new ParseQueryPager<T>(query, objectsPerPage) {
@Override
protected ParseQuery<T> createQuery(int page) {
// Workaround for backwards compatibility
ParseQuery<T> query = new ParseQuery<>(getQuery());
if (paginationEnabled) {
setPageOnQuery(page, query);
}
return query;
}
};
cts = new CancellationTokenSource();
}

return pager;
}
}

private List<T> getObjects() {
return getPager().getObjects();
}

/** {@inheritDoc} **/
@Override
public T getItem(int index) {
if (index == getPaginationCellRow()) {
return null;
}
return objects.get(index);
return getObjects().get(index);
}

/** {@inheritDoc} **/
Expand Down Expand Up @@ -337,18 +349,15 @@ public void unregisterDataSetObserver(DataSetObserver observer) {
* Remove all elements from the list.
*/
public void clear() {
objectPages.clear();
cancelAllQueries();
syncObjectsWithPages();
notifyDataSetChanged();
currentPage = 0;
}

private void cancelAllQueries() {
for (ParseQuery q : runningQueries) {
q.cancel();
synchronized (lock) {
if (cts != null) {
cts.cancel();
}
pager = null;
cts = null;
}
runningQueries.clear();

notifyDataSetChanged();
}

/**
Expand All @@ -359,118 +368,47 @@ private void cancelAllQueries() {
* {@code false}.
*/
public void loadObjects() {
loadObjects(0, true);
loadNextPage(true);
}

private void loadObjects(final int page, final boolean shouldClear) {
final ParseQuery<T> query = queryFactory.create();

if (objectsPerPage > 0 && paginationEnabled) {
setPageOnQuery(page, query);
private void loadNextPage(final boolean shouldClear) {
synchronized (lock) {
if (shouldClear && pager != null) {
cts.cancel();
pager = null;
}
}

notifyOnLoadingListeners();

// Create a new page
if (page >= objectPages.size()) {
objectPages.add(page, new ArrayList<T>());
}

// In the case of CACHE_THEN_NETWORK, two callbacks will be called. Using this flag to keep
// track of the callbacks.
final Capture<Boolean> firstCallBack = new Capture<>(true);

runningQueries.add(query);

// TODO convert to Tasks and CancellationTokens
// (depends on https://github.com/ParsePlatform/Parse-SDK-Android/issues/6)
query.findInBackground(new FindCallback<T>() {
getPager().loadNextPage(new FindCallback<T>() {
@Override
public void done(List<T> foundObjects, ParseException e) {
if (!runningQueries.contains(query)) {
public void done(List<T> results, ParseException e) {
if (results == null && e == null) { // cancelled
return;
}
// In the case of CACHE_THEN_NETWORK, two callbacks will be called. We can only remove the
// query after the second callback.
if (Parse.isLocalDatastoreEnabled() ||
(query.getCachePolicy() != CachePolicy.CACHE_THEN_NETWORK) ||
(query.getCachePolicy() == CachePolicy.CACHE_THEN_NETWORK && !firstCallBack.get())) {
runningQueries.remove(query);
}

// Backwards compatibility
if ((!Parse.isLocalDatastoreEnabled() &&
query.getCachePolicy() == CachePolicy.CACHE_ONLY) &&
query.getCachePolicy() == ParseQuery.CachePolicy.CACHE_ONLY) &&
(e != null) && e.getCode() == ParseException.CACHE_MISS) {
// no-op on cache miss
return;
}

if ((e != null) &&
((e.getCode() == ParseException.CONNECTION_FAILED) ||
(e.getCode() != ParseException.CACHE_MISS))) {
hasNextPage = true;
} else if (foundObjects != null) {
if (shouldClear && firstCallBack.get()) {
runningQueries.remove(query);
cancelAllQueries();
runningQueries.add(query); // allow 2nd callback
objectPages.clear();
objectPages.add(new ArrayList<T>());
currentPage = page;
firstCallBack.set(false);
}

// Only advance the page, this prevents second call back from CACHE_THEN_NETWORK to
// reset the page.
if (page >= currentPage) {
currentPage = page;
notifyDataSetChanged();

// since we set limit == objectsPerPage + 1
hasNextPage = (foundObjects.size() > objectsPerPage);
}

if (paginationEnabled && foundObjects.size() > objectsPerPage) {
// Remove the last object, fetched in order to tell us whether there was a "next page"
foundObjects.remove(objectsPerPage);
}

List<T> currentPage = objectPages.get(page);
currentPage.clear();
currentPage.addAll(foundObjects);

syncObjectsWithPages();

// executes on the UI thread
notifyDataSetChanged();
}

notifyOnLoadedListeners(foundObjects, e);
notifyOnLoadedListeners(results, e);
}
});
}

/**
* This is a helper function to sync the objects with objectPages. This is only used with the
* CACHE_THEN_NETWORK option.
*/
private void syncObjectsWithPages() {
objects.clear();
for (List<T> pageOfObjects : objectPages) {
objects.addAll(pageOfObjects);
}
}, cts.getToken());
}

/**
* Loads the next page of objects, appends to table, and notifies the UI that the model has
* changed.
*/
public void loadNextPage() {
if (objects.size() == 0 && runningQueries.size() == 0) {
loadObjects(0, false);
}
else {
loadObjects(currentPage + 1, false);
}
loadNextPage(false);
}

/**
Expand All @@ -482,7 +420,7 @@ public void loadNextPage() {
*/
@Override
public int getCount() {
int count = objects.size();
int count = getObjects().size();

if (shouldShowPaginationCell()) {
count++;
Expand Down Expand Up @@ -689,7 +627,7 @@ public void setAutoload(boolean autoload) {
return;
}
this.autoload = autoload;
if (this.autoload && !dataSetObservers.isEmpty() && objects.isEmpty()) {
if (this.autoload && !dataSetObservers.isEmpty() && getObjects().isEmpty()) {
loadObjects();
}
}
Expand Down Expand Up @@ -725,11 +663,12 @@ private View getDefaultView(Context context) {
}

private int getPaginationCellRow() {
return objects.size();
return getObjects().size();
}

private boolean shouldShowPaginationCell() {
return paginationEnabled && objects.size() > 0 && hasNextPage;
ParseQueryPager<T> pager = getPager();
return paginationEnabled && pager.getObjects().size() > 0 && pager.hasNextPage();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit, since we have getObjects() helper method, let's just use the helper method so it is easy for us to change in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm going to leave it so that getObjects() and hasNextPage() are ensured to be called on the same instance of pager

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

}

private void notifyOnLoadingListeners() {
Expand Down
Loading