I have a customer detail page which load's a customer detail from "customer" table and allow some editing to some details, one which is customer's location. The customer's location is a spinner which drop down item is loaded from "location" table.
So my current implementation is I have a CustomerActivity which have a CustomerViewModel and LocationViewModel
public class CustomerActivity extends AppCompatActivity {
...
onCreate(#Nullable Bundle savedInstanceState) {
...
customerViewModel.getCustomer(customerId).observe(this, new Observer<Customer>() {
#Override
public void onChanged(#Nullable Customer customer) {
// bind to view via databinding
}
});
locationViewModel.getLocations().observe(this, new Observer<Location>() {
#Override
public void onChanged(#Nullable List<Location> locations) {
locationSpinnerAdapter.setLocations(locations);
}
});
}
}
My question is how do I set the location spinner with the value from "customer" table since both the onChanged from both viewmodels executed order may differ (sometimes customer loads faster while other locations load faster).
I'd considered loading the locations only after the customer is loaded, but is there anyway I can load both concurrently while populating the customer's location spinner with the value from "customer" table?
Yes, you can by using flags.
public class CustomerActivity extends AppCompatActivity {
private Customer customer = null;
private List<Location> locations = null;
...
onCreate(#Nullable Bundle savedInstanceState) {
...
customerViewModel.getCustomer(customerId).observe(this, new Observer<Customer>() {
#Override
public void onChanged(#Nullable Customer customer) {
// bind to view via databinding
this.customer = customer;
if(locations != null){
both are loaded
}
}
});
locationViewModel.getLocations().observe(this, new Observer<Location>() {
#Override
public void onChanged(#Nullable List<Location> locations) {
locationSpinnerAdapter.setLocations(locations);
this.locations = locations;
if(customer != null){
//customer and locations loaded
}
}
});
}
}
It sounds like you want something similar to RxJava's combineLatest operator.
You can implement your own version using MediatorLiveData.
Related
I'm trying to re-structure my codebase to implement MVVM architecture and 3-layered architecture using Android Architecture Components.
The MainActivity.java file contains a Fragment, and this Fragment is where most of the user interactions take place. The user provides an input, and based on that input I want to be able to hit the Firebase Firestore database to fetch the relevant data and then display it on the UI. This approach should follow the Separation of Concerns principle.
Currently, upon receiving user input on the Fragment, I relay the input to its parent activity, i.e., MainActivity.java using an Interface, and then call a function FetchUserRequestedData. Once I receive the data, I display it appropriately on the UI (not a part of the Fragment).
However, I want my 1) business logic layer, 2) database access layer, and 3) UI layer in separate packages. (To ensure that I follow the n-layered architecture pattern)
Also, with Firestore as the database for my app, I'm not sure how the flow of both, input from user and output data from Firestore should be handled all the way from the data layer to the UI model. Note that Firestore database calls are asynchronous.
How can I set up my codebase to follow the correct communication model between the three layers so that it follows both MVVM and n-layered architecture?
Code is like this:
MainActivity.java
public class MainActivity extends AppCompatActivity implements FragmentClass.DataPasser {
FirebaseAuth firebaseAuth;
FirebaseUser firebaseUser;
private ArrayList<DataType> dataList = new ArrayList<>();
DataProvider dataProvider = new DataProvider();
.
.
.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
.
.
.
}
#Override
protected void onStart() {
super.onStart();
.
.
.
}
#Override
public void onUserInput(DataType input) {
dataList = dataProvider.ProvideData(input);
//update UI using this dataList
}
}
FragmentClass.java
public class FragmentClass extends CustomFragmentClass {
DataPasser dataPasser;
#Override
public void onAttach(Context context) {
super.onAttach(context);
dataPasser = (DataPasser) context;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle inState) {
super.onCreateView(inflater, container, inState);
// Do app specific setup logic.
return Content.getContentView();
}
#Override
public void userProvidedInput(DataType userInput) { //part of CustomFragmentClass
super.userProvidedInput(userInput);
dataPasser.onUserInput(userInput); //defined in MainActivity.java
}
public interface DataPasser { //interface class to send user input to main activity
public void onUserInput(DataType input);
}
}
DataProvider.java
public class DataProvider {
private ArrayList<DataType> dataList = new ArrayList<>();
DataFetcher dataFetcher = new DataFetcher();
public ArrayList<DataType> ProvideData(String userInput) {
dataList = dataFetcher(userInput);
return dataList;
}
private ArrayList<DataType> dataFetcher(String userInput) {
ArrayList<DataType> fetchedData = new ArrayList<>();
fetchedData = dataFetcher.DatabaseCaller(userInput);
return fetchedData;
}
}
DataFetcher.java
public class DataFetcher {
ArrayList<DataType> dataList;
FirebaseFirestore firestore = FirebaseFirestore.getInstance();
public ArrayList<DataType> DatabaseCaller(String userInput) {
//formulate query based on userInput
//hit database and fetch dataList
return dataList;
}
}
I have an Android app which uses firestore as its database. I have followed this series of blog posts to set up my firestore database in my app : https://firebase.googleblog.com/2017/12/using-android-architecture-components.html and then followed this stackoverflow entry to change my code to work for firestore: Android Architecture Components with Firebase specifically Firestore.
After this I was successful to display the result of my query in a recycler view, however when I added the swap to update (I do soft delete by setting a isActive flag to false) action in my app, LiveData was inconsistent in refreshing the RecyclerView. Here is my code snippets:
MainActivity.java
TaskViewModel viewModel =
ViewModelProviders.of(this).get(TaskViewModel.class);
LiveData<LinkedList<TaskProperties>> liveData = viewModel.getTaskPropertiesLiveData();
final MainActivity mainActivityReference = this;
liveData.observe(this, new Observer<LinkedList<TaskProperties>>() {
#Override
public void onChanged(#Nullable LinkedList<TaskProperties> taskProperties) {
if (taskProperties != null) {
// Get a handle to the RecyclerView.
mRecyclerView = findViewById(R.id.recyclerview);
// Create an adapter and supply the data to be displayed.
mAdapter = new TaskListAdapter(mainActivityReference, taskProperties);
// Connect the adapter with the RecyclerView.
ItemTouchHelper.Callback callback = new SimpleItemTouchHelperCallback(mAdapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.setAdapter(mAdapter);
// Give the RecyclerView a default layout manager.
mRecyclerView.setLayoutManager(new LinearLayoutManager(mainActivityReference));
}
}
});
View Model:
public class TaskViewModel extends ViewModel {
private LinkedList<TaskProperties> taskProperties;
private static final Query PROJECT_REF = FirebaseFirestore.getInstance().collection("project").whereEqualTo("active", true);
private final FirebaseQueryLiveData liveData = new FirebaseQueryLiveData(PROJECT_REF);
public TaskViewModel() {
taskPropertiesLiveData.addSource(liveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable final QuerySnapshot querySnapshot) {
if (querySnapshot != null) {
new Thread(new Runnable() {
#Override
public void run() {
taskProperties = new LinkedList<TaskProperties>();
for (DocumentSnapshot document : querySnapshot.getDocuments()) {
taskProperties.addLast(document.toObject(TaskProperties.class));
}
taskPropertiesLiveData.postValue(taskProperties);
}
}).start();
} else {
taskPropertiesLiveData.setValue(null);
}
}
});
}
#NonNull
public LiveData<LinkedList<TaskProperties>> getTaskPropertiesLiveData() {
return taskPropertiesLiveData;
}
}
Code in the callback class to remove :
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemDismiss(viewHolder.getAdapterPosition());
}
Constructor in Adapter:-
public TaskListAdapter(Context context,LinkedList<TaskProperties> taskList) {
mInflater = LayoutInflater.from(context);
this.taskList = taskList;
}
Code in Adapter to remove:-
public void onItemDismiss(int position) {
TaskDao taskDao = new TaskDao();
taskDao.softDeleteTaskInDB(taskList.get(position));
}
Code in DAO class to update( soft delete) :-
public void softDeleteTaskInDB(TaskProperties taskProperties){
taskProperties.setActive(false);
database.collection("project")
.document(taskProperties.getTask())
.set(taskProperties).
addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(DEBUG_TAG, "DocumentSnapshot successfully written!");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(DEBUG_TAG, "Error writing document", e);
}
});
Log.i(DEBUG_TAG,taskProperties.getTask());
}
I have observed that LiveData was able to refresh the view when I was deleting one component from the end of the list, however when I deleted from the middle of the list the view sometimes does not refresh properly. From the logs I found that the position that is being passed into the adapter class is working fine, however the tasklist array does not have the most updated value.
For example if the task list contains :-
Cat
Dog
Mouse
Rabbit
Tiger
and if delete Mouse and then Rabbit in quick succession, the onItemDismiss in adapter class receives position 3 in both cases, but the taskList variable in the Adapter class still contains Mouse at position 3. This means the LiveData might not have refreshed the RecyclerView.
Can someone please tell me where am I going wrong?
Thanks,
Sangho
I'm studying Android architecture components specially ROOM but I'm a beginner, so I'm trying create a simple app that store a user and show me the user name on the screen (the user can be just a test in code not need UI to create).
When I insert the user on DB I got error because I can't call it on main thread, so where I can call it? I see some people using AsyncTask but I don't think this is the correct way or using another library like RXjava, I see some people using Live data that I don't understand how/where use live data to insert data on DB.
that is what I have:
MainActivity:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
usersViewModel = ViewModelProviders.of(this).get(UsersViewModel.class);
usersViewModel.getUsers();
User testUser = new User("Test");
usersViewModel.saveUser(testUser);
}
User:
#Entity
public class User {
public #PrimaryKey(autoGenerate = true) int id;
public String name;
public User(String name) {
this.name = name;
}
//Gets and Sets
}
UserDAO:
public interface UserDao {
#Query("SELECT * FROM user")
LiveData<List<User>> getUsers();
#Insert
void insert(User user);
}
UserViewModel:
public class UsersViewModel extends AndroidViewModel {
private LiveData<List<User>> users;
private AppDatabase mDb;
public UsersViewModel(#NonNull Application application) {
super(application);
mDb = AppDatabase.getInMemoryDatabase(this.getApplication().getApplicationContext());
users = mDb.userModel().getUsers();
}
LiveData<List<User>>getUsers(){
return users;
}
void saveUser(User user) {
mDb.userModel().insert(user);
}
}
i see some people using asyncTask but i dont think this is the corret way or using another library like RXjava
It is correct
where i can call it?
On a separate thread. The simplest non-Android example would be
// works (pre-java8)
Thread t = new Thread() {
#Override
public void run() {
mAppDatabase.userDao().insert(u);
}
};
t.start();
The Android example architecture code is a good starting point for doing RX work.
Kotlin also provides simpler threading syntax
.There are several ways of calling code on Background thread:
Java way:
new Thread(()-> callDb()).start();
The standard "old" android is AsyncTask:
new AsyncTask<Void,Void,Void>(){
#Override
protected Void doInBackground(final Void... voids)
{
callDb();
return null;
}
}.execute();
Most modern applications would use RxJava for this:
Observable.fromCallable(()-> doDbStuff()).subscribeOn(Schedulers.io()).subscribe();
call SaveData() in main
private void SaveData(String name)
{
User testUser = new User();
testUser.setName(name);
new AsyncTask<Object, Object, Long[]>() {
#Override
protected Long[] doInBackground(Object... Long)
{
final Long[] l = mAppDatabase.UserDAO().saveUser(testUser);
return l;
}
#Override
protected void onPostExecute(Long[] l)
{
if(l.length == 1)
{
//ok saved
}
else
{
//no not saved
}
}
}.execute();
}
Simple thing I would like to do (see in the picture)
Display a view with info coming from 2 different places in Firebase so that it behaves in a professional way scrolling UP and DOWN
I have a list of movies and on each of them I would like the user to specify a rating and see it
In DB I created 2 structures to have the list of movies on one side and the ratings per user on the other
Problem using FirebaseRecyclerAdapter
My problem is that scrolling fast up and down the list, the visualization of the information coming from the second reference (the rating) is loaded on a different time (asynchronous call) and this is not acceptable to see this (little) delay building the view. Is this a limitation of FirebaseRecyclerView?
Because viewHolders are reused in the recycleView I reset and reload each time in populateView() the rating values and this doesn't help. Once retrieved I'm oblidged to get them again if the user scroll the view (see the setOnlistener in populateView()
Setting a listener in populateView cause also to have as many listener as the number of times populateView() is executed (if you scroll UP and DOWN it's many times).
Solutions / Workaround ?
Is there a correct way to do it preventing the problem? Or is it a limitation?
What about performance with my implementation where the listener is inside populateView() and there are MANY listener created?
Below some things I'm thinking on:
Prevent viewHolders to be recycled and just load once?
Override some other methods of RecyclerView? I tried with parseSnapshot() but it's the same problem...
Change the DB structure to have all the info in one list (I don't think it's the good one because it means adding rating information of each user to movie list)
Add a loading spinner on the rating part so that the rating is displayed only when the asyncrhonous call to firebase is completed (don't like it) without the today effect of: "changing star color in front of the user".
My Implementation
From FirebaseRecyclerAdapter
#Override
protected void populateViewHolder(final MovieViewHolder viewHolder, final Movie movie, final int position) {
String movieId = this.getRef(position).getKey();
// Oblidged to show no rating at the beginning because otherwise
// if a viewHolder is reused it has the values from another movie
viewHolder.showNoRating();
//---------------------------------------------
// Set values in the viewHolder from the model
//---------------------------------------------
viewHolder.movieTitle.setText(movie.getTitle());
viewHolder.movieDescription.setText(movie.getDescription());
//-----------------------------------------------------
// Ratings info are in another DB location... get them
// but call is asynchronous so PROBLEM when SCROLLING!
//-----------------------------------------------------
DatabaseReference ratingMovieRef = mDbRef.child(Constants.FIREBASE_LOCATION_RATINGS).child(currentUserId).child(movieId);
ratingQuoteRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RatingMovie ratingMovie = dataSnapshot.getValue(RatingMovie.class);
Rating rating = Rating.NO_RATING;
if (ratingMovie != null) {
rating = Rating.valueOf(ratingMovie.getRating());
}
// Set the rating in the viewholder (through anhelper method)
viewHolder.showActiveRating(rating);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
from MovieViewHolder
public class QuoteViewHolder extends RecyclerView.ViewHolder {
public CardView cardView;
public TextView movieTitle;
public TextView movieDescription;
public ImageView ratingOneStar;
public ImageView ratingTwoStar;
public ImageView ratingThreeStar;
public QuoteViewHolder(View itemView) {
super(itemView);
movieTitle = (TextView)itemView.findViewById(R.id.movie_title);
movieDescription = (TextView)itemView.findViewById(R.id.movie_descr);
// rating
ratingOneStar = (ImageView)itemView.findViewById(R.id.rating_one);
ratingTwoStar = (ImageView)itemView.findViewById(R.id.rating_two);
ratingThreeStar = (ImageView)itemView.findViewById(R.id.rating_three);
}
/**
* Helper to show the color on stars depending on rating value
*/
public void showActiveRating(Rating rating){
if (rating.equals(Rating.ONE)) {
// just set the good color on ratingOneStar and the others
...
}
else if (rating.equals(Rating.TWO)) {
// just set the good color
...
} else if (rating.equals(Rating.THREE)) {
// just set the good color
...
}
/**
* Initialize the rating icons to unselected.
* Important because the view holder can be reused and if not initalised values from other moviecan be seen
*/
public void initialiseNoRating(){
ratingOneStar.setColorFilter(ContextCompat.getColor(itemView.getContext(), R.color.light_grey));
ratingTwoStar.setColorFilter(....
ratingThreeStar.SetColorFilter(...
}
You can sort of cache the ratings using a ChildEventListener. Basically just create a separat one just for the Ratings node, and have it store the ratings in a Map. Then using the RecyclerAdapter you will retrieve from the Map if the rating is available, if it is not, have the rating listener update the recyclerview as soon as is has downloaded the rating. This is one strategy you could go about, doing it, you will have to manually copy/paste some classes from the FirebaseUI library and set some fields public for this to work.
Usage would be something like this
private MovieRatingConnection ratingConnection;
// inside onCreate
ratingConnection = new MovieRatingConnection(userId, new MovieRatingConnection.RatingChangeListener() {
#Override
public void onRatingChanged(DataSnapshot dataSnapshot) {
if (recyclerAdapter != null) {
if (dataSnapshot != null) {
int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey());
recyclerAdapter.notifyItemChanged(index);
}
}
}
});
Query movieQuery = FirebaseDatabase.getInstance().getReference().child("Movies");
recyclerAdapter = new FirebaseRecyclerAdapter(movieQuery...) {
#Override
public void populateViewHolder(RecyclerView.ViewHolder viewHolder, Object model, int position) {
//...
final String key = getRef(position).getKey();
viewHolder.showActiveRating(ratingConnection.getRating(key));
}
};
and MovieRatingConnection would be a class like this
public class MovieRatingConnection {
private MovieRatingListener listener;
public MovieRatingConnection(String userId, RatingChangeListener changeListener) {
Query query = FirebaseDatabase.getInstance().getReference().child("MovieRatings").child(userId);
listener = new MovieRatingListener(query, changeListener);
}
public Rating getRating(String key) {
return listener.getRating(key);
}
public void cleanup() {
if (listener != null) {
listener.unregister();
}
}
public static class MovieRatingListener implements ChildEventListener {
public interface RatingChangeListener {
public void onRatingChanged(DataSnapshot snapshot);
}
private Query query;
private HashMap<String, Rating> ratingMap = new HashMap<>();
private RatingChangeListener changeListener;
public MovieRatingListener(Query query, RatingChangeListener changeListener) {
this.query = query;
this.changeListener = changeListener;
query.addChildEventListener(this);
}
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
ratingMap.remove(dataSnapshot.getKey());
changeListener.onRatingChanged(null);
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
public Rating getRating(String key) {
if (ratingMap.get(key) != null) {
return ratingMap.get(key);
} else {
return new Rating(); // default value/null object
}
}
public void unregister() {
query.removeEventListener(this);
}
}
}
So I'll try to keep this question as to-the-point as possible, but it will involve code snippets that traverse an entire codepath.
For context, I am fairly new and completely self-taught for Android dev, so please notify me of any clear misunderstandings/poor organization throughout. The main focus of the question is bug I am experiencing now, which is that, after a network request, the variable that was supposed to be set as a result of that network request is null, because the code moved forward before the network request completed.
Here is my activity method. It is supposed to populate the mFriends variable with the result of mUserPresenter.getUserList(), which is (unfortunately) null:
/**
* Grabs a list of friends, populates list with UserAdapter
*/
#Override
public void onResume(){
super.onResume();
mUserPresenter = new UserPresenter();
mFriends = mUserPresenter.getUserList();
if (mGridView.getAdapter() == null) {
UserAdapter adapter = new UserAdapter(getActivity(), mFriends);
mGridView.setAdapter(adapter);
}
else{
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
}
Here is how I am structuring my UserPresenter method getUserList:
public List<User> getUserList()
{
ApiService.get_friends(this);
return mUserList;
}
The real magic happens in the ApiService class:
public static void get_friends(final UserPresenter userPresenter){
ApiEndpointInterface apiService = prepareService();
apiService.get_friends().
observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Action1<List<User>>()
{
#Override
public void call(List<User> users) {
userPresenter.setList(users);
}
}
);
}
My thinking was, that by calling userPresenter.setList(users) in ApiService, that would set mUserList to the response from the api request. However, instead, mUserList == null at the time that getUserList responds.
Any ideas of how I can structure this?
I have also started to learn something similar. Here, I would rather use callbacks.
In your presenter,
public void setList(List<User> users) {
yourView.setUserList(users);
}
And your activity which implements a view (MVP)
#Override
public void setUserList(List<User> users) {
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
Also, check that retrofit is not returning null list.
I have a made a small app when I was learning about all this. It fetches user data from GitHub and shows in a list. I was also working with ORMLite and Picasso so some db stuff is there. Dagger Dependency is also used (but you can ignore that). Here's the link.
Here's how my Presenter behaves:
private DataRetrieverImpl dataRetriever;
#Override
public void getUserList(String name) {
dataRetriever.getUserList(name);
}
#Override
public void onEvent(DataRetrieverEvent event) {
UserList userList = (UserList)event.getData();
mainView.setItems(userList);
}
DataRetrieverImpl works as a module (sort of).
private DataRetriever dataRetriever;
restAdapter = new RestAdapter.Builder().setEndpoint(SERVER_END_POINT).build();
dataRetriever = restAdapter.create(DataRetriever.class);
public void getUserList(final String name) {
Log.i(TAG, "getting user list for: " + name);
Observable<UserList> observable = dataRetriever.getUserList(name);
Log.i(TAG, "subscribe to get userlist");
observable.subscribe(new Action1<UserList>() {
#Override
public void call(UserList userList) {
eventBus.post(new DataRetrieverEvent("UserList", userList));
// save to database
for (User user : userList.getItems()) {
Log.i(TAG, user.getLogin());
try {
dbHelper.create(user);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
throwable.printStackTrace();
}
});
}
And DataRetriever is interface for retrofit. I'm sorry for the naming confusion.
public interface DataRetriever {
#GET("/search/users")
public Observable<UserList> getUserList(#Query("q") String name);
}
Any my Activity,
#Override
public void setItems(final UserList userList) {
runOnUiThread(new Runnable() {
#Override
public void run() {
UserAdapter userAdapter = (UserAdapter)recyclerView.getAdapter();
userAdapter.setUserList(userList);
userAdapter.notifyItemRangeInserted(0, userAdapter.getItemCount());
}
});
}