From f0186c25aec7e21aec20d383f835437a97236422 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 12 Jul 2017 17:30:22 -0700 Subject: [PATCH 01/37] FirebaseUI for Firestore Change-Id: Iea99b7a5628bd38cd979dfa1a7aa21943b155e13 --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 13 +- .../com/firebase/uidemo/ChooserActivity.java | 4 + .../firestore/FirestoreChatActivity.java | 120 ++++++++++++ app/src/main/res/values/strings.xml | 2 + common/.gitignore | 1 + common/build.gradle | 29 +++ common/proguard-rules.pro | 25 +++ common/src/main/AndroidManifest.xml | 2 + .../database/BaseObservableSnapshotArray.java | 97 ++++++++++ .../ui/database/BaseSnapshotParser.java | 11 ++ .../firebase/ui/database/Preconditions.java | 0 common/src/main/res/values/strings.xml | 3 + constants.gradle | 2 +- database/build.gradle | 2 + .../firebase/ui/database/FirebaseArray.java | 4 +- .../ui/database/ObservableSnapshotArray.java | 65 +------ .../firebase/ui/database/SnapshotParser.java | 10 +- firestore/.gitignore | 1 + firestore/build.gradle | 37 ++++ firestore/proguard-rules.pro | 25 +++ firestore/src/main/AndroidManifest.xml | 2 + .../ui/firestore/ChangeEventListener.java | 19 ++ .../firebase/ui/firestore/FirestoreArray.java | 173 ++++++++++++++++++ .../firestore/FirestoreRecyclerAdapter.java | 71 +++++++ .../firebase/ui/firestore/SnapshotParser.java | 11 ++ firestore/src/main/res/values/strings.xml | 3 + settings.gradle | 2 +- 28 files changed, 660 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java create mode 100644 common/.gitignore create mode 100644 common/build.gradle create mode 100644 common/proguard-rules.pro create mode 100644 common/src/main/AndroidManifest.xml create mode 100644 common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java create mode 100644 common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java rename {database => common}/src/main/java/com/firebase/ui/database/Preconditions.java (100%) create mode 100644 common/src/main/res/values/strings.xml create mode 100644 firestore/.gitignore create mode 100644 firestore/build.gradle create mode 100644 firestore/proguard-rules.pro create mode 100644 firestore/src/main/AndroidManifest.xml create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java create mode 100644 firestore/src/main/res/values/strings.xml diff --git a/app/build.gradle b/app/build.gradle index 5658ad3cb..20d6f61b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,7 @@ dependencies { compile project(path: ':auth') compile project(path: ':database') + compile project(path: ':firestore') compile project(path: ':storage') compile "com.google.android.gms:play-services-auth:$firebaseVersion" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb2589255..27c027c31 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,7 @@ - + @@ -19,8 +18,10 @@ + + @@ -42,10 +43,12 @@ android:name=".auth.SignedInActivity" android:label="@string/name_auth_ui" /> - + + + diff --git a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java index d1c36ac63..d1a8d5034 100644 --- a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java +++ b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java @@ -27,6 +27,7 @@ import com.firebase.uidemo.auth.AuthUiActivity; import com.firebase.uidemo.database.ChatActivity; +import com.firebase.uidemo.firestore.FirestoreChatActivity; import com.firebase.uidemo.storage.ImageActivity; import butterknife.BindView; @@ -50,18 +51,21 @@ protected void onCreate(Bundle savedInstanceState) { private static class ActivityChooserAdapter extends RecyclerView.Adapter { private static final Class[] CLASSES = new Class[]{ ChatActivity.class, + FirestoreChatActivity.class, AuthUiActivity.class, ImageActivity.class, }; private static final int[] DESCRIPTION_NAMES = new int[]{ R.string.name_chat, + R.string.name_firestore_chat, R.string.name_auth_ui, R.string.name_image }; private static final int[] DESCRIPTION_IDS = new int[]{ R.string.desc_chat, + R.string.desc_firestore_chat, R.string.desc_auth_ui, R.string.desc_image }; diff --git a/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java new file mode 100644 index 000000000..5951df3db --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java @@ -0,0 +1,120 @@ +package com.firebase.uidemo.firestore; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.firebase.ui.firestore.FirestoreRecyclerAdapter; +import com.firebase.uidemo.R; +import com.firebase.uidemo.database.Chat; +import com.firebase.uidemo.database.ChatHolder; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * TODO + */ +public class FirestoreChatActivity extends AppCompatActivity { + + private static final String TAG = "FirestoreChat"; + + @BindView(R.id.messagesList) + RecyclerView mRecycler; + + @BindView(R.id.sendButton) + Button mSendButton; + + @BindView(R.id.messageEdit) + EditText mMessageEdit; + + @BindView(R.id.emptyTextView) + TextView mEmptyListMessage; + + private FirebaseFirestore mFirestore; + private FirestoreRecyclerAdapter mAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + ButterKnife.bind(this); + + mFirestore = FirebaseFirestore.getInstance(); + Query query = mFirestore.collection("chats").limit(50); + + LinearLayoutManager manager = new LinearLayoutManager(this); + mAdapter = new FirestoreRecyclerAdapter(query, Chat.class) { + @Override + public void onBindViewHolder(ChatHolder holder, int i, Chat model) { + holder.bind(model); + } + + @Override + public ChatHolder onCreateViewHolder(ViewGroup group, int i) { + View view = LayoutInflater.from(group.getContext()) + .inflate(R.layout.message, group, false); + + return new ChatHolder(view); + } + + @Override + public void onDataChanged() { + // If there are no chat messages, show a view that invites the user to add a message. + mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + }; + + mRecycler.setLayoutManager(manager); + mRecycler.setAdapter(mAdapter); + } + + @Override + protected void onStart() { + super.onStart(); + mAdapter.startListening(); + } + + @Override + protected void onStop() { + super.onStop(); + mAdapter.stopListening(); + } + + @OnClick(R.id.sendButton) + public void onSendClick() { + // TODO: Real UID + String uid = "123456"; + String name = "User " + uid.substring(0, 6); + + Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); + + mFirestore.collection("chats").add(chat) + .addOnCompleteListener(this, + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.e(TAG, "Failed to write message", task.getException()); + } + } + }); + + mMessageEdit.setText(""); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cfaef35a..71348ac17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,10 +2,12 @@ Firebase UI ChatActivity + FirestoreChatActivity Auth UI demo Storage Image Demo Demonstrates using a FirebaseRecyclerAdapter to load data from Firebase Database into a RecyclerView for a basic chat app. + Demonstrates using a FirestoreRecyclerAdapter to load data from Cloud Firestore into a RecyclerView for a basic chat app. Demonstrates the Firebase Auth UI flow, with customization options. Demonstrates displaying an image from Cloud Storage using Glide. diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 000000000..79e36900e --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' +apply from: '../library/quality/quality.gradle' +check.dependsOn 'compileDebugAndroidTestJavaWithJavac' + +android { + compileSdkVersion compileSdk + buildToolsVersion buildTools + + defaultConfig { + minSdkVersion minSdk + targetSdkVersion targetSdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } +} + +dependencies { + compile "com.android.support:recyclerview-v7:$supportLibraryVersion" + // Needed to override play services + compile "com.android.support:support-v4:$supportLibraryVersion" +} diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro new file mode 100644 index 000000000..e39333687 --- /dev/null +++ b/common/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml new file mode 100644 index 000000000..070894592 --- /dev/null +++ b/common/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java new file mode 100644 index 000000000..acc94d52a --- /dev/null +++ b/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java @@ -0,0 +1,97 @@ +package com.firebase.ui.database; + +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; + +import java.util.AbstractList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * TODO + * @param the snapshot class. + * @param the listener class. + * @param the model object class. + */ +public abstract class BaseObservableSnapshotArray extends AbstractList { + + protected final List mListeners = new CopyOnWriteArrayList<>(); + protected final BaseSnapshotParser mParser; + + private boolean mHasDataChanged = false; + + /** + * Create an BaseObservableSnapshotArray with a custom {@link BaseSnapshotParser}. + * + * @param parser the {@link BaseSnapshotParser} to use + */ + public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { + mParser = Preconditions.checkNotNull(parser); + } + + /** + * TODO + */ + @CallSuper + public L addChangeEventListener(@NonNull L listener) { + Preconditions.checkNotNull(listener); + mListeners.add(listener); + + return listener; + } + + /** + * TODO + */ + @CallSuper + public void removeChangeEventListener(@NonNull L listener) { + Preconditions.checkNotNull(listener); + mListeners.remove(listener); + } + + /** + * TODO + */ + @CallSuper + public void removeAllListeners() { + for (L listener : mListeners) { + removeChangeEventListener(listener); + } + } + + protected abstract List getSnapshots(); + + /** + * @return true if the array is listening for change events from the Firebase + * database, false otherwise + */ + public final boolean isListening() { + return !mListeners.isEmpty(); + } + + /** + * @return true if the provided listener is listening for changes + */ + public final boolean isListening(L listener) { + return mListeners.contains(listener); + } + + /** + * Get the Snapshot at a given position converted to an object of the parametrized + * type. This uses the {@link BaseSnapshotParser} passed to the constructor. If the parser was not + * initialized this will throw an unchecked exception. + */ + public E getObject(int index) { + return mParser.parseSnapshot(get(index)); + } + + @Override + public S get(int index) { + return getSnapshots().get(index); + } + + @Override + public int size() { + return getSnapshots().size(); + } +} diff --git a/common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java b/common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java new file mode 100644 index 000000000..c73fc555a --- /dev/null +++ b/common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java @@ -0,0 +1,11 @@ +package com.firebase.ui.database; + +public interface BaseSnapshotParser { + /** + * This method parses the Snapshot into the requested type. + * + * @param snapshot the Snapshot to extract the model from + * @return the model extracted from the DataSnapshot + */ + T parseSnapshot(S snapshot); +} diff --git a/database/src/main/java/com/firebase/ui/database/Preconditions.java b/common/src/main/java/com/firebase/ui/database/Preconditions.java similarity index 100% rename from database/src/main/java/com/firebase/ui/database/Preconditions.java rename to common/src/main/java/com/firebase/ui/database/Preconditions.java diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml new file mode 100644 index 000000000..9e44af55b --- /dev/null +++ b/common/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + common + diff --git a/constants.gradle b/constants.gradle index 7e9a3781c..6ccea50ab 100644 --- a/constants.gradle +++ b/constants.gradle @@ -10,6 +10,6 @@ project.ext { buildTools = '25.0.3' // Remember to also update in .travis.yml - firebaseVersion = '11.0.1' + firebaseVersion = '11.0.6' supportLibraryVersion = '25.4.0' } diff --git a/database/build.gradle b/database/build.gradle index 3e2309d6d..9a8e95798 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -23,6 +23,8 @@ android { } dependencies { + compile project(path: ':common') + compile "com.android.support:recyclerview-v7:$supportLibraryVersion" // Needed to override play services compile "com.android.support:support-v4:$supportLibraryVersion" diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index e26620372..764f11bea 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -28,7 +28,9 @@ /** * This class implements a collection on top of a Firebase location. */ -public class FirebaseArray extends CachingObservableSnapshotArray implements ChildEventListener, ValueEventListener { +public class FirebaseArray extends CachingObservableSnapshotArray + implements ChildEventListener, ValueEventListener { + private Query mQuery; private List mSnapshots = new ArrayList<>(); diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 446b4e2cc..f5b3d83d3 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -6,9 +6,7 @@ import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; -import java.util.AbstractList; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; /** * Exposes a collection of items in Firebase as a {@link List} of {@link DataSnapshot}. To observe @@ -16,9 +14,8 @@ * * @param a POJO class to which the DataSnapshots can be converted. */ -public abstract class ObservableSnapshotArray extends AbstractList { - protected final List mListeners = new CopyOnWriteArrayList<>(); - protected final SnapshotParser mParser; +public abstract class ObservableSnapshotArray + extends BaseObservableSnapshotArray { private boolean mHasDataChanged = false; @@ -30,7 +27,7 @@ public abstract class ObservableSnapshotArray extends AbstractList clazz) { - this(new ClassSnapshotParser<>(clazz)); + super(new ClassSnapshotParser<>(clazz)); } /** @@ -39,7 +36,7 @@ public ObservableSnapshotArray(@NonNull Class clazz) { * @param parser the {@link SnapshotParser} to use */ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { - mParser = Preconditions.checkNotNull(parser); + super(parser); } /** @@ -49,12 +46,12 @@ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { */ @CallSuper public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { - Preconditions.checkNotNull(listener); + super.addChangeEventListener(listener); - mListeners.add(listener); for (int i = 0; i < size(); i++) { listener.onChildChanged(ChangeEventListener.EventType.ADDED, get(i), i, -1); } + if (mHasDataChanged) { listener.onDataChanged(); } @@ -67,7 +64,7 @@ public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener l */ @CallSuper public void removeChangeEventListener(@NonNull ChangeEventListener listener) { - mListeners.remove(listener); + super.removeChangeEventListener(listener); // Reset mHasDataChanged if there are no more listeners if (!isListening()) { @@ -75,20 +72,6 @@ public void removeChangeEventListener(@NonNull ChangeEventListener listener) { } } - /** - * Removes all {@link ChangeEventListener}s. The list will be empty after this call returns. - * - * @see #removeChangeEventListener(ChangeEventListener) - */ - @CallSuper - public void removeAllListeners() { - for (ChangeEventListener listener : mListeners) { - removeChangeEventListener(listener); - } - } - - protected abstract List getSnapshots(); - protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, DataSnapshot snapshot, int index) { @@ -116,38 +99,4 @@ protected final void notifyListenersOnCancelled(DatabaseError error) { listener.onCancelled(error); } } - - /** - * @return true if {@link FirebaseArray} is listening for change events from the Firebase - * database, false otherwise - */ - public final boolean isListening() { - return !mListeners.isEmpty(); - } - - /** - * @return true if the provided {@link ChangeEventListener} is listening for changes - */ - public final boolean isListening(ChangeEventListener listener) { - return mListeners.contains(listener); - } - - /** - * Get the {@link DataSnapshot} at a given position converted to an object of the parametrized - * type. This uses the {@link SnapshotParser} passed to the constructor. If the parser was not - * initialized this will throw an unchecked exception. - */ - public E getObject(int index) { - return mParser.parseSnapshot(get(index)); - } - - @Override - public DataSnapshot get(int index) { - return getSnapshots().get(index); - } - - @Override - public int size() { - return getSnapshots().size(); - } } diff --git a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java index 8801f54f9..aff3bd5e6 100644 --- a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java +++ b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java @@ -2,12 +2,4 @@ import com.google.firebase.database.DataSnapshot; -public interface SnapshotParser { - /** - * This method parses the DataSnapshot into the requested type. - * - * @param snapshot the DataSnapshot to extract the model from - * @return the model extracted from the DataSnapshot - */ - T parseSnapshot(DataSnapshot snapshot); -} +public interface SnapshotParser extends BaseSnapshotParser {} diff --git a/firestore/.gitignore b/firestore/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/firestore/.gitignore @@ -0,0 +1 @@ +/build diff --git a/firestore/build.gradle b/firestore/build.gradle new file mode 100644 index 000000000..d59b88c1e --- /dev/null +++ b/firestore/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'com.android.library' +apply from: '../library/quality/quality.gradle' +check.dependsOn 'compileDebugAndroidTestJavaWithJavac' + +android { + compileSdkVersion compileSdk + buildToolsVersion buildTools + + defaultConfig { + minSdkVersion minSdk + targetSdkVersion targetSdk + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + } +} + +dependencies { + compile project(path: ':common') + + compile "com.android.support:recyclerview-v7:$supportLibraryVersion" + // Needed to override play services + compile "com.android.support:support-v4:$supportLibraryVersion" + + compile "com.google.firebase:firebase-firestore:$firebaseVersion" + + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'com.android.support.test:rules:0.5' +} diff --git a/firestore/proguard-rules.pro b/firestore/proguard-rules.pro new file mode 100644 index 000000000..e39333687 --- /dev/null +++ b/firestore/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/firestore/src/main/AndroidManifest.xml b/firestore/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a19a3dfd9 --- /dev/null +++ b/firestore/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java new file mode 100644 index 000000000..bcebe8ed5 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java @@ -0,0 +1,19 @@ +package com.firebase.ui.firestore; + +import com.google.firebase.firestore.DocumentChange; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestoreException; + +/** + * TODO + */ +public interface ChangeEventListener { + + // TODO: add oldIndex if necessary + void onChildChanged(DocumentChange.Type eventType, DocumentSnapshot snapshot, int index); + + void onDataChanged(); + + void onError(FirebaseFirestoreException e); + +} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java new file mode 100644 index 000000000..d800796bc --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -0,0 +1,173 @@ +package com.firebase.ui.firestore; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.firebase.ui.database.BaseObservableSnapshotArray; +import com.google.firebase.firestore.DocumentChange; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.EventListener; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QuerySnapshot; + +import java.util.ArrayList; +import java.util.List; + +/** + * TODO(samstern): Document + * TODO(samstern): What to do about sorting? + */ +public class FirestoreArray + extends BaseObservableSnapshotArray + implements EventListener { + + private static final String TAG = "FirestoreArray"; + + private Query mQuery; + private ListenerRegistration mRegistration; + + private List mSnapshots; + + public FirestoreArray(Query query, final Class modelClass) { + this(query, new SnapshotParser() { + @Override + public T parseSnapshot(DocumentSnapshot snapshot) { + return snapshot.toObject(modelClass); + } + }); + } + + // TODO: What about caching? + // TODO: What about intermediate OSA class? + public FirestoreArray(Query query, SnapshotParser parser) { + super(parser); + + mQuery = query; + mSnapshots = new ArrayList<>(); + } + + public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { + boolean wasListening = isListening(); + super.addChangeEventListener(listener); + + // Only start listening once we've added our first listener + if (!wasListening) { + startListening(); + } + + // TODO: Look at what observable snapshot array does here. + + return listener; + } + + public void removeChangeEventListener(@NonNull ChangeEventListener listener) { + super.removeChangeEventListener(listener); + + // Stop listening when we have no listeners + if (!isListening()) { + stopListening(); + } + } + + @Override + public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { + if (e != null) { + Log.w(TAG, "Error in snapshot listener", e); + notifyOnError(e); + return; + } + + // Break down each document event + List changes = snapshots.getDocumentChanges(); + for (DocumentChange change : changes) { + DocumentSnapshot doc = change.getDocument(); + switch (change.getType()) { + case ADDED: + onDocumentAdded(doc); + break; + case REMOVED: + onDocumentRemoved(doc); + break; + case MODIFIED: + onDocumentModified(doc); + break; + } + } + + notifyOnDataChanged(); + } + + + @Override + protected List getSnapshots() { + return mSnapshots; + } + + private void startListening() { + mRegistration = mQuery.addSnapshotListener(this); + } + + private void stopListening() { + if (mRegistration != null) { + mRegistration.remove(); + mRegistration = null; + } + + mSnapshots.clear(); + } + + private void onDocumentAdded(DocumentSnapshot doc) { + mSnapshots.add(doc); + notifyOnChildChanged(DocumentChange.Type.ADDED, doc, mSnapshots.size() - 1); + } + + private void onDocumentRemoved(DocumentSnapshot doc) { + int ind = getDocumentIndex(doc); + if (ind >= 0) { + mSnapshots.remove(ind); + notifyOnChildChanged(DocumentChange.Type.REMOVED, doc, ind); + } + } + + private void onDocumentModified(DocumentSnapshot doc) { + int ind = getDocumentIndex(doc); + if (ind >= 0) { + mSnapshots.set(ind, doc); + notifyOnChildChanged(DocumentChange.Type.MODIFIED, doc, ind); + } + } + + private void notifyOnChildChanged(DocumentChange.Type type, + DocumentSnapshot snapshot, + int index) { + + for (ChangeEventListener listener : mListeners) { + listener.onChildChanged(type, snapshot, index); + } + } + + private void notifyOnError(FirebaseFirestoreException e) { + for (ChangeEventListener listener : mListeners) { + listener.onError(e); + } + } + + private void notifyOnDataChanged() { + for (ChangeEventListener listener : mListeners) { + listener.onDataChanged(); + } + } + + private int getDocumentIndex(DocumentSnapshot doc) { + String id = doc.getReference().getId(); + for (int i = 0; i < mSnapshots.size(); i++) { + if (mSnapshots.get(i).getReference().getId().equals(id)) { + return i; + } + } + + return -1; + } +} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java new file mode 100644 index 000000000..bad7bd8b9 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -0,0 +1,71 @@ +package com.firebase.ui.firestore; + +import android.support.v7.widget.RecyclerView; + +import com.google.firebase.firestore.DocumentChange; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.Query; + +// TODO: Document +public abstract class FirestoreRecyclerAdapter + extends RecyclerView.Adapter implements ChangeEventListener { + + private Class mModelClass; + private FirestoreArray mArray; + + public FirestoreRecyclerAdapter(Query query, Class modelClass) { + mModelClass = modelClass; + mArray = new FirestoreArray(query, modelClass); + } + + public abstract void onBindViewHolder(VH vh, int i, T model); + + public void startListening() { + mArray.addChangeEventListener(this); + } + + public void stopListening() { + mArray.removeChangeEventListener(this); + } + + @Override + public void onBindViewHolder(VH vh, int i) { + T model = mArray.getObject(i); + onBindViewHolder(vh, i, model); + } + + @Override + public int getItemCount() { + return mArray.size(); + } + + @Override + public void onChildChanged(DocumentChange.Type eventType, + DocumentSnapshot snapshot, + int index) { + switch (eventType) { + case ADDED: + // TODO: Why doesn't this work +// notifyItemInserted(index); + notifyDataSetChanged(); + break; + case REMOVED: + notifyItemRemoved(index); + break; + case MODIFIED: + notifyItemChanged(index); + break; + } + } + + @Override + public void onDataChanged() { + // No-op + } + + @Override + public void onError(FirebaseFirestoreException e) { + // No-op + } +} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java new file mode 100644 index 000000000..7a2bd9378 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java @@ -0,0 +1,11 @@ +package com.firebase.ui.firestore; + +import com.firebase.ui.database.BaseSnapshotParser; +import com.google.firebase.firestore.DocumentSnapshot; + +/** + * TODO + */ +public interface SnapshotParser extends BaseSnapshotParser { + +} diff --git a/firestore/src/main/res/values/strings.xml b/firestore/src/main/res/values/strings.xml new file mode 100644 index 000000000..3d5b6e627 --- /dev/null +++ b/firestore/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + firestore + diff --git a/settings.gradle b/settings.gradle index 7d083779a..e576a2cd8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library', ':database', ':auth', ':storage' +include ':app', ':library', ':database', ':auth', ':storage', ':firestore', ':common' From 887765d57e2f1d417c8bdcce0ba957cb08c4d481 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 4 Aug 2017 11:48:31 -0700 Subject: [PATCH 02/37] Ordering, timestamps, and better events Change-Id: I1b58b20c7fb6b247223b59207b90f8ec0e594ceb --- .../uidemo/database/AbstractChat.java | 15 ++++ .../com/firebase/uidemo/database/Chat.java | 6 +- .../firebase/uidemo/database/ChatHolder.java | 2 +- .../com/firebase/uidemo/firestore/Chat.java | 59 +++++++++++++ .../firestore/FirestoreChatActivity.java | 51 ++++++++++-- constants.gradle | 2 +- .../ui/firestore/ChangeEventListener.java | 12 ++- .../firebase/ui/firestore/FirestoreArray.java | 82 ++++++++----------- .../firestore/FirestoreRecyclerAdapter.java | 19 ++--- 9 files changed, 180 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/com/firebase/uidemo/database/AbstractChat.java create mode 100644 app/src/main/java/com/firebase/uidemo/firestore/Chat.java diff --git a/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java b/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java new file mode 100644 index 000000000..9682c67ff --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/AbstractChat.java @@ -0,0 +1,15 @@ +package com.firebase.uidemo.database; + +/** + * Common interface for chat messages, helps share code between RTDB and Firestore examples. + */ +public abstract class AbstractChat { + + public abstract String getName(); + + public abstract String getMessage(); + + public abstract String getUid(); + + +} diff --git a/app/src/main/java/com/firebase/uidemo/database/Chat.java b/app/src/main/java/com/firebase/uidemo/database/Chat.java index c4fc6efdb..270c4907d 100644 --- a/app/src/main/java/com/firebase/uidemo/database/Chat.java +++ b/app/src/main/java/com/firebase/uidemo/database/Chat.java @@ -1,6 +1,10 @@ package com.firebase.uidemo.database; -public class Chat { +import com.google.firebase.database.IgnoreExtraProperties; + +@IgnoreExtraProperties +public class Chat extends AbstractChat { + private String mName; private String mMessage; private String mUid; diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java index ce3be5248..e94a634ab 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java @@ -38,7 +38,7 @@ public ChatHolder(View itemView) { mGray300 = ContextCompat.getColor(itemView.getContext(), R.color.material_gray_300); } - public void bind(Chat chat) { + public void bind(AbstractChat chat) { setName(chat.getName()); setText(chat.getMessage()); diff --git a/app/src/main/java/com/firebase/uidemo/firestore/Chat.java b/app/src/main/java/com/firebase/uidemo/firestore/Chat.java new file mode 100644 index 000000000..7e0a0a9d0 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/firestore/Chat.java @@ -0,0 +1,59 @@ +package com.firebase.uidemo.firestore; + +import com.firebase.uidemo.database.AbstractChat; +import com.google.firebase.firestore.IgnoreExtraProperties; +import com.google.firebase.firestore.ServerTimestamp; + +import java.util.Date; + +@IgnoreExtraProperties +public class Chat extends AbstractChat { + + private String mName; + private String mMessage; + private String mUid; + private @ServerTimestamp Date mTimestamp; + + public Chat() { + // Needed for Firebase + } + + public Chat(String name, String message, String uid) { + mName = name; + mMessage = message; + mUid = uid; + } + + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public String getMessage() { + return mMessage; + } + + public void setMessage(String message) { + mMessage = message; + } + + public String getUid() { + return mUid; + } + + public void setUid(String uid) { + mUid = uid; + } + + @ServerTimestamp + public Date getTimestamp() { + return mTimestamp; + } + + public void setTimestamp(Date timestamp) { + mTimestamp = timestamp; + } +} diff --git a/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java index 5951df3db..a2c62a3aa 100644 --- a/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java @@ -12,13 +12,15 @@ import android.widget.Button; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import com.firebase.ui.firestore.FirestoreRecyclerAdapter; import com.firebase.uidemo.R; -import com.firebase.uidemo.database.Chat; import com.firebase.uidemo.database.ChatHolder; import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.Task; +import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.Query; @@ -30,7 +32,7 @@ /** * TODO */ -public class FirestoreChatActivity extends AppCompatActivity { +public class FirestoreChatActivity extends AppCompatActivity implements FirebaseAuth.AuthStateListener { private static final String TAG = "FirestoreChat"; @@ -46,6 +48,7 @@ public class FirestoreChatActivity extends AppCompatActivity { @BindView(R.id.emptyTextView) TextView mEmptyListMessage; + private FirebaseAuth mAuth; private FirebaseFirestore mFirestore; private FirestoreRecyclerAdapter mAdapter; @@ -55,8 +58,14 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_chat); ButterKnife.bind(this); + // Enable verbose Firestore logging + FirebaseFirestore.setLoggingEnabled(true); + + mAuth = FirebaseAuth.getInstance(); mFirestore = FirebaseFirestore.getInstance(); - Query query = mFirestore.collection("chats").limit(50); + + // Get the last 50 chat messages, ordered by timestamp + Query query = mFirestore.collection("chats").orderBy("timestamp").limit(50); LinearLayoutManager manager = new LinearLayoutManager(this); mAdapter = new FirestoreRecyclerAdapter(query, Chat.class) { @@ -84,22 +93,52 @@ public void onDataChanged() { mRecycler.setAdapter(mAdapter); } + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth auth) { + if (auth.getCurrentUser() != null) { + mAdapter.startListening(); + mSendButton.setEnabled(true); + } else { + mAdapter.stopListening(); + mSendButton.setEnabled(false); + } + } + @Override protected void onStart() { super.onStart(); - mAdapter.startListening(); + + signInAnonymously(); + mAuth.addAuthStateListener(this); } @Override protected void onStop() { super.onStop(); + + mAuth.removeAuthStateListener(this); mAdapter.stopListening(); } + private void signInAnonymously() { + if (mAuth.getCurrentUser() != null) { + return; + } + + mAuth.signInAnonymously() + .addOnFailureListener(this, new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + Log.w(TAG, "signIn:failure", e); + Toast.makeText(FirestoreChatActivity.this, + "Authentication failed.", Toast.LENGTH_LONG).show(); + } + }); + } + @OnClick(R.id.sendButton) public void onSendClick() { - // TODO: Real UID - String uid = "123456"; + String uid = mAuth.getCurrentUser().getUid(); String name = "User " + uid.substring(0, 6); Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); diff --git a/constants.gradle b/constants.gradle index 49ab0c839..589e44be2 100644 --- a/constants.gradle +++ b/constants.gradle @@ -10,6 +10,6 @@ project.ext { buildTools = '25.0.3' // Remember to also update in .travis.yml - firebaseVersion = '11.0.6' + firebaseVersion = '11.0.8' supportLibraryVersion = '25.4.0' } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java index bcebe8ed5..c7cdbdae1 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java @@ -1,6 +1,5 @@ package com.firebase.ui.firestore; -import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; @@ -9,8 +8,15 @@ */ public interface ChangeEventListener { - // TODO: add oldIndex if necessary - void onChildChanged(DocumentChange.Type eventType, DocumentSnapshot snapshot, int index); + enum Type { + ADDED, + REMOVED, + MODIFIED, + MOVED + } + + void onChildChanged(Type type, DocumentSnapshot snapshot, + int oldIndex, int newIndex); void onDataChanged(); diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index d800796bc..83c004f79 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -17,7 +17,6 @@ /** * TODO(samstern): Document - * TODO(samstern): What to do about sorting? */ public class FirestoreArray extends BaseObservableSnapshotArray @@ -83,16 +82,38 @@ public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { List changes = snapshots.getDocumentChanges(); for (DocumentChange change : changes) { DocumentSnapshot doc = change.getDocument(); - switch (change.getType()) { - case ADDED: - onDocumentAdded(doc); - break; - case REMOVED: - onDocumentRemoved(doc); - break; - case MODIFIED: - onDocumentModified(doc); - break; + + DocumentChange.Type changeType = change.getType(); + int oldIndex = change.getOldIndex(); + int newIndex = change.getNewIndex(); + + if (changeType == DocumentChange.Type.MODIFIED) { + if (oldIndex == newIndex) { + Log.d(TAG, "Modified (inplace): " + oldIndex); + + mSnapshots.set(oldIndex, doc); + notifyOnChildChanged(ChangeEventListener.Type.MODIFIED, doc, + oldIndex, newIndex); + } else { + Log.d(TAG, "Modified (moved): " + oldIndex + " --> " + newIndex); + + mSnapshots.remove(oldIndex); + mSnapshots.add(newIndex, doc); + notifyOnChildChanged(ChangeEventListener.Type.MOVED, doc, + oldIndex, newIndex); + } + } else if (changeType == DocumentChange.Type.REMOVED) { + Log.d(TAG, "Removed: " + oldIndex); + + mSnapshots.remove(oldIndex); + notifyOnChildChanged(ChangeEventListener.Type.REMOVED, doc, + oldIndex, -1); + } else if (changeType == DocumentChange.Type.ADDED) { + Log.d(TAG, "Added: " + newIndex); + + mSnapshots.add(newIndex, doc); + notifyOnChildChanged(ChangeEventListener.Type.ADDED, doc, + -1, newIndex); } } @@ -118,33 +139,13 @@ private void stopListening() { mSnapshots.clear(); } - private void onDocumentAdded(DocumentSnapshot doc) { - mSnapshots.add(doc); - notifyOnChildChanged(DocumentChange.Type.ADDED, doc, mSnapshots.size() - 1); - } - - private void onDocumentRemoved(DocumentSnapshot doc) { - int ind = getDocumentIndex(doc); - if (ind >= 0) { - mSnapshots.remove(ind); - notifyOnChildChanged(DocumentChange.Type.REMOVED, doc, ind); - } - } - - private void onDocumentModified(DocumentSnapshot doc) { - int ind = getDocumentIndex(doc); - if (ind >= 0) { - mSnapshots.set(ind, doc); - notifyOnChildChanged(DocumentChange.Type.MODIFIED, doc, ind); - } - } - - private void notifyOnChildChanged(DocumentChange.Type type, + private void notifyOnChildChanged(ChangeEventListener.Type type, DocumentSnapshot snapshot, - int index) { + int oldIndex, + int newIndex) { for (ChangeEventListener listener : mListeners) { - listener.onChildChanged(type, snapshot, index); + listener.onChildChanged(type, snapshot, oldIndex, newIndex); } } @@ -159,15 +160,4 @@ private void notifyOnDataChanged() { listener.onDataChanged(); } } - - private int getDocumentIndex(DocumentSnapshot doc) { - String id = doc.getReference().getId(); - for (int i = 0; i < mSnapshots.size(); i++) { - if (mSnapshots.get(i).getReference().getId().equals(id)) { - return i; - } - } - - return -1; - } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index bad7bd8b9..65535a546 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -2,7 +2,6 @@ import android.support.v7.widget.RecyclerView; -import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.Query; @@ -41,21 +40,21 @@ public int getItemCount() { } @Override - public void onChildChanged(DocumentChange.Type eventType, - DocumentSnapshot snapshot, - int index) { - switch (eventType) { + public void onChildChanged(Type type, DocumentSnapshot snapshot, + int oldIndex, int newIndex) { + + switch (type) { case ADDED: - // TODO: Why doesn't this work -// notifyItemInserted(index); - notifyDataSetChanged(); + notifyItemInserted(newIndex); break; case REMOVED: - notifyItemRemoved(index); + notifyItemRemoved(oldIndex); break; case MODIFIED: - notifyItemChanged(index); + notifyItemChanged(newIndex); break; + case MOVED: + notifyItemMoved(oldIndex, newIndex); } } From 3615c004daceb1f1ff5f94a49f2935820ac9b5ca Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 4 Aug 2017 14:57:16 -0700 Subject: [PATCH 03/37] Unify some changes, add caching. Change-Id: I620df70ce9652b76b5299d543d4178adc3804c13 --- .../database/BaseObservableSnapshotArray.java | 34 +++++ .../ui/database/ObservableSnapshotArray.java | 34 +---- .../ui/firestore/CachingSnapshotParser.java | 50 +++++++ .../firebase/ui/firestore/FirestoreArray.java | 124 ++++++++++-------- 4 files changed, 157 insertions(+), 85 deletions(-) create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java diff --git a/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java index acc94d52a..8083b66ce 100644 --- a/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java @@ -29,6 +29,24 @@ public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { mParser = Preconditions.checkNotNull(parser); } + /** + * Called when the {@link BaseObservableSnapshotArray} is active and should start listening to the + * Firebase database. + */ + @CallSuper + protected void onCreate() {} + + /** + * Called when the {@link BaseObservableSnapshotArray} is inactive and should stop listening to the + * Firebase database. + *

+ * All data should also be cleared here. + */ + @CallSuper + protected void onDestroy() { + mHasDataChanged = false; + } + /** * TODO */ @@ -47,6 +65,10 @@ public L addChangeEventListener(@NonNull L listener) { public void removeChangeEventListener(@NonNull L listener) { Preconditions.checkNotNull(listener); mListeners.remove(listener); + + if (!isListening()) { + onDestroy(); + } } /** @@ -61,6 +83,18 @@ public void removeAllListeners() { protected abstract List getSnapshots(); + protected boolean hasDataChanged() { + return mHasDataChanged; + } + + protected void setHasDataChanged(boolean hasDataChanged) { + mHasDataChanged = hasDataChanged; + } + + protected BaseSnapshotParser getSnapshotParser() { + return mParser; + } + /** * @return true if the array is listening for change events from the Firebase * database, false otherwise diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 10742f9c3..70137af96 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -17,8 +17,6 @@ public abstract class ObservableSnapshotArray extends BaseObservableSnapshotArray { - private boolean mHasDataChanged = false; - /** * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular * class. @@ -49,12 +47,13 @@ public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener l super.addChangeEventListener(listener); boolean wasListening = isListening(); + // TODO(samstern): Can some of this be moved into common? mListeners.add(listener); for (int i = 0; i < size(); i++) { listener.onChildChanged(ChangeEventListener.EventType.ADDED, get(i), i, -1); } - if (mHasDataChanged) { + if (hasDataChanged()) { listener.onDataChanged(); } @@ -63,33 +62,6 @@ public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener l return listener; } - /** - * Called when the {@link ObservableSnapshotArray} is active and should start listening to the - * Firebase database. - */ - @CallSuper - protected void onCreate() {} - - /** - * Detach a {@link com.google.firebase.database.ChildEventListener} from this array. - */ - @CallSuper - public void removeChangeEventListener(@NonNull ChangeEventListener listener) { - super.removeChangeEventListener(listener); - - if (!isListening()) { onDestroy(); } - } - - /** - * Called when the {@link ObservableSnapshotArray} is inactive and should stop listening to the - * Firebase database. - *

- * All data should also be cleared here. - */ - @CallSuper - protected void onDestroy() { - mHasDataChanged = false; - } protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, DataSnapshot snapshot, @@ -107,7 +79,7 @@ protected final void notifyChangeEventListeners(ChangeEventListener.EventType ty } protected final void notifyListenersOnDataChanged() { - mHasDataChanged = true; + setHasDataChanged(true); for (ChangeEventListener listener : mListeners) { listener.onDataChanged(); } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java new file mode 100644 index 000000000..52f123b10 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java @@ -0,0 +1,50 @@ +package com.firebase.ui.firestore; + +import android.support.annotation.RestrictTo; + +import com.google.firebase.firestore.DocumentSnapshot; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of {@link SnapshotParser} that caches results, + * so parsing a snapshot repeatedly is not expensive. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class CachingSnapshotParser implements SnapshotParser { + + private Map mObjectCache = new HashMap<>(); + private SnapshotParser mInnerParser; + + public CachingSnapshotParser(SnapshotParser innerParser) { + mInnerParser = innerParser; + } + + @Override + public T parseSnapshot(DocumentSnapshot snapshot) { + String id = snapshot.getId(); + if (mObjectCache.containsKey(id)) { + return mObjectCache.get(id); + } else { + T object = mInnerParser.parseSnapshot(snapshot); + mObjectCache.put(id, object); + return object; + } + } + + /** + * Clear all data in the cache. + */ + public void clearData() { + mObjectCache.clear(); + } + + /** + * Invalidate the cache for a certain document ID. + */ + public void invalidate(String id) { + mObjectCache.remove(id); + } + +} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 83c004f79..bc0ae5356 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -1,6 +1,5 @@ package com.firebase.ui.firestore; -import android.support.annotation.NonNull; import android.util.Log; import com.firebase.ui.database.BaseObservableSnapshotArray; @@ -28,6 +27,7 @@ public class FirestoreArray private ListenerRegistration mRegistration; private List mSnapshots; + private CachingSnapshotParser mCache; public FirestoreArray(Query query, final Class modelClass) { this(query, new SnapshotParser() { @@ -38,36 +38,29 @@ public T parseSnapshot(DocumentSnapshot snapshot) { }); } - // TODO: What about caching? - // TODO: What about intermediate OSA class? public FirestoreArray(Query query, SnapshotParser parser) { - super(parser); + super(new CachingSnapshotParser<>(parser)); mQuery = query; mSnapshots = new ArrayList<>(); - } - - public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { - boolean wasListening = isListening(); - super.addChangeEventListener(listener); - // Only start listening once we've added our first listener - if (!wasListening) { - startListening(); - } + // TODO(samstern): This smells... + mCache = (CachingSnapshotParser) getSnapshotParser(); + } - // TODO: Look at what observable snapshot array does here. + @Override + protected void onCreate() { + super.onCreate(); - return listener; + startListening(); } - public void removeChangeEventListener(@NonNull ChangeEventListener listener) { - super.removeChangeEventListener(listener); + @Override + protected void onDestroy() { + super.onDestroy(); - // Stop listening when we have no listeners - if (!isListening()) { - stopListening(); - } + stopListening(); + mCache.clearData(); } @Override @@ -81,45 +74,63 @@ public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { // Break down each document event List changes = snapshots.getDocumentChanges(); for (DocumentChange change : changes) { - DocumentSnapshot doc = change.getDocument(); - - DocumentChange.Type changeType = change.getType(); - int oldIndex = change.getOldIndex(); - int newIndex = change.getNewIndex(); - - if (changeType == DocumentChange.Type.MODIFIED) { - if (oldIndex == newIndex) { - Log.d(TAG, "Modified (inplace): " + oldIndex); - - mSnapshots.set(oldIndex, doc); - notifyOnChildChanged(ChangeEventListener.Type.MODIFIED, doc, - oldIndex, newIndex); - } else { - Log.d(TAG, "Modified (moved): " + oldIndex + " --> " + newIndex); - - mSnapshots.remove(oldIndex); - mSnapshots.add(newIndex, doc); - notifyOnChildChanged(ChangeEventListener.Type.MOVED, doc, - oldIndex, newIndex); - } - } else if (changeType == DocumentChange.Type.REMOVED) { - Log.d(TAG, "Removed: " + oldIndex); - - mSnapshots.remove(oldIndex); - notifyOnChildChanged(ChangeEventListener.Type.REMOVED, doc, - oldIndex, -1); - } else if (changeType == DocumentChange.Type.ADDED) { - Log.d(TAG, "Added: " + newIndex); - - mSnapshots.add(newIndex, doc); - notifyOnChildChanged(ChangeEventListener.Type.ADDED, doc, - -1, newIndex); + switch (change.getType()) { + case ADDED: + onDocumentAdded(change); + break; + case REMOVED: + onDocumentRemoved(change); + break; + case MODIFIED: + onDocumentModified(change); + break; } } notifyOnDataChanged(); } + private void onDocumentAdded(DocumentChange change) { + Log.d(TAG, "Added: " + change.getNewIndex()); + + // Add the document to the set + mSnapshots.add(change.getNewIndex(), change.getDocument()); + notifyOnChildChanged(ChangeEventListener.Type.ADDED, change.getDocument(), + -1, change.getNewIndex()); + } + + private void onDocumentRemoved(DocumentChange change) { + Log.d(TAG, "Removed: " + change.getOldIndex()); + + // Invalidate snapshot cache (doc removed) + mCache.invalidate(change.getDocument().getId()); + + // Remove the document from the set + mSnapshots.remove(change.getOldIndex()); + notifyOnChildChanged(ChangeEventListener.Type.REMOVED, change.getDocument(), + change.getOldIndex(), -1); + } + + private void onDocumentModified(DocumentChange change) { + // Invalidate snapshot cache (doc changed) + mCache.invalidate(change.getDocument().getId()); + + // Decide if the object was modified in place or if it moved + if (change.getOldIndex() == change.getNewIndex()) { + Log.d(TAG, "Modified (inplace): " + change.getOldIndex()); + + mSnapshots.set(change.getOldIndex(), change.getDocument()); + notifyOnChildChanged(ChangeEventListener.Type.MODIFIED, change.getDocument(), + change.getOldIndex(), change.getNewIndex()); + } else { + Log.d(TAG, "Modified (moved): " + change.getOldIndex() + " --> " + change.getNewIndex()); + + mSnapshots.remove(change.getOldIndex()); + mSnapshots.add(change.getNewIndex(), change.getDocument()); + notifyOnChildChanged(ChangeEventListener.Type.MOVED, change.getDocument(), + change.getOldIndex(), change.getNewIndex()); + } + } @Override protected List getSnapshots() { @@ -127,6 +138,11 @@ protected List getSnapshots() { } private void startListening() { + if (mRegistration != null) { + Log.d(TAG, "startListening: already listening."); + return; + } + mRegistration = mQuery.addSnapshotListener(this); } From 4ac2819bdc6f8349cdb0514e323d97671835620c Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 4 Aug 2017 15:02:14 -0700 Subject: [PATCH 04/37] Cleanup some noise Change-Id: Ic512aefa085a82e6335605bba999560e53c03251 --- common/proguard-rules.pro | 25 ------------------- common/src/main/res/values/strings.xml | 3 --- constants.gradle | 2 +- .../firebase/ui/database/FirebaseArray.java | 3 +-- firestore/.gitignore | 1 - firestore/proguard-rules.pro | 25 ------------------- firestore/src/main/res/values/strings.xml | 3 --- 7 files changed, 2 insertions(+), 60 deletions(-) delete mode 100644 common/proguard-rules.pro delete mode 100644 common/src/main/res/values/strings.xml delete mode 100644 firestore/.gitignore delete mode 100644 firestore/proguard-rules.pro delete mode 100644 firestore/src/main/res/values/strings.xml diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro deleted file mode 100644 index e39333687..000000000 --- a/common/proguard-rules.pro +++ /dev/null @@ -1,25 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml deleted file mode 100644 index 9e44af55b..000000000 --- a/common/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - common - diff --git a/constants.gradle b/constants.gradle index 6c496e615..7850635e2 100644 --- a/constants.gradle +++ b/constants.gradle @@ -1,5 +1,5 @@ project.ext { - submodules = ['database', 'auth', 'storage'] + submodules = ['database', 'auth', 'storage', 'firestore'] group = 'com.firebaseui' version = '2.2.0' pomdesc = 'Firebase UI Android' diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 1ae581ebe..9a1a102ac 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -26,8 +26,7 @@ /** * This class implements a collection on top of a Firebase location. */ -public class FirebaseArray extends CachingObservableSnapshotArray - implements ChildEventListener, ValueEventListener { +public class FirebaseArray extends CachingObservableSnapshotArray implements ChildEventListener, ValueEventListener { private Query mQuery; private List mSnapshots = new ArrayList<>(); diff --git a/firestore/.gitignore b/firestore/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/firestore/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/firestore/proguard-rules.pro b/firestore/proguard-rules.pro deleted file mode 100644 index e39333687..000000000 --- a/firestore/proguard-rules.pro +++ /dev/null @@ -1,25 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/firestore/src/main/res/values/strings.xml b/firestore/src/main/res/values/strings.xml deleted file mode 100644 index 3d5b6e627..000000000 --- a/firestore/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - firestore - From 5ddf442818916325a691960fed0373c220b17868 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 4 Aug 2017 17:04:52 -0700 Subject: [PATCH 05/37] Some more fiddling Change-Id: Ie28d94d6ffa7bdbadd8e72a064538a3bf28dfc05 --- .../ui/common/BaseCachingSnapshotParser.java | 54 +++++++++++++++++ .../BaseObservableSnapshotArray.java | 58 ++++++++++++------- .../BaseSnapshotParser.java | 5 +- .../{database => common}/Preconditions.java | 4 +- .../CachingObservableSnapshotArray.java | 30 ++++------ .../ui/database/CachingSnapshotParser.java | 20 +++++++ .../ui/database/ChangeEventListener.java | 1 + .../ui/database/ClassSnapshotParser.java | 1 + .../ui/database/ObservableSnapshotArray.java | 8 +++ .../firebase/ui/database/SnapshotParser.java | 1 + .../ui/firestore/CachingSnapshotParser.java | 46 +++------------ .../ui/firestore/ChangeEventListener.java | 2 +- .../firebase/ui/firestore/FirestoreArray.java | 8 +-- .../firebase/ui/firestore/SnapshotParser.java | 2 +- 14 files changed, 154 insertions(+), 86 deletions(-) create mode 100644 common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java rename common/src/main/java/com/firebase/ui/{database => common}/BaseObservableSnapshotArray.java (78%) rename common/src/main/java/com/firebase/ui/{database => common}/BaseSnapshotParser.java (89%) rename common/src/main/java/com/firebase/ui/{database => common}/Preconditions.java (83%) create mode 100644 database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java diff --git a/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java new file mode 100644 index 000000000..0396dcf61 --- /dev/null +++ b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java @@ -0,0 +1,54 @@ +package com.firebase.ui.common; + +import android.support.annotation.RestrictTo; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of {@link BaseSnapshotParser} that caches results, + * so parsing a snapshot repeatedly is not expensive. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public abstract class BaseCachingSnapshotParser implements BaseSnapshotParser { + + private Map mObjectCache = new HashMap<>(); + private BaseSnapshotParser mInnerParser; + + // TODO(samstern): Can probably use this in RTDB module as well, CachingOSA is not necessary. + public BaseCachingSnapshotParser(BaseSnapshotParser innerParser) { + mInnerParser = innerParser; + } + + /** + * Get a unique identifier for a snapshot, should not depend on snapshot content. + */ + public abstract String getId(S snapshot); + + @Override + public T parseSnapshot(S snapshot) { + String id = getId(snapshot); + if (mObjectCache.containsKey(id)) { + return mObjectCache.get(id); + } else { + T object = mInnerParser.parseSnapshot(snapshot); + mObjectCache.put(id, object); + return object; + } + } + + /** + * Clear all data in the cache. + */ + public void clearData() { + mObjectCache.clear(); + } + + /** + * Invalidate the cache for a certain document ID. + */ + public void invalidate(String id) { + mObjectCache.remove(id); + } + +} diff --git a/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java similarity index 78% rename from common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java rename to common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index 8083b66ce..ceca4d8bf 100644 --- a/common/src/main/java/com/firebase/ui/database/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -1,4 +1,4 @@ -package com.firebase.ui.database; +package com.firebase.ui.common; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; @@ -8,7 +8,9 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * TODO + * Exposes a collection of {@link S} items in a database as a {@link List} of {@link E} objects. + * To observe the list attach a {@link L} listener. + * * @param the snapshot class. * @param the listener class. * @param the model object class. @@ -16,10 +18,16 @@ public abstract class BaseObservableSnapshotArray extends AbstractList { protected final List mListeners = new CopyOnWriteArrayList<>(); - protected final BaseSnapshotParser mParser; + protected BaseSnapshotParser mParser; private boolean mHasDataChanged = false; + /** + * Default constructor. Must set the {@link BaseSnapshotParser} before the first operation + * or an exception will be thrown. + */ + public BaseObservableSnapshotArray() {} + /** * Create an BaseObservableSnapshotArray with a custom {@link BaseSnapshotParser}. * @@ -48,7 +56,7 @@ protected void onDestroy() { } /** - * TODO + * Attach a listener to this array. */ @CallSuper public L addChangeEventListener(@NonNull L listener) { @@ -59,7 +67,8 @@ public L addChangeEventListener(@NonNull L listener) { } /** - * TODO + * Remove a listener from the array. If no listeners remain, {@link #onDestroy()} + * will be called. */ @CallSuper public void removeChangeEventListener(@NonNull L listener) { @@ -72,7 +81,7 @@ public void removeChangeEventListener(@NonNull L listener) { } /** - * TODO + * Remove all listeners from the array. */ @CallSuper public void removeAllListeners() { @@ -81,20 +90,6 @@ public void removeAllListeners() { } } - protected abstract List getSnapshots(); - - protected boolean hasDataChanged() { - return mHasDataChanged; - } - - protected void setHasDataChanged(boolean hasDataChanged) { - mHasDataChanged = hasDataChanged; - } - - protected BaseSnapshotParser getSnapshotParser() { - return mParser; - } - /** * @return true if the array is listening for change events from the Firebase * database, false otherwise @@ -116,6 +111,10 @@ public final boolean isListening(L listener) { * initialized this will throw an unchecked exception. */ public E getObject(int index) { + if (mParser == null) { + throw new IllegalStateException("getObject() called before snapshot parser set."); + } + return mParser.parseSnapshot(get(index)); } @@ -128,4 +127,23 @@ public S get(int index) { public int size() { return getSnapshots().size(); } + + // TODO(samstern): Do we need this? + protected abstract List getSnapshots(); + + protected boolean hasDataChanged() { + return mHasDataChanged; + } + + protected void setHasDataChanged(boolean hasDataChanged) { + mHasDataChanged = hasDataChanged; + } + + protected BaseSnapshotParser getSnapshotParser() { + return mParser; + } + + protected void setSnapshotParser(BaseSnapshotParser parser) { + mParser = parser; + } } diff --git a/common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java b/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java similarity index 89% rename from common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java rename to common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java index c73fc555a..f32675ffb 100644 --- a/common/src/main/java/com/firebase/ui/database/BaseSnapshotParser.java +++ b/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java @@ -1,6 +1,8 @@ -package com.firebase.ui.database; +package com.firebase.ui.common; + public interface BaseSnapshotParser { + /** * This method parses the Snapshot into the requested type. * @@ -8,4 +10,5 @@ public interface BaseSnapshotParser { * @return the model extracted from the DataSnapshot */ T parseSnapshot(S snapshot); + } diff --git a/common/src/main/java/com/firebase/ui/database/Preconditions.java b/common/src/main/java/com/firebase/ui/common/Preconditions.java similarity index 83% rename from common/src/main/java/com/firebase/ui/database/Preconditions.java rename to common/src/main/java/com/firebase/ui/common/Preconditions.java index 0fb15b2fa..24bed48e7 100644 --- a/common/src/main/java/com/firebase/ui/database/Preconditions.java +++ b/common/src/main/java/com/firebase/ui/common/Preconditions.java @@ -1,4 +1,4 @@ -package com.firebase.ui.database; +package com.firebase.ui.common; import android.support.annotation.RestrictTo; @@ -6,7 +6,7 @@ * Convenience class for checking argument conditions. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class Preconditions { +public class Preconditions { public static T checkNotNull(T o) { if (o == null) throw new IllegalArgumentException("Argument cannot be null."); return o; diff --git a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java index 9f376e0fa..e30016394 100644 --- a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java @@ -4,43 +4,35 @@ import com.google.firebase.database.DataSnapshot; -import java.util.HashMap; -import java.util.Map; - /** * An extension of {@link ObservableSnapshotArray} that caches the result of {@link #getObject(int)} * so that repeated calls for the same key are not expensive (unless the underlying snapshot has * changed). */ public abstract class CachingObservableSnapshotArray extends ObservableSnapshotArray { - private Map mObjectCache = new HashMap<>(); + + private final CachingSnapshotParser mCache; /** * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) */ public CachingObservableSnapshotArray(@NonNull Class tClass) { - super(tClass); + this(new ClassSnapshotParser<>(tClass)); } /** * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) */ public CachingObservableSnapshotArray(@NonNull SnapshotParser parser) { - super(parser); + super(); + + mCache = new CachingSnapshotParser<>(parser); + setSnapshotParser(mCache); } @Override public T getObject(int index) { - String key = get(index).getKey(); - - // Return from the cache if possible, otherwise populate the cache and return - if (mObjectCache.containsKey(key)) { - return mObjectCache.get(key); - } else { - T object = super.getObject(index); - mObjectCache.put(key, object); - return object; - } + return mCache.parseSnapshot(get(index)); } @Override @@ -53,13 +45,13 @@ protected void onDestroy() { @Deprecated protected void clearData() { getSnapshots().clear(); - mObjectCache.clear(); + mCache.clearData(); } protected DataSnapshot removeData(int index) { DataSnapshot snapshot = getSnapshots().remove(index); if (snapshot != null) { - mObjectCache.remove(snapshot.getKey()); + mCache.invalidate(snapshot.getKey()); } return snapshot; @@ -67,6 +59,6 @@ protected DataSnapshot removeData(int index) { protected void updateData(int index, DataSnapshot snapshot) { getSnapshots().set(index, snapshot); - mObjectCache.remove(snapshot.getKey()); + mCache.invalidate(snapshot.getKey()); } } diff --git a/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java b/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java new file mode 100644 index 000000000..80164a840 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java @@ -0,0 +1,20 @@ +package com.firebase.ui.database; + +import com.firebase.ui.common.BaseCachingSnapshotParser; +import com.firebase.ui.common.BaseSnapshotParser; +import com.google.firebase.database.DataSnapshot; + +/** + * Implementation of {@link BaseCachingSnapshotParser} for {@link DataSnapshot}. + */ +public class CachingSnapshotParser extends BaseCachingSnapshotParser { + + public CachingSnapshotParser(BaseSnapshotParser innerParser) { + super(innerParser); + } + + @Override + public String getId(DataSnapshot snapshot) { + return snapshot.getKey(); + } +} diff --git a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java index 055fe5bce..9ded9ee68 100644 --- a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java +++ b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java @@ -5,6 +5,7 @@ import com.google.firebase.database.DatabaseError; public interface ChangeEventListener { + /** * The type of event received when a child has been updated. */ diff --git a/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java index 215a78972..aaf28f378 100644 --- a/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java +++ b/database/src/main/java/com/firebase/ui/database/ClassSnapshotParser.java @@ -2,6 +2,7 @@ import android.support.annotation.NonNull; +import com.firebase.ui.common.Preconditions; import com.google.firebase.database.DataSnapshot; /** diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 70137af96..417190585 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -3,6 +3,7 @@ import android.support.annotation.CallSuper; import android.support.annotation.NonNull; +import com.firebase.ui.common.BaseObservableSnapshotArray; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -17,6 +18,13 @@ public abstract class ObservableSnapshotArray extends BaseObservableSnapshotArray { + /** + * Default constructor. Must set the snapshot parser before user. + */ + public ObservableSnapshotArray() { + super(); + } + /** * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular * class. diff --git a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java index aff3bd5e6..049039fec 100644 --- a/database/src/main/java/com/firebase/ui/database/SnapshotParser.java +++ b/database/src/main/java/com/firebase/ui/database/SnapshotParser.java @@ -1,5 +1,6 @@ package com.firebase.ui.database; +import com.firebase.ui.common.BaseSnapshotParser; import com.google.firebase.database.DataSnapshot; public interface SnapshotParser extends BaseSnapshotParser {} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java index 52f123b10..650f91a40 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java @@ -1,50 +1,20 @@ package com.firebase.ui.firestore; -import android.support.annotation.RestrictTo; - +import com.firebase.ui.common.BaseCachingSnapshotParser; +import com.firebase.ui.common.BaseSnapshotParser; import com.google.firebase.firestore.DocumentSnapshot; -import java.util.HashMap; -import java.util.Map; - /** - * Implementation of {@link SnapshotParser} that caches results, - * so parsing a snapshot repeatedly is not expensive. + * Implementation of {@link BaseCachingSnapshotParser} for {@link DocumentSnapshot}. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CachingSnapshotParser implements SnapshotParser { +public class CachingSnapshotParser extends BaseCachingSnapshotParser { - private Map mObjectCache = new HashMap<>(); - private SnapshotParser mInnerParser; - - public CachingSnapshotParser(SnapshotParser innerParser) { - mInnerParser = innerParser; + public CachingSnapshotParser(BaseSnapshotParser innerParser) { + super(innerParser); } @Override - public T parseSnapshot(DocumentSnapshot snapshot) { - String id = snapshot.getId(); - if (mObjectCache.containsKey(id)) { - return mObjectCache.get(id); - } else { - T object = mInnerParser.parseSnapshot(snapshot); - mObjectCache.put(id, object); - return object; - } - } - - /** - * Clear all data in the cache. - */ - public void clearData() { - mObjectCache.clear(); - } - - /** - * Invalidate the cache for a certain document ID. - */ - public void invalidate(String id) { - mObjectCache.remove(id); + public String getId(DocumentSnapshot snapshot) { + return snapshot.getId(); } - } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java index c7cdbdae1..d218cf404 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java @@ -4,7 +4,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException; /** - * TODO + * TODO: This could be a common interface, it just needs to know about the snapshot and error types. */ public interface ChangeEventListener { diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index bc0ae5356..0c7412867 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -2,7 +2,7 @@ import android.util.Log; -import com.firebase.ui.database.BaseObservableSnapshotArray; +import com.firebase.ui.common.BaseObservableSnapshotArray; import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; @@ -39,13 +39,13 @@ public T parseSnapshot(DocumentSnapshot snapshot) { } public FirestoreArray(Query query, SnapshotParser parser) { - super(new CachingSnapshotParser<>(parser)); + super(); mQuery = query; mSnapshots = new ArrayList<>(); + mCache = new CachingSnapshotParser<>(parser); - // TODO(samstern): This smells... - mCache = (CachingSnapshotParser) getSnapshotParser(); + setSnapshotParser(mCache); } @Override diff --git a/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java index 7a2bd9378..ab80bbe2d 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java @@ -1,6 +1,6 @@ package com.firebase.ui.firestore; -import com.firebase.ui.database.BaseSnapshotParser; +import com.firebase.ui.common.BaseSnapshotParser; import com.google.firebase.firestore.DocumentSnapshot; /** From f9d8aba96dcaa9bf4ce8af42f61283b58d66d84c Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 7 Aug 2017 12:25:49 -0700 Subject: [PATCH 06/37] Add some comments, switch the order of indexes in events. Change-Id: I27a584c7fec5ef89feb4a0e171a24e8babac99ea --- .../ui/common/BaseSnapshotParser.java | 7 +++- .../ui/firestore/ChangeEventListener.java | 41 ++++++++++++++++++- .../firebase/ui/firestore/FirestoreArray.java | 15 ++++--- .../firestore/FirestoreRecyclerAdapter.java | 2 +- .../firebase/ui/firestore/SnapshotParser.java | 6 +-- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java b/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java index f32675ffb..fa54fab45 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java +++ b/common/src/main/java/com/firebase/ui/common/BaseSnapshotParser.java @@ -1,6 +1,11 @@ package com.firebase.ui.common; - +/** + * Common interface for snapshot parsers. + * + * @param snapshot type. + * @param parsed object type. + */ public interface BaseSnapshotParser { /** diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java index d218cf404..fd0596f5a 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java @@ -4,22 +4,61 @@ import com.google.firebase.firestore.FirebaseFirestoreException; /** + * Listener for changes to a {@link FirestoreArray}. * TODO: This could be a common interface, it just needs to know about the snapshot and error types. */ public interface ChangeEventListener { + /** + * The type of change to an element of the array, + */ enum Type { + + /** + * An element was added to the array. + */ ADDED, + + /** + * An element was removed from the array. + */ REMOVED, + + /** + * An element in the array has new content. + */ MODIFIED, + + /** + * An element in the array has a new position, and also new content. + */ MOVED } + /** + * A callback for when a child event occurs in a FirestoreArray. + * @param type The type of the event. + * @param snapshot The {@link DocumentSnapshot} of the changed child. + * @param newIndex The new index of the element, or -1 of it is no longer + * @param oldIndex The previous index of the element, or -1 if it was not +* previously tracked. + */ void onChildChanged(Type type, DocumentSnapshot snapshot, - int oldIndex, int newIndex); + int newIndex, int oldIndex); + /** + * Callback triggered after all child events in a particular snapshot have been + * processed. + *

+ * Useful for batch events, such as removing a loading indicator after initial load + * or a large update batch. + */ void onDataChanged(); + /** + * Callback when an error has been detected in the underlying Firestore query listener. + * @param e the error that occurred. + */ void onError(FirebaseFirestoreException e); } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 0c7412867..e887b8638 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -15,7 +15,7 @@ import java.util.List; /** - * TODO(samstern): Document + * Exposes a Firestore query as an observable list of objects. */ public class FirestoreArray extends BaseObservableSnapshotArray @@ -96,7 +96,7 @@ private void onDocumentAdded(DocumentChange change) { // Add the document to the set mSnapshots.add(change.getNewIndex(), change.getDocument()); notifyOnChildChanged(ChangeEventListener.Type.ADDED, change.getDocument(), - -1, change.getNewIndex()); + change.getNewIndex(), -1); } private void onDocumentRemoved(DocumentChange change) { @@ -108,7 +108,7 @@ private void onDocumentRemoved(DocumentChange change) { // Remove the document from the set mSnapshots.remove(change.getOldIndex()); notifyOnChildChanged(ChangeEventListener.Type.REMOVED, change.getDocument(), - change.getOldIndex(), -1); + -1, change.getOldIndex()); } private void onDocumentModified(DocumentChange change) { @@ -121,14 +121,14 @@ private void onDocumentModified(DocumentChange change) { mSnapshots.set(change.getOldIndex(), change.getDocument()); notifyOnChildChanged(ChangeEventListener.Type.MODIFIED, change.getDocument(), - change.getOldIndex(), change.getNewIndex()); + change.getNewIndex(), change.getOldIndex()); } else { Log.d(TAG, "Modified (moved): " + change.getOldIndex() + " --> " + change.getNewIndex()); mSnapshots.remove(change.getOldIndex()); mSnapshots.add(change.getNewIndex(), change.getDocument()); notifyOnChildChanged(ChangeEventListener.Type.MOVED, change.getDocument(), - change.getOldIndex(), change.getNewIndex()); + change.getNewIndex(), change.getOldIndex()); } } @@ -157,11 +157,10 @@ private void stopListening() { private void notifyOnChildChanged(ChangeEventListener.Type type, DocumentSnapshot snapshot, - int oldIndex, - int newIndex) { + int newIndex, int oldIndex) { for (ChangeEventListener listener : mListeners) { - listener.onChildChanged(type, snapshot, oldIndex, newIndex); + listener.onChildChanged(type, snapshot, newIndex, oldIndex); } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index 65535a546..e33c53a7e 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -41,7 +41,7 @@ public int getItemCount() { @Override public void onChildChanged(Type type, DocumentSnapshot snapshot, - int oldIndex, int newIndex) { + int newIndex, int oldIndex) { switch (type) { case ADDED: diff --git a/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java index ab80bbe2d..6f537af1f 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/SnapshotParser.java @@ -4,8 +4,6 @@ import com.google.firebase.firestore.DocumentSnapshot; /** - * TODO + * Base interface for a {@link BaseSnapshotParser} for {@link DocumentSnapshot}. */ -public interface SnapshotParser extends BaseSnapshotParser { - -} +public interface SnapshotParser extends BaseSnapshotParser {} From d8dbf874f9f574a20678713128010f638b2172f6 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Mon, 7 Aug 2017 13:39:47 -0700 Subject: [PATCH 07/37] Unify change event listeners Change-Id: Ie40689d23e77b8885d369856b86479252382946a --- .../firestore/FirestoreChatActivity.java | 2 +- .../ui/common/BaseChangeEventListener.java | 35 ++++++++++ .../common/BaseObservableSnapshotArray.java | 25 ++++++- .../firebase/ui/common/ChangeEventType.java | 28 ++++++++ .../com/firebase/ui/database/TestUtils.java | 5 +- .../ui/database/ChangeEventListener.java | 69 ++----------------- .../firebase/ui/database/FirebaseArray.java | 9 +-- .../ui/database/FirebaseIndexArray.java | 15 ++-- .../ui/database/FirebaseListAdapter.java | 5 +- .../ui/database/FirebaseRecyclerAdapter.java | 5 +- .../ui/database/ObservableSnapshotArray.java | 32 ++++----- .../ui/firestore/ChangeEventListener.java | 58 +--------------- .../firebase/ui/firestore/FirestoreArray.java | 32 ++++++--- .../firestore/FirestoreRecyclerAdapter.java | 19 +++-- 14 files changed, 167 insertions(+), 172 deletions(-) create mode 100644 common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java create mode 100644 common/src/main/java/com/firebase/ui/common/ChangeEventType.java diff --git a/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java index a2c62a3aa..21ba09da8 100644 --- a/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/firestore/FirestoreChatActivity.java @@ -30,7 +30,7 @@ import butterknife.OnClick; /** - * TODO + * Class demonstrating a simple real-time chat app that relies on {@link FirestoreRecyclerAdapter}. */ public class FirestoreChatActivity extends AppCompatActivity implements FirebaseAuth.AuthStateListener { diff --git a/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java new file mode 100644 index 000000000..47d2cba77 --- /dev/null +++ b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java @@ -0,0 +1,35 @@ +package com.firebase.ui.common; + +/** + * TODO(samstern): Document + */ +public interface BaseChangeEventListener { + + /** + * A callback for when a child event occurs. + * + * @param type The type of the event. + * @param snapshot The snapshot of the changed child. + * @param newIndex The new index of the element, or -1 of it is no longer present + * @param oldIndex The previous index of the element, or -1 if it was not + * previously tracked. + */ + void onChildChanged(ChangeEventType type, S snapshot, + int newIndex, int oldIndex); + + /** + * Callback triggered after all child events in a particular snapshot have been + * processed. + *

+ * Useful for batch events, such as removing a loading indicator after initial load + * or a large update batch. + */ + void onDataChanged(); + + /** + * Callback when an error has been detected in the underlying listener. + * @param e the error that occurred. + */ + void onError(E e); + +} diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index ceca4d8bf..69ed76812 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -17,8 +17,8 @@ */ public abstract class BaseObservableSnapshotArray extends AbstractList { - protected final List mListeners = new CopyOnWriteArrayList<>(); - protected BaseSnapshotParser mParser; + private final List mListeners = new CopyOnWriteArrayList<>(); + private BaseSnapshotParser mParser; private boolean mHasDataChanged = false; @@ -44,6 +44,13 @@ public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { @CallSuper protected void onCreate() {} + /** + * Called when a new listener has been added to the array. This is a good time to pass initial + * state and fire backlogged events + * @param listener the added listener. + */ + protected void onListenerAdded(L listener) {}; + /** * Called when the {@link BaseObservableSnapshotArray} is inactive and should stop listening to the * Firebase database. @@ -61,7 +68,14 @@ protected void onDestroy() { @CallSuper public L addChangeEventListener(@NonNull L listener) { Preconditions.checkNotNull(listener); + boolean wasListening = isListening(); + mListeners.add(listener); + onListenerAdded(listener); + + if (!wasListening) { + onCreate(); + } return listener; } @@ -90,6 +104,13 @@ public void removeAllListeners() { } } + /** + * Get all active listeners. + */ + public List getListeners() { + return mListeners; + } + /** * @return true if the array is listening for change events from the Firebase * database, false otherwise diff --git a/common/src/main/java/com/firebase/ui/common/ChangeEventType.java b/common/src/main/java/com/firebase/ui/common/ChangeEventType.java new file mode 100644 index 000000000..a246319e3 --- /dev/null +++ b/common/src/main/java/com/firebase/ui/common/ChangeEventType.java @@ -0,0 +1,28 @@ +package com.firebase.ui.common; + +/** + * Enumeration of change event types for children of an observable snapshot array. + */ +public enum ChangeEventType { + + /** + * An element was added to the array. + */ + ADDED, + + /** + * An element was removed from the array. + */ + REMOVED, + + /** + * An element in the array has new content. + */ + CHANGED, + + /** + * An element in the array has new content, which caused a change in position. + */ + MOVED + +} diff --git a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java index 29da0b4d0..1266d119f 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java +++ b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java @@ -2,6 +2,7 @@ import android.content.Context; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.database.DataSnapshot; @@ -40,7 +41,7 @@ public static ChangeEventListener runAndWaitUntil(ObservableSnapshotArray array, final Semaphore semaphore = new Semaphore(0); ChangeEventListener listener = array.addChangeEventListener(new ChangeEventListener() { @Override - public void onChildChanged(ChangeEventListener.EventType type, + public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { @@ -52,7 +53,7 @@ public void onDataChanged() { } @Override - public void onCancelled(DatabaseError error) { + public void onError(DatabaseError error) { throw new IllegalStateException(error.toException()); } }); diff --git a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java index 9ded9ee68..95ddbafc9 100644 --- a/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java +++ b/database/src/main/java/com/firebase/ui/database/ChangeEventListener.java @@ -1,69 +1,10 @@ package com.firebase.ui.database; -import com.google.firebase.database.ChildEventListener; +import com.firebase.ui.common.BaseChangeEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; -public interface ChangeEventListener { - - /** - * The type of event received when a child has been updated. - */ - enum EventType { - /** - * An onChildAdded event was received. - * - * @see ChildEventListener#onChildAdded(DataSnapshot, String) - */ - ADDED, - /** - * An onChildChanged event was received. - * - * @see ChildEventListener#onChildChanged(DataSnapshot, String) - */ - CHANGED, - /** - * An onChildRemoved event was received. - * - * @see ChildEventListener#onChildRemoved(DataSnapshot) - */ - REMOVED, - /** - * An onChildMoved event was received. - * - * @see ChildEventListener#onChildMoved(DataSnapshot, String) - */ - MOVED - } - - /** - * A callback for when a child has changed in FirebaseArray. - * - * @param type The type of event received - * @param snapshot the {@link DataSnapshot} of the changed child. - * @param index The index at which the change occurred - * @param oldIndex If {@code type} is a moved event, the previous index of the moved child. For - * any other event, {@code oldIndex} will be -1. - */ - void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex); - - /** - * This method will be triggered each time updates from the database have been completely - * processed. So the first time this method is called, the initial data has been loaded - - * including the case when no data at all is available. Each next time the method is called, a - * complete update (potentially consisting of updates to multiple child items) has been - * completed. - *

- * You would typically override this method to hide a loading indicator (after the initial load) - * or to complete a batch update to a UI element. - */ - void onDataChanged(); - - /** - * This method will be triggered in the event that this listener either failed at the server, or - * is removed as a result of the security and Firebase Database rules. - * - * @param error A description of the error that occurred - */ - void onCancelled(DatabaseError error); -} +/** + * Listener for changes to {@link FirebaseArray}. + */ +public interface ChangeEventListener extends BaseChangeEventListener {} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 9a1a102ac..4d29d4685 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -14,6 +14,7 @@ package com.firebase.ui.database; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -87,7 +88,7 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildKey) { mSnapshots.add(index, snapshot); - notifyChangeEventListeners(ChangeEventListener.EventType.ADDED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.ADDED, snapshot, index); } @Override @@ -95,7 +96,7 @@ public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { int index = getIndexForKey(snapshot.getKey()); updateData(index, snapshot); - notifyChangeEventListeners(ChangeEventListener.EventType.CHANGED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.CHANGED, snapshot, index); } @Override @@ -103,7 +104,7 @@ public void onChildRemoved(DataSnapshot snapshot) { int index = getIndexForKey(snapshot.getKey()); removeData(index); - notifyChangeEventListeners(ChangeEventListener.EventType.REMOVED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); } @Override @@ -114,7 +115,7 @@ public void onChildMoved(DataSnapshot snapshot, String previousChildKey) { int newIndex = previousChildKey == null ? 0 : (getIndexForKey(previousChildKey) + 1); mSnapshots.add(newIndex, snapshot); - notifyChangeEventListeners(ChangeEventListener.EventType.MOVED, + notifyChangeEventListeners(ChangeEventType.MOVED, snapshot, newIndex, oldIndex); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 8a104c488..940168777 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -17,6 +17,7 @@ import android.support.v7.widget.RecyclerView; import android.util.Log; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -104,7 +105,7 @@ protected void onDestroy() { } @Override - public void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex) { + public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { switch (type) { case ADDED: onKeyAdded(snapshot); @@ -131,7 +132,7 @@ public void onDataChanged() { } @Override - public void onCancelled(DatabaseError error) { + public void onError(DatabaseError error) { Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); } @@ -177,7 +178,7 @@ protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { DataSnapshot snapshot = removeData(oldIndex); mHasPendingMoveOrDelete = true; mDataSnapshots.add(index, snapshot); - notifyChangeEventListeners(EventType.MOVED, snapshot, index, oldIndex); + notifyChangeEventListeners(ChangeEventType.MOVED, snapshot, index, oldIndex); } } @@ -189,7 +190,7 @@ protected void onKeyRemoved(DataSnapshot data, int index) { if (isKeyAtIndex(key, index)) { DataSnapshot snapshot = removeData(index); mHasPendingMoveOrDelete = true; - notifyChangeEventListeners(EventType.REMOVED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); } } @@ -234,12 +235,12 @@ public void onDataChange(DataSnapshot snapshot) { if (isKeyAtIndex(key, index)) { // We already know about this data, just update it updateData(index, snapshot); - notifyChangeEventListeners(EventType.CHANGED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.CHANGED, snapshot, index); notifyListenersOnDataChanged(); } else { // We don't already know about this data, add it mDataSnapshots.add(index, snapshot); - notifyChangeEventListeners(EventType.ADDED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.ADDED, snapshot, index); mKeysWithPendingData.remove(key); if (mKeysWithPendingData.isEmpty()) notifyListenersOnDataChanged(); @@ -248,7 +249,7 @@ public void onDataChange(DataSnapshot snapshot) { if (isKeyAtIndex(key, index)) { // This data has disappeared, remove it removeData(index); - notifyChangeEventListeners(EventType.REMOVED, snapshot, index); + notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); notifyListenersOnDataChanged(); } else { // Data does not exist diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java index a4e46a57b..afc30ade6 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java @@ -14,6 +14,7 @@ import android.widget.BaseAdapter; import android.widget.ListView; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -140,7 +141,7 @@ void cleanup(LifecycleOwner source, Lifecycle.Event event) { } @Override - public void onChildChanged(ChangeEventListener.EventType type, + public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { @@ -152,7 +153,7 @@ public void onDataChanged() { } @Override - public void onCancelled(DatabaseError error) { + public void onError(DatabaseError error) { Log.w(TAG, error.toException()); } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index da5e68ba5..a1e2f0a26 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -11,6 +11,7 @@ import android.view.View; import android.view.ViewGroup; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -144,7 +145,7 @@ void cleanup(LifecycleOwner source, Lifecycle.Event event) { } @Override - public void onChildChanged(ChangeEventListener.EventType type, + public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { @@ -171,7 +172,7 @@ public void onDataChanged() { } @Override - public void onCancelled(DatabaseError error) { + public void onError(DatabaseError error) { Log.w(TAG, error.toException()); } diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 417190585..de34151f0 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import com.firebase.ui.common.BaseObservableSnapshotArray; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -47,55 +48,52 @@ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { /** * Attach a {@link ChangeEventListener} to this array. The listener will receive one {@link - * ChangeEventListener.EventType#ADDED} event for each item that already exists in the array at + * ChangeEventType#ADDED} event for each item that already exists in the array at * the time of attachment, and then receive all future child events. */ @CallSuper public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { - super.addChangeEventListener(listener); - boolean wasListening = isListening(); + return super.addChangeEventListener(listener); + } + + @Override + protected void onListenerAdded(ChangeEventListener listener) { + super.onListenerAdded(listener); - // TODO(samstern): Can some of this be moved into common? - mListeners.add(listener); for (int i = 0; i < size(); i++) { - listener.onChildChanged(ChangeEventListener.EventType.ADDED, get(i), i, -1); + listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); } if (hasDataChanged()) { listener.onDataChanged(); } - - if (!wasListening) { onCreate(); } - - return listener; } - - protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + protected final void notifyChangeEventListeners(ChangeEventType type, DataSnapshot snapshot, int index) { notifyChangeEventListeners(type, snapshot, index, -1); } - protected final void notifyChangeEventListeners(ChangeEventListener.EventType type, + protected final void notifyChangeEventListeners(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { - for (ChangeEventListener listener : mListeners) { + for (ChangeEventListener listener : getListeners()) { listener.onChildChanged(type, snapshot, index, oldIndex); } } protected final void notifyListenersOnDataChanged() { setHasDataChanged(true); - for (ChangeEventListener listener : mListeners) { + for (ChangeEventListener listener : getListeners()) { listener.onDataChanged(); } } protected final void notifyListenersOnCancelled(DatabaseError error) { - for (ChangeEventListener listener : mListeners) { - listener.onCancelled(error); + for (ChangeEventListener listener : getListeners()) { + listener.onError(error); } } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java index fd0596f5a..980167b4e 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ChangeEventListener.java @@ -1,64 +1,10 @@ package com.firebase.ui.firestore; +import com.firebase.ui.common.BaseChangeEventListener; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; /** * Listener for changes to a {@link FirestoreArray}. - * TODO: This could be a common interface, it just needs to know about the snapshot and error types. */ -public interface ChangeEventListener { - - /** - * The type of change to an element of the array, - */ - enum Type { - - /** - * An element was added to the array. - */ - ADDED, - - /** - * An element was removed from the array. - */ - REMOVED, - - /** - * An element in the array has new content. - */ - MODIFIED, - - /** - * An element in the array has a new position, and also new content. - */ - MOVED - } - - /** - * A callback for when a child event occurs in a FirestoreArray. - * @param type The type of the event. - * @param snapshot The {@link DocumentSnapshot} of the changed child. - * @param newIndex The new index of the element, or -1 of it is no longer - * @param oldIndex The previous index of the element, or -1 if it was not -* previously tracked. - */ - void onChildChanged(Type type, DocumentSnapshot snapshot, - int newIndex, int oldIndex); - - /** - * Callback triggered after all child events in a particular snapshot have been - * processed. - *

- * Useful for batch events, such as removing a loading indicator after initial load - * or a large update batch. - */ - void onDataChanged(); - - /** - * Callback when an error has been detected in the underlying Firestore query listener. - * @param e the error that occurred. - */ - void onError(FirebaseFirestoreException e); - -} +public interface ChangeEventListener extends BaseChangeEventListener {} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index e887b8638..6d4617b3f 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -3,6 +3,7 @@ import android.util.Log; import com.firebase.ui.common.BaseObservableSnapshotArray; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.EventListener; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.List; + /** * Exposes a Firestore query as an observable list of objects. */ @@ -63,6 +65,20 @@ protected void onDestroy() { mCache.clearData(); } + @Override + protected void onListenerAdded(ChangeEventListener listener) { + super.onListenerAdded(listener); + + for (int i = 0; i < size(); i++) { + listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); + } + + // TODO(samstern): track hasDataChanged() + if (hasDataChanged()) { + listener.onDataChanged(); + } + } + @Override public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { if (e != null) { @@ -95,7 +111,7 @@ private void onDocumentAdded(DocumentChange change) { // Add the document to the set mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventListener.Type.ADDED, change.getDocument(), + notifyOnChildChanged(ChangeEventType.ADDED, change.getDocument(), change.getNewIndex(), -1); } @@ -107,7 +123,7 @@ private void onDocumentRemoved(DocumentChange change) { // Remove the document from the set mSnapshots.remove(change.getOldIndex()); - notifyOnChildChanged(ChangeEventListener.Type.REMOVED, change.getDocument(), + notifyOnChildChanged(ChangeEventType.REMOVED, change.getDocument(), -1, change.getOldIndex()); } @@ -120,14 +136,14 @@ private void onDocumentModified(DocumentChange change) { Log.d(TAG, "Modified (inplace): " + change.getOldIndex()); mSnapshots.set(change.getOldIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventListener.Type.MODIFIED, change.getDocument(), + notifyOnChildChanged(ChangeEventType.CHANGED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } else { Log.d(TAG, "Modified (moved): " + change.getOldIndex() + " --> " + change.getNewIndex()); mSnapshots.remove(change.getOldIndex()); mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventListener.Type.MOVED, change.getDocument(), + notifyOnChildChanged(ChangeEventType.MOVED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } } @@ -155,23 +171,23 @@ private void stopListening() { mSnapshots.clear(); } - private void notifyOnChildChanged(ChangeEventListener.Type type, + private void notifyOnChildChanged(ChangeEventType type, DocumentSnapshot snapshot, int newIndex, int oldIndex) { - for (ChangeEventListener listener : mListeners) { + for (ChangeEventListener listener : getListeners()) { listener.onChildChanged(type, snapshot, newIndex, oldIndex); } } private void notifyOnError(FirebaseFirestoreException e) { - for (ChangeEventListener listener : mListeners) { + for (ChangeEventListener listener : getListeners()) { listener.onError(e); } } private void notifyOnDataChanged() { - for (ChangeEventListener listener : mListeners) { + for (ChangeEventListener listener : getListeners()) { listener.onDataChanged(); } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index e33c53a7e..3a25bc31b 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -2,20 +2,25 @@ import android.support.v7.widget.RecyclerView; +import com.firebase.ui.common.BaseObservableSnapshotArray; +import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.Query; -// TODO: Document +/** + * RecyclerView adapter that listenes to an {@link FirestoreArray} and displays data in real time, + * + * @param model class, for parsing {@link DocumentSnapshot}. + * @param viewholder class. + */ public abstract class FirestoreRecyclerAdapter extends RecyclerView.Adapter implements ChangeEventListener { - private Class mModelClass; - private FirestoreArray mArray; + private BaseObservableSnapshotArray mArray; public FirestoreRecyclerAdapter(Query query, Class modelClass) { - mModelClass = modelClass; - mArray = new FirestoreArray(query, modelClass); + mArray = new FirestoreArray<>(query, modelClass); } public abstract void onBindViewHolder(VH vh, int i, T model); @@ -40,7 +45,7 @@ public int getItemCount() { } @Override - public void onChildChanged(Type type, DocumentSnapshot snapshot, + public void onChildChanged(ChangeEventType type, DocumentSnapshot snapshot, int newIndex, int oldIndex) { switch (type) { @@ -50,7 +55,7 @@ public void onChildChanged(Type type, DocumentSnapshot snapshot, case REMOVED: notifyItemRemoved(oldIndex); break; - case MODIFIED: + case CHANGED: notifyItemChanged(newIndex); break; case MOVED: From d2fa47e4ed283a40871534f5c40635d12e8b58fc Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 8 Aug 2017 08:45:47 -0700 Subject: [PATCH 08/37] Move more into the base class Change-Id: I5153b73dff61ac0052838980dfb8a61d816bebc5 --- .../common/BaseObservableSnapshotArray.java | 21 ++++++++++++++- .../firebase/ui/database/FirebaseArray.java | 12 ++++----- .../ui/database/FirebaseIndexArray.java | 10 +++---- .../ui/database/ObservableSnapshotArray.java | 24 +---------------- .../firebase/ui/firestore/FirestoreArray.java | 26 +++++-------------- 5 files changed, 37 insertions(+), 56 deletions(-) diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index 69ed76812..e72d9d133 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -15,7 +15,8 @@ * @param the listener class. * @param the model object class. */ -public abstract class BaseObservableSnapshotArray extends AbstractList { +public abstract class BaseObservableSnapshotArray, E> + extends AbstractList { private final List mListeners = new CopyOnWriteArrayList<>(); private BaseSnapshotParser mParser; @@ -149,6 +150,24 @@ public int size() { return getSnapshots().size(); } + protected void notifyListenersOnChildChanged(ChangeEventType type, + S snapshot, + int newIndex, + int oldIndex) { + for (L listener : getListeners()) { + listener.onChildChanged(type, snapshot, newIndex, oldIndex); + } + } + + protected void notifyListenersOnDataChanged() { + mHasDataChanged = true; + + for (L listener : getListeners()) { + listener.onDataChanged(); + } + } + + // TODO(samstern): Do we need this? protected abstract List getSnapshots(); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 4d29d4685..fba38e9c9 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -88,7 +88,7 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildKey) { mSnapshots.add(index, snapshot); - notifyChangeEventListeners(ChangeEventType.ADDED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); } @Override @@ -96,7 +96,7 @@ public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { int index = getIndexForKey(snapshot.getKey()); updateData(index, snapshot); - notifyChangeEventListeners(ChangeEventType.CHANGED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); } @Override @@ -104,7 +104,7 @@ public void onChildRemoved(DataSnapshot snapshot) { int index = getIndexForKey(snapshot.getKey()); removeData(index); - notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); } @Override @@ -115,10 +115,8 @@ public void onChildMoved(DataSnapshot snapshot, String previousChildKey) { int newIndex = previousChildKey == null ? 0 : (getIndexForKey(previousChildKey) + 1); mSnapshots.add(newIndex, snapshot); - notifyChangeEventListeners(ChangeEventType.MOVED, - snapshot, - newIndex, - oldIndex); + notifyListenersOnChildChanged(ChangeEventType.MOVED, snapshot, + newIndex, oldIndex); } @Override diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 940168777..678c650dd 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -178,7 +178,7 @@ protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { DataSnapshot snapshot = removeData(oldIndex); mHasPendingMoveOrDelete = true; mDataSnapshots.add(index, snapshot); - notifyChangeEventListeners(ChangeEventType.MOVED, snapshot, index, oldIndex); + notifyListenersOnChildChanged(ChangeEventType.MOVED, snapshot, index, oldIndex); } } @@ -190,7 +190,7 @@ protected void onKeyRemoved(DataSnapshot data, int index) { if (isKeyAtIndex(key, index)) { DataSnapshot snapshot = removeData(index); mHasPendingMoveOrDelete = true; - notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); } } @@ -235,12 +235,12 @@ public void onDataChange(DataSnapshot snapshot) { if (isKeyAtIndex(key, index)) { // We already know about this data, just update it updateData(index, snapshot); - notifyChangeEventListeners(ChangeEventType.CHANGED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); notifyListenersOnDataChanged(); } else { // We don't already know about this data, add it mDataSnapshots.add(index, snapshot); - notifyChangeEventListeners(ChangeEventType.ADDED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); mKeysWithPendingData.remove(key); if (mKeysWithPendingData.isEmpty()) notifyListenersOnDataChanged(); @@ -249,7 +249,7 @@ public void onDataChange(DataSnapshot snapshot) { if (isKeyAtIndex(key, index)) { // This data has disappeared, remove it removeData(index); - notifyChangeEventListeners(ChangeEventType.REMOVED, snapshot, index); + notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); notifyListenersOnDataChanged(); } else { // Data does not exist diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index de34151f0..18803f874 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -69,29 +69,7 @@ protected void onListenerAdded(ChangeEventListener listener) { } } - protected final void notifyChangeEventListeners(ChangeEventType type, - DataSnapshot snapshot, - int index) { - notifyChangeEventListeners(type, snapshot, index, -1); - } - - protected final void notifyChangeEventListeners(ChangeEventType type, - DataSnapshot snapshot, - int index, - int oldIndex) { - for (ChangeEventListener listener : getListeners()) { - listener.onChildChanged(type, snapshot, index, oldIndex); - } - } - - protected final void notifyListenersOnDataChanged() { - setHasDataChanged(true); - for (ChangeEventListener listener : getListeners()) { - listener.onDataChanged(); - } - } - - protected final void notifyListenersOnCancelled(DatabaseError error) { + protected void notifyListenersOnCancelled(DatabaseError error) { for (ChangeEventListener listener : getListeners()) { listener.onError(error); } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 6d4617b3f..99f85d865 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -103,7 +103,7 @@ public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { } } - notifyOnDataChanged(); + notifyListenersOnDataChanged(); } private void onDocumentAdded(DocumentChange change) { @@ -111,7 +111,7 @@ private void onDocumentAdded(DocumentChange change) { // Add the document to the set mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventType.ADDED, change.getDocument(), + notifyListenersOnChildChanged(ChangeEventType.ADDED, change.getDocument(), change.getNewIndex(), -1); } @@ -123,7 +123,7 @@ private void onDocumentRemoved(DocumentChange change) { // Remove the document from the set mSnapshots.remove(change.getOldIndex()); - notifyOnChildChanged(ChangeEventType.REMOVED, change.getDocument(), + notifyListenersOnChildChanged(ChangeEventType.REMOVED, change.getDocument(), -1, change.getOldIndex()); } @@ -136,14 +136,14 @@ private void onDocumentModified(DocumentChange change) { Log.d(TAG, "Modified (inplace): " + change.getOldIndex()); mSnapshots.set(change.getOldIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventType.CHANGED, change.getDocument(), + notifyListenersOnChildChanged(ChangeEventType.CHANGED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } else { Log.d(TAG, "Modified (moved): " + change.getOldIndex() + " --> " + change.getNewIndex()); mSnapshots.remove(change.getOldIndex()); mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyOnChildChanged(ChangeEventType.MOVED, change.getDocument(), + notifyListenersOnChildChanged(ChangeEventType.MOVED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } } @@ -171,24 +171,10 @@ private void stopListening() { mSnapshots.clear(); } - private void notifyOnChildChanged(ChangeEventType type, - DocumentSnapshot snapshot, - int newIndex, int oldIndex) { - - for (ChangeEventListener listener : getListeners()) { - listener.onChildChanged(type, snapshot, newIndex, oldIndex); - } - } - + // TODO: There should be a way to move this into the base class private void notifyOnError(FirebaseFirestoreException e) { for (ChangeEventListener listener : getListeners()) { listener.onError(e); } } - - private void notifyOnDataChanged() { - for (ChangeEventListener listener : getListeners()) { - listener.onDataChanged(); - } - } } From b8db413cc6fcbdce028814567d176b236248d364 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 8 Aug 2017 13:21:26 -0700 Subject: [PATCH 09/37] Unify listener addition, fix hasDataChanged Change-Id: Iaf07157fabd5e1a1c698be2832e11716e53812d1 --- .../common/BaseObservableSnapshotArray.java | 36 +++++++------------ .../firebase/ui/database/FirebaseArray.java | 20 ++++++++--- .../ui/database/ObservableSnapshotArray.java | 14 ++------ .../firebase/ui/firestore/FirestoreArray.java | 29 ++++++--------- 4 files changed, 40 insertions(+), 59 deletions(-) diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index e72d9d133..f4084d406 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -21,6 +21,9 @@ public abstract class BaseObservableSnapshotArray mListeners = new CopyOnWriteArrayList<>(); private BaseSnapshotParser mParser; + /** + * True if there has been a "data changed" event since the array was created, false otherwise. + */ private boolean mHasDataChanged = false; /** @@ -50,7 +53,16 @@ protected void onCreate() {} * state and fire backlogged events * @param listener the added listener. */ - protected void onListenerAdded(L listener) {}; + @CallSuper + protected void onListenerAdded(L listener) { + for (int i = 0; i < size(); i++) { + listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); + } + + if (mHasDataChanged) { + listener.onDataChanged(); + } + }; /** * Called when the {@link BaseObservableSnapshotArray} is inactive and should stop listening to the @@ -140,16 +152,6 @@ public E getObject(int index) { return mParser.parseSnapshot(get(index)); } - @Override - public S get(int index) { - return getSnapshots().get(index); - } - - @Override - public int size() { - return getSnapshots().size(); - } - protected void notifyListenersOnChildChanged(ChangeEventType type, S snapshot, int newIndex, @@ -167,18 +169,6 @@ protected void notifyListenersOnDataChanged() { } } - - // TODO(samstern): Do we need this? - protected abstract List getSnapshots(); - - protected boolean hasDataChanged() { - return mHasDataChanged; - } - - protected void setHasDataChanged(boolean hasDataChanged) { - mHasDataChanged = hasDataChanged; - } - protected BaseSnapshotParser getSnapshotParser() { return mParser; } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index fba38e9c9..1b3292451 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -60,11 +60,6 @@ private void init(Query query) { mQuery = query; } - @Override - protected List getSnapshots() { - return mSnapshots; - } - @Override protected void onCreate() { super.onCreate(); @@ -141,6 +136,21 @@ private int getIndexForKey(String key) { throw new IllegalArgumentException("Key not found"); } + @Override + public DataSnapshot get(int i) { + return mSnapshots.get(i); + } + + @Override + protected List getSnapshots() { + return mSnapshots; + } + + @Override + public int size() { + return mSnapshots.size(); + } + @Override public boolean equals(Object obj) { if (this == obj) return true; diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 18803f874..8909f8d63 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -56,18 +56,8 @@ public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener l return super.addChangeEventListener(listener); } - @Override - protected void onListenerAdded(ChangeEventListener listener) { - super.onListenerAdded(listener); - - for (int i = 0; i < size(); i++) { - listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); - } - - if (hasDataChanged()) { - listener.onDataChanged(); - } - } + // TODO(samstern): Can I remove this? + protected abstract List getSnapshots(); protected void notifyListenersOnCancelled(DatabaseError error) { for (ChangeEventListener listener : getListeners()) { diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 99f85d865..8fbaab487 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -65,20 +65,6 @@ protected void onDestroy() { mCache.clearData(); } - @Override - protected void onListenerAdded(ChangeEventListener listener) { - super.onListenerAdded(listener); - - for (int i = 0; i < size(); i++) { - listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); - } - - // TODO(samstern): track hasDataChanged() - if (hasDataChanged()) { - listener.onDataChanged(); - } - } - @Override public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { if (e != null) { @@ -106,6 +92,16 @@ public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { notifyListenersOnDataChanged(); } + @Override + public DocumentSnapshot get(int i) { + return mSnapshots.get(i); + } + + @Override + public int size() { + return mSnapshots.size(); + } + private void onDocumentAdded(DocumentChange change) { Log.d(TAG, "Added: " + change.getNewIndex()); @@ -148,11 +144,6 @@ private void onDocumentModified(DocumentChange change) { } } - @Override - protected List getSnapshots() { - return mSnapshots; - } - private void startListening() { if (mRegistration != null) { Log.d(TAG, "startListening: already listening."); From 981f7ef0c329726e3e18d60a1e57711592245174 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 8 Aug 2017 13:39:55 -0700 Subject: [PATCH 10/37] Add OSA to Firestore Change-Id: Ib0b685558bf4b7c19c1a12fc2b2dd2863cf3fcc6 --- .../ui/common/BaseChangeEventListener.java | 2 +- .../common/BaseObservableSnapshotArray.java | 4 ++- .../com/firebase/ui/database/TestUtils.java | 8 +++-- .../CachingObservableSnapshotArray.java | 4 +++ .../ui/database/ObservableSnapshotArray.java | 15 --------- .../firebase/ui/firestore/FirestoreArray.java | 11 +------ .../firestore/FirestoreRecyclerAdapter.java | 3 +- .../ui/firestore/ObservableSnapshotArray.java | 33 +++++++++++++++++++ 8 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java diff --git a/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java index 47d2cba77..195e15be2 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java +++ b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java @@ -1,7 +1,7 @@ package com.firebase.ui.common; /** - * TODO(samstern): Document + * Event listener for changes in an {@link BaseObservableSnapshotArray}. */ public interface BaseChangeEventListener { diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index f4084d406..5ab7cd97d 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -76,7 +76,9 @@ protected void onDestroy() { } /** - * Attach a listener to this array. + * Attach a {@link BaseChangeEventListener} to this array. The listener will receive one {@link + * ChangeEventType#ADDED} event for each item that already exists in the array at + * the time of attachment, and then receive all future child events. */ @CallSuper public L addChangeEventListener(@NonNull L listener) { diff --git a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java index 1266d119f..2e23fad5f 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java +++ b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java @@ -35,9 +35,11 @@ private static FirebaseApp initializeApp(Context context) { .build(), APP_NAME); } - public static ChangeEventListener runAndWaitUntil(ObservableSnapshotArray array, - Runnable task, - Callable done) throws InterruptedException { + public static ChangeEventListener runAndWaitUntil( + ObservableSnapshotArray array, + Runnable task, + Callable done) throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); ChangeEventListener listener = array.addChangeEventListener(new ChangeEventListener() { @Override diff --git a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java index e30016394..c5ced5f67 100644 --- a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java @@ -4,6 +4,8 @@ import com.google.firebase.database.DataSnapshot; +import java.util.List; + /** * An extension of {@link ObservableSnapshotArray} that caches the result of {@link #getObject(int)} * so that repeated calls for the same key are not expensive (unless the underlying snapshot has @@ -35,6 +37,8 @@ public T getObject(int index) { return mCache.parseSnapshot(get(index)); } + protected abstract List getSnapshots(); + @Override protected void onDestroy() { super.onDestroy(); diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 8909f8d63..916631085 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -1,10 +1,8 @@ package com.firebase.ui.database; -import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import com.firebase.ui.common.BaseObservableSnapshotArray; -import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -46,19 +44,6 @@ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { super(parser); } - /** - * Attach a {@link ChangeEventListener} to this array. The listener will receive one {@link - * ChangeEventType#ADDED} event for each item that already exists in the array at - * the time of attachment, and then receive all future child events. - */ - @CallSuper - public ChangeEventListener addChangeEventListener(@NonNull ChangeEventListener listener) { - return super.addChangeEventListener(listener); - } - - // TODO(samstern): Can I remove this? - protected abstract List getSnapshots(); - protected void notifyListenersOnCancelled(DatabaseError error) { for (ChangeEventListener listener : getListeners()) { listener.onError(error); diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 8fbaab487..4eb3d7e8d 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -2,7 +2,6 @@ import android.util.Log; -import com.firebase.ui.common.BaseObservableSnapshotArray; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; @@ -19,8 +18,7 @@ /** * Exposes a Firestore query as an observable list of objects. */ -public class FirestoreArray - extends BaseObservableSnapshotArray +public class FirestoreArray extends ObservableSnapshotArray implements EventListener { private static final String TAG = "FirestoreArray"; @@ -161,11 +159,4 @@ private void stopListening() { mSnapshots.clear(); } - - // TODO: There should be a way to move this into the base class - private void notifyOnError(FirebaseFirestoreException e) { - for (ChangeEventListener listener : getListeners()) { - listener.onError(e); - } - } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index 3a25bc31b..f2ac8acdc 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -2,7 +2,6 @@ import android.support.v7.widget.RecyclerView; -import com.firebase.ui.common.BaseObservableSnapshotArray; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; @@ -17,7 +16,7 @@ public abstract class FirestoreRecyclerAdapter extends RecyclerView.Adapter implements ChangeEventListener { - private BaseObservableSnapshotArray mArray; + private ObservableSnapshotArray mArray; public FirestoreRecyclerAdapter(Query query, Class modelClass) { mArray = new FirestoreArray<>(query, modelClass); diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java new file mode 100644 index 000000000..db9c1466b --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java @@ -0,0 +1,33 @@ +package com.firebase.ui.firestore; + +import android.support.annotation.NonNull; + +import com.firebase.ui.common.BaseObservableSnapshotArray; +import com.firebase.ui.common.BaseSnapshotParser; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestoreException; + +/** + * Subclass of {@link BaseObservableSnapshotArray} for Firestore data. + */ +public abstract class ObservableSnapshotArray + extends BaseObservableSnapshotArray { + + public ObservableSnapshotArray() { + super(); + } + + /** + * See {@link BaseObservableSnapshotArray#BaseObservableSnapshotArray(BaseSnapshotParser)} + */ + public ObservableSnapshotArray(@NonNull SnapshotParser parser) { + super(parser); + } + + // TODO: There should be a way to move this into the base class + protected void notifyOnError(FirebaseFirestoreException e) { + for (ChangeEventListener listener : getListeners()) { + listener.onError(e); + } + } +} From 43ee4e5c09800c5415ecdc5342748a6fb5e4414a Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Tue, 8 Aug 2017 13:47:23 -0700 Subject: [PATCH 11/37] Move errors up to base class Change-Id: I5fddbe75f2c99c5ae832db2e44f09105634cbef3 --- .../common/BaseObservableSnapshotArray.java | 23 ++++++++++++------- .../firebase/ui/database/FirebaseArray.java | 2 +- .../ui/database/FirebaseIndexArray.java | 12 +++++++++- .../ui/database/ObservableSnapshotArray.java | 18 ++++++++------- .../firebase/ui/firestore/FirestoreArray.java | 2 +- .../ui/firestore/ObservableSnapshotArray.java | 13 +++-------- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index 5ab7cd97d..3c554f877 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -8,18 +8,19 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * Exposes a collection of {@link S} items in a database as a {@link List} of {@link E} objects. + * Exposes a collection of {@link S} items in a database as a {@link List} of {@link T} objects. * To observe the list attach a {@link L} listener. * * @param the snapshot class. + * @param the error type raised for the listener. * @param the listener class. - * @param the model object class. + * @param the model object class. */ -public abstract class BaseObservableSnapshotArray, E> +public abstract class BaseObservableSnapshotArray, T> extends AbstractList { private final List mListeners = new CopyOnWriteArrayList<>(); - private BaseSnapshotParser mParser; + private BaseSnapshotParser mParser; /** * True if there has been a "data changed" event since the array was created, false otherwise. @@ -37,7 +38,7 @@ public BaseObservableSnapshotArray() {} * * @param parser the {@link BaseSnapshotParser} to use */ - public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { + public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { mParser = Preconditions.checkNotNull(parser); } @@ -146,7 +147,7 @@ public final boolean isListening(L listener) { * type. This uses the {@link BaseSnapshotParser} passed to the constructor. If the parser was not * initialized this will throw an unchecked exception. */ - public E getObject(int index) { + public T getObject(int index) { if (mParser == null) { throw new IllegalStateException("getObject() called before snapshot parser set."); } @@ -171,11 +172,17 @@ protected void notifyListenersOnDataChanged() { } } - protected BaseSnapshotParser getSnapshotParser() { + protected void notifyListenersOnError(E e) { + for (L listener : getListeners()) { + listener.onError(e); + } + } + + protected BaseSnapshotParser getSnapshotParser() { return mParser; } - protected void setSnapshotParser(BaseSnapshotParser parser) { + protected void setSnapshotParser(BaseSnapshotParser parser) { mParser = parser; } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 1b3292451..94617e580 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -121,7 +121,7 @@ public void onDataChange(DataSnapshot dataSnapshot) { @Override public void onCancelled(DatabaseError error) { - notifyListenersOnCancelled(error); + notifyListenersOnError(error); } private int getIndexForKey(String key) { diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 678c650dd..ec334c9db 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -194,6 +194,16 @@ protected void onKeyRemoved(DataSnapshot data, int index) { } } + @Override + public DataSnapshot get(int i) { + return mDataSnapshots.get(i); + } + + @Override + public int size() { + return mKeySnapshots.size(); + } + @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -260,7 +270,7 @@ public void onDataChange(DataSnapshot snapshot) { @Override public void onCancelled(DatabaseError error) { - notifyListenersOnCancelled(error); + notifyListenersOnError(error); } } } diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 916631085..67d13d85b 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -12,10 +12,10 @@ * Exposes a collection of items in Firebase as a {@link List} of {@link DataSnapshot}. To observe * the list attach a {@link com.google.firebase.database.ChildEventListener}. * - * @param a POJO class to which the DataSnapshots can be converted. + * @param a POJO class to which the DataSnapshots can be converted. */ -public abstract class ObservableSnapshotArray - extends BaseObservableSnapshotArray { +public abstract class ObservableSnapshotArray + extends BaseObservableSnapshotArray { /** * Default constructor. Must set the snapshot parser before user. @@ -31,7 +31,7 @@ public ObservableSnapshotArray() { * @param clazz the class as which DataSnapshots should be parsed. * @see ClassSnapshotParser */ - public ObservableSnapshotArray(@NonNull Class clazz) { + public ObservableSnapshotArray(@NonNull Class clazz) { super(new ClassSnapshotParser<>(clazz)); } @@ -40,13 +40,15 @@ public ObservableSnapshotArray(@NonNull Class clazz) { * * @param parser the {@link SnapshotParser} to use */ - public ObservableSnapshotArray(@NonNull SnapshotParser parser) { + public ObservableSnapshotArray(@NonNull SnapshotParser parser) { super(parser); } + /** + * Use {@link BaseObservableSnapshotArray#notifyListenersOnError(Object)}. + */ + @Deprecated protected void notifyListenersOnCancelled(DatabaseError error) { - for (ChangeEventListener listener : getListeners()) { - listener.onError(error); - } + notifyListenersOnError(error); } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 4eb3d7e8d..93876c1f0 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -67,7 +67,7 @@ protected void onDestroy() { public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { if (e != null) { Log.w(TAG, "Error in snapshot listener", e); - notifyOnError(e); + notifyListenersOnError(e); return; } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java index db9c1466b..4c08ffe34 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java @@ -10,8 +10,8 @@ /** * Subclass of {@link BaseObservableSnapshotArray} for Firestore data. */ -public abstract class ObservableSnapshotArray - extends BaseObservableSnapshotArray { +public abstract class ObservableSnapshotArray + extends BaseObservableSnapshotArray { public ObservableSnapshotArray() { super(); @@ -20,14 +20,7 @@ public ObservableSnapshotArray() { /** * See {@link BaseObservableSnapshotArray#BaseObservableSnapshotArray(BaseSnapshotParser)} */ - public ObservableSnapshotArray(@NonNull SnapshotParser parser) { + public ObservableSnapshotArray(@NonNull SnapshotParser parser) { super(parser); } - - // TODO: There should be a way to move this into the base class - protected void notifyOnError(FirebaseFirestoreException e) { - for (ChangeEventListener listener : getListeners()) { - listener.onError(e); - } - } } From b79c707f471eba78598a4be4f615a6eeccabafea Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 6 Sep 2017 10:22:00 -0700 Subject: [PATCH 12/37] Add tests Change-Id: I71ee6b3021a4e697dc1acbaa97f3bdedaffc0581 --- .../ui/firestore/FirestoreArrayTest.java | 287 ++++++++++++++++++ firestore/src/main/AndroidManifest.xml | 6 +- 2 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java diff --git a/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java b/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java new file mode 100644 index 000000000..411fda997 --- /dev/null +++ b/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java @@ -0,0 +1,287 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.firestore; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import com.firebase.ui.common.ChangeEventType; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.FirebaseFirestoreSettings; +import com.google.firebase.firestore.Query; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +@RunWith(AndroidJUnit4.class) +public class FirestoreArrayTest { + + /** + * Simple document class containing only an integer field. + */ + private static class IntegerDocument { + + public int field; + + public IntegerDocument() {} + + public IntegerDocument(int field) { + this.field = field; + } + + } + + /** + * Simple listener that logs all events, to make test debugging easier. + */ + private static class LoggingListener implements ChangeEventListener { + + private static final String TAG = "FirestoreTest_Listener"; + + @Override + public void onChildChanged(ChangeEventType type, + DocumentSnapshot snapshot, + int newIndex, + int oldIndex) { + Log.d(TAG, "onChildChanged: " + type + " at index " + newIndex); + } + + @Override + public void onDataChanged() { + Log.d(TAG, "onDataChanged"); + } + + @Override + public void onError(FirebaseFirestoreException e) { + Log.w(TAG, "onError", e); + } + } + + private static final String TAG = "FirestoreTest"; + + private static final String FIREBASE_APP_NAME = "test-app"; + + private static final int TIMEOUT = 30000; + + private static final int INITIAL_SIZE = 3; + + private FirebaseFirestore mFirestore; + private CollectionReference mCollectionRef; + private Query mQuery; + private FirestoreArray mArray; + private ChangeEventListener mListener; + + @Before + public void setUp() throws Exception { + FirebaseApp app = getAppInstance(InstrumentationRegistry.getContext()); + + // Configure Firestore and disable persistence + mFirestore = FirebaseFirestore.getInstance(app); + mFirestore.setFirestoreSettings(new FirebaseFirestoreSettings.Builder() + .setPersistenceEnabled(false) + .build()); + + // Get a fresh 'test' subcollection for each test + mCollectionRef = FirebaseFirestore.getInstance(app).collection("firestorearray") + .document().collection("test"); + + Log.d(TAG, "Test Collection: " + getConsoleLink(mCollectionRef)); + + // Query is the whole collection ordered by field + mQuery = mCollectionRef.orderBy("field", Query.Direction.ASCENDING); + mArray = new FirestoreArray<>(mQuery, IntegerDocument.class); + + // Add a listener to the array so that it's active + mListener = mArray.addChangeEventListener(new LoggingListener()); + + // Add some initial data + runAndVerify(new Callable>() { + @Override + public Task call() throws Exception { + List tasks = new ArrayList<>(); + for (int i = 0; i < INITIAL_SIZE; i++) { + tasks.add(mCollectionRef.document().set(new IntegerDocument(i))); + } + + return Tasks.whenAll(tasks.toArray(new Task[tasks.size()])); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.size() == INITIAL_SIZE; + } + }); + } + + @After + public void tearDown() throws Exception { + if (mArray != null && mListener != null) { + mArray.removeChangeEventListener(mListener); + } else { + Log.w(TAG, "mArray is null in tearDown"); + } + } + + /** + * Append a single document and confirm size increases by one. + */ + @Test + public void testPushIncreasesSize() throws Exception { + runAndVerify(new Callable>() { + @Override + public Task call() throws Exception { + return mCollectionRef.document().set(new IntegerDocument(4)); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + return mArray.size() == (INITIAL_SIZE + 1); + } + }); + } + + /** + * Append a single document to the query and confirm that size increases by one and that the + * document is added to the end of the array. + */ + @Test + public void testAddToEnd() throws Exception { + final int value = 4; + + runAndVerify(new Callable>() { + @Override + public Task call() throws Exception { + return mCollectionRef.document().set(new IntegerDocument(value)); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + if (mArray.size() == (INITIAL_SIZE + 1)) { + if (mArray.getObject(mArray.size() - 1).field == value) { + return true; + } + } + + return false; + } + }); + } + + /** + * Append a single document to the query and confirm that size increases by one and that the + * document is added to the beginning of the array. + */ + @Test + public void testAddToBeginning() throws Exception { + final int value = -1; + + runAndVerify(new Callable>() { + @Override + public Task call() throws Exception { + return mCollectionRef.document().set(new IntegerDocument(value)); + } + }, new Callable() { + @Override + public Boolean call() throws Exception { + if (mArray.size() == (INITIAL_SIZE + 1)) { + if (mArray.getObject(0).field == value) { + return true; + } + } + + return false; + } + }); + } + + /** + * Runs some setup action, waits until it is complete, and then waits for a verification + * condition to be met. Times out after {@link #TIMEOUT}. + */ + @SuppressWarnings("unchecked") + private void runAndVerify(Callable> setup, + Callable verify) throws Exception { + + final Semaphore semaphore = new Semaphore(0); + long startTime = System.currentTimeMillis(); + + // Run the setup action and release the semaphore when it is complete + Task task = setup.call(); + task.addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + semaphore.release(); + } + }); + + // Wait for the verification condition to be met, or time out + boolean isDone = false; + while (!isDone && (System.currentTimeMillis() - startTime < TIMEOUT)) { + boolean acquired = semaphore.tryAcquire(1, TimeUnit.SECONDS); + if (acquired) { + try { + isDone = verify.call(); + } catch (Exception e) { + Log.w(TAG, "error in verification callable", e); + } + } + } + + assertTrue("Timed out waiting for setup callable.", task.isComplete()); + assertTrue("Timed out waiting for expected results.", isDone); + } + + private FirebaseApp getAppInstance(Context context) { + try { + return FirebaseApp.getInstance(FIREBASE_APP_NAME); + } catch (IllegalStateException e) { + return initializeApp(context); + } + } + + private FirebaseApp initializeApp(Context context) { + return FirebaseApp.initializeApp(context, new FirebaseOptions.Builder() + .setApplicationId("firebaseuitests-app123") + .setApiKey("AIzaSyCIA_uf-5Y4G83vlZmjMmCM_wkX62iWXf0") + .setProjectId("firebaseuitests") + .build(), FIREBASE_APP_NAME); + } + + private String getConsoleLink(CollectionReference reference) { + String base = "https://console.firebase.google.com/project/firebaseuitests/database/firestore/data/"; + return base + reference.getPath(); + } +} diff --git a/firestore/src/main/AndroidManifest.xml b/firestore/src/main/AndroidManifest.xml index a19a3dfd9..3469dc429 100644 --- a/firestore/src/main/AndroidManifest.xml +++ b/firestore/src/main/AndroidManifest.xml @@ -1,2 +1,6 @@ + package="com.google.firebase.firestore.firestore"> + + + + From 74c954f2b6015423cef8c7a24608fca61d4afcf8 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 6 Sep 2017 10:24:52 -0700 Subject: [PATCH 13/37] Version number Change-Id: I0c7b419c983d88bcb5bf21949817e162729512b5 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index e08cc2261..d9ed6c9a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -1,7 +1,7 @@ project.ext { submodules = ['database', 'auth', 'storage', 'firestore'] group = 'com.firebaseui' - version = '2.3.0' + version = '3.0.0-SNAPSHOT' pomdesc = 'Firebase UI Android' compileSdk = 26 From c8f0d4ae8c0c92a2e036f0fe1a7757f25525e0d3 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Wed, 6 Sep 2017 11:11:24 -0700 Subject: [PATCH 14/37] Add more constructors Change-Id: I5778ad28795bb97a56ff04aa70f53e3d553bbefb --- app/build.gradle | 3 + .../ui/common/BaseCachingSnapshotParser.java | 1 - constants.gradle | 1 + database/build.gradle | 6 +- firestore/build.gradle | 8 +- .../firestore/FirestoreRecyclerAdapter.java | 95 ++++++++++++++++++- 6 files changed, 103 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a75fd2a71..0f02fb9d0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,6 +48,9 @@ dependencies { compile('com.facebook.android:facebook-android-sdk:4.25.0') compile("com.twitter.sdk.android:twitter-core:3.0.0@aar") { transitive = true } + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" + // The following dependencies are not required to use the Firebase UI library. // They are used to make some aspects of the demo app implementation simpler for // demonstrative purposes, and you may find them useful in your own apps; YMMV. diff --git a/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java index 0396dcf61..b53c96e0d 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java +++ b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java @@ -15,7 +15,6 @@ public abstract class BaseCachingSnapshotParser implements BaseSnapshotPar private Map mObjectCache = new HashMap<>(); private BaseSnapshotParser mInnerParser; - // TODO(samstern): Can probably use this in RTDB module as well, CachingOSA is not necessary. public BaseCachingSnapshotParser(BaseSnapshotParser innerParser) { mInnerParser = innerParser; } diff --git a/constants.gradle b/constants.gradle index d9ed6c9a5..845a881cb 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,4 +12,5 @@ project.ext { firebaseVersion = '11.0.8' supportLibraryVersion = '26.0.1' + architectureVersion = '1.0.0-alpha8' } diff --git a/database/build.gradle b/database/build.gradle index 54c9175aa..6daf9395b 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -29,9 +29,9 @@ dependencies { // Needed to override play services compile "com.android.support:support-v4:$supportLibraryVersion" - compile 'android.arch.lifecycle:runtime:1.0.0-alpha8' - compile 'android.arch.lifecycle:extensions:1.0.0-alpha8' - annotationProcessor 'android.arch.lifecycle:compiler:1.0.0-alpha8' + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" + annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" compile "com.google.firebase:firebase-database:$firebaseVersion" diff --git a/firestore/build.gradle b/firestore/build.gradle index d59b88c1e..aae2e6965 100644 --- a/firestore/build.gradle +++ b/firestore/build.gradle @@ -31,7 +31,11 @@ dependencies { compile "com.google.firebase:firebase-firestore:$firebaseVersion" + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" + annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" + androidTestCompile 'junit:junit:4.12' - androidTestCompile 'com.android.support.test:runner:0.5' - androidTestCompile 'com.android.support.test:rules:0.5' + androidTestCompile 'com.android.support.test:runner:1.0.0' + androidTestCompile 'com.android.support.test:rules:1.0.0' } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index f2ac8acdc..d9b214d09 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -1,6 +1,12 @@ package com.firebase.ui.firestore; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.OnLifecycleEvent; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; +import android.util.Log; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentSnapshot; @@ -14,22 +20,102 @@ * @param viewholder class. */ public abstract class FirestoreRecyclerAdapter - extends RecyclerView.Adapter implements ChangeEventListener { + extends RecyclerView.Adapter + implements ChangeEventListener, LifecycleObserver { + + private static final String TAG = "FirestoreRecycler"; private ObservableSnapshotArray mArray; + /** + * Create a new RecyclerView adapter to bind data from a Firestore query where each + * {@link DocumentSnapshot} is converted to the specified model class. + * + * See {@link #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner)}. + * + * @param query the Firestore query. + * @param modelClass the model class. + */ public FirestoreRecyclerAdapter(Query query, Class modelClass) { + this(query, modelClass, null); + } + + /** + * Create a new RecyclerView adapter bound to a LifecycleOwner. + * + * See {@link #FirestoreRecyclerAdapter(Query, Class)} + */ + public FirestoreRecyclerAdapter(Query query, Class modelClass, LifecycleOwner owner) { mArray = new FirestoreArray<>(query, modelClass); + if (owner != null) { + owner.getLifecycle().addObserver(this); + } } - public abstract void onBindViewHolder(VH vh, int i, T model); + /** + * Create a new RecyclerView adapter to bind data from a Firestore query where each + * {@link DocumentSnapshot} is parsed by the specified parser. + * + * See {@link #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner)}. + * + * @param query the Firestore query. + * @param parser the snapshot parser. + */ + public FirestoreRecyclerAdapter(Query query, SnapshotParser parser) { + this(query, parser, null); + } + + /** + * Create a new RecyclerView adapter bound to a LifecycleOwner. + * + * See {@link #FirestoreRecyclerAdapter(Query, SnapshotParser)}. + */ + public FirestoreRecyclerAdapter(Query query, SnapshotParser parser, LifecycleOwner owner) { + mArray = new FirestoreArray(query, parser); + if (owner != null) { + owner.getLifecycle().addObserver(this); + } + } + /** + * Create a new RecyclerView adapter to bind data from an {@link ObservableSnapshotArray}. + * + * @param array the observable array of data from Firestore. + * @param owner (optional) a LifecycleOwner to observe. + */ + public FirestoreRecyclerAdapter(ObservableSnapshotArray array, + @Nullable LifecycleOwner owner) { + mArray = array; + if (owner != null) { + owner.getLifecycle().addObserver(this); + } + } + + /** + * Called when data has been added/changed and an item needs to be displayed. + * + * @param vh the view to populate. + * @param i the position in the list of the view being populated. + * @param model the model object containing the data that should be used to populate the view. + */ + protected abstract void onBindViewHolder(VH vh, int i, T model); + + @OnLifecycleEvent(Lifecycle.Event.ON_START) public void startListening() { - mArray.addChangeEventListener(this); + if (!mArray.isListening(this)) { + mArray.addChangeEventListener(this); + } } + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void stopListening() { mArray.removeChangeEventListener(this); + notifyDataSetChanged(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + void cleanup(LifecycleOwner source) { + source.getLifecycle().removeObserver(this); } @Override @@ -64,11 +150,10 @@ public void onChildChanged(ChangeEventType type, DocumentSnapshot snapshot, @Override public void onDataChanged() { - // No-op } @Override public void onError(FirebaseFirestoreException e) { - // No-op + Log.w(TAG, "onError", e); } } From 9f00117a587b2f0adef3d15c862a2c5303522fc9 Mon Sep 17 00:00:00 2001 From: Sam Stern Date: Fri, 8 Sep 2017 08:42:13 -0700 Subject: [PATCH 15/37] Fix typo Change-Id: Ic083122f69a66b01c72f06496a72ade6a760fa94 --- .../main/java/com/firebase/uidemo/database/ChatActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java index 1037538e3..6429b0a2c 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java @@ -146,7 +146,7 @@ protected FirebaseRecyclerAdapter getAdapter() { lastFifty, this) { @Override - public void populateViewHolder(ChatHolder holder, Cgit hat chat, int position) { + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { holder.bind(chat); } From e5fbab804b64d5ebbd8fc0bc9bbefcfc03dd296b Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Mon, 11 Sep 2017 11:46:58 -0700 Subject: [PATCH 16/37] Lockdown firebase arrays and improve index add performance (#898) --- .../firebase/ui/database/FirebaseArray.java | 3 ++- .../ui/database/FirebaseIndexArray.java | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index f25617ef3..36e057e51 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -26,7 +26,8 @@ /** * This class implements a collection on top of a Firebase location. */ -public class FirebaseArray extends CachingObservableSnapshotArray implements ChildEventListener, ValueEventListener { +public class FirebaseArray extends CachingObservableSnapshotArray + implements ChildEventListener, ValueEventListener { private Query mQuery; private List mSnapshots = new ArrayList<>(); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 21c05a685..762c3ff01 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -28,7 +28,8 @@ import java.util.List; import java.util.Map; -public class FirebaseIndexArray extends CachingObservableSnapshotArray implements ChangeEventListener { +public class FirebaseIndexArray extends CachingObservableSnapshotArray + implements ChangeEventListener { private static final String TAG = "FirebaseIndexArray"; private DatabaseReference mDataRef; @@ -107,7 +108,7 @@ protected void onDestroy() { public void onChildChanged(EventType type, DataSnapshot snapshot, int index, int oldIndex) { switch (type) { case ADDED: - onKeyAdded(snapshot); + onKeyAdded(snapshot, index); break; case MOVED: onKeyMoved(snapshot, index, oldIndex); @@ -175,16 +176,16 @@ private boolean isKeyAtIndex(String key, int index) { return index >= 0 && index < size() && mDataSnapshots.get(index).getKey().equals(key); } - protected void onKeyAdded(DataSnapshot data) { + private void onKeyAdded(DataSnapshot data, int newIndex) { String key = data.getKey(); DatabaseReference ref = mDataRef.child(key); mKeysWithPendingUpdate.add(key); // Start listening - mRefs.put(ref, ref.addValueEventListener(new DataRefListener())); + mRefs.put(ref, ref.addValueEventListener(new DataRefListener(newIndex))); } - protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { + private void onKeyMoved(DataSnapshot data, int index, int oldIndex) { String key = data.getKey(); // We can't use `returnOrFindIndexForKey(...)` for `oldIndex` or it might find the updated @@ -199,7 +200,7 @@ protected void onKeyMoved(DataSnapshot data, int index, int oldIndex) { } } - protected void onKeyRemoved(DataSnapshot data, int index) { + private void onKeyRemoved(DataSnapshot data, int index) { String key = data.getKey(); ValueEventListener listener = mRefs.remove(mDataRef.getRef().child(key)); if (listener != null) mDataRef.child(key).removeEventListener(listener); @@ -243,10 +244,14 @@ public String toString() { /** * A ValueEventListener attached to the joined child data. */ - protected class DataRefListener implements ValueEventListener { + private final class DataRefListener implements ValueEventListener { /** Cached index to skip searching for the current index on each update */ private int currentIndex; + public DataRefListener(int index) { + currentIndex = index; + } + @Override public void onDataChange(DataSnapshot snapshot) { String key = snapshot.getKey(); From fea9eb3c45b958df5f78df90668ce81199afbd91 Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Tue, 12 Sep 2017 13:07:55 -0700 Subject: [PATCH 17/37] Cleanup (#2) --- app/src/main/AndroidManifest.xml | 15 +- .../com/firebase/uidemo/ChooserActivity.java | 8 +- .../uidemo/auth/SignedInActivity.java | 1 - .../uidemo/database/AbstractChat.java | 1 - .../uidemo/{ => database}/firestore/Chat.java | 4 +- .../firestore/FirestoreChatActivity.java | 4 +- .../uidemo/database/{ => realtime}/Chat.java | 3 +- .../database/{ => realtime}/ChatActivity.java | 11 +- .../{ => realtime}/ChatIndexActivity.java | 3 +- app/src/main/res/layout/activity_chat.xml | 2 +- app/src/main/res/xml-v25/shortcuts.xml | 2 +- .../ui/auth/provider/TwitterProvider.java | 1 - common/.gitignore | 1 - common/build.gradle | 8 +- common/src/main/AndroidManifest.xml | 3 +- .../ui/common/BaseCachingSnapshotParser.java | 38 ++--- .../ui/common/BaseChangeEventListener.java | 3 +- .../common/BaseObservableSnapshotArray.java | 158 ++++++++---------- .../firebase/ui/common/ChangeEventType.java | 8 +- database/build.gradle | 14 +- .../com/firebase/ui/database/TestUtils.java | 3 +- database/src/main/AndroidManifest.xml | 6 - .../CachingObservableSnapshotArray.java | 68 -------- .../ui/database/CachingSnapshotParser.java | 4 +- .../firebase/ui/database/FirebaseArray.java | 32 ++-- .../ui/database/FirebaseIndexArray.java | 48 +++--- .../ui/database/FirebaseListAdapter.java | 2 +- .../ui/database/FirebaseRecyclerAdapter.java | 10 +- .../ui/database/ObservableSnapshotArray.java | 16 +- firestore/build.gradle | 12 +- firestore/src/main/AndroidManifest.xml | 5 +- .../ui/firestore/CachingSnapshotParser.java | 7 +- .../firebase/ui/firestore/FirestoreArray.java | 132 +++++++-------- .../firestore/FirestoreRecyclerAdapter.java | 132 ++++++++------- .../ui/firestore/ObservableSnapshotArray.java | 11 +- gradle/wrapper/gradle-wrapper.jar | Bin 54713 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- 37 files changed, 317 insertions(+), 462 deletions(-) rename app/src/main/java/com/firebase/uidemo/{ => database}/firestore/Chat.java (92%) rename app/src/main/java/com/firebase/uidemo/{ => database}/firestore/FirestoreChatActivity.java (98%) rename app/src/main/java/com/firebase/uidemo/database/{ => realtime}/Chat.java (89%) rename app/src/main/java/com/firebase/uidemo/database/{ => realtime}/ChatActivity.java (95%) rename app/src/main/java/com/firebase/uidemo/database/{ => realtime}/ChatIndexActivity.java (95%) delete mode 100644 common/.gitignore delete mode 100644 database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88b69db61..770663918 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + @@ -31,16 +32,16 @@ + android:name=".database.firestore.FirestoreChatActivity" + android:label="@string/name_chat" /> (query, Chat.class) { @Override - public void onBindViewHolder(ChatHolder holder, int i, Chat model) { + public void onBindViewHolder(ChatHolder holder, int position, Chat model) { holder.bind(model); } diff --git a/app/src/main/java/com/firebase/uidemo/database/Chat.java b/app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java similarity index 89% rename from app/src/main/java/com/firebase/uidemo/database/Chat.java rename to app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java index 270c4907d..3e37eb98a 100644 --- a/app/src/main/java/com/firebase/uidemo/database/Chat.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/Chat.java @@ -1,5 +1,6 @@ -package com.firebase.uidemo.database; +package com.firebase.uidemo.database.realtime; +import com.firebase.uidemo.database.AbstractChat; import com.google.firebase.database.IgnoreExtraProperties; @IgnoreExtraProperties diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java similarity index 95% rename from app/src/main/java/com/firebase/uidemo/database/ChatActivity.java rename to app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java index 6429b0a2c..68cc31497 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.uidemo.database; +package com.firebase.uidemo.database.realtime; import android.arch.lifecycle.LifecycleRegistry; import android.arch.lifecycle.LifecycleRegistryOwner; @@ -30,6 +30,7 @@ import com.firebase.ui.database.FirebaseRecyclerAdapter; import com.firebase.uidemo.R; +import com.firebase.uidemo.database.ChatHolder; import com.firebase.uidemo.util.SignInResultNotifier; import com.google.android.gms.tasks.OnSuccessListener; import com.google.firebase.auth.AuthResult; @@ -64,9 +65,9 @@ protected void onCreate(Bundle savedInstanceState) { mAuth = FirebaseAuth.getInstance(); mAuth.addAuthStateListener(this); - mSendButton = (Button) findViewById(R.id.sendButton); - mMessageEdit = (EditText) findViewById(R.id.messageEdit); - mEmptyListMessage = (TextView) findViewById(R.id.emptyTextView); + mSendButton = findViewById(R.id.sendButton); + mMessageEdit = findViewById(R.id.messageEdit); + mEmptyListMessage = findViewById(R.id.emptyTextView); mChatRef = FirebaseDatabase.getInstance().getReference().child("chats"); @@ -75,7 +76,7 @@ protected void onCreate(Bundle savedInstanceState) { mManager = new LinearLayoutManager(this); mManager.setReverseLayout(false); - mMessages = (RecyclerView) findViewById(R.id.messagesList); + mMessages = findViewById(R.id.messagesList); mMessages.setHasFixedSize(true); mMessages.setLayoutManager(mManager); diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java similarity index 95% rename from app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java rename to app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java index e2e4f7671..b4c90048c 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java @@ -1,10 +1,11 @@ -package com.firebase.uidemo.database; +package com.firebase.uidemo.database.realtime; import android.view.View; import com.firebase.ui.database.FirebaseIndexRecyclerAdapter; import com.firebase.ui.database.FirebaseRecyclerAdapter; import com.firebase.uidemo.R; +import com.firebase.uidemo.database.ChatHolder; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 7c0171e08..70b8bf9be 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".database.ChatActivity"> + tools:context=".database.realtime.ChatActivity"> + android:targetClass="com.firebase.uidemo.database.realtime.ChatActivity"/> diff --git a/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java b/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java index 65dd7bf8a..c6d9aa05f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java +++ b/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java @@ -22,7 +22,6 @@ import com.twitter.sdk.android.core.identity.TwitterAuthClient; import com.twitter.sdk.android.core.models.User; - public class TwitterProvider extends Callback implements IdpProvider { private static final String TAG = "TwitterProvider"; diff --git a/common/.gitignore b/common/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/common/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/common/build.gradle b/common/build.gradle index 79e36900e..91ff58417 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.library' apply from: '../library/quality/quality.gradle' -check.dependsOn 'compileDebugAndroidTestJavaWithJavac' android { compileSdkVersion compileSdk @@ -11,7 +10,6 @@ android { targetSdkVersion targetSdk versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -23,7 +21,13 @@ android { } dependencies { + // Common dependencies across database modules. Each module still has to provide an + // Architecture Components annotation processor and optionally, test dependencies. + compile "com.android.support:recyclerview-v7:$supportLibraryVersion" // Needed to override play services compile "com.android.support:support-v4:$supportLibraryVersion" + + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" } diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 070894592..194ab3e3a 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java index b53c96e0d..efed7560b 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java +++ b/common/src/main/java/com/firebase/ui/common/BaseCachingSnapshotParser.java @@ -1,22 +1,22 @@ package com.firebase.ui.common; import android.support.annotation.RestrictTo; - -import java.util.HashMap; -import java.util.Map; +import android.util.LruCache; /** - * Implementation of {@link BaseSnapshotParser} that caches results, - * so parsing a snapshot repeatedly is not expensive. + * Implementation of {@link BaseSnapshotParser} that caches results, so parsing a snapshot + * repeatedly is not expensive. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public abstract class BaseCachingSnapshotParser implements BaseSnapshotParser { - private Map mObjectCache = new HashMap<>(); - private BaseSnapshotParser mInnerParser; + private static final int MAX_CACHE_SIZE = 100; + + private final LruCache mObjectCache = new LruCache<>(MAX_CACHE_SIZE); + private final BaseSnapshotParser mParser; - public BaseCachingSnapshotParser(BaseSnapshotParser innerParser) { - mInnerParser = innerParser; + public BaseCachingSnapshotParser(BaseSnapshotParser parser) { + mParser = parser; } /** @@ -27,27 +27,27 @@ public BaseCachingSnapshotParser(BaseSnapshotParser innerParser) { @Override public T parseSnapshot(S snapshot) { String id = getId(snapshot); - if (mObjectCache.containsKey(id)) { - return mObjectCache.get(id); - } else { - T object = mInnerParser.parseSnapshot(snapshot); + T result = mObjectCache.get(id); + if (result == null) { + T object = mParser.parseSnapshot(snapshot); mObjectCache.put(id, object); - return object; + result = object; } + return result; } /** * Clear all data in the cache. */ - public void clearData() { - mObjectCache.clear(); + public void clear() { + mObjectCache.evictAll(); } /** - * Invalidate the cache for a certain document ID. + * Invalidate the cache for a certain document. */ - public void invalidate(String id) { - mObjectCache.remove(id); + public void invalidate(S snapshot) { + mObjectCache.remove(getId(snapshot)); } } diff --git a/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java index 195e15be2..56fbe297b 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java +++ b/common/src/main/java/com/firebase/ui/common/BaseChangeEventListener.java @@ -14,8 +14,7 @@ public interface BaseChangeEventListener { * @param oldIndex The previous index of the element, or -1 if it was not * previously tracked. */ - void onChildChanged(ChangeEventType type, S snapshot, - int newIndex, int oldIndex); + void onChildChanged(ChangeEventType type, S snapshot, int newIndex, int oldIndex); /** * Callback triggered after all child events in a particular snapshot have been diff --git a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java index 3c554f877..df33de175 100644 --- a/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java +++ b/common/src/main/java/com/firebase/ui/common/BaseObservableSnapshotArray.java @@ -8,8 +8,8 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * Exposes a collection of {@link S} items in a database as a {@link List} of {@link T} objects. - * To observe the list attach a {@link L} listener. + * Exposes a collection of {@link S} items in a database as a {@link List} of {@link T} objects. To + * observe the list, attach an {@link L} listener. * * @param the snapshot class. * @param the error type raised for the listener. @@ -20,66 +20,51 @@ public abstract class BaseObservableSnapshotArray { private final List mListeners = new CopyOnWriteArrayList<>(); - private BaseSnapshotParser mParser; + private final BaseCachingSnapshotParser mCachingParser; /** - * True if there has been a "data changed" event since the array was created, false otherwise. + * True if there has been a "data changed" event since the array was created or last reset, + * false otherwise. */ private boolean mHasDataChanged = false; - /** - * Default constructor. Must set the {@link BaseSnapshotParser} before the first operation - * or an exception will be thrown. - */ - public BaseObservableSnapshotArray() {} - /** * Create an BaseObservableSnapshotArray with a custom {@link BaseSnapshotParser}. * * @param parser the {@link BaseSnapshotParser} to use */ - public BaseObservableSnapshotArray(@NonNull BaseSnapshotParser parser) { - mParser = Preconditions.checkNotNull(parser); + public BaseObservableSnapshotArray(@NonNull BaseCachingSnapshotParser parser) { + mCachingParser = Preconditions.checkNotNull(parser); } - /** - * Called when the {@link BaseObservableSnapshotArray} is active and should start listening to the - * Firebase database. - */ - @CallSuper - protected void onCreate() {} + @NonNull + protected abstract List getSnapshots(); - /** - * Called when a new listener has been added to the array. This is a good time to pass initial - * state and fire backlogged events - * @param listener the added listener. - */ - @CallSuper - protected void onListenerAdded(L listener) { - for (int i = 0; i < size(); i++) { - listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); - } + @Override + public S get(int index) { + return getSnapshots().get(index); + } - if (mHasDataChanged) { - listener.onDataChanged(); - } - }; + @Override + public int size() { + return getSnapshots().size(); + } /** - * Called when the {@link BaseObservableSnapshotArray} is inactive and should stop listening to the - * Firebase database. - *

- * All data should also be cleared here. + * Get the Snapshot at a given position converted to an object of the parametrized type. This + * uses the {@link BaseSnapshotParser} passed to the constructor. */ - @CallSuper - protected void onDestroy() { - mHasDataChanged = false; + public T getObject(int index) { + return mCachingParser.parseSnapshot(get(index)); } /** * Attach a {@link BaseChangeEventListener} to this array. The listener will receive one {@link - * ChangeEventType#ADDED} event for each item that already exists in the array at - * the time of attachment, and then receive all future child events. + * ChangeEventType#ADDED} event for each item that already exists in the array at the time of + * attachment, a {@link BaseChangeEventListener#onDataChanged()} event if one has occurred, and + * then receive all future child events. + *

+ * If this is the first listener, {@link #onCreate()} will be called. */ @CallSuper public L addChangeEventListener(@NonNull L listener) { @@ -87,31 +72,38 @@ public L addChangeEventListener(@NonNull L listener) { boolean wasListening = isListening(); mListeners.add(listener); - onListenerAdded(listener); - if (!wasListening) { - onCreate(); + // Catch up new listener to existing state + for (int i = 0; i < size(); i++) { + listener.onChildChanged(ChangeEventType.ADDED, get(i), i, -1); + } + if (mHasDataChanged) { + listener.onDataChanged(); } + if (!wasListening) { onCreate(); } + return listener; } /** - * Remove a listener from the array. If no listeners remain, {@link #onDestroy()} - * will be called. + * Remove a listener from the array. + *

+ * If no listeners remain, {@link #onDestroy()} will be called. */ @CallSuper public void removeChangeEventListener(@NonNull L listener) { Preconditions.checkNotNull(listener); + + boolean wasListening = isListening(); + mListeners.remove(listener); - if (!isListening()) { - onDestroy(); - } + if (!isListening() && wasListening) { onDestroy(); } } /** - * Remove all listeners from the array. + * Remove all listeners from the array and reset its state. */ @CallSuper public void removeAllListeners() { @@ -121,68 +113,64 @@ public void removeAllListeners() { } /** - * Get all active listeners. + * Called when the {@link BaseObservableSnapshotArray} is active and should start listening to + * the Firebase database. */ - public List getListeners() { - return mListeners; + @CallSuper + protected void onCreate() {} + + /** + * Called when the {@link BaseObservableSnapshotArray} is inactive and should stop listening to + * the Firebase database. + *

+ * All data and saved state should also be cleared here. + */ + @CallSuper + protected void onDestroy() { + mHasDataChanged = false; + getSnapshots().clear(); + mCachingParser.clear(); } /** - * @return true if the array is listening for change events from the Firebase - * database, false otherwise + * @return true if the array is listening for change events from the Firebase database, false + * otherwise */ - public final boolean isListening() { + public boolean isListening() { return !mListeners.isEmpty(); } /** * @return true if the provided listener is listening for changes */ - public final boolean isListening(L listener) { + public boolean isListening(L listener) { return mListeners.contains(listener); } - /** - * Get the Snapshot at a given position converted to an object of the parametrized - * type. This uses the {@link BaseSnapshotParser} passed to the constructor. If the parser was not - * initialized this will throw an unchecked exception. - */ - public T getObject(int index) { - if (mParser == null) { - throw new IllegalStateException("getObject() called before snapshot parser set."); + protected final void notifyOnChildChanged(ChangeEventType type, + S snapshot, + int newIndex, + int oldIndex) { + if (type == ChangeEventType.CHANGED || type == ChangeEventType.REMOVED) { + mCachingParser.invalidate(snapshot); } - return mParser.parseSnapshot(get(index)); - } - - protected void notifyListenersOnChildChanged(ChangeEventType type, - S snapshot, - int newIndex, - int oldIndex) { - for (L listener : getListeners()) { + for (L listener : mListeners) { listener.onChildChanged(type, snapshot, newIndex, oldIndex); } } - protected void notifyListenersOnDataChanged() { + protected final void notifyOnDataChanged() { mHasDataChanged = true; - for (L listener : getListeners()) { + for (L listener : mListeners) { listener.onDataChanged(); } } - protected void notifyListenersOnError(E e) { - for (L listener : getListeners()) { + protected final void notifyOnError(E e) { + for (L listener : mListeners) { listener.onError(e); } } - - protected BaseSnapshotParser getSnapshotParser() { - return mParser; - } - - protected void setSnapshotParser(BaseSnapshotParser parser) { - mParser = parser; - } } diff --git a/common/src/main/java/com/firebase/ui/common/ChangeEventType.java b/common/src/main/java/com/firebase/ui/common/ChangeEventType.java index a246319e3..194822830 100644 --- a/common/src/main/java/com/firebase/ui/common/ChangeEventType.java +++ b/common/src/main/java/com/firebase/ui/common/ChangeEventType.java @@ -11,14 +11,14 @@ public enum ChangeEventType { ADDED, /** - * An element was removed from the array. + * An element in the array has new content. */ - REMOVED, + CHANGED, /** - * An element in the array has new content. + * An element was removed from the array. */ - CHANGED, + REMOVED, /** * An element in the array has new content, which caused a change in position. diff --git a/database/build.gradle b/database/build.gradle index 6daf9395b..70a0740aa 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -24,18 +24,10 @@ android { dependencies { compile project(path: ':common') - - compile "com.android.support:recyclerview-v7:$supportLibraryVersion" - // Needed to override play services - compile "com.android.support:support-v4:$supportLibraryVersion" - - compile "android.arch.lifecycle:runtime:$architectureVersion" - compile "android.arch.lifecycle:extensions:$architectureVersion" - annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" - compile "com.google.firebase:firebase-database:$firebaseVersion" + annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" androidTestCompile 'junit:junit:4.12' - androidTestCompile 'com.android.support.test:runner:1.0.0' - androidTestCompile 'com.android.support.test:rules:1.0.0' + androidTestCompile 'com.android.support.test:runner:1.0.1' + androidTestCompile 'com.android.support.test:rules:1.0.1' } diff --git a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java index 2e23fad5f..437ee8be5 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java +++ b/database/src/androidTest/java/com/firebase/ui/database/TestUtils.java @@ -19,7 +19,6 @@ public class TestUtils { private static final String APP_NAME = "firebaseui-tests"; private static final int TIMEOUT = 10000; - public static FirebaseApp getAppInstance(Context context) { try { return FirebaseApp.getInstance(APP_NAME); @@ -45,7 +44,7 @@ public static ChangeEventListener runAndWaitUntil( @Override public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, - int index, + int newIndex, int oldIndex) { semaphore.release(); } diff --git a/database/src/main/AndroidManifest.xml b/database/src/main/AndroidManifest.xml index b185d5bc7..c3b1964a0 100644 --- a/database/src/main/AndroidManifest.xml +++ b/database/src/main/AndroidManifest.xml @@ -4,10 +4,4 @@ - - - - diff --git a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java deleted file mode 100644 index c5ced5f67..000000000 --- a/database/src/main/java/com/firebase/ui/database/CachingObservableSnapshotArray.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firebase.ui.database; - -import android.support.annotation.NonNull; - -import com.google.firebase.database.DataSnapshot; - -import java.util.List; - -/** - * An extension of {@link ObservableSnapshotArray} that caches the result of {@link #getObject(int)} - * so that repeated calls for the same key are not expensive (unless the underlying snapshot has - * changed). - */ -public abstract class CachingObservableSnapshotArray extends ObservableSnapshotArray { - - private final CachingSnapshotParser mCache; - - /** - * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) - */ - public CachingObservableSnapshotArray(@NonNull Class tClass) { - this(new ClassSnapshotParser<>(tClass)); - } - - /** - * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) - */ - public CachingObservableSnapshotArray(@NonNull SnapshotParser parser) { - super(); - - mCache = new CachingSnapshotParser<>(parser); - setSnapshotParser(mCache); - } - - @Override - public T getObject(int index) { - return mCache.parseSnapshot(get(index)); - } - - protected abstract List getSnapshots(); - - @Override - protected void onDestroy() { - super.onDestroy(); - clearData(); - } - - /** @deprecated use {@link ObservableSnapshotArray#onDestroy()} instead */ - @Deprecated - protected void clearData() { - getSnapshots().clear(); - mCache.clearData(); - } - - protected DataSnapshot removeData(int index) { - DataSnapshot snapshot = getSnapshots().remove(index); - if (snapshot != null) { - mCache.invalidate(snapshot.getKey()); - } - - return snapshot; - } - - protected void updateData(int index, DataSnapshot snapshot) { - getSnapshots().set(index, snapshot); - mCache.invalidate(snapshot.getKey()); - } -} diff --git a/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java b/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java index 80164a840..b7df8a039 100644 --- a/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java +++ b/database/src/main/java/com/firebase/ui/database/CachingSnapshotParser.java @@ -9,8 +9,8 @@ */ public class CachingSnapshotParser extends BaseCachingSnapshotParser { - public CachingSnapshotParser(BaseSnapshotParser innerParser) { - super(innerParser); + public CachingSnapshotParser(BaseSnapshotParser parser) { + super(parser); } @Override diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 0dc328daf..951d4d5e4 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -14,6 +14,8 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; + import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; @@ -27,7 +29,7 @@ /** * This class implements a collection on top of a Firebase location. */ -public class FirebaseArray extends CachingObservableSnapshotArray +public class FirebaseArray extends ObservableSnapshotArray implements ChildEventListener, ValueEventListener { private Query mQuery; private List mSnapshots = new ArrayList<>(); @@ -82,24 +84,23 @@ public void onChildAdded(DataSnapshot snapshot, String previousChildKey) { } mSnapshots.add(index, snapshot); - - notifyListenersOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); + notifyOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); } @Override public void onChildChanged(DataSnapshot snapshot, String previousChildKey) { int index = getIndexForKey(snapshot.getKey()); - updateData(index, snapshot); - notifyListenersOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); + mSnapshots.set(index, snapshot); + notifyOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); } @Override public void onChildRemoved(DataSnapshot snapshot) { int index = getIndexForKey(snapshot.getKey()); - removeData(index); - notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); + mSnapshots.remove(index); + notifyOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); } @Override @@ -110,17 +111,17 @@ public void onChildMoved(DataSnapshot snapshot, String previousChildKey) { int newIndex = previousChildKey == null ? 0 : getIndexForKey(previousChildKey) + 1; mSnapshots.add(newIndex, snapshot); - notifyListenersOnChildChanged(ChangeEventType.MOVED, snapshot, newIndex, oldIndex); + notifyOnChildChanged(ChangeEventType.MOVED, snapshot, newIndex, oldIndex); } @Override public void onDataChange(DataSnapshot dataSnapshot) { - notifyListenersOnDataChanged(); + notifyOnDataChanged(); } @Override public void onCancelled(DatabaseError error) { - notifyListenersOnError(error); + notifyOnError(error); } private int getIndexForKey(String key) { @@ -135,21 +136,12 @@ private int getIndexForKey(String key) { throw new IllegalArgumentException("Key not found"); } - @Override - public DataSnapshot get(int i) { - return mSnapshots.get(i); - } - + @NonNull @Override protected List getSnapshots() { return mSnapshots; } - @Override - public int size() { - return mSnapshots.size(); - } - @Override public boolean equals(Object obj) { if (this == obj) return true; diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index 17eb9ef77..de5d88bd8 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -14,6 +14,7 @@ package com.firebase.ui.database; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -29,7 +30,7 @@ import java.util.List; import java.util.Map; -public class FirebaseIndexArray extends CachingObservableSnapshotArray +public class FirebaseIndexArray extends ObservableSnapshotArray implements ChangeEventListener { private static final String TAG = "FirebaseIndexArray"; @@ -106,20 +107,20 @@ protected void onDestroy() { } @Override - public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int index, int oldIndex) { + public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int newIndex, int oldIndex) { switch (type) { case ADDED: - onKeyAdded(snapshot, index); + onKeyAdded(snapshot, newIndex); break; case MOVED: - onKeyMoved(snapshot, index, oldIndex); + onKeyMoved(snapshot, newIndex, oldIndex); break; case CHANGED: // This is a no-op, we don't care when a key 'changes' since that should not // be a supported operation break; case REMOVED: - onKeyRemoved(snapshot, index); + onKeyRemoved(snapshot, newIndex); break; } } @@ -127,7 +128,7 @@ public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, int inde @Override public void onDataChanged() { if (mHasPendingMoveOrDelete || mKeySnapshots.isEmpty()) { - notifyListenersOnDataChanged(); + notifyOnDataChanged(); mHasPendingMoveOrDelete = false; } } @@ -137,6 +138,7 @@ public void onError(DatabaseError error) { Log.e(TAG, "A fatal error occurred retrieving the necessary keys to populate your adapter."); } + @NonNull @Override protected List getSnapshots() { return mDataSnapshots; @@ -193,12 +195,12 @@ private void onKeyMoved(DataSnapshot data, int index, int oldIndex) { // index instead of the old one. Unfortunately, this does mean move events will be // incorrectly ignored if our list is a subset of the key list e.g. a key has null data. if (isKeyAtIndex(key, oldIndex)) { - DataSnapshot snapshot = removeData(oldIndex); + DataSnapshot snapshot = mDataSnapshots.remove(oldIndex); int realIndex = returnOrFindIndexForKey(index, key); mHasPendingMoveOrDelete = true; mDataSnapshots.add(realIndex, snapshot); - notifyListenersOnChildChanged(ChangeEventType.MOVED, snapshot, index, oldIndex); + notifyOnChildChanged(ChangeEventType.MOVED, snapshot, realIndex, oldIndex); } } @@ -209,22 +211,12 @@ private void onKeyRemoved(DataSnapshot data, int index) { int realIndex = returnOrFindIndexForKey(index, key); if (isKeyAtIndex(key, realIndex)) { - DataSnapshot snapshot = removeData(realIndex); + DataSnapshot snapshot = mDataSnapshots.remove(realIndex); mHasPendingMoveOrDelete = true; - notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, realIndex, -1); + notifyOnChildChanged(ChangeEventType.REMOVED, snapshot, realIndex, -1); } } - @Override - public DataSnapshot get(int i) { - return mDataSnapshots.get(i); - } - - @Override - public int size() { - return mKeySnapshots.size(); - } - @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -272,18 +264,18 @@ public void onDataChange(DataSnapshot snapshot) { if (snapshot.getValue() != null) { if (isKeyAtIndex(key, index)) { // We already know about this data, just update it - updateData(index, snapshot); - notifyListenersOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); + mDataSnapshots.set(index, snapshot); + notifyOnChildChanged(ChangeEventType.CHANGED, snapshot, index, -1); } else { // We don't already know about this data, add it mDataSnapshots.add(index, snapshot); - notifyListenersOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); + notifyOnChildChanged(ChangeEventType.ADDED, snapshot, index, -1); } } else { if (isKeyAtIndex(key, index)) { // This data has disappeared, remove it - removeData(index); - notifyListenersOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); + mDataSnapshots.remove(index); + notifyOnChildChanged(ChangeEventType.REMOVED, snapshot, index, -1); } else { // Data does not exist Log.w(TAG, "Key not found at ref: " + snapshot.getRef()); @@ -293,15 +285,15 @@ public void onDataChange(DataSnapshot snapshot) { // In theory, we would only want to pop the queue if this listener was just added // i.e. `snapshot.value != null && isKeyAtIndex(...)`. However, if the developer makes a // mistake and `snapshot.value == null`, we will never pop the queue and - // `notifyListenersOnDataChanged()` will never be called. Thus, we pop the queue anytime + // `notifyOnDataChanged()` will never be called. Thus, we pop the queue anytime // an update is received. mKeysWithPendingUpdate.remove(key); - if (mKeysWithPendingUpdate.isEmpty()) notifyListenersOnDataChanged(); + if (mKeysWithPendingUpdate.isEmpty()) notifyOnDataChanged(); } @Override public void onCancelled(DatabaseError error) { - notifyListenersOnError(error); + notifyOnError(error); } } } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java index afc30ade6..b47edc8b8 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java @@ -143,7 +143,7 @@ void cleanup(LifecycleOwner source, Lifecycle.Event event) { @Override public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, - int index, + int newIndex, int oldIndex) { notifyDataSetChanged(); } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index a1e2f0a26..554df8b9d 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -147,20 +147,20 @@ void cleanup(LifecycleOwner source, Lifecycle.Event event) { @Override public void onChildChanged(ChangeEventType type, DataSnapshot snapshot, - int index, + int newIndex, int oldIndex) { switch (type) { case ADDED: - notifyItemInserted(index); + notifyItemInserted(newIndex); break; case CHANGED: - notifyItemChanged(index); + notifyItemChanged(newIndex); break; case REMOVED: - notifyItemRemoved(index); + notifyItemRemoved(newIndex); break; case MOVED: - notifyItemMoved(oldIndex, index); + notifyItemMoved(oldIndex, newIndex); break; default: throw new IllegalStateException("Incomplete case statement"); diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index 67d13d85b..d979e68da 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -16,14 +16,6 @@ */ public abstract class ObservableSnapshotArray extends BaseObservableSnapshotArray { - - /** - * Default constructor. Must set the snapshot parser before user. - */ - public ObservableSnapshotArray() { - super(); - } - /** * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular * class. @@ -32,7 +24,7 @@ public ObservableSnapshotArray() { * @see ClassSnapshotParser */ public ObservableSnapshotArray(@NonNull Class clazz) { - super(new ClassSnapshotParser<>(clazz)); + this(new ClassSnapshotParser<>(clazz)); } /** @@ -41,14 +33,14 @@ public ObservableSnapshotArray(@NonNull Class clazz) { * @param parser the {@link SnapshotParser} to use */ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { - super(parser); + super(new CachingSnapshotParser<>(parser)); } /** - * Use {@link BaseObservableSnapshotArray#notifyListenersOnError(Object)}. + * Use {@link BaseObservableSnapshotArray#notifyOnError(Object)}. */ @Deprecated protected void notifyListenersOnCancelled(DatabaseError error) { - notifyListenersOnError(error); + notifyOnError(error); } } diff --git a/firestore/build.gradle b/firestore/build.gradle index aae2e6965..886f747f2 100644 --- a/firestore/build.gradle +++ b/firestore/build.gradle @@ -24,18 +24,10 @@ android { dependencies { compile project(path: ':common') - - compile "com.android.support:recyclerview-v7:$supportLibraryVersion" - // Needed to override play services - compile "com.android.support:support-v4:$supportLibraryVersion" - compile "com.google.firebase:firebase-firestore:$firebaseVersion" - - compile "android.arch.lifecycle:runtime:$architectureVersion" - compile "android.arch.lifecycle:extensions:$architectureVersion" annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" androidTestCompile 'junit:junit:4.12' - androidTestCompile 'com.android.support.test:runner:1.0.0' - androidTestCompile 'com.android.support.test:rules:1.0.0' + androidTestCompile 'com.android.support.test:runner:1.0.1' + androidTestCompile 'com.android.support.test:rules:1.0.1' } diff --git a/firestore/src/main/AndroidManifest.xml b/firestore/src/main/AndroidManifest.xml index 3469dc429..7aa4ca568 100644 --- a/firestore/src/main/AndroidManifest.xml +++ b/firestore/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + diff --git a/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java index 650f91a40..8eb6c9f94 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/CachingSnapshotParser.java @@ -7,10 +7,11 @@ /** * Implementation of {@link BaseCachingSnapshotParser} for {@link DocumentSnapshot}. */ -public class CachingSnapshotParser extends BaseCachingSnapshotParser { +public class CachingSnapshotParser extends BaseCachingSnapshotParser + implements SnapshotParser { - public CachingSnapshotParser(BaseSnapshotParser innerParser) { - super(innerParser); + public CachingSnapshotParser(BaseSnapshotParser parser) { + super(parser); } @Override diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index 93876c1f0..f0678684c 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -1,6 +1,6 @@ package com.firebase.ui.firestore; -import android.util.Log; +import android.support.annotation.NonNull; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentChange; @@ -9,28 +9,49 @@ import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryListenOptions; import com.google.firebase.firestore.QuerySnapshot; import java.util.ArrayList; import java.util.List; - /** * Exposes a Firestore query as an observable list of objects. */ public class FirestoreArray extends ObservableSnapshotArray - implements EventListener { + implements EventListener { + private final Query mQuery; + private final QueryListenOptions mOptions; + private ListenerRegistration mRegistration; - private static final String TAG = "FirestoreArray"; + private final List mSnapshots = new ArrayList<>(); - private Query mQuery; - private ListenerRegistration mRegistration; + /** + * Create a new FirestoreArray that parses snapshots as members of a given class. + * + * @param query the Firebase location to watch for data changes + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + */ + public FirestoreArray(Query query, Class modelClass) { + this(query, new QueryListenOptions(), modelClass); + } - private List mSnapshots; - private CachingSnapshotParser mCache; + /** + * Create a new FirestoreArray with a custom {@link SnapshotParser}. + * + * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) + * @see FirestoreArray#FirestoreArray(Query, Class) + */ + public FirestoreArray(Query query, SnapshotParser parser) { + this(query, new QueryListenOptions(), parser); + } - public FirestoreArray(Query query, final Class modelClass) { - this(query, new SnapshotParser() { + /** + * @param query the options to use when listening for the query + * @see FirestoreArray#FirestoreArray(Query, Class) + */ + public FirestoreArray(Query query, QueryListenOptions options, final Class modelClass) { + this(query, options, new SnapshotParser() { @Override public T parseSnapshot(DocumentSnapshot snapshot) { return snapshot.toObject(modelClass); @@ -38,36 +59,39 @@ public T parseSnapshot(DocumentSnapshot snapshot) { }); } - public FirestoreArray(Query query, SnapshotParser parser) { - super(); - + /** + * @param query the options to use when listening for the query + * @see FirestoreArray#FirestoreArray(Query, SnapshotParser) + */ + public FirestoreArray(Query query, QueryListenOptions options, SnapshotParser parser) { + super(parser); mQuery = query; - mSnapshots = new ArrayList<>(); - mCache = new CachingSnapshotParser<>(parser); + mOptions = options; + } - setSnapshotParser(mCache); + @NonNull + @Override + protected List getSnapshots() { + return mSnapshots; } @Override protected void onCreate() { super.onCreate(); - - startListening(); + mRegistration = mQuery.addSnapshotListener(mOptions, this); } @Override protected void onDestroy() { super.onDestroy(); - - stopListening(); - mCache.clearData(); + mRegistration.remove(); + mRegistration = null; } @Override public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { if (e != null) { - Log.w(TAG, "Error in snapshot listener", e); - notifyListenersOnError(e); + notifyOnError(e); return; } @@ -87,76 +111,32 @@ public void onEvent(QuerySnapshot snapshots, FirebaseFirestoreException e) { } } - notifyListenersOnDataChanged(); - } - - @Override - public DocumentSnapshot get(int i) { - return mSnapshots.get(i); - } - - @Override - public int size() { - return mSnapshots.size(); + notifyOnDataChanged(); } private void onDocumentAdded(DocumentChange change) { - Log.d(TAG, "Added: " + change.getNewIndex()); - - // Add the document to the set mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyListenersOnChildChanged(ChangeEventType.ADDED, change.getDocument(), - change.getNewIndex(), -1); + notifyOnChildChanged(ChangeEventType.ADDED, change.getDocument(), change.getNewIndex(), -1); } private void onDocumentRemoved(DocumentChange change) { - Log.d(TAG, "Removed: " + change.getOldIndex()); - - // Invalidate snapshot cache (doc removed) - mCache.invalidate(change.getDocument().getId()); - - // Remove the document from the set mSnapshots.remove(change.getOldIndex()); - notifyListenersOnChildChanged(ChangeEventType.REMOVED, change.getDocument(), - -1, change.getOldIndex()); + notifyOnChildChanged( + ChangeEventType.REMOVED, change.getDocument(), -1, change.getOldIndex()); } private void onDocumentModified(DocumentChange change) { - // Invalidate snapshot cache (doc changed) - mCache.invalidate(change.getDocument().getId()); - - // Decide if the object was modified in place or if it moved if (change.getOldIndex() == change.getNewIndex()) { - Log.d(TAG, "Modified (inplace): " + change.getOldIndex()); - - mSnapshots.set(change.getOldIndex(), change.getDocument()); - notifyListenersOnChildChanged(ChangeEventType.CHANGED, change.getDocument(), + // Document modified only + mSnapshots.set(change.getNewIndex(), change.getDocument()); + notifyOnChildChanged(ChangeEventType.CHANGED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } else { - Log.d(TAG, "Modified (moved): " + change.getOldIndex() + " --> " + change.getNewIndex()); - + // Document moved and possibly also modified mSnapshots.remove(change.getOldIndex()); mSnapshots.add(change.getNewIndex(), change.getDocument()); - notifyListenersOnChildChanged(ChangeEventType.MOVED, change.getDocument(), + notifyOnChildChanged(ChangeEventType.MOVED, change.getDocument(), change.getNewIndex(), change.getOldIndex()); } } - - private void startListening() { - if (mRegistration != null) { - Log.d(TAG, "startListening: already listening."); - return; - } - - mRegistration = mQuery.addSnapshotListener(this); - } - - private void stopListening() { - if (mRegistration != null) { - mRegistration.remove(); - mRegistration = null; - } - - mSnapshots.clear(); - } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index d9b214d09..67172eeed 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -4,7 +4,6 @@ import android.arch.lifecycle.LifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; -import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -14,10 +13,11 @@ import com.google.firebase.firestore.Query; /** - * RecyclerView adapter that listenes to an {@link FirestoreArray} and displays data in real time, + * RecyclerView adapter that listens to a {@link FirestoreArray} and displays its data in real + * time. * - * @param model class, for parsing {@link DocumentSnapshot}. - * @param viewholder class. + * @param model class, for parsing {@link DocumentSnapshot}s. + * @param {@link RecyclerView.ViewHolder} class. */ public abstract class FirestoreRecyclerAdapter extends RecyclerView.Adapter @@ -25,91 +25,76 @@ public abstract class FirestoreRecyclerAdapter mArray; + private ObservableSnapshotArray mSnapshots; /** - * Create a new RecyclerView adapter to bind data from a Firestore query where each - * {@link DocumentSnapshot} is converted to the specified model class. - * - * See {@link #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner)}. + * Create a new RecyclerView adapter to bind data from an {@link ObservableSnapshotArray}. * - * @param query the Firestore query. - * @param modelClass the model class. + * @param snapshots the observable array of data from Firestore. + * @param owner (optional) a LifecycleOwner to observe. */ - public FirestoreRecyclerAdapter(Query query, Class modelClass) { - this(query, modelClass, null); + public FirestoreRecyclerAdapter(ObservableSnapshotArray snapshots, LifecycleOwner owner) { + mSnapshots = snapshots; + if (owner != null) { + owner.getLifecycle().addObserver(this); + } } /** - * Create a new RecyclerView adapter bound to a LifecycleOwner. - * - * See {@link #FirestoreRecyclerAdapter(Query, Class)} + * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) */ - public FirestoreRecyclerAdapter(Query query, Class modelClass, LifecycleOwner owner) { - mArray = new FirestoreArray<>(query, modelClass); - if (owner != null) { - owner.getLifecycle().addObserver(this); - } + public FirestoreRecyclerAdapter(ObservableSnapshotArray snapshots) { + this(snapshots, null); } /** - * Create a new RecyclerView adapter to bind data from a Firestore query where each - * {@link DocumentSnapshot} is parsed by the specified parser. - * - * See {@link #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner)}. + * Create a new RecyclerView adapter to bind data from a Firestore query where each {@link + * DocumentSnapshot} is converted to the specified model class. * - * @param query the Firestore query. - * @param parser the snapshot parser. + * @param query the Firestore query. + * @param modelClass the model class. + * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) */ - public FirestoreRecyclerAdapter(Query query, SnapshotParser parser) { - this(query, parser, null); + public FirestoreRecyclerAdapter(Query query, Class modelClass, LifecycleOwner owner) { + this(new FirestoreArray<>(query, modelClass), owner); } /** - * Create a new RecyclerView adapter bound to a LifecycleOwner. - * - * See {@link #FirestoreRecyclerAdapter(Query, SnapshotParser)}. + * @see #FirestoreRecyclerAdapter(Query, Class, LifecycleOwner) */ - public FirestoreRecyclerAdapter(Query query, SnapshotParser parser, LifecycleOwner owner) { - mArray = new FirestoreArray(query, parser); - if (owner != null) { - owner.getLifecycle().addObserver(this); - } + public FirestoreRecyclerAdapter(Query query, Class modelClass) { + this(query, modelClass, null); } /** - * Create a new RecyclerView adapter to bind data from an {@link ObservableSnapshotArray}. + * Create a new RecyclerView adapter to bind data from a Firestore query where each {@link + * DocumentSnapshot} is parsed by the specified parser. * - * @param array the observable array of data from Firestore. - * @param owner (optional) a LifecycleOwner to observe. + * @param query the Firestore query. + * @param parser the snapshot parser. + * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) */ - public FirestoreRecyclerAdapter(ObservableSnapshotArray array, - @Nullable LifecycleOwner owner) { - mArray = array; - if (owner != null) { - owner.getLifecycle().addObserver(this); - } + public FirestoreRecyclerAdapter(Query query, SnapshotParser parser, LifecycleOwner owner) { + this(new FirestoreArray<>(query, parser), owner); } /** - * Called when data has been added/changed and an item needs to be displayed. - * - * @param vh the view to populate. - * @param i the position in the list of the view being populated. - * @param model the model object containing the data that should be used to populate the view. + * @see #FirestoreRecyclerAdapter(Query, SnapshotParser, LifecycleOwner) */ - protected abstract void onBindViewHolder(VH vh, int i, T model); + public FirestoreRecyclerAdapter(Query query, SnapshotParser parser) { + this(query, parser, null); + } @OnLifecycleEvent(Lifecycle.Event.ON_START) public void startListening() { - if (!mArray.isListening(this)) { - mArray.addChangeEventListener(this); + if (!mSnapshots.isListening(this)) { + mSnapshots.addChangeEventListener(this); } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) public void stopListening() { - mArray.removeChangeEventListener(this); + mSnapshots.removeChangeEventListener(this); notifyDataSetChanged(); } @@ -118,33 +103,37 @@ void cleanup(LifecycleOwner source) { source.getLifecycle().removeObserver(this); } - @Override - public void onBindViewHolder(VH vh, int i) { - T model = mArray.getObject(i); - onBindViewHolder(vh, i, model); + public ObservableSnapshotArray getSnapshots() { + return mSnapshots; + } + + public T getItem(int position) { + return mSnapshots.getObject(position); } @Override public int getItemCount() { - return mArray.size(); + return mSnapshots.size(); } @Override public void onChildChanged(ChangeEventType type, DocumentSnapshot snapshot, int newIndex, int oldIndex) { - switch (type) { case ADDED: notifyItemInserted(newIndex); break; - case REMOVED: - notifyItemRemoved(oldIndex); - break; case CHANGED: notifyItemChanged(newIndex); break; + case REMOVED: + notifyItemRemoved(oldIndex); + break; case MOVED: notifyItemMoved(oldIndex, newIndex); + break; + default: + throw new IllegalStateException("Incomplete case statement"); } } @@ -156,4 +145,19 @@ public void onDataChanged() { public void onError(FirebaseFirestoreException e) { Log.w(TAG, "onError", e); } + + @Override + public void onBindViewHolder(VH holder, int position) { + onBindViewHolder(holder, position, getItem(position)); + } + + /** + * Called when data has been added/changed and an item needs to be displayed. + * + * @param holder the view to populate. + * @param position the position in the list of the view being populated. + * @param model the model object containing the data that should be used to populate the + * view. + */ + protected abstract void onBindViewHolder(VH holder, int position, T model); } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java index 4c08ffe34..219ad2b15 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/ObservableSnapshotArray.java @@ -2,8 +2,8 @@ import android.support.annotation.NonNull; +import com.firebase.ui.common.BaseCachingSnapshotParser; import com.firebase.ui.common.BaseObservableSnapshotArray; -import com.firebase.ui.common.BaseSnapshotParser; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; @@ -12,15 +12,10 @@ */ public abstract class ObservableSnapshotArray extends BaseObservableSnapshotArray { - - public ObservableSnapshotArray() { - super(); - } - /** - * See {@link BaseObservableSnapshotArray#BaseObservableSnapshotArray(BaseSnapshotParser)} + * @see BaseObservableSnapshotArray#BaseObservableSnapshotArray(BaseCachingSnapshotParser) */ public ObservableSnapshotArray(@NonNull SnapshotParser parser) { - super(parser); + super(new CachingSnapshotParser<>(parser)); } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f4c49141aa406e46f25ac5543d51e47c4bbebee..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 100644 GIT binary patch delta 51 zcmdnFnt97=<_)?>_*obNyxBRtR)5G9W?*0t-0XNnN01ZFom_U+md*YMP=)~jwbBnZ delta 56 zcmdn8ntA7H<_)?>M6T$1T{C522=Hd-IQn?r3ke1W2C>aHM|1=^;oQlES8dr`jsRsC E0AkY Date: Tue, 12 Sep 2017 15:23:12 -0700 Subject: [PATCH 18/37] Replace adapter constructors with options (#7) --- .../firestore/FirestoreChatActivity.java | 8 +- .../database/realtime/ChatActivity.java | 17 +- .../database/realtime/ChatIndexActivity.java | 17 +- .../com/firebase/ui/common/Preconditions.java | 13 ++ database/build.gradle | 3 + .../database/FirebaseArrayOfObjectsTest.java | 2 +- .../ui/database/FirebaseArrayTest.java | 2 +- .../FirebaseIndexArrayOfObjectsTest.java | 2 +- .../ui/database/FirebaseIndexArrayTest.java | 2 +- .../firebase/ui/database/FirebaseArray.java | 13 +- .../ui/database/FirebaseIndexArray.java | 13 +- .../ui/database/FirebaseIndexListAdapter.java | 69 ------- .../FirebaseIndexRecyclerAdapter.java | 81 -------- .../ui/database/FirebaseListAdapter.java | 91 +-------- .../ui/database/FirebaseListOptions.java | 159 +++++++++++++++ .../ui/database/FirebaseRecyclerAdapter.java | 87 +------- .../ui/database/FirebaseRecyclerOptions.java | 187 ++++++++++++++++++ .../ui/database/ObservableSnapshotArray.java | 10 - firestore/build.gradle | 3 + .../ui/firestore/FirestoreArrayTest.java | 4 +- .../ui/firestore/ClassSnapshotParser.java | 23 +++ .../firebase/ui/firestore/FirestoreArray.java | 40 +--- .../firestore/FirestoreRecyclerAdapter.java | 61 +----- .../firestore/FirestoreRecyclerOptions.java | 135 +++++++++++++ 24 files changed, 589 insertions(+), 453 deletions(-) delete mode 100644 database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java delete mode 100644 database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java create mode 100644 database/src/main/java/com/firebase/ui/database/FirebaseListOptions.java create mode 100644 database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/ClassSnapshotParser.java create mode 100644 firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java diff --git a/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java index 1569ab13a..475d7b14c 100644 --- a/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/firestore/FirestoreChatActivity.java @@ -15,6 +15,7 @@ import android.widget.Toast; import com.firebase.ui.firestore.FirestoreRecyclerAdapter; +import com.firebase.ui.firestore.FirestoreRecyclerOptions; import com.firebase.uidemo.R; import com.firebase.uidemo.database.ChatHolder; import com.google.android.gms.tasks.OnCompleteListener; @@ -68,7 +69,12 @@ protected void onCreate(Bundle savedInstanceState) { Query query = mFirestore.collection("chats").orderBy("timestamp").limit(50); LinearLayoutManager manager = new LinearLayoutManager(this); - mAdapter = new FirestoreRecyclerAdapter(query, Chat.class) { + + FirestoreRecyclerOptions options = new FirestoreRecyclerOptions.Builder() + .setQuery(query, Chat.class) + .build(); + + mAdapter = new FirestoreRecyclerAdapter(options) { @Override public void onBindViewHolder(ChatHolder holder, int position, Chat model) { holder.bind(model); diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java index 68cc31497..34abcedcc 100644 --- a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java @@ -29,6 +29,7 @@ import android.widget.Toast; import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerOptions; import com.firebase.uidemo.R; import com.firebase.uidemo.database.ChatHolder; import com.firebase.uidemo.util.SignInResultNotifier; @@ -140,12 +141,16 @@ public void onItemRangeInserted(int positionStart, int itemCount) { protected FirebaseRecyclerAdapter getAdapter() { Query lastFifty = mChatRef.limitToLast(50); - return new FirebaseRecyclerAdapter( - Chat.class, - R.layout.message, - ChatHolder.class, - lastFifty, - this) { + + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() + .setViewHolder(R.layout.message, ChatHolder.class) + .setQuery(lastFifty, Chat.class) + .setLifecycleOwner(this) + .build(); + + return new FirebaseRecyclerAdapter(options) { + @Override public void populateViewHolder(ChatHolder holder, Chat chat, int position) { holder.bind(chat); diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java index b4c90048c..74b92e9b6 100644 --- a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java @@ -2,8 +2,8 @@ import android.view.View; -import com.firebase.ui.database.FirebaseIndexRecyclerAdapter; import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerOptions; import com.firebase.uidemo.R; import com.firebase.uidemo.database.ChatHolder; import com.google.firebase.auth.FirebaseAuth; @@ -33,13 +33,14 @@ protected FirebaseRecyclerAdapter getAdapter() { .child("chatIndices") .child(FirebaseAuth.getInstance().getCurrentUser().getUid()); - return new FirebaseIndexRecyclerAdapter( - Chat.class, - R.layout.message, - ChatHolder.class, - mChatIndicesRef.limitToLast(50), - mChatRef, - this) { + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() + .setIndexedQuery(mChatIndicesRef.limitToFirst(50), mChatRef, Chat.class) + .setViewHolder(R.layout.message, ChatHolder.class) + .setLifecycleOwner(this) + .build(); + + return new FirebaseRecyclerAdapter(options) { @Override public void populateViewHolder(ChatHolder holder, Chat chat, int position) { holder.bind(chat); diff --git a/common/src/main/java/com/firebase/ui/common/Preconditions.java b/common/src/main/java/com/firebase/ui/common/Preconditions.java index 24bed48e7..11a452242 100644 --- a/common/src/main/java/com/firebase/ui/common/Preconditions.java +++ b/common/src/main/java/com/firebase/ui/common/Preconditions.java @@ -7,8 +7,21 @@ */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class Preconditions { + public static T checkNotNull(T o) { if (o == null) throw new IllegalArgumentException("Argument cannot be null."); return o; } + + public static void assertNull(Object object, String message) { + if (object != null) { + throw new RuntimeException(message); + } + } + + public static void assertNonNull(Object object, String message) { + if (object == null) { + throw new RuntimeException(message); + } + } } diff --git a/database/build.gradle b/database/build.gradle index 70a0740aa..e504b2d45 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -25,6 +25,9 @@ android { dependencies { compile project(path: ':common') compile "com.google.firebase:firebase-database:$firebaseVersion" + + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" androidTestCompile 'junit:junit:4.12' diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java index 4d8c9ea4f..a7cf7e632 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayOfObjectsTest.java @@ -46,7 +46,7 @@ public void setUp() throws Exception { .getReference() .child("firebasearray") .child("objects"); - mArray = new FirebaseArray<>(mRef, Bean.class); + mArray = new FirebaseArray<>(mRef, new ClassSnapshotParser<>(Bean.class)); mRef.removeValue(); mListener = runAndWaitUntil(mArray, new Runnable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java index ae5b75a81..2ff2958e2 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseArrayTest.java @@ -43,7 +43,7 @@ public class FirebaseArrayTest { public void setUp() throws Exception { FirebaseApp app = getAppInstance(InstrumentationRegistry.getContext()); mRef = FirebaseDatabase.getInstance(app).getReference().child("firebasearray"); - mArray = new FirebaseArray<>(mRef, Integer.class); + mArray = new FirebaseArray<>(mRef, new ClassSnapshotParser<>(Integer.class)); mRef.removeValue(); mListener = runAndWaitUntil(mArray, new Runnable() { @Override diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java index 3377d5c56..ff8a2b612 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayOfObjectsTest.java @@ -46,7 +46,7 @@ public void setUp() throws Exception { mRef = databaseInstance.getReference().child("firebasearray").child("objects"); mKeyRef = databaseInstance.getReference().child("firebaseindexarray").child("objects"); - mArray = new FirebaseIndexArray<>(mKeyRef, mRef, Bean.class); + mArray = new FirebaseIndexArray<>(mKeyRef, mRef, new ClassSnapshotParser<>(Bean.class)); mRef.removeValue(); mKeyRef.removeValue(); diff --git a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java index 1f4c2d444..42f4b04a7 100644 --- a/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java +++ b/database/src/androidTest/java/com/firebase/ui/database/FirebaseIndexArrayTest.java @@ -47,7 +47,7 @@ public void setUp() throws Exception { mRef = databaseInstance.getReference().child("firebasearray"); mKeyRef = databaseInstance.getReference().child("firebaseindexarray"); - mArray = new FirebaseIndexArray<>(mKeyRef, mRef, Integer.class); + mArray = new FirebaseIndexArray<>(mKeyRef, mRef, new ClassSnapshotParser<>(Integer.class)); mRef.removeValue(); mKeyRef.removeValue(); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java index 951d4d5e4..c7b7403dc 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseArray.java @@ -35,23 +35,12 @@ public class FirebaseArray extends ObservableSnapshotArray private List mSnapshots = new ArrayList<>(); /** - * Create a new FirebaseArray that parses snapshots as members of a given class. + * Create a new FirebaseArray with a custom {@link SnapshotParser}. * * @param query The Firebase location to watch for data changes. Can also be a slice of a * location, using some combination of {@code limit()}, {@code startAt()}, and * {@code endAt()}. - * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) - */ - public FirebaseArray(Query query, Class tClass) { - super(tClass); - init(query); - } - - /** - * Create a new FirebaseArray with a custom {@link SnapshotParser}. - * * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) - * @see FirebaseArray#FirebaseArray(Query, Class) */ public FirebaseArray(Query query, SnapshotParser parser) { super(parser); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java index de5d88bd8..752aa4f41 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseIndexArray.java @@ -54,25 +54,14 @@ public class FirebaseIndexArray extends ObservableSnapshotArray private boolean mHasPendingMoveOrDelete; /** - * Create a new FirebaseIndexArray that parses snapshots as members of a given class. + * Create a new FirebaseIndexArray with a custom {@link SnapshotParser}. * * @param keyQuery The Firebase location containing the list of keys to be found in {@code * dataRef}. Can also be a slice of a location, using some combination of {@code * limit()}, {@code startAt()}, and {@code endAt()}. * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code * keyQuery}'s location represents a list item in the {@link RecyclerView}. - * @see ObservableSnapshotArray#ObservableSnapshotArray(Class) - */ - public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, Class tClass) { - super(tClass); - init(keyQuery, dataRef); - } - - /** - * Create a new FirebaseIndexArray with a custom {@link SnapshotParser}. - * * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) - * @see FirebaseIndexArray#FirebaseIndexArray(Query, DatabaseReference, Class) */ public FirebaseIndexArray(Query keyQuery, DatabaseReference dataRef, SnapshotParser parser) { super(parser); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java deleted file mode 100644 index e678b3e77..000000000 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexListAdapter.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.firebase.ui.database; - -import android.arch.lifecycle.LifecycleOwner; -import android.content.Context; -import android.support.annotation.LayoutRes; -import android.widget.ListView; - -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.Query; - -public abstract class FirebaseIndexListAdapter extends FirebaseListAdapter { - /** - * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the - * model class - * @param keyQuery The Firebase location containing the list of keys to be found in {@code - * dataRef}. Can also be a slice of a location, using some combination of {@code - * limit()}, {@code startAt()}, and {@code endAt()}. Note, this can also be a - * {@link DatabaseReference}. - * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code - * keyQuery}'s location represents a list item in the {@link ListView}. - * @see FirebaseListAdapter#FirebaseListAdapter(Context, ObservableSnapshotArray, int, - * LifecycleOwner) - */ - public FirebaseIndexListAdapter(Context context, - SnapshotParser parser, - @LayoutRes int modelLayout, - Query keyQuery, - DatabaseReference dataRef, - LifecycleOwner owner) { - super(context, new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout, owner); - } - - /** - * @see #FirebaseIndexListAdapter(Context, SnapshotParser, int, Query, DatabaseReference, - * LifecycleOwner) - */ - public FirebaseIndexListAdapter(Context context, - SnapshotParser parser, - @LayoutRes int modelLayout, - Query keyQuery, - DatabaseReference dataRef) { - super(context, new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout); - } - - /** - * @see #FirebaseIndexListAdapter(Context, SnapshotParser, int, Query, DatabaseReference, - * LifecycleOwner) - */ - public FirebaseIndexListAdapter(Context context, - Class modelClass, - @LayoutRes int modelLayout, - Query keyQuery, - DatabaseReference dataRef, - LifecycleOwner owner) { - this(context, new ClassSnapshotParser<>(modelClass), modelLayout, keyQuery, dataRef, owner); - } - - /** - * @see #FirebaseIndexListAdapter(Context, SnapshotParser, int, Query, DatabaseReference) - */ - public FirebaseIndexListAdapter(Context context, - Class modelClass, - @LayoutRes int modelLayout, - Query keyQuery, - DatabaseReference dataRef) { - this(context, new ClassSnapshotParser<>(modelClass), modelLayout, keyQuery, dataRef); - } -} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java deleted file mode 100644 index 268ce7563..000000000 --- a/database/src/main/java/com/firebase/ui/database/FirebaseIndexRecyclerAdapter.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.firebase.ui.database; - -import android.arch.lifecycle.LifecycleOwner; -import android.support.annotation.LayoutRes; -import android.support.v7.widget.RecyclerView; - -import com.google.firebase.database.DataSnapshot; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.Query; - -public abstract class FirebaseIndexRecyclerAdapter - extends FirebaseRecyclerAdapter { - /** - * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the - * model class - * @param keyQuery The Firebase location containing the list of keys to be found in {@code - * dataRef}. Can also be a slice of a location, using some combination of {@code - * limit()}, {@code startAt()}, and {@code endAt()}. Note, this can also be a - * {@link DatabaseReference}. - * @param dataRef The Firebase location to watch for data changes. Each key key found at {@code - * keyQuery}'s location represents a list item in the {@link RecyclerView}. - * @see FirebaseRecyclerAdapter#FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class, - * LifecycleOwner) - */ - public FirebaseIndexRecyclerAdapter(SnapshotParser parser, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query keyQuery, - DatabaseReference dataRef, - LifecycleOwner owner) { - super(new FirebaseIndexArray<>(keyQuery, dataRef, parser), - modelLayout, - viewHolderClass, - owner); - } - - /** - * @see #FirebaseIndexRecyclerAdapter(SnapshotParser, int, Class, Query, DatabaseReference, - * LifecycleOwner) - */ - public FirebaseIndexRecyclerAdapter(SnapshotParser parser, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query keyQuery, - DatabaseReference dataRef) { - super(new FirebaseIndexArray<>(keyQuery, dataRef, parser), modelLayout, viewHolderClass); - } - - /** - * @see #FirebaseIndexRecyclerAdapter(SnapshotParser, int, Class, Query, DatabaseReference, - * LifecycleOwner) - */ - public FirebaseIndexRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query keyQuery, - DatabaseReference dataRef, - LifecycleOwner owner) { - this(new ClassSnapshotParser<>(modelClass), - modelLayout, - viewHolderClass, - keyQuery, - dataRef, - owner); - } - - /** - * @see #FirebaseIndexRecyclerAdapter(SnapshotParser, int, Class, Query, DatabaseReference) - */ - public FirebaseIndexRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query keyQuery, - DatabaseReference dataRef) { - this(new ClassSnapshotParser<>(modelClass), - modelLayout, - viewHolderClass, - keyQuery, - dataRef); - } -} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java index b47edc8b8..3b83a3184 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListAdapter.java @@ -1,24 +1,18 @@ package com.firebase.ui.database; -import android.app.Activity; import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; -import android.content.Context; -import android.support.annotation.LayoutRes; -import android.support.v4.app.FragmentActivity; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.ListView; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.Query; /** * This class is a generic way of backing an Android {@link android.widget.ListView} with a Firebase @@ -34,87 +28,16 @@ public abstract class FirebaseListAdapter extends BaseAdapter implements FirebaseAdapter { private static final String TAG = "FirebaseListAdapter"; - protected final Context mContext; protected final ObservableSnapshotArray mSnapshots; protected final int mLayout; - /** - * @param context The {@link Activity} containing the {@link ListView} - * @param snapshots The data used to populate the adapter - * @param modelLayout This is the layout 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 modelClass. - * @param owner the lifecycle owner used to automatically listen and cleanup after {@link - * FragmentActivity#onStart()} and {@link FragmentActivity#onStop()} events - * reflectively. - */ - public FirebaseListAdapter(Context context, - ObservableSnapshotArray snapshots, - @LayoutRes int modelLayout, - LifecycleOwner owner) { - mContext = context; - mSnapshots = snapshots; - mLayout = modelLayout; - - if (owner != null) { owner.getLifecycle().addObserver(this); } - } - - /** - * @see #FirebaseListAdapter(Context, ObservableSnapshotArray, int, LifecycleOwner) - */ - public FirebaseListAdapter(Context context, - ObservableSnapshotArray snapshots, - @LayoutRes int modelLayout) { - this(context, snapshots, modelLayout, null); - startListening(); - } - - /** - * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model - * class - * @param query The Firebase location to watch for data changes. Can also be a slice of a - * location, using some combination of {@code limit()}, {@code startAt()}, and - * {@code endAt()}. Note, this can also be a {@link DatabaseReference}. - * @see #FirebaseListAdapter(Context, ObservableSnapshotArray, int) - */ - public FirebaseListAdapter(Context context, - SnapshotParser parser, - @LayoutRes int modelLayout, - Query query) { - this(context, new FirebaseArray<>(query, parser), modelLayout); - } + public FirebaseListAdapter(FirebaseListOptions options) { + mSnapshots = options.getSnapshots(); + mLayout = options.getLayout(); - /** - * @see #FirebaseListAdapter(Context, SnapshotParser, int, Query) - * @see #FirebaseListAdapter(Context, ObservableSnapshotArray, int, LifecycleOwner) - */ - public FirebaseListAdapter(Context context, - SnapshotParser parser, - @LayoutRes int modelLayout, - Query query, - LifecycleOwner owner) { - this(context, new FirebaseArray<>(query, parser), modelLayout, owner); - } - - /** - * @see #FirebaseListAdapter(Context, SnapshotParser, int, Query) - */ - public FirebaseListAdapter(Context context, - Class modelClass, - @LayoutRes int modelLayout, - Query query) { - this(context, new ClassSnapshotParser<>(modelClass), modelLayout, query); - } - - /** - * @see #FirebaseListAdapter(Context, SnapshotParser, int, Query, LifecycleOwner) - */ - public FirebaseListAdapter(Context context, - Class modelClass, - @LayoutRes int modelLayout, - Query query, - LifecycleOwner owner) { - this(context, new ClassSnapshotParser<>(modelClass), modelLayout, query, owner); + if (options.getOwner() != null) { + options.getOwner().getLifecycle().addObserver(this); + } } @Override @@ -181,7 +104,7 @@ public long getItemId(int i) { @Override public View getView(int position, View view, ViewGroup viewGroup) { if (view == null) { - view = LayoutInflater.from(mContext).inflate(mLayout, viewGroup, false); + view = LayoutInflater.from(viewGroup.getContext()).inflate(mLayout, viewGroup, false); } T model = getItem(position); diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseListOptions.java b/database/src/main/java/com/firebase/ui/database/FirebaseListOptions.java new file mode 100644 index 000000000..ce7aec14b --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/FirebaseListOptions.java @@ -0,0 +1,159 @@ +package com.firebase.ui.database; + +import android.arch.lifecycle.LifecycleOwner; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; + +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.Query; + +import static com.firebase.ui.common.Preconditions.assertNonNull; +import static com.firebase.ui.common.Preconditions.assertNull; + +/** + * Options to configure a {@link FirebaseListAdapter}. + * + * @see Builder + */ +public class FirebaseListOptions { + + private static final String ERR_SNAPSHOTS_SET = "Snapshot array already set. " + + "Call only one of setSnapshotArray, setQuery, or setIndexedQuery."; + + private final ObservableSnapshotArray mSnapshots; + private final @LayoutRes int mLayout; + private final LifecycleOwner mOwner; + + private FirebaseListOptions(ObservableSnapshotArray snapshots, + @LayoutRes int layout, + LifecycleOwner owner) { + mSnapshots = snapshots; + mLayout = layout; + mOwner = owner; + } + + /** + * Get the {@link ObservableSnapshotArray} to observe. + */ + public ObservableSnapshotArray getSnapshots() { + return mSnapshots; + } + + /** + * Get the resource ID of the layout file for a list item. + */ + @LayoutRes + public int getLayout() { + return mLayout; + } + + /** + * Get the (optional) {@link LifecycleOwner}. + */ + @Nullable + public LifecycleOwner getOwner() { + return mOwner; + } + + /** + * Builder for {@link FirebaseListOptions}. + * @param the model class for the {@link FirebaseListAdapter}. + */ + public static class Builder { + + private ObservableSnapshotArray mSnapshots; + private @LayoutRes Integer mLayout; + private LifecycleOwner mOwner; + + /** + * Directly set the {@link ObservableSnapshotArray} to observe. + * + * Do not call this method after calling {@code setQuery}. + */ + public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = snapshots; + return this; + } + + /** + * Set the query to listen on and a {@link SnapshotParser} to parse data snapshots. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, SnapshotParser parser) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = new FirebaseArray(query, parser); + return this; + } + + /** + * Set the query to listen on and a {@link Class} to which data snapshots should be + * converted. Do not call this method after calling + * {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, Class modelClass) { + return setQuery(query, new ClassSnapshotParser(modelClass)); + } + + /** + * Set an indexed query to listen on and a {@link SnapshotParser} to parse data snapshots. + * The keyQuery is used to find a list of IDs, which are then queried at the dataRef. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + SnapshotParser parser) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = new FirebaseIndexArray(keyQuery, dataRef, parser); + return this; + } + + /** + * Set an indexed query to listen on and a {@link Class} to which data snapshots should + * be converted. The keyQuery is used to find a list of keys, which are then queried + * at the dataRef. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + Class modelClass) { + return setIndexedQuery(keyQuery, dataRef, new ClassSnapshotParser(modelClass)); + } + + /** + * Set the resource ID for the item layout. + */ + public Builder setLayout(@LayoutRes int layout) { + mLayout = layout; + return this; + } + + /** + * Set the optional {@link LifecycleOwner}. Listening will stop/start after the + * appropriate lifecycle events. + */ + public Builder setLifecycleOwner(LifecycleOwner owner) { + mOwner = owner; + return this; + } + + /** + * Build a {@link FirebaseListOptions} from the provided arguments. + */ + public FirebaseListOptions build() { + assertNonNull(mSnapshots, "Snapshot array cannot be null. " + + "Call setQuery or setSnapshotArray."); + assertNonNull(mLayout, "Layout cannot be null. " + + "Call setLayout."); + + return new FirebaseListOptions<>(mSnapshots, mLayout, mOwner); + } + + } +} diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index 554df8b9d..815dd4ab6 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -3,8 +3,6 @@ import android.arch.lifecycle.Lifecycle; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.OnLifecycleEvent; -import android.support.annotation.LayoutRes; -import android.support.v4.app.FragmentActivity; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -15,7 +13,6 @@ import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.Query; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -41,83 +38,17 @@ public abstract class FirebaseRecyclerAdapter snapshots, - @LayoutRes int modelLayout, - Class viewHolderClass, - LifecycleOwner owner) { - mSnapshots = snapshots; - mViewHolderClass = viewHolderClass; - mModelLayout = modelLayout; - - if (owner != null) { owner.getLifecycle().addObserver(this); } - } - - /** - * @see #FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class, LifecycleOwner) - */ - public FirebaseRecyclerAdapter(ObservableSnapshotArray snapshots, - @LayoutRes int modelLayout, - Class viewHolderClass) { - this(snapshots, modelLayout, viewHolderClass, null); - startListening(); - } - - /** - * @param parser a custom {@link SnapshotParser} to convert a {@link DataSnapshot} to the model - * class - * @param query The Firebase location to watch for data changes. Can also be a slice of a - * location, using some combination of {@code limit()}, {@code startAt()}, and - * {@code endAt()}. Note, this can also be a {@link DatabaseReference}. - * @see #FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class) - */ - public FirebaseRecyclerAdapter(SnapshotParser parser, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query query) { - this(new FirebaseArray<>(query, parser), modelLayout, viewHolderClass); - } - - /** - * @see #FirebaseRecyclerAdapter(SnapshotParser, int, Class, Query) - * @see #FirebaseRecyclerAdapter(ObservableSnapshotArray, int, Class, LifecycleOwner) - */ - public FirebaseRecyclerAdapter(SnapshotParser parser, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query query, - LifecycleOwner owner) { - this(new FirebaseArray<>(query, parser), modelLayout, viewHolderClass, owner); - } - - /** - * @see #FirebaseRecyclerAdapter(SnapshotParser, int, Class, Query) - */ - public FirebaseRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query query) { - this(new ClassSnapshotParser<>(modelClass), modelLayout, viewHolderClass, query); - } + public FirebaseRecyclerAdapter(FirebaseRecyclerOptions options) { + mSnapshots = options.getSnapshots(); + mViewHolderClass = options.getViewHolderClass(); + mModelLayout = options.getModelLayout(); - /** - * @see #FirebaseRecyclerAdapter(SnapshotParser, int, Class, Query, LifecycleOwner) - */ - public FirebaseRecyclerAdapter(Class modelClass, - @LayoutRes int modelLayout, - Class viewHolderClass, - Query query, - LifecycleOwner owner) { - this(new ClassSnapshotParser<>(modelClass), modelLayout, viewHolderClass, query, owner); + if (options.getOwner() != null) { + options.getOwner().getLifecycle().addObserver(this); + } } @Override diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java new file mode 100644 index 000000000..fbe862af4 --- /dev/null +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java @@ -0,0 +1,187 @@ +package com.firebase.ui.database; + +import android.arch.lifecycle.LifecycleOwner; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; + +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.Query; + +import static com.firebase.ui.common.Preconditions.assertNonNull; +import static com.firebase.ui.common.Preconditions.assertNull; + +/** + * Options to configure a {@link FirebaseRecyclerAdapter}. + * + * @see Builder + */ +public class FirebaseRecyclerOptions { + + private static final String ERR_SNAPSHOTS_SET = "Snapshot array already set. " + + "Call only one of setSnapshotArray, setQuery, or setIndexedQuery."; + + private static final String ERR_SNAPSHOTS_NULL = "Snapshot array cannot be null. " + + "Call one of setSnapshotArray, setQuery, or setIndexedQuery."; + + private static final String ERR_VIEWHOLDER_NULL = "View holder class cannot be null. " + + "Call setViewHolder."; + + private final ObservableSnapshotArray mSnapshots; + private final Class mViewHolderClass; + private final int mModelLayout; + private final LifecycleOwner mOwner; + + private FirebaseRecyclerOptions(ObservableSnapshotArray snapshots, + Class viewHolderClass, + @LayoutRes int modelLayout, + LifecycleOwner owner) { + mSnapshots = snapshots; + mViewHolderClass = viewHolderClass; + mModelLayout = modelLayout; + mOwner = owner; + } + + /** + * Get the {@link ObservableSnapshotArray} to listen to. + */ + public ObservableSnapshotArray getSnapshots() { + return mSnapshots; + } + + /** + * Get the class of the {@link android.support.v7.widget.RecyclerView.ViewHolder} for + * each RecyclerView item. + */ + public Class getViewHolderClass() { + return mViewHolderClass; + } + + /** + * Get the resource ID of the layout for each RecyclerView item. + */ + @LayoutRes + public int getModelLayout() { + return mModelLayout; + } + + /** + * Get the (optional) LifecycleOwner. Listening will start/stop after the appropriate + * lifecycle events. + */ + @Nullable + public LifecycleOwner getOwner() { + return mOwner; + } + + /** + * Builder for a {@link FirebaseRecyclerOptions}. + * + * @param the model class for the {@link FirebaseRecyclerAdapter}. + * @param the ViewHolder class for the {@link FirebaseRecyclerAdapter}. + */ + public static class Builder { + + private ObservableSnapshotArray mSnapshots; + private Class mViewHolderClass; + private int mModelLayout; + private LifecycleOwner mOwner; + + /** + * Directly set the {@link ObservableSnapshotArray} to be listened to. + * + * Do not call this method after calling {@code setQuery}. + */ + public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = snapshots; + return this; + } + + /** + * Set the Firebase query to listen to, along with a {@link SnapshotParser} to + * parse snapshots into model objects. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, SnapshotParser snapshotParser) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = new FirebaseArray(query, snapshotParser); + return this; + } + + /** + * Set the Firebase query to listen to, along with a {@link Class} to which snapshots + * should be parsed. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, Class modelClass) { + return setQuery(query, new ClassSnapshotParser(modelClass)); + } + + + /** + * Set an indexed Firebase query to listen to, along with a {@link SnapshotParser} to + * parse snapshots into model objects. Keys are identified by the {@code keyQuery} and then + * data is fetched using those keys from the {@code dataRef}. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + SnapshotParser snapshotParser) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = new FirebaseIndexArray(keyQuery, dataRef, snapshotParser); + return this; + } + + /** + * Set an indexed Firebase query to listen to, along with a {@link Class} to which + * snapshots should be parsed. Keys are identified by the {@code keyQuery} and then + * data is fetched using those keys from the {@code dataRef}. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + Class modelClass) { + return setIndexedQuery(keyQuery, dataRef, new ClassSnapshotParser(modelClass)); + } + + /** + * Set the layout resource ID and class for the + * {@link android.support.v7.widget.RecyclerView.ViewHolder} for each RecyclerView item. + */ + public Builder setViewHolder(@LayoutRes int modelLayout, Class viewHolderClass) { + mModelLayout = modelLayout; + mViewHolderClass = viewHolderClass; + + return this; + } + + /** + * Set the (optional) {@link LifecycleOwner}. Listens will start and stop after the + * appropriate lifecycle events. + */ + public Builder setLifecycleOwner(LifecycleOwner owner) { + mOwner = owner; + return this; + } + + /** + * Build a {@link FirebaseRecyclerOptions} from the provided arguments. + */ + public FirebaseRecyclerOptions build() { + assertNonNull(mSnapshots, ERR_SNAPSHOTS_NULL); + assertNonNull(mViewHolderClass, ERR_VIEWHOLDER_NULL); + + return new FirebaseRecyclerOptions<>( + mSnapshots, mViewHolderClass, mModelLayout, mOwner); + } + } + +} diff --git a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java index d979e68da..a1daf8140 100644 --- a/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java +++ b/database/src/main/java/com/firebase/ui/database/ObservableSnapshotArray.java @@ -16,16 +16,6 @@ */ public abstract class ObservableSnapshotArray extends BaseObservableSnapshotArray { - /** - * Create an ObservableSnapshotArray where snapshots are parsed as objects of a particular - * class. - * - * @param clazz the class as which DataSnapshots should be parsed. - * @see ClassSnapshotParser - */ - public ObservableSnapshotArray(@NonNull Class clazz) { - this(new ClassSnapshotParser<>(clazz)); - } /** * Create an ObservableSnapshotArray with a custom {@link SnapshotParser}. diff --git a/firestore/build.gradle b/firestore/build.gradle index 886f747f2..0b4c8454c 100644 --- a/firestore/build.gradle +++ b/firestore/build.gradle @@ -25,6 +25,9 @@ android { dependencies { compile project(path: ':common') compile "com.google.firebase:firebase-firestore:$firebaseVersion" + + compile "android.arch.lifecycle:runtime:$architectureVersion" + compile "android.arch.lifecycle:extensions:$architectureVersion" annotationProcessor "android.arch.lifecycle:compiler:$architectureVersion" androidTestCompile 'junit:junit:4.12' diff --git a/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java b/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java index 411fda997..ff395c1e8 100644 --- a/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java +++ b/firestore/src/androidTest/java/com/firebase/ui/firestore/FirestoreArrayTest.java @@ -32,6 +32,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryListenOptions; import org.junit.After; import org.junit.Before; @@ -122,7 +123,8 @@ public void setUp() throws Exception { // Query is the whole collection ordered by field mQuery = mCollectionRef.orderBy("field", Query.Direction.ASCENDING); - mArray = new FirestoreArray<>(mQuery, IntegerDocument.class); + mArray = new FirestoreArray<>(mQuery, new QueryListenOptions(), + new ClassSnapshotParser<>(IntegerDocument.class)); // Add a listener to the array so that it's active mListener = mArray.addChangeEventListener(new LoggingListener()); diff --git a/firestore/src/main/java/com/firebase/ui/firestore/ClassSnapshotParser.java b/firestore/src/main/java/com/firebase/ui/firestore/ClassSnapshotParser.java new file mode 100644 index 000000000..0416bd9b4 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/ClassSnapshotParser.java @@ -0,0 +1,23 @@ +package com.firebase.ui.firestore; + +import com.firebase.ui.common.Preconditions; +import com.google.firebase.firestore.DocumentSnapshot; + +/** + * An implementation of {@link SnapshotParser} that converts {@link DocumentSnapshot} to + * a class using {@link DocumentSnapshot#toObject(Class)}. + */ +public class ClassSnapshotParser implements SnapshotParser { + + private final Class mModelClass; + + public ClassSnapshotParser(Class modelClass) { + mModelClass = Preconditions.checkNotNull(modelClass); + } + + @Override + public T parseSnapshot(DocumentSnapshot snapshot) { + return snapshot.toObject(mModelClass); + } + +} diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java index f0678684c..5433c03c9 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreArray.java @@ -27,43 +27,17 @@ public class FirestoreArray extends ObservableSnapshotArray private final List mSnapshots = new ArrayList<>(); /** - * Create a new FirestoreArray that parses snapshots as members of a given class. + * Create a new FirestoreArray. * - * @param query the Firebase location to watch for data changes - * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) - */ - public FirestoreArray(Query query, Class modelClass) { - this(query, new QueryListenOptions(), modelClass); - } - - /** - * Create a new FirestoreArray with a custom {@link SnapshotParser}. + * @param query query to listen to. + * @param options options for the query listen. + * @param parser parser for DocumentSnapshots. * * @see ObservableSnapshotArray#ObservableSnapshotArray(SnapshotParser) - * @see FirestoreArray#FirestoreArray(Query, Class) - */ - public FirestoreArray(Query query, SnapshotParser parser) { - this(query, new QueryListenOptions(), parser); - } - - /** - * @param query the options to use when listening for the query - * @see FirestoreArray#FirestoreArray(Query, Class) - */ - public FirestoreArray(Query query, QueryListenOptions options, final Class modelClass) { - this(query, options, new SnapshotParser() { - @Override - public T parseSnapshot(DocumentSnapshot snapshot) { - return snapshot.toObject(modelClass); - } - }); - } - - /** - * @param query the options to use when listening for the query - * @see FirestoreArray#FirestoreArray(Query, SnapshotParser) */ - public FirestoreArray(Query query, QueryListenOptions options, SnapshotParser parser) { + public FirestoreArray(Query query, + QueryListenOptions options, + SnapshotParser parser) { super(parser); mQuery = query; mOptions = options; diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index 67172eeed..32c1d260d 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -10,7 +10,6 @@ import com.firebase.ui.common.ChangeEventType; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestoreException; -import com.google.firebase.firestore.Query; /** * RecyclerView adapter that listens to a {@link FirestoreArray} and displays its data in real @@ -28,61 +27,15 @@ public abstract class FirestoreRecyclerAdapter mSnapshots; /** - * Create a new RecyclerView adapter to bind data from an {@link ObservableSnapshotArray}. - * - * @param snapshots the observable array of data from Firestore. - * @param owner (optional) a LifecycleOwner to observe. - */ - public FirestoreRecyclerAdapter(ObservableSnapshotArray snapshots, LifecycleOwner owner) { - mSnapshots = snapshots; - if (owner != null) { - owner.getLifecycle().addObserver(this); - } - } - - /** - * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) - */ - public FirestoreRecyclerAdapter(ObservableSnapshotArray snapshots) { - this(snapshots, null); - } - - /** - * Create a new RecyclerView adapter to bind data from a Firestore query where each {@link - * DocumentSnapshot} is converted to the specified model class. - * - * @param query the Firestore query. - * @param modelClass the model class. - * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) - */ - public FirestoreRecyclerAdapter(Query query, Class modelClass, LifecycleOwner owner) { - this(new FirestoreArray<>(query, modelClass), owner); - } - - /** - * @see #FirestoreRecyclerAdapter(Query, Class, LifecycleOwner) - */ - public FirestoreRecyclerAdapter(Query query, Class modelClass) { - this(query, modelClass, null); - } - - /** - * Create a new RecyclerView adapter to bind data from a Firestore query where each {@link - * DocumentSnapshot} is parsed by the specified parser. - * - * @param query the Firestore query. - * @param parser the snapshot parser. - * @see #FirestoreRecyclerAdapter(ObservableSnapshotArray, LifecycleOwner) + * Create a new RecyclerView adapter that listens to a Firestore Query. See + * {@link FirestoreRecyclerOptions} for configuration options. */ - public FirestoreRecyclerAdapter(Query query, SnapshotParser parser, LifecycleOwner owner) { - this(new FirestoreArray<>(query, parser), owner); - } + public FirestoreRecyclerAdapter(FirestoreRecyclerOptions options) { + mSnapshots = options.getSnapshots(); - /** - * @see #FirestoreRecyclerAdapter(Query, SnapshotParser, LifecycleOwner) - */ - public FirestoreRecyclerAdapter(Query query, SnapshotParser parser) { - this(query, parser, null); + if (options.getOwner() != null) { + options.getOwner().getLifecycle().addObserver(this); + } } @OnLifecycleEvent(Lifecycle.Event.ON_START) diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java new file mode 100644 index 000000000..b49a356d4 --- /dev/null +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java @@ -0,0 +1,135 @@ +package com.firebase.ui.firestore; + +import android.arch.lifecycle.LifecycleOwner; +import android.support.annotation.Nullable; + +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryListenOptions; + +import static com.firebase.ui.common.Preconditions.assertNonNull; +import static com.firebase.ui.common.Preconditions.assertNull; + +/** + * Options to configure an {@link FirestoreRecyclerAdapter}. + * + * @see Builder + */ +public class FirestoreRecyclerOptions { + + private static final String ERR_SNAPSHOTS_SET = "Snapshot array already set. " + + "Call only one of setSnapshotArray or setQuery"; + + private static final String ERR_SNAPSHOTS_NULL = "Snapshot array cannot be null. " + + "Call one of setSnapshotArray or setQuery"; + + private ObservableSnapshotArray mSnapshots; + private LifecycleOwner mOwner; + + private FirestoreRecyclerOptions(ObservableSnapshotArray snapshots, + @Nullable LifecycleOwner owner) { + mSnapshots = snapshots; + mOwner = owner; + } + + /** + * Get the {@link ObservableSnapshotArray} to observe. + */ + public ObservableSnapshotArray getSnapshots() { + return mSnapshots; + } + + /** + * Get the (optional) {@link LifecycleOwner}. + */ + @Nullable + public LifecycleOwner getOwner() { + return mOwner; + } + + /** + * Builder for {@link FirestoreRecyclerOptions}. + * @param the model class for the {@link FirestoreRecyclerAdapter}. + */ + public static class Builder { + + private ObservableSnapshotArray mSnapshots; + private LifecycleOwner mOwner; + + /** + * Directly set the {@link ObservableSnapshotArray}. + * + * Do not call this method after calling {@code setQuery}. + */ + public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = snapshots; + return this; + } + + /** + * Set the query to use (with options) and provide a custom {@link SnapshotParser}. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, + QueryListenOptions options, + SnapshotParser parser) { + assertNull(mSnapshots, ERR_SNAPSHOTS_SET); + + mSnapshots = new FirestoreArray(query, options, parser); + return this; + } + + + /** + * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default + * {@link QueryListenOptions}. + */ + public Builder setQuery(Query query, + SnapshotParser parser) { + return setQuery(query, new QueryListenOptions(), parser); + } + + /** + * Set the query to use (with options) and provide a model class to which each snapshot + * will be converted. + * + * Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. + */ + public Builder setQuery(Query query, + QueryListenOptions options, + Class modelClass) { + return setQuery(query, options, new ClassSnapshotParser(modelClass)); + } + + /** + * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default + * {@link QueryListenOptions}. + */ + public Builder setQuery(Query query, + Class modelClass) { + return setQuery(query, new QueryListenOptions(), modelClass); + } + + /** + * Set a {@link LifecycleOwner} for the adapter. Listening will stop/start after the + * appropriate lifecycle events. + */ + public Builder setLifecycleOwner(LifecycleOwner owner) { + mOwner = owner; + return this; + } + + /** + * Build a {@link FirestoreRecyclerOptions} from the provided arguments. + */ + public FirestoreRecyclerOptions build() { + assertNonNull(mSnapshots, ERR_SNAPSHOTS_NULL); + + return new FirestoreRecyclerOptions<>(mSnapshots, mOwner); + } + + } + +} From e71ddc4c3bee7d1fa78c8e351c50d1aafe458e08 Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Wed, 13 Sep 2017 11:53:46 -0700 Subject: [PATCH 19/37] Make FirebaseRecyclerAdapter use RecyclerView naming scheme and methods (#12) --- .../database/realtime/ChatActivity.java | 16 ++- .../database/realtime/ChatIndexActivity.java | 17 ++- .../ui/database/FirebaseRecyclerAdapter.java | 53 +------- .../ui/database/FirebaseRecyclerOptions.java | 118 ++++++------------ .../firestore/FirestoreRecyclerAdapter.java | 8 +- .../firestore/FirestoreRecyclerOptions.java | 34 +++-- 6 files changed, 83 insertions(+), 163 deletions(-) diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java index 34abcedcc..bdcb2df24 100644 --- a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatActivity.java @@ -22,7 +22,9 @@ import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; @@ -142,18 +144,22 @@ public void onItemRangeInserted(int positionStart, int itemCount) { protected FirebaseRecyclerAdapter getAdapter() { Query lastFifty = mChatRef.limitToLast(50); - FirebaseRecyclerOptions options = - new FirebaseRecyclerOptions.Builder() - .setViewHolder(R.layout.message, ChatHolder.class) + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() .setQuery(lastFifty, Chat.class) .setLifecycleOwner(this) .build(); return new FirebaseRecyclerAdapter(options) { + @Override + public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ChatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.message, parent, false)); + } @Override - public void populateViewHolder(ChatHolder holder, Chat chat, int position) { - holder.bind(chat); + protected void onBindViewHolder(ChatHolder holder, int position, Chat model) { + holder.bind(model); } @Override diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java index 74b92e9b6..a61e3a962 100644 --- a/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/ChatIndexActivity.java @@ -1,6 +1,8 @@ package com.firebase.uidemo.database.realtime; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import com.firebase.ui.database.FirebaseRecyclerAdapter; import com.firebase.ui.database.FirebaseRecyclerOptions; @@ -33,17 +35,22 @@ protected FirebaseRecyclerAdapter getAdapter() { .child("chatIndices") .child(FirebaseAuth.getInstance().getCurrentUser().getUid()); - FirebaseRecyclerOptions options = - new FirebaseRecyclerOptions.Builder() + FirebaseRecyclerOptions options = + new FirebaseRecyclerOptions.Builder() .setIndexedQuery(mChatIndicesRef.limitToFirst(50), mChatRef, Chat.class) - .setViewHolder(R.layout.message, ChatHolder.class) .setLifecycleOwner(this) .build(); return new FirebaseRecyclerAdapter(options) { @Override - public void populateViewHolder(ChatHolder holder, Chat chat, int position) { - holder.bind(chat); + public ChatHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ChatHolder(LayoutInflater.from(parent.getContext()) + .inflate(R.layout.message, parent, false)); + } + + @Override + protected void onBindViewHolder(ChatHolder holder, int position, Chat model) { + holder.bind(model); } @Override diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java index 815dd4ab6..0432b6ca1 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerAdapter.java @@ -5,18 +5,12 @@ import android.arch.lifecycle.OnLifecycleEvent; import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import com.firebase.ui.common.ChangeEventType; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - /** * This class is a generic way of backing a {@link RecyclerView} with a Firebase location. It * handles all of the child events at the given Firebase location and marshals received data into @@ -34,17 +28,13 @@ public abstract class FirebaseRecyclerAdapter mSnapshots; - protected final Class mViewHolderClass; - protected final int mModelLayout; /** * Initialize a {@link RecyclerView.Adapter} that listens to a Firebase query. See * {@link FirebaseRecyclerOptions} for configuration options. */ - public FirebaseRecyclerAdapter(FirebaseRecyclerOptions options) { + public FirebaseRecyclerAdapter(FirebaseRecyclerOptions options) { mSnapshots = options.getSnapshots(); - mViewHolderClass = options.getViewHolderClass(); - mModelLayout = options.getModelLayout(); if (options.getOwner() != null) { options.getOwner().getLifecycle().addObserver(this); @@ -123,44 +113,13 @@ public int getItemCount() { } @Override - public VH onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - try { - Constructor constructor = mViewHolderClass.getConstructor(View.class); - return constructor.newInstance(view); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - @Override - public int getItemViewType(int position) { - return mModelLayout; - } - - @Override - public void onBindViewHolder(VH viewHolder, int position) { - T model = getItem(position); - populateViewHolder(viewHolder, model, position); + public void onBindViewHolder(VH holder, int position) { + onBindViewHolder(holder, position, getItem(position)); } /** - * Each time the data at the given Firebase location changes, this method will be called for - * each item that needs to be displayed. The first two arguments correspond to the mLayout and - * mModelClass given to the constructor of this class. The third argument is the item's position - * in the list. - *

- * Your implementation should populate the view using the data contained in the model. - * - * @param viewHolder The view to populate - * @param model The object containing the data used to populate the view - * @param position The position in the list of the view being populated + * @param model the model object containing the data that should be used to populate the view. + * @see #onBindViewHolder(RecyclerView.ViewHolder, int) */ - protected abstract void populateViewHolder(VH viewHolder, T model, int position); + protected abstract void onBindViewHolder(VH holder, int position, T model); } diff --git a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java index fbe862af4..ee4b93e43 100644 --- a/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java +++ b/database/src/main/java/com/firebase/ui/database/FirebaseRecyclerOptions.java @@ -1,9 +1,7 @@ package com.firebase.ui.database; import android.arch.lifecycle.LifecycleOwner; -import android.support.annotation.LayoutRes; import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Query; @@ -16,29 +14,19 @@ * * @see Builder */ -public class FirebaseRecyclerOptions { +public class FirebaseRecyclerOptions { private static final String ERR_SNAPSHOTS_SET = "Snapshot array already set. " + "Call only one of setSnapshotArray, setQuery, or setIndexedQuery."; - - private static final String ERR_SNAPSHOTS_NULL = "Snapshot array cannot be null. " + + private static final String ERR_SNAPSHOTS_NULL = "Snapshot array cannot be null. " + "Call one of setSnapshotArray, setQuery, or setIndexedQuery."; - private static final String ERR_VIEWHOLDER_NULL = "View holder class cannot be null. " + - "Call setViewHolder."; - private final ObservableSnapshotArray mSnapshots; - private final Class mViewHolderClass; - private final int mModelLayout; private final LifecycleOwner mOwner; private FirebaseRecyclerOptions(ObservableSnapshotArray snapshots, - Class viewHolderClass, - @LayoutRes int modelLayout, - LifecycleOwner owner) { + @Nullable LifecycleOwner owner) { mSnapshots = snapshots; - mViewHolderClass = viewHolderClass; - mModelLayout = modelLayout; mOwner = owner; } @@ -50,24 +38,8 @@ public ObservableSnapshotArray getSnapshots() { } /** - * Get the class of the {@link android.support.v7.widget.RecyclerView.ViewHolder} for - * each RecyclerView item. - */ - public Class getViewHolderClass() { - return mViewHolderClass; - } - - /** - * Get the resource ID of the layout for each RecyclerView item. - */ - @LayoutRes - public int getModelLayout() { - return mModelLayout; - } - - /** - * Get the (optional) LifecycleOwner. Listening will start/stop after the appropriate - * lifecycle events. + * Get the (optional) LifecycleOwner. Listening will start/stop after the appropriate lifecycle + * events. */ @Nullable public LifecycleOwner getOwner() { @@ -78,21 +50,18 @@ public LifecycleOwner getOwner() { * Builder for a {@link FirebaseRecyclerOptions}. * * @param the model class for the {@link FirebaseRecyclerAdapter}. - * @param the ViewHolder class for the {@link FirebaseRecyclerAdapter}. */ - public static class Builder { + public static class Builder { private ObservableSnapshotArray mSnapshots; - private Class mViewHolderClass; - private int mModelLayout; private LifecycleOwner mOwner; /** * Directly set the {@link ObservableSnapshotArray} to be listened to. - * + *

* Do not call this method after calling {@code setQuery}. */ - public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { + public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { assertNull(mSnapshots, ERR_SNAPSHOTS_SET); mSnapshots = snapshots; @@ -100,74 +69,63 @@ public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { } /** - * Set the Firebase query to listen to, along with a {@link SnapshotParser} to - * parse snapshots into model objects. - * + * Set the Firebase query to listen to, along with a {@link SnapshotParser} to parse + * snapshots into model objects. + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ - public Builder setQuery(Query query, SnapshotParser snapshotParser) { + public Builder setQuery(Query query, SnapshotParser snapshotParser) { assertNull(mSnapshots, ERR_SNAPSHOTS_SET); - mSnapshots = new FirebaseArray(query, snapshotParser); + mSnapshots = new FirebaseArray<>(query, snapshotParser); return this; } /** - * Set the Firebase query to listen to, along with a {@link Class} to which snapshots - * should be parsed. - * + * Set the Firebase query to listen to, along with a {@link Class} to which snapshots should + * be parsed. + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ - public Builder setQuery(Query query, Class modelClass) { - return setQuery(query, new ClassSnapshotParser(modelClass)); + public Builder setQuery(Query query, Class modelClass) { + return setQuery(query, new ClassSnapshotParser<>(modelClass)); } /** - * Set an indexed Firebase query to listen to, along with a {@link SnapshotParser} to - * parse snapshots into model objects. Keys are identified by the {@code keyQuery} and then - * data is fetched using those keys from the {@code dataRef}. - * + * Set an indexed Firebase query to listen to, along with a {@link SnapshotParser} to parse + * snapshots into model objects. Keys are identified by the {@code keyQuery} and then data + * is fetched using those keys from the {@code dataRef}. + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ - public Builder setIndexedQuery(Query keyQuery, - DatabaseReference dataRef, - SnapshotParser snapshotParser) { + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + SnapshotParser snapshotParser) { assertNull(mSnapshots, ERR_SNAPSHOTS_SET); - mSnapshots = new FirebaseIndexArray(keyQuery, dataRef, snapshotParser); + mSnapshots = new FirebaseIndexArray<>(keyQuery, dataRef, snapshotParser); return this; } /** - * Set an indexed Firebase query to listen to, along with a {@link Class} to which - * snapshots should be parsed. Keys are identified by the {@code keyQuery} and then - * data is fetched using those keys from the {@code dataRef}. - * + * Set an indexed Firebase query to listen to, along with a {@link Class} to which snapshots + * should be parsed. Keys are identified by the {@code keyQuery} and then data is fetched + * using those keys from the {@code dataRef}. + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ - public Builder setIndexedQuery(Query keyQuery, - DatabaseReference dataRef, - Class modelClass) { - return setIndexedQuery(keyQuery, dataRef, new ClassSnapshotParser(modelClass)); - } - - /** - * Set the layout resource ID and class for the - * {@link android.support.v7.widget.RecyclerView.ViewHolder} for each RecyclerView item. - */ - public Builder setViewHolder(@LayoutRes int modelLayout, Class viewHolderClass) { - mModelLayout = modelLayout; - mViewHolderClass = viewHolderClass; - - return this; + public Builder setIndexedQuery(Query keyQuery, + DatabaseReference dataRef, + Class modelClass) { + return setIndexedQuery(keyQuery, dataRef, new ClassSnapshotParser<>(modelClass)); } /** * Set the (optional) {@link LifecycleOwner}. Listens will start and stop after the * appropriate lifecycle events. */ - public Builder setLifecycleOwner(LifecycleOwner owner) { + public Builder setLifecycleOwner(LifecycleOwner owner) { mOwner = owner; return this; } @@ -175,12 +133,10 @@ public Builder setLifecycleOwner(LifecycleOwner owner) { /** * Build a {@link FirebaseRecyclerOptions} from the provided arguments. */ - public FirebaseRecyclerOptions build() { + public FirebaseRecyclerOptions build() { assertNonNull(mSnapshots, ERR_SNAPSHOTS_NULL); - assertNonNull(mViewHolderClass, ERR_VIEWHOLDER_NULL); - return new FirebaseRecyclerOptions<>( - mSnapshots, mViewHolderClass, mModelLayout, mOwner); + return new FirebaseRecyclerOptions<>(mSnapshots, mOwner); } } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java index 32c1d260d..8cb6dcda5 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerAdapter.java @@ -105,12 +105,8 @@ public void onBindViewHolder(VH holder, int position) { } /** - * Called when data has been added/changed and an item needs to be displayed. - * - * @param holder the view to populate. - * @param position the position in the list of the view being populated. - * @param model the model object containing the data that should be used to populate the - * view. + * @param model the model object containing the data that should be used to populate the view. + * @see #onBindViewHolder(RecyclerView.ViewHolder, int) */ protected abstract void onBindViewHolder(VH holder, int position, T model); } diff --git a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java index b49a356d4..014f7a08e 100644 --- a/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java +++ b/firestore/src/main/java/com/firebase/ui/firestore/FirestoreRecyclerOptions.java @@ -18,7 +18,6 @@ public class FirestoreRecyclerOptions { private static final String ERR_SNAPSHOTS_SET = "Snapshot array already set. " + "Call only one of setSnapshotArray or setQuery"; - private static final String ERR_SNAPSHOTS_NULL = "Snapshot array cannot be null. " + "Call one of setSnapshotArray or setQuery"; @@ -48,6 +47,7 @@ public LifecycleOwner getOwner() { /** * Builder for {@link FirestoreRecyclerOptions}. + * * @param the model class for the {@link FirestoreRecyclerAdapter}. */ public static class Builder { @@ -57,7 +57,7 @@ public static class Builder { /** * Directly set the {@link ObservableSnapshotArray}. - * + *

* Do not call this method after calling {@code setQuery}. */ public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { @@ -69,7 +69,7 @@ public Builder setSnapshotArray(ObservableSnapshotArray snapshots) { /** * Set the query to use (with options) and provide a custom {@link SnapshotParser}. - * + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ public Builder setQuery(Query query, @@ -77,38 +77,34 @@ public Builder setQuery(Query query, SnapshotParser parser) { assertNull(mSnapshots, ERR_SNAPSHOTS_SET); - mSnapshots = new FirestoreArray(query, options, parser); + mSnapshots = new FirestoreArray<>(query, options, parser); return this; } /** - * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default - * {@link QueryListenOptions}. + * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default {@link + * QueryListenOptions}. */ - public Builder setQuery(Query query, - SnapshotParser parser) { + public Builder setQuery(Query query, SnapshotParser parser) { return setQuery(query, new QueryListenOptions(), parser); } /** - * Set the query to use (with options) and provide a model class to which each snapshot - * will be converted. - * + * Set the query to use (with options) and provide a model class to which each snapshot will + * be converted. + *

* Do not call this method after calling {@link #setSnapshotArray(ObservableSnapshotArray)}. */ - public Builder setQuery(Query query, - QueryListenOptions options, - Class modelClass) { - return setQuery(query, options, new ClassSnapshotParser(modelClass)); + public Builder setQuery(Query query, QueryListenOptions options, Class modelClass) { + return setQuery(query, options, new ClassSnapshotParser<>(modelClass)); } /** - * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default - * {@link QueryListenOptions}. + * Calls {@link #setQuery(Query, QueryListenOptions, Class)} with the default {@link + * QueryListenOptions}. */ - public Builder setQuery(Query query, - Class modelClass) { + public Builder setQuery(Query query, Class modelClass) { return setQuery(query, new QueryListenOptions(), modelClass); } From fbb31a9fbec3eca58ab36dfa62e146369d3ce2cc Mon Sep 17 00:00:00 2001 From: Alex Saveau Date: Wed, 13 Sep 2017 12:58:04 -0700 Subject: [PATCH 20/37] Revamp sample (#903) --- app/src/main/AndroidManifest.xml | 26 +-- .../com/firebase/uidemo/ChooserActivity.java | 20 +- .../uidemo/database/ChatActivity.java | 187 ------------------ .../realtime/RealtimeDbChatActivity.java | 162 +++++++++++++++ .../RealtimeDbChatIndexActivity.java} | 30 ++- .../uidemo/util/LifecycleActivity.java | 16 ++ app/src/main/res/layout/activity_chat.xml | 3 +- app/src/main/res/values/strings.xml | 93 +++++---- app/src/main/res/xml-v25/shortcuts.xml | 22 +-- gradle/wrapper/gradle-wrapper.jar | Bin 54713 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- 11 files changed, 287 insertions(+), 275 deletions(-) delete mode 100644 app/src/main/java/com/firebase/uidemo/database/ChatActivity.java create mode 100644 app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java rename app/src/main/java/com/firebase/uidemo/database/{ChatIndexActivity.java => realtime/RealtimeDbChatIndexActivity.java} (73%) create mode 100644 app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e6d93103a..32e430588 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,33 +21,35 @@ + + - - - - + android:label="@string/title_auth_activity" /> + android:label="@string/title_auth_activity" /> + + + + - + + android:label="@string/title_storage_activity" /> diff --git a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java index d1c36ac63..374d39a88 100644 --- a/app/src/main/java/com/firebase/uidemo/ChooserActivity.java +++ b/app/src/main/java/com/firebase/uidemo/ChooserActivity.java @@ -26,7 +26,7 @@ import android.widget.TextView; import com.firebase.uidemo.auth.AuthUiActivity; -import com.firebase.uidemo.database.ChatActivity; +import com.firebase.uidemo.database.realtime.RealtimeDbChatActivity; import com.firebase.uidemo.storage.ImageActivity; import butterknife.BindView; @@ -49,21 +49,21 @@ protected void onCreate(Bundle savedInstanceState) { private static class ActivityChooserAdapter extends RecyclerView.Adapter { private static final Class[] CLASSES = new Class[]{ - ChatActivity.class, AuthUiActivity.class, + RealtimeDbChatActivity.class, ImageActivity.class, }; private static final int[] DESCRIPTION_NAMES = new int[]{ - R.string.name_chat, - R.string.name_auth_ui, - R.string.name_image + R.string.title_auth_activity, + R.string.title_realtime_database_activity, + R.string.title_storage_activity }; private static final int[] DESCRIPTION_IDS = new int[]{ - R.string.desc_chat, - R.string.desc_auth_ui, - R.string.desc_image + R.string.desc_auth, + R.string.desc_realtime_database, + R.string.desc_storage }; @Override @@ -92,8 +92,8 @@ private static class ActivityStarterHolder extends RecyclerView.ViewHolder imple public ActivityStarterHolder(View itemView) { super(itemView); - mTitle = (TextView) itemView.findViewById(R.id.text1); - mDescription = (TextView) itemView.findViewById(R.id.text2); + mTitle = itemView.findViewById(R.id.text1); + mDescription = itemView.findViewById(R.id.text2); } private void bind(Class aClass, @StringRes int name, @StringRes int description) { diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java deleted file mode 100644 index 6429b0a2c..000000000 --- a/app/src/main/java/com/firebase/uidemo/database/ChatActivity.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.firebase.uidemo.database; - -import android.arch.lifecycle.LifecycleRegistry; -import android.arch.lifecycle.LifecycleRegistryOwner; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; -import android.widget.Toast; - -import com.firebase.ui.database.FirebaseRecyclerAdapter; -import com.firebase.uidemo.R; -import com.firebase.uidemo.util.SignInResultNotifier; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.firebase.auth.AuthResult; -import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.FirebaseDatabase; -import com.google.firebase.database.Query; - -public class ChatActivity extends AppCompatActivity - implements FirebaseAuth.AuthStateListener, View.OnClickListener, LifecycleRegistryOwner { - private static final String TAG = "RecyclerViewDemo"; - - // TODO remove once arch components are merged into support lib - private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); - - private FirebaseAuth mAuth; - protected DatabaseReference mChatRef; - private Button mSendButton; - protected EditText mMessageEdit; - - private RecyclerView mMessages; - private LinearLayoutManager mManager; - private FirebaseRecyclerAdapter mAdapter; - protected TextView mEmptyListMessage; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_chat); - - mAuth = FirebaseAuth.getInstance(); - mAuth.addAuthStateListener(this); - - mSendButton = (Button) findViewById(R.id.sendButton); - mMessageEdit = (EditText) findViewById(R.id.messageEdit); - mEmptyListMessage = (TextView) findViewById(R.id.emptyTextView); - - mChatRef = FirebaseDatabase.getInstance().getReference().child("chats"); - - mSendButton.setOnClickListener(this); - - mManager = new LinearLayoutManager(this); - mManager.setReverseLayout(false); - - mMessages = (RecyclerView) findViewById(R.id.messagesList); - mMessages.setHasFixedSize(true); - mMessages.setLayoutManager(mManager); - - if (isSignedIn()) { attachRecyclerViewAdapter(); } - } - - @Override - public void onStart() { - super.onStart(); - - // Default Database rules do not allow unauthenticated reads, so we need to - // sign in before attaching the RecyclerView adapter otherwise the Adapter will - // not be able to read any data from the Database. - if (!isSignedIn()) { signInAnonymously(); } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (mAuth != null) { - mAuth.removeAuthStateListener(this); - } - } - - @Override - public void onClick(View v) { - String uid = mAuth.getCurrentUser().getUid(); - String name = "User " + uid.substring(0, 6); - - Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); - mChatRef.push().setValue(chat, new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference reference) { - if (error != null) { - Log.e(TAG, "Failed to write message", error.toException()); - } - } - }); - - mMessageEdit.setText(""); - } - - @Override - public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { - updateUI(); - } - - private void attachRecyclerViewAdapter() { - mAdapter = getAdapter(); - - // Scroll to bottom on new messages - mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { - @Override - public void onItemRangeInserted(int positionStart, int itemCount) { - mManager.smoothScrollToPosition(mMessages, null, mAdapter.getItemCount()); - } - }); - - mMessages.setAdapter(mAdapter); - } - - protected FirebaseRecyclerAdapter getAdapter() { - Query lastFifty = mChatRef.limitToLast(50); - return new FirebaseRecyclerAdapter( - Chat.class, - R.layout.message, - ChatHolder.class, - lastFifty, - this) { - @Override - public void populateViewHolder(ChatHolder holder, Chat chat, int position) { - holder.bind(chat); - } - - @Override - public void onDataChanged() { - // If there are no chat messages, show a view that invites the user to add a message. - mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - }; - } - - private void signInAnonymously() { - Toast.makeText(this, "Signing in...", Toast.LENGTH_SHORT).show(); - mAuth.signInAnonymously() - .addOnSuccessListener(this, new OnSuccessListener() { - @Override - public void onSuccess(AuthResult result) { - attachRecyclerViewAdapter(); - } - }) - .addOnCompleteListener(new SignInResultNotifier(this)); - } - - private boolean isSignedIn() { - return mAuth.getCurrentUser() != null; - } - - private void updateUI() { - // Sending only allowed when signed in - mSendButton.setEnabled(isSignedIn()); - mMessageEdit.setEnabled(isSignedIn()); - } - - @Override - public LifecycleRegistry getLifecycle() { - return mRegistry; - } -} diff --git a/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java new file mode 100644 index 000000000..3db81e870 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatActivity.java @@ -0,0 +1,162 @@ +package com.firebase.uidemo.database.realtime; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.firebase.ui.auth.ui.ImeHelper; +import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.uidemo.R; +import com.firebase.uidemo.database.Chat; +import com.firebase.uidemo.database.ChatHolder; +import com.firebase.uidemo.util.LifecycleActivity; +import com.firebase.uidemo.util.SignInResultNotifier; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.Query; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; + +/** + * Class demonstrating how to setup a {@link RecyclerView} with an adapter while taking sign-in + * states into consideration. Also demonstrates adding data to a ref and then reading it back using + * the {@link FirebaseRecyclerAdapter} to build a simple chat app. + *

+ * For a general intro to the RecyclerView, see Creating + * Lists. + */ +public abstract class RealtimeDbChatActivity extends LifecycleActivity + implements FirebaseAuth.AuthStateListener { + private static final String TAG = "RealtimeDatabaseDemo"; + + /** + * Get the last 50 chat messages. + */ + protected static final Query sChatQuery = + FirebaseDatabase.getInstance().getReference().child("chats").limitToLast(50); + + @BindView(R.id.messagesList) + RecyclerView mRecyclerView; + + @BindView(R.id.sendButton) + Button mSendButton; + + @BindView(R.id.messageEdit) + EditText mMessageEdit; + + @BindView(R.id.emptyTextView) + TextView mEmptyListMessage; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + ButterKnife.bind(this); + + mRecyclerView.setHasFixedSize(true); + mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + + ImeHelper.setImeOnDoneListener(mMessageEdit, new ImeHelper.DonePressedListener() { + @Override + public void onDonePressed() { + onSendClick(); + } + }); + } + + @Override + public void onStart() { + super.onStart(); + if (isSignedIn()) { attachRecyclerViewAdapter(); } + FirebaseAuth.getInstance().addAuthStateListener(this); + } + + @Override + protected void onStop() { + super.onStop(); + FirebaseAuth.getInstance().removeAuthStateListener(this); + } + + @Override + public void onAuthStateChanged(@NonNull FirebaseAuth auth) { + mSendButton.setEnabled(isSignedIn()); + mMessageEdit.setEnabled(isSignedIn()); + + if (isSignedIn()) { + attachRecyclerViewAdapter(); + } else { + Toast.makeText(this, R.string.signing_in, Toast.LENGTH_SHORT).show(); + auth.signInAnonymously().addOnCompleteListener(new SignInResultNotifier(this)); + } + } + + private boolean isSignedIn() { + return FirebaseAuth.getInstance().getCurrentUser() != null; + } + + private void attachRecyclerViewAdapter() { + final RecyclerView.Adapter adapter = newAdapter(); + + // Scroll to bottom on new messages + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mRecyclerView.smoothScrollToPosition(adapter.getItemCount()); + } + }); + + mRecyclerView.setAdapter(adapter); + } + + @OnClick(R.id.sendButton) + public void onSendClick() { + String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); + + onAddMessage(new Chat(name, mMessageEdit.getText().toString(), uid)); + + mMessageEdit.setText(""); + } + + protected RecyclerView.Adapter newAdapter() { + return new FirebaseRecyclerAdapter( + Chat.class, + R.layout.message, + ChatHolder.class, + sChatQuery, + this) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.bind(chat); + } + + @Override + public void onDataChanged() { + // If there are no chat messages, show a view that invites the user to add a message. + mEmptyListMessage.setVisibility(getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + }; + } + + protected void onAddMessage(Chat chat) { + sChatQuery.getRef().push().setValue(chat, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference reference) { + if (error != null) { + Log.e(TAG, "Failed to write message", error.toException()); + } + } + }); + } +} diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java similarity index 73% rename from app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java rename to app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java index e2e4f7671..384657f28 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java +++ b/app/src/main/java/com/firebase/uidemo/database/realtime/RealtimeDbChatIndexActivity.java @@ -1,32 +1,21 @@ -package com.firebase.uidemo.database; +package com.firebase.uidemo.database.realtime; import android.view.View; import com.firebase.ui.database.FirebaseIndexRecyclerAdapter; import com.firebase.ui.database.FirebaseRecyclerAdapter; import com.firebase.uidemo.R; +import com.firebase.uidemo.database.Chat; +import com.firebase.uidemo.database.ChatHolder; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; -public class ChatIndexActivity extends ChatActivity { +public class RealtimeDbChatIndexActivity extends RealtimeDbChatActivity { private DatabaseReference mChatIndicesRef; @Override - public void onClick(View v) { - String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); - String name = "User " + uid.substring(0, 6); - Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); - - DatabaseReference chatRef = mChatRef.push(); - mChatIndicesRef.child(chatRef.getKey()).setValue(true); - chatRef.setValue(chat); - - mMessageEdit.setText(""); - } - - @Override - protected FirebaseRecyclerAdapter getAdapter() { + protected FirebaseRecyclerAdapter newAdapter() { mChatIndicesRef = FirebaseDatabase.getInstance() .getReference() .child("chatIndices") @@ -37,7 +26,7 @@ protected FirebaseRecyclerAdapter getAdapter() { R.layout.message, ChatHolder.class, mChatIndicesRef.limitToLast(50), - mChatRef, + sChatQuery.getRef(), this) { @Override public void populateViewHolder(ChatHolder holder, Chat chat, int position) { @@ -51,4 +40,11 @@ public void onDataChanged() { } }; } + + @Override + protected void onAddMessage(Chat chat) { + DatabaseReference chatRef = sChatQuery.getRef().push(); + mChatIndicesRef.child(chatRef.getKey()).setValue(true); + chatRef.setValue(chat); + } } diff --git a/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java b/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java new file mode 100644 index 000000000..808cb071d --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/util/LifecycleActivity.java @@ -0,0 +1,16 @@ +package com.firebase.uidemo.util; + +import android.arch.lifecycle.LifecycleRegistry; +import android.arch.lifecycle.LifecycleRegistryOwner; +import android.support.v7.app.AppCompatActivity; + +// TODO remove once arch components are merged into support lib +@SuppressWarnings("Registered") +public class LifecycleActivity extends AppCompatActivity implements LifecycleRegistryOwner { + private final LifecycleRegistry mRegistry = new LifecycleRegistry(this); + + @Override + public LifecycleRegistry getLifecycle() { + return mRegistry; + } +} diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml index 7c0171e08..fc6a78ae4 100644 --- a/app/src/main/res/layout/activity_chat.xml +++ b/app/src/main/res/layout/activity_chat.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".database.ChatActivity"> + tools:context=".database.realtime.RealtimeDbChatActivity">