diff --git a/README.md b/README.md index 1f9ab0555..7d97e82a8 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ We can now use that in our activity to allow sending a message: Firebase.setAndroidContext(this); Firebase ref = new Firebase("https://nanochat.firebaseio.com"); - mAdapter = new FirebaseListAdapter(ChatMessage.class, android.R.layout.two_line_list_item, this, ref) { + mAdapter = new FirebaseListAdapter(this, ChatMessage.class, android.R.layout.two_line_list_item, ref) { @Override protected void populateView(View view, ChatMessage chatMessage) { ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); @@ -218,6 +218,54 @@ We can now use that in our activity to allow sending a message: Et voila: a minimal, yet fully functional, chat app in about 30 lines of code. Not bad, right? +## Using a RecyclerView + +RecyclerView is the new preferred way to handle potentially long lists of items. Since Firebase collections +can contain many items, there is an `FirebaseRecyclerViewAdapter` too. Here's how you use it: + +1. Create a custom ViewHolder class +2. Create a custom subclass FirebaseListAdapter + +The rest of the steps is the same as for the `FirebaseListAdapter` above, so be sure to read that first. + +### Create a custom ViewHolder + +A ViewHolder is similar to container of a ViewGroup that allows simple lookup of the sub-views of the group. +If we use the same layout as before (`android.R.layout.two_line_list_item`), there are two `TextView`s in there. +We can wrap that in a ViewHolder with: + + private static class ChatMessageViewHolder extends RecyclerView.ViewHolder { + TextView messageText; + TextView nameText; + + public ChatMessageViewHolder(View itemView) { + super(itemView); + nameText = (TextView)itemView.findViewById(android.R.id.text1); + messageText = (TextView) itemView.findViewById(android.R.id.text2); + } + } + +There's nothing magical going on here; we're just mapping numeric IDs and casts into a nice, type-safe contract. + +### Create a custom FirebaseListAdapter + +Just like we did for FirebaseListAdapter, we'll create an anonymous subclass for our ChatMessages: + + RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler); + recycler.setHasFixedSize(true); + recycler.setLayoutManager(new LinearLayoutManager(this)); + + mAdapter = new FirebaseRecyclerViewAdapter(ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, mRef) { + @Override + public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder, ChatMessage chatMessage) { + chatMessageViewHolder.nameText.setText(chatMessage.getName()); + chatMessageViewHolder.messageText.setText(chatMessage.getMessage()); + } + }; + recycler.setAdapter(mAdapter); + +Like before, we get a custom RecyclerView populated with data from Firebase by setting the properties to the correct fields. + ## Installing locally We are still working on deploying FirebaseUI to Maven Central. In the meantime, you can download the @@ -229,7 +277,7 @@ with: ## Deployment -### To get the build server ready to build/deploy FirebaseUI-Android +### To get the build server ready to build FirebaseUI-Android * Install a JDK (if it's not installed yet): * `sudo apt-get install default-jdk` @@ -262,8 +310,22 @@ with: sonatypeUsername=YourSonatypeJiraUsername sonatypePassword=YourSonatypeJiraPassword +### to build a release + +* build the project in Android Studio or with Gradle +* this generates the main binary: `library/build/outputs/aar/library-debug.aar` +* open the Gradle projects tab, by clicking the tiny gradle tab on the right (or View > Tool Windows > Gradle) +* select :library > Tasks > other > bundleReleaseJavadoc +* this generates the javadoc: `library/build/outputs/library-javadoc.jar` + + +### to tag a release on Github + +* ensure that all your changes are on master and that your local build is on master +* ensure that the correct version number is in both `library/build.gradle` and `library/pom.xml` + -### to build/deploy +### to deploy a release to Maven Central * log onto the build box * checkout and update the master branch diff --git a/library/build.gradle b/library/build.gradle index 1019f5a06..8890e2663 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,13 +8,13 @@ android { minSdkVersion 10 targetSdkVersion 22 versionCode 1 - versionName "0.1.0" + versionName "0.2.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } + } } packagingOptions { exclude 'META-INF/LICENSE' @@ -22,76 +22,20 @@ android { exclude 'META-INF/NOTICE' } } -//android.libraryVariants.all { variant -> -// task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) { -// description "Generates Javadoc for $variant.name." -// source = variant.javaCompile.source -// ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" -// classpath = files(variant.javaCompile.classpath.files) + files(ext.androidJar) -// options.links("http://docs.oracle.com/javase/7/docs/api/"); -// options.links("http://d.android.com/reference/"); -// } -//} - -apply plugin: 'maven' -apply plugin: 'signing' - -version = "0.1.0" -group = "com.firebase" - -configurations { - archives { - extendsFrom configurations.default +android.libraryVariants.all { variant -> + task("generate${variant.name.capitalize()}Javadoc", type: Javadoc) { + description "Generates Javadoc for $variant.name." + source = variant.javaCompile.source + ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" + classpath = files(variant.javaCompile.classpath.files) + files(ext.androidJar) + options.links("http://docs.oracle.com/javase/7/docs/api/"); + options.links("http://d.android.com/reference/"); } -} - -signing { - required { has("release") && gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives -} - -uploadArchives { - configuration = configurations.archives - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: sonatypeRepo) { - authentication(userName: sonatypeUsername, - password: sonatypePassword) - } - - pom.project { - name 'FirebaseUI' - packaging 'aar' - description 'FirebaseUI library for Android applications' - url 'https://github.com/firebase/FirebaseUI-Android' - - scm { - url 'scm:git@github.com/firebase/FirebaseUI-Android' - connection 'scm:git:git@github.com:firebase/FirebaseUI-Android.git' - developerConnection 'scm:git:git@github.com:firebase/FirebaseUI-Android.git' - } - - organization { - name 'Firebase' - url 'https://www.firebase.com/' - } - - licenses { - license { - name 'MIT' - url 'http://firebase.mit-license.org' - } - } - - developers { - developer { - id 'puf' - name 'Frank van Puffelen' - email 'puf@google.com' - } - } - } + task("bundle${variant.name.capitalize()}Javadoc", type: Jar) { + description "Bundles Javadoc into zip for $variant.name." + classifier = "javadoc" + destinationDir = file("build/outputs") + from tasks["generate${variant.name.capitalize()}Javadoc"] } } diff --git a/library/library.iml b/library/library.iml index abc281b66..4e6a41c2f 100644 --- a/library/library.iml +++ b/library/library.iml @@ -1,5 +1,5 @@ - + @@ -65,6 +65,7 @@ + @@ -86,7 +87,6 @@ - diff --git a/library/pom.xml b/library/pom.xml index 6cdf87d3b..8d60ebbff 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -6,7 +6,7 @@ FirebaseUI-Android FirebaseUI library for Android applications https://github.com/firebase/FirebaseUI-Android - 0.1.0 + 0.2.0 aar scm:git@github.com/firebase/FirebaseUI-Android @@ -26,4 +26,19 @@ puf@google.com + + + + com.simpligility.maven.plugins + android-maven-plugin + 4.1.0 + true + + + false + + + + + diff --git a/library/src/main/java/com/firebase/ui/FirebaseListAdapter.java b/library/src/main/java/com/firebase/ui/FirebaseListAdapter.java index 74a151dcb..26a444609 100644 --- a/library/src/main/java/com/firebase/ui/FirebaseListAdapter.java +++ b/library/src/main/java/com/firebase/ui/FirebaseListAdapter.java @@ -43,6 +43,21 @@ * instance of your list item mLayout and an instance your class that holds your data. Simply populate the view however * you like and this class will handle updating the list as the data changes. * + *
+ * {@code
+ *     Firebase ref = new Firebase("https://.firebaseio.com");
+ *     ListAdapter adapter = new FirebaseListAdapter(this, ChatMessage.class, android.R.layout.two_line_list_item, mRef)
+ *     {
+ *         protected void populateView(View view, ChatMessage chatMessage)
+ *         {
+ *             ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName());
+ *             ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getMessage());
+ *         }
+ *     };
+ *     listView.setListAdapter(adapter);
+ * }
+ * 
+ * * @param The class type to use as a model for the data contained in the children of the given Firebase location */ public abstract class FirebaseListAdapter extends BaseAdapter { diff --git a/library/src/main/java/com/firebase/ui/FirebaseRecyclerViewAdapter.java b/library/src/main/java/com/firebase/ui/FirebaseRecyclerViewAdapter.java index 682cb4431..7df24602a 100644 --- a/library/src/main/java/com/firebase/ui/FirebaseRecyclerViewAdapter.java +++ b/library/src/main/java/com/firebase/ui/FirebaseRecyclerViewAdapter.java @@ -29,40 +29,76 @@ package com.firebase.ui; 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.client.ChildEventListener; -import com.firebase.client.DataSnapshot; import com.firebase.client.Firebase; -import com.firebase.client.FirebaseError; import com.firebase.client.Query; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; /** * This class is a generic way of backing an RecyclerView with a Firebase location. * It handles all of the child events at the given Firebase location. It marshals received data into the given * class type. * - * @param The collection type + * To use this class in your app, subclass it passing in all required parameters and implement the + * populateViewHolder method. + * + *
+ * {@code
+ *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
+ *         TextView messageText;
+ *         TextView nameText;
+ *
+ *         public ChatMessageViewHolder(View itemView) {
+ *             super(itemView);
+ *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
+ *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
+ *         }
+ *     }
+ *
+ *     FirebaseRecyclerViewAdapter adapter;
+ *     ref = new Firebase("https://.firebaseio.com");
+ *
+ *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
+ *     recycler.setHasFixedSize(true);
+ *     recycler.setLayoutManager(new LinearLayoutManager(this));
+ *
+ *     adapter = new FirebaseRecyclerViewAdapter(ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, mRef) {
+ *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder, ChatMessage chatMessage) {
+ *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
+ *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
+ *         }
+ *     };
+ *     recycler.setAdapter(mAdapter);
+ * }
+ * 
+ * + * @param The Java class that maps to the type of objects stored in the Firebase location. + * @param The ViewHolder class that contains the Views in the layout that is shown for each object. */ public abstract class FirebaseRecyclerViewAdapter extends RecyclerView.Adapter { - FirebaseArray mSnapshots; Class mModelClass; - protected RecyclerViewClickListener clickListener; - + protected int mModelLayout; + Class mViewHolderClass; + FirebaseArray mSnapshots; /** - * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some - * combination of limit(), startAt(), and endAt(), * @param modelClass Firebase will marshall the data at a location into an instance of a class that you provide + * @param modelLayout This is the layout used to represent a single item in the list. You will be responsible for populating an + * instance of the corresponding view with the data from an instance of modelClass. + * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. + * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some + * combination of limit(), startAt(), and endAt() */ - public FirebaseRecyclerViewAdapter(Query ref, Class modelClass) { + public FirebaseRecyclerViewAdapter(Class modelClass, int modelLayout, Class viewHolderClass, Query ref) { mModelClass = modelClass; + mModelLayout = modelLayout; + mViewHolderClass = viewHolderClass; mSnapshots = new FirebaseArray(ref); mSnapshots.setOnChangedListener(new FirebaseArray.OnChangedListener() { @@ -86,9 +122,21 @@ public void onChanged(EventType type, int index, int oldIndex) { } } }); + } + /** + * @param modelClass Firebase will marshall the data at a location into an instance of a class that you provide + * @param modelLayout This is the layout used to represent a single item in the list. You will be responsible for populating an + * instance of the corresponding view with the data from an instance of modelClass. + * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. + * @param ref The Firebase location to watch for data changes. Can also be a slice of a location, using some + * combination of limit(), startAt(), and endAt() + */ + public FirebaseRecyclerViewAdapter(Class modelClass, int modelLayout, Class viewHolderClass, Firebase ref) { + this(modelClass, modelLayout, viewHolderClass, (Query)ref); } + public void cleanup() { mSnapshots.cleanup(); } @@ -110,11 +158,28 @@ public long getItemId(int position) { return mSnapshots.getItem(position).getKey().hashCode(); } - public void setClickListener(RecyclerViewClickListener clickListener) { - this.clickListener = clickListener; + @Override + public VH onCreateViewHolder(ViewGroup parent, int viewType) { + ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(mModelLayout, 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); + } } - - public interface RecyclerViewClickListener { - public void onItemClicked(int position); + @Override + public void onBindViewHolder(VH viewHolder, int i) { + T model = getItem(i); + populateViewHolder(viewHolder, model); } + + abstract public void populateViewHolder(VH viewHolder, T model); + } diff --git a/release.sh b/release.sh index 890fc1371..c046288c4 100755 --- a/release.sh +++ b/release.sh @@ -29,6 +29,8 @@ if [[ ! -z $DERP ]]; then echo "Cancelling release, please update library/build.gradle with the desired version" fi +# TODO: Ensure this version is also on pom.xml + # Ensure there is not an existing git tag for the new version # XXX this is wrong; needs to be semver sorted as my other scripts are LAST_GIT_TAG="$(git tag --list | tail -1 | awk -F 'v' '{print $2}')" @@ -48,20 +50,17 @@ fi # GENERATE RELEASE BUILD # ########################## -#gradle clean assembleRelease generateReleaseJavadoc -gradle clean assembleRelease -# gradle uploadArchives +#gradle clean assembleRelease assembleDebug bundleReleaseJavadoc +gradle clean :app:compileDebugSources :app:compileDebugAndroidTestSources :library:compileDebugSources :library:compileDebugAndroidTestSources bundleReleaseJavadoc ################### # DEPLOY TO MAVEN # ################### read -p "Next, make sure this repo is clean and up to date. We will be kicking off a deploy to maven." DERP -#the next line perform an entire build+release from maven, meaning it does not generate an aar -mvn release:clean release:prepare release:perform -Dtag=v$VERSION #the next line installs the output of build.gradle into (local) maven, but does not tag it in git #mvn install:install-file -Dfile=library/build/outputs/aar/library-release.aar -DgroupId=com.firebase -DartifactId=firebase-ui -Dversion=$VERSION -Dpackaging=aar -#this runs a gradle task that uploads the aar file to maven central -#gradle uploadArchives +#the next line signs and deploys the aar file to maven +mvn gpg:sign-and-deploy-file -Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=ossrh -DpomFile=library/pom.xml -Dfile=library/build/outputs/aar/library-release.aar -Dversion=$VERSION if [[ $? -ne 0 ]]; then echo "error: Error building and releasing to maven." @@ -85,7 +84,7 @@ fi echo "Manual steps:" echo " 1) release maven repo at http://oss.sonatype.org/" -# echo " 2) Deploy new docs: $> firebase deploy" +#echo " 2) Deploy new docs: $> firebase deploy" echo " 3) Update the release notes for FirebaseUI-Android version ${VERSION} on GitHub and add aar for downloading" #echo " 4) Update firebase-versions.json in the firebase-clients repo with the changelog information" #echo " 5) Tweet @FirebaseRelease: 'v${VERSION} of FirebaseUI-Android is available https://github.com/firebase/FirebaseUI-Android"