I'm developing my first android app using Firebase. It is also my first android app overall, so I'm not expirienced, but already got some expirience with programming lanugages such as Java or PHP. I'm searching for professional advice since I've run into some problems with my code organization. I already did some research but was not able to find a good solution for this.
My app uses Firebase Authentication and Firebase Realtime Database.
I'm going to explain all relevant parts for my problem.
The main activity...
Checks if a user is logged in, if not, start the authentication activity
Contains a Object DatabaseHandler, which is responsible for the database access, this object needs a UserInfo object to be instantiated
Contains a FirebaseAuth object attached with a AuthStateListener
This AuthStateListener check if a user is logged in, if so it will re-instantiate the DatabaseHandler object with the current user
Should be able to display some data retrieved from the DatabaseHandler
My problem
I already need to have access to the DatabaseHandler object in the onCreate() method of the MainActivity to display some data, but since the DatabaseHandler gets instantiated in the method onAuthStateChanged(), to make sure there is already a user logged in, this does not work, because onCreate() is called before onAuthStateChanged()
My thoughts
I'm not entirely sure how to solve this problem, but my first thought was to restructure my project in a way that my main activity only checks which activities to call, instead of displaying data on it's own. I'm still in the early phase of the project, so this should not be much of a problem.
Just wanted to know if this will work or if there is any better solution to this.
Let me know what you think
Here an example of the activity with code that assumes you want to display a List of Books:
public class MainActivity extends AppCompatActivity {
private List<Book> bookList = new ArrayList<>();
private RecyclerView recyclerView;
private BookAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
adapter = new BookAdapter(bookList);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setAdapter(mAdapter);
FirebaseAuth auth = FirebaseAuth.getInstance();
FirebaseUser user = auth.getCurrentUser();
if (user != null) {
// User is still logged in
// Get UserInfo and instantiate DatabaseHandler
populateList();
} else {
// No user is logged in, go to auth activity
}
}
private void populateList() {
// Get Books from Firebase and add them to the adapter
Book book = new Book();
bookList.add(book);
// Notify the adapter, so that it updates the UI
adapter.notifyDataSetChanged();
}
}
With this you do the following:
create and instantiate all objects in onCreate
check for user in onCreate and act accordingly
get Data and display it from populateList, which allows to re-populate the list without going through all of the onCreate stuff (remember to clear the list/adapter
Related
I am working on an android project and I've been trying to use Firebase Firestore collections. Everything seems fine up until the second activity retrieving the intent with the parsed list of watches.
public class MainActivity extends AppCompatActivity {
private FirebaseFirestore db = FirebaseFirestore.getInstance();
private CollectionReference watchesRef = db.collection("watches");
private List<Watch> watches = new ArrayList<>();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
watchesRef.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Watch watch = document.toObject(Watch.class);
watches.add(watch);
}
Intent intent = new Intent(MainActivity.this, WatchesActivity.class);
intent.putParcelableArrayListExtra("watches", (ArrayList<Watch>) watches);
startActivity(intent);
} else {
Log.d("MainActivity", "Error getting documents: ", task.getException());
}
}
});
Say I wanted to print out each watch upon retrieval from firstore, it works just fine. When debugging, I can see the list getting filled with the watches. Debugging goes through to the next activity:
Intent intent = getIntent();
if (intent.hasExtra("watches")) {
watches = intent.getParcelableArrayListExtra("watches");
}
Right up until watches = intent.getParcelableArrayListExtra("watches"); I can see the data just fine, then I get this error java.lang.RuntimeException: Parcel android.os.Parcel#f983085: Unmarshalling unknown type code 7536745 at offset 316
KEEP IN MIND: When I clear the list then add static data, it works just fine.
Overall your approach to Android app architecture is suffering here, fetching a potentially large list of data and then attempting to pass it via an Intent to another Activity is an architecture that won't scale well.
It may fail when the parcel data size exceeds 1MB.
It may fail because Watch doesn't use only primitives or hasn't implemented itself well as Parcelable.
It may fail because when the user opens that Activity, the Watch list is so old and out-dated that your updated app crashes.
A better way to architect the app is to have the Activity that you navigate to fetch the data it needs itself.
If/when you use Intents, keep the payloads minimal. Try to pass just a single id with which the recipient can retrieve from local database or fetch from the Internet with.
I have my fire store set up and connected to my app. There are these categories and I want when I click on one category to fetch all subcategory from that category. Can someone explain to me step by step how to do that?
public class BeautyIzbornik extends Activity {
ImageButton imageButton;
private FirebaseFirestore db = FirebaseFirestore.getInstance();
private CollectionReference firemRef = db.collection("beauty");
private FirmeAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.firmeinfo);
setUpRecyclerView();
}
private void setUpRecyclerView(){
Query query= firemRef.orderBy("logo",Query.Direction.DESCENDING);
FirestoreRecyclerOptions<Firme> options = new FirestoreRecyclerOptions.Builder<Firme>()
.setQuery(query,Firme.class)
.build();
adapter= new FirmeAdapter(options);
RecyclerView recyclerView = findViewById(R.id.recycler_view2);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
}
#Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
#Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
}
my Fire store looks like this
This is my main category and subcategories:
This are subcollection from one subcategory:
And now in my Main activity, I open the main category and I want when I press on subcategory(whic are all displeyed in my main activity) I want to open new activity and fill it with items from that subcategory... I have more subcategories and items but I hope you understand what I want to show.
so i want to have my Main activity filled with categoriys,secnd activity to fill with subcategorys and third activity where i will display data of some items that is clicked in secnd activity i hope you understand me my englis is bad :D
It would be more useful if you provided the database schema. But the idea is the same on all schemas.
Query query= firemRef.document("myDocumentHERE");
...
You can get the collection of the "myDocumenthere" by doing the following
Query query= firemRef.document("myDocumentHERE").collection("myCollection");
I'd recommend reading the official documentation, there's code examples there and its explained much better.
Source
Can i get documents from collecions i have on Firestore
yes you can! do you want to display your data in recycler?
Try this. Watch Now
to retrieve your beauty in document table, please use this:
firebaseFirestore...
String postId = doc.getDocument().getId(); //retrieving 'beauty` in document table
MyContent myContent = doc.getDocument().toObject(MyContent.class.withId(postId));
in Adapter class you will retrieve it again using this:
final String postId = contentList.get(position).PostId;
firebaseFirestore.collection("kategorije").document(postId).collection("beauty").addSnapshotListener(new EventListener<QuerySnapshot>() ...
please see his videos for details.
I have to pull data from server for the first time when user lands on the fragment and then data should persist in the application unless user logs out but I tried this way.
public class AttendanceFragment : Fragment
{
private static ListView listView;
private static ProgressBar progress;
private static List<DA_ClassSectionAttendance> dataList=new List<DA_ClassSectionAttendance>();
// If i instantiate this variable 'dataList' here
//it will be persisted even the user logs out I know its declared as static
// because I am accessing this variable on broadcast receiver.
// But I want this re-instantiated after user logs out but HOW?
private static AttendanceListAdapter attendanceAdapter;
private static DA_Attendance daAttendance;
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// dataList = new List<DA_ClassSectionAttendance>(); if I instantiate this variable here everytime this fragment created or restores dataList.Count is zero or null
attendanceAdapter = new AttendanceListAdapter(this.Activity, dataList);
if((dataList==null || dataList.Count==0)) // pull data from server for the first time when fragment is created but I want this method call when user logs out as well.
{
GetClassSection(); // this method pulls data from server
}
//set whether MenuOption show/hide from toolbar
HasOptionsMenu = true;
}
Thank you
You can release the variables in onDestroy of the fragment. If you need to persist the data then you need to save it in DB. You can use SQLlite or realDB based on your requirement. Then when user logs out, clear the DB at that time. Hope it clears
I'm looking for the best implementation pattern in Android to update a list when one of the elements change in a different activity.
Imagine this user journey:
An async process fetches ten (10) contact profiles from a web server. These are placed in an array and an adapter is notified. The ten (10) contact profiles are now displayed in a list.
The user clicks on contact profile five (5). It opens up an activity with details of this contact profile. The user decides they like it and clicks 'add to favourite'. This triggers an async request to the web server that the user has favourited contact profile five (5).
The user clicks back. They are now presented again with the list. The problem is the list is outdated now and doesn't show that profile five (5) is favourited.
Do you:
Async call the web server for the updated data and notify the adapter to refresh the entire list. This seems inefficient as the call for the list can take a couple of seconds.
On favouriting the profile store the object somewhere (perhaps in a singleton service) marked for 'refresh'. OnResume in the List activity do you sniff the variable and update just that element in the list.
Ensure the list array is static available. Update the array from the detail activity. OnResume in the activity always notify the adapter for a refresh.
Ensure the list array and adapter is static available. Update the array and notify the adapter from the detail activity.
Any other options? What is the best design principle for this?
Async call the web server for the updated data and notify the adapter
to refresh the entire list. This seems inefficient as the call for the
list can take a couple of seconds.
As you say, it's very inefficient. Creating an Object is expensive in Android. Creating a List of many object is much more expensive.
On favouriting the profile store the object somewhere (perhaps in a
singleton service) marked for 'refresh'. OnResume in the List activity
do you sniff the variable and update just that element in the list.
This is not a good solution because there is a probability that the app crashes before we refresh the object or the app get killed by the device.
Ensure the list array is static available. Update the array from the
detail activity. OnResume in the activity always notify the adapter
for a refresh.
Updating the array via a static method or variable is not a good solution because it makes your detail Activity get coupled with the list. Also, you can't make sure that only the detail activity that change the list if your project get bigger.
Ensure the list array and adapter is static available. Update the
array and notify the adapter from the detail activity.
Same as the above, static variable or object is a no go.
You better use an Event Bus system like EventBus.
Whenever you clicks 'add to favourite' in detail activity, send the async request to update favourite to the web server and also send Event to the list activity to update the specific profile object. For example, if your profile has id "777" and the profile is favourited in detail activity then you need to send the Event something like this in your :
btnFavourite.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Send event when click favourite.
EventBus.getDefault.post(new RefreshProfileEvent(id, true);
}
});
RefreshProfileEvent is a simple pojo:
public class RefreshProfileEvent {
private String id;
private boolean isFavourited;
public RefreshProfileEvent(String id, boolean isFavourited) {
this.id = id;
this.isFavourited = isFavourited;
}
//getter and setter
}
Then you can receive the Event in your list activity to update the selected profile:
public class YourListActivity {
...
#Override
protected onCreate() {
...
EventBus.getDefault().register(this);
}
#Override
protected onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
#Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(RefreshProfileEvent event) {
// Refresh specific profile
// For example, your profile is saved in List<Profile> mProfiles
// Search for profile by its id.
for(int i = 0; i < mProfiles.size(); i++) {
if(mProfiles.getId().equals(event.getId()) {
// Refresh the profile in the adapter.
// I assume the adapter is RecyclerView adapter named mAdapter
mProfiles.get(i).isFavourited(true);
mAdapter.notifyItemChanged(i);
// Stop searching.
break;
}
}
}
You don't need to wait for AsyncTask request result returned by the server. Just make the profile favourited first and silently waiting for the result. If your request success, don't do anything. But if the request error, make the profile unfavourited and send unobstructive message like SnackBar to inform the user.
Third option is the best when a user changes the data in detail activity the array should be changed and then when the use returns to main activity call Adapter.notifyDataSetChanged(); will do the trick
For an ArrayAdapter , notifyDataSetChanged only works if you use the add() , insert() , remove() , and clear() on the Adapter.
You can do something like this:
#Override
protected void onResume() {
super.onResume();
Refresh();
}
public void Refresh(){
items = //response....
CustomAdapter adapter = new CustomAdapter(MainActivity.this,items);
list.setAdapter(adapter);
}
On every onResume activity it will refresh the list. Hope it helps you.
I was wondering does it matter where you instantiate adapters dataset with Realm? I like to fetch all the data that any adapter needs in the adapters constructor and thus instantiate the dataset there, but almost all examples I've seen fetch the data beforehand in the activity creating the adapter and then pass it to the adapter as a parameter.
With SQLite this seems even more arbitrary, but since I'm using Realm I need to open a realm connection every time I want to access the database and to keep the data available I need to keep the connection open. Keeping this connection open in the activity seems unnecessary since you might need to make queries in the adapter thus having to open a connection to realm within the adapter anyways.
Is there some higher reason to fetch the dataset beforehand or is this just a matter of preference?
since I'm using Realm I need to open a realm connection every time I want to access the database
Wrong, you just need 1 open instance for that given thread in order to access the database.
Keeping this connection open in the activity "seems unnecessary" since you might need to make queries in the adapter
In which case you can have the activity-level Realm instance as a "scoped dependency", that you can share through the Context via getSystemService() if that's what you like to do.
public class MyActivity extends Activity {
Realm realm;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
//...
}
#Override
public void onDestroy() {
if(realm != null) {
realm.close();
}
super.onDestroy();
}
...
#Override
public Object getSystemService(String key) {
if("REALM".equals(key)) {
return realm;
}
return super.getSystemService(key);
}
}
public class MyAdapter extends RecyclerView.Adapter<MyModelViewHolder> {
private final Context context;
Realm realm;
RealmResults<MyModel> results;
private final RealmChangeListener listener = new RealmChangeListener() {
#Override
public void onChange(Object element) {
notifyDataSetChanged();
}
}
public MyAdapter(Context context) {
this.context = context;
//noinspection ResourceType
realm = (Realm)context.getSystemService("REALM");
results = realm.where(MyModel.class).findAll();
results.addChangeListener(listener);
}
...
}
thus having to open a connection to realm within the adapter anyways.
wrong
Is there some higher reason to fetch the dataset beforehand or is this just a matter of preference?
It's because your Adapter, which is just supposed to describe how to show the elements of a dataset, become a God that also determines the data that it must show.
Although to be fair, it's actually harder to externally manage the data-set; something must hold a strong reference to the result set anyways. So when I don't really bother with unit-testability, I do obtain the results inside the Adapter itself. It does work.