diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52b5e1fca..ce2b85437 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,9 @@ + mAdapter; - private TextView mEmptyListMessage; + protected FirebaseRecyclerAdapter mAdapter; + protected TextView mEmptyListMessage; @Override protected void onCreate(Bundle savedInstanceState) { @@ -64,28 +62,9 @@ protected void onCreate(Bundle savedInstanceState) { mMessageEdit = (EditText) findViewById(R.id.messageEdit); mEmptyListMessage = (TextView) findViewById(R.id.emptyTextView); - mRef = FirebaseDatabase.getInstance().getReference(); - mChatRef = mRef.child("chats"); + mChatRef = FirebaseDatabase.getInstance().getReference().child("chats"); - mSendButton.setOnClickListener(new View.OnClickListener() { - @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(""); - } - }); + mSendButton.setOnClickListener(this); mManager = new LinearLayoutManager(this); mManager.setReverseLayout(false); @@ -126,33 +105,30 @@ public void onDestroy() { } @Override - public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { - updateUI(); - } + public void onClick(View v) { + String uid = mAuth.getCurrentUser().getUid(); + String name = "User " + uid.substring(0, 6); - private void attachRecyclerViewAdapter() { - Query lastFifty = mChatRef.limitToLast(50); - mAdapter = new FirebaseRecyclerAdapter( - Chat.class, R.layout.message, ChatHolder.class, lastFifty) { + Chat chat = new Chat(name, mMessageEdit.getText().toString(), uid); + mChatRef.push().setValue(chat, new DatabaseReference.CompletionListener() { @Override - public void populateViewHolder(ChatHolder holder, Chat chat, int position) { - holder.setName(chat.getName()); - holder.setText(chat.getMessage()); - - FirebaseUser currentUser = mAuth.getCurrentUser(); - if (currentUser != null && chat.getUid().equals(currentUser.getUid())) { - holder.setIsSender(true); - } else { - holder.setIsSender(false); + public void onComplete(DatabaseError error, DatabaseReference reference) { + if (error != null) { + Log.e(TAG, "Failed to write message", error.toException()); } } + }); - @Override - public void onDataChanged() { - // If there are no chat messages, show a view that invites the user to add a message. - mEmptyListMessage.setVisibility(mAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); - } - }; + 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() { @@ -165,6 +141,26 @@ public void onItemRangeInserted(int positionStart, int itemCount) { mMessages.setAdapter(mAdapter); } + protected FirebaseRecyclerAdapter getAdapter() { + Query lastFifty = mChatRef.limitToLast(50); + return new FirebaseRecyclerAdapter( + Chat.class, + R.layout.message, + ChatHolder.class, + lastFifty) { + @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() 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 7c95dccdb..ce3be5248 100644 --- a/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java +++ b/app/src/main/java/com/firebase/uidemo/database/ChatHolder.java @@ -13,6 +13,8 @@ import android.widget.TextView; import com.firebase.uidemo.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; public class ChatHolder extends RecyclerView.ViewHolder { private final TextView mNameField; @@ -36,7 +38,23 @@ public ChatHolder(View itemView) { mGray300 = ContextCompat.getColor(itemView.getContext(), R.color.material_gray_300); } - public void setIsSender(boolean isSender) { + public void bind(Chat chat) { + setName(chat.getName()); + setText(chat.getMessage()); + + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + setIsSender(currentUser != null && chat.getUid().equals(currentUser.getUid())); + } + + private void setName(String name) { + mNameField.setText(name); + } + + private void setText(String text) { + mTextField.setText(text); + } + + private void setIsSender(boolean isSender) { final int color; if (isSender) { color = mGreen300; @@ -56,12 +74,4 @@ public void setIsSender(boolean isSender) { ((RotateDrawable) mRightArrow.getBackground()).getDrawable() .setColorFilter(color, PorterDuff.Mode.SRC); } - - public void setName(String name) { - mNameField.setText(name); - } - - public void setText(String text) { - mTextField.setText(text); - } } diff --git a/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java b/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java new file mode 100644 index 000000000..10186ad61 --- /dev/null +++ b/app/src/main/java/com/firebase/uidemo/database/ChatIndexActivity.java @@ -0,0 +1,67 @@ +package com.firebase.uidemo.database; + +import android.os.Bundle; +import android.view.View; + +import com.firebase.ui.database.FirebaseIndexRecyclerAdapter; +import com.firebase.ui.database.FirebaseRecyclerAdapter; +import com.firebase.uidemo.R; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; + +public class ChatIndexActivity extends ChatActivity { + private DatabaseReference mChatIndicesRef; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mChatIndicesRef = FirebaseDatabase.getInstance().getReference().child("chatIndices"); + } + + @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() { + return new FirebaseIndexRecyclerAdapter( + Chat.class, + R.layout.message, + ChatHolder.class, + mChatIndicesRef.limitToLast(50), + mChatRef) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.bind(chat); + } + + @Override + public void onChildChanged(EventType type, + DataSnapshot snapshot, + int index, + int oldIndex) { + super.onChildChanged(type, snapshot, index, oldIndex); + + // TODO temporary fix for https://github.com/firebase/FirebaseUI-Android/issues/546 + onDataChanged(); + } + + @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); + } + }; + } +} diff --git a/database/README.md b/database/README.md index 0b572b71b..28124e387 100644 --- a/database/README.md +++ b/database/README.md @@ -1,15 +1,15 @@ -# firebase-ui-database +# FirebaseUI Database -## Using FirebaseUI to Populate a ListView +## Using FirebaseUI to populate a `RecyclerView` To use the FirebaseUI to display Firebase data, we need a few things: 1. A Java class that represents our database objects - 1. A custom list adapter to map from a collection from Firebase to Android + 1. A custom recycler view adapter to map from a collection from Firebase to Android ### Creating a model class -In your app, create a class that represents the data from Firebase that you want to show in the ListView. +In your app, create a class that represents the data from Firebase that you want to show in the RecyclerView. So say we have these chat messages in our Firebase database: @@ -61,7 +61,7 @@ public static class Chat { A few things to note here: * The fields have the exact same name as the properties in Firebase. This allows Firebase to automatically map the properties to these fields. - * There is a default (parameterless constructor) that is necessary for Firebase to be able to create a new instance of this class. + * There is a default (parameter-less constructor) that is necessary for Firebase to be able to create a new instance of this class. * There is a convenience constructor that takes the member fields, so that we easily create a fully initialized `Chat` in our app * the `getText`, `getUid`, and `getName` methods are so-called getters and follow a JavaBean pattern @@ -96,11 +96,11 @@ we get the `Chat` objects from the `DataSnapshot` with `getValue(Chat.class)`. T then read the properties that it got from the database and map them to the fields of our `Chat` class. But when we build our app using FirebaseUI, we often won't need to register our own EventListener. The -`FirebaseListAdapter` takes care of that for us. +`FirebaseRecyclerAdapter` takes care of that for us. -### Find the ListView +### Find the RecyclerView -We'll assume you've already added a `ListView` to your layout and have looked it up in the `onCreate` method of your activity: +We'll assume you've already added a `RecyclerView` to your layout and have looked it up in the `onCreate` method of your activity: ```java @Override @@ -108,10 +108,40 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); } ``` +### 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: + +```java +public static class ChatHolder extends RecyclerView.ViewHolder { + private final TextView mNameField; + private final TextView mTextField; + + public ChatHolder(View itemView) { + super(itemView); + mNameField = (TextView) itemView.findViewById(android.R.id.text1); + mTextField = (TextView) itemView.findViewById(android.R.id.text2); + } + + public void setName(String name) { + mNameField.setText(name); + } + + public void setText(String text) { + mTextField.setText(text); + } +} +``` + +There's nothing magical going on here; we're just mapping numeric IDs and casts into a nice, type-safe contract. + ### Connect to Firebase First we'll set up a reference to the database of chat messages: @@ -122,15 +152,17 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); } ``` -### Create custom FirebaseListAdapter subclass +### Create custom FirebaseRecyclerAdapter subclass -Next, we need to create a subclass of the `FirebaseListAdapter` with the correct parameters and implement its `populateView` method: +Next, we need to create a subclass of the `FirebaseRecyclerAdapter` with the correct parameters +and implement its `populateViewHolder` method: ```java @Override @@ -138,31 +170,36 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); - - mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { - @Override - protected void populateView(View view, Chat chatMessage, int position) { - ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); - ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + mAdapter = new FirebaseRecyclerAdapter( + Chat.class, + ChatHolder.class, + R.layout.message, + ref) { + @Override + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.setName(chat.getName()); + holder.setText(chat.getMessage()); } }; - messagesView.setAdapter(mAdapter); + + messages.setAdapter(mAdapter); } ``` -In this last snippet we create a subclass of `FirebaseListAdapter`. +In this last snippet we create a subclass of `FirebaseRecyclerAdapter`. We tell is that it is of type ``, so that it is a type-safe collection. We also tell it to use `Chat.class` when reading messages from the database. Next we say that each message will be displayed in a `android.R.layout.two_line_list_item`, which is a built-in layout in Android that has two `TextView` elements under each other. Then we say that the adapter belongs to `this` activity and that it needs to monitor the data location in `ref`. -We also have to override the `populateView()` method, from the `FirebaseListAdapter`. The -`FirebaseListAdapter` will call our `populateView` method for each `Chat` it finds in the database. +We also have to override the `populateViewHolder` method, from the `FirebaseRecyclerAdapter`. The +`FirebaseRecyclerAdapter` will call our `populateViewHolder` method for each `Chat` it finds in the database. It passes us the `Chat` and a `View`, which is an instance of the `android.R.layout.two_line_list_item` we specified in the constructor. So what we do in our subclass is map the fields from `chatMessage` to the correct `TextView` controls from the `view`. The code is a bit verbose, but hey... that's Java and Android for you. @@ -170,7 +207,7 @@ correct `TextView` controls from the `view`. The code is a bit verbose, but hey. ### Clean up When the Activity is Destroyed Finally, we need to clean up after ourselves. When the activity is destroyed, we need to call `cleanup()` -on the `ListAdapter` so that it can stop listening for changes in the Firebase database. +on the adapter so that it can stop listening for changes in the Firebase database. ```java @Override @@ -191,25 +228,31 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ListView messagesView = (ListView) findViewById(R.id.messages_list); + RecyclerView messages = (RecyclerView) findViewById(R.id.messages); + messages.setLayoutManager(new LinearLayoutManager(this)); DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); - mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { + mAdapter = new FirebaseRecyclerAdapter( + Chat.class, + ChatHolder.class, + R.layout.message, + ref) { @Override - protected void populateView(View view, Chat chatMessage, int position) { - ((TextView)view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); - ((TextView)view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + public void populateViewHolder(ChatHolder holder, Chat chat, int position) { + holder.setName(chat.getName()); + holder.setText(chat.getMessage()); } }; - messagesView.setAdapter(mAdapter); - final EditText mMessage = (EditText) findViewById(R.id.message_text); + messages.setAdapter(mAdapter); + + final EditText message = (EditText) findViewById(R.id.message_text); findViewById(R.id.send_button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - ref.push().setValue(new Chat("puf", "1234", mMessage.getText().toString())); - mMessage.setText(""); + ref.push().setValue(new Chat("puf", "1234", message.getText().toString())); + message.setText(""); } }); } @@ -223,82 +266,38 @@ protected void onDestroy() { You're done! You now have a minimal, yet fully functional, chat app in about 30 lines of code. Not bad, right? -## Using FirebaseUI to Populate 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 `FirebaseRecyclerAdapter` too. Here's how you use it: - -1. Create a custom ViewHolder class -2. Create a custom subclass FirebaseRecyclerAdapter - -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: +## Using FirebaseUI to populate a `ListView` +ListView is the older, yet simpler way to handle lists of items. Using it is analogous to +using a `FirebaseRecyclerAdapter`, but with `FirebaseListAdapter` instead and no `ViewHolder`: ```java -public static class ChatHolder extends RecyclerView.ViewHolder { - private final TextView mNameField; - private final TextView mTextField; - - public ChatHolder(View itemView) { - super(itemView); - mNameField = (TextView) itemView.findViewById(android.R.id.text1); - mTextField = (TextView) itemView.findViewById(android.R.id.text2); - } - - public void setName(String name) { - mNameField.setText(name); - } - - public void setText(String text) { - mTextField.setText(text); - } -} -``` - -There's nothing magical going on here; we're just mapping numeric IDs and casts into a nice, type-safe contract. +ListView messagesView = (ListView) findViewById(R.id.messages_list); -### Create a custom FirebaseRecyclerAdapter - -Just like we did for `FirebaseListAdapter`, we'll create an anonymous subclass for our Chats, but this time we'll use `FirebaseRecyclerAdapter`: - -```java -RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler); -recycler.setHasFixedSize(true); -recycler.setLayoutManager(new LinearLayoutManager(this)); +DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); -mAdapter = new FirebaseRecyclerAdapter(Chat.class, android.R.layout.two_line_list_item, ChatHolder.class, mRef) { +mAdapter = new FirebaseListAdapter(this, Chat.class, android.R.layout.two_line_list_item, ref) { @Override - public void populateViewHolder(ChatHolder chatMessageViewHolder, Chat chatMessage, int position) { - chatMessageViewHolder.setName(chatMessage.getName()); - chatMessageViewHolder.setText(chatMessage.getText()); + protected void populateView(View view, Chat chatMessage, int position) { + ((TextView) view.findViewById(android.R.id.text1)).setText(chatMessage.getName()); + ((TextView) view.findViewById(android.R.id.text2)).setText(chatMessage.getText()); + } }; -recycler.setAdapter(mAdapter); +messagesView.setAdapter(mAdapter); ``` -Like before, we get a custom RecyclerView populated with data from Firebase by setting the properties to the correct fields. - ## Using FirebaseUI with indexed data -If your data is [properly indexed](https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure), change your adapter initalization like so: +If your data is [properly indexed](https://firebase.google.com/docs/database/android/structure-data#best_practices_for_data_structure), change your adapter initialization like so: For a `RecyclerView`, use `FirebaseIndexRecyclerAdapter` instead of `FirebaseRecyclerAdapter`: ```java -new FirebaseIndexRecyclerAdapter(Chat.class, - android.R.layout.two_line_list_item, - ChatHolder.class, - keyRef, // The Firebase location containing the list of keys to be found in dataRef. - dataRef) //The Firebase location to watch for data changes. Each key key found at keyRef's location represents a list item in the RecyclerView. -``` - -And for a `ListView`, use `FirebaseIndexListAdapter`; -```java -new FirebaseIndexListAdapter(this, Chat.class, android.R.layout.two_line_list_item, keyRef, dataRef) +new FirebaseIndexRecyclerAdapter( + Chat.class, + android.R.layout.two_line_list_item, + ChatHolder.class, + keyRef, // The Firebase location containing the list of keys to be found in dataRef. + dataRef) //The Firebase location to watch for data changes. Each key key found at keyRef's location represents a list item in the RecyclerView. ``` `keyRef` is the location of your keys, and `dataRef` is the location of your data.