i am working on a project in which i am trying to implement Android architecture components with data binding.
I have an activity, viewmodel and repository and i am crating a login page right now.
I have to make two repository functions to make the user login, the first function will return the uid and by passing this uid, the second function will return the details. once everything is done then only i want to redirect the user to inside page. I have tried with Transformations.switchMap, which is not working.
Please help...
public class LoginViewModel extends ViewModel {
private final VendorRepository vendorRepository;
private final ResourceProvider resourceProvider;
public MutableLiveData<String> error = new MutableLiveData<>();
public MutableLiveData<Boolean> loading = new MutableLiveData<>();
private MutableLiveData<Resource<String>> vendorId = new MutableLiveData<>();
public LiveData<Resource<Vendor>> vendor;
public LoginViewModel() {
this.vendorRepository = VendorRepository.getInstance();
this.resourceProvider = ResourceProvider.getInstance();
/*vendor = Transformations.switchMap(vendorId, new Function<Resource<String>, LiveData<Resource<Vendor>>>() {
#Override
public LiveData<Resource<Vendor>> apply(Resource<String> input) {
if (input!=null&&input.status.equals(Status.SUCCESS))
return vendorRepository.getVendor(vendorId.getValue().data);
else return null;
}
});*/
}
/**
* called when a user clicks on the login button
* if the inputs are valid, will call the login function
*
* #param email entered email
* #param password entered password
*/
public void onClickLogin(String email, String password) {
//loading.setValue(true);
if (validInputs(email, password)) {
vendorId = vendorRepository.login(
email, password
);
} else loading.setValue(false);
}}
This is my viewmodel
public class VendorRepository {
private static VendorRepository INSTANCE;
private FirebaseAuth firebaseAuth;
private FirebaseFirestore firebaseFirestore;
public static VendorRepository getInstance() {
if (INSTANCE == null)
INSTANCE = new VendorRepository();
return INSTANCE;
}
private VendorRepository() {
this.firebaseAuth = FirebaseAuth.getInstance();
this.firebaseFirestore = FirebaseFirestore.getInstance();
}
public MutableLiveData<Resource<String>> login(String email, String password) {
final MutableLiveData<Resource<String>> data = new MutableLiveData<>();
data.setValue(Resource.<String>loading(null));
firebaseAuth
.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
data.postValue(Resource.success(task.getResult().getUser().getUid()));
} else {
data.postValue(Resource.<String>error(task.getException().getMessage(), null));
}
}
});
return data;
}
public LiveData<Resource<Vendor>> getVendor(String id) {
final MutableLiveData<Resource<Vendor>> data = new MutableLiveData<>();
data.postValue(Resource.<Vendor>loading(null));
firebaseFirestore
.collection(DB_VENDOR)
.document(id)
.get()
.addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
Vendor vendor = task.getResult().toObject(Vendor.class);
data.postValue(Resource.success(vendor));
} else {
data.postValue(Resource.<Vendor>error(task.getException().getMessage(), null));
}
}
});
return data;
} }
This is my repo
You need to add an observer of the vendorId livedata somewhere in your activity, possibly in onCreate(Bundle) after you have initialized the viewModel.
Your code will look something like this.
public final class MyActivity extends AppCompatActivity {
...
LoginViewModel mViewModel;
#Override protected final void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
mViewModel = ViewModelProviders.of(this)
.get(LoginViewModel.class);
//make vendorId public in the viewModel
mViewModel.vendorId.observe ( this, new Observer<String> (){
#Override public void onChanged(#Nullable final String vendorId) {
VendorRepository.getInstance().getVendor(vendorId);
}
});
//Similarly, add observer for the vendor live data
}
}
Related
Even though I am using ViewModel, whenever the device is rotated, the data in the Recyclerview disappears. I had to put the makeSearch() method inside the onClick() method because I need to get the text that the button grabs and use it as the search parameter. Is there a better way I can handle this to avoid this problem? My code is right here:
SearchActivity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
// What happens when the search button is clicked
materialButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Objects.requireNonNull(textInputEditText.getText()).toString().isEmpty()) {
textInputEditText.setError("Type a search query");
} else {
mSearchInput = Objects.requireNonNull(textInputEditText.getText()).toString();
textInputEditText.setText("");
makeSearch();
}
}
});
}
// Gets the ViewModel, Observes the Question LiveData and delivers it to the Recyclerview
private void makeSearch() {
final SearchAdapter searchAdapter = new SearchAdapter();
SearchViewModel mSearchViewModel = new ViewModelProvider(this,
new CustomSearchViewModelFactory(new SearchRepository())).get(SearchViewModel.class);
mSearchViewModel.setQuery(mSearchInput);
mSearchViewModel.getQuestionLiveData().observe(this, new Observer<List<Question>>() {
#Override
public void onChanged(List<Question> questions) {
mQuestions = questions;
searchAdapter.setQuestions(questions);
}
});
mRecyclerView.setAdapter(searchAdapter);
searchAdapter.setOnClickListener(mOnClickListener);
}
SearchViewModel:
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<>();
private LiveData<List<Question>> mQuestionLiveData = Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
SearchViewModel(SearchRepository searchRepository) {
this.mSearchRepository = searchRepository;
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
}
SearchRepository:
public class SearchRepository {
//private String inTitle;
private MutableLiveData<List<Question>> mQuestions = new MutableLiveData<>();
public SearchRepository() {
//getQuestionsWithTextInTitle();
}
private void getQuestionsWithTextInTitle(String inTitle) {
ApiService apiService = RestApiClient.getApiService(ApiService.class);
Call<QuestionsResponse> call = apiService.getQuestionsWithTextInTitle(inTitle);
call.enqueue(new Callback<QuestionsResponse>() {
#Override
public void onResponse(Call<QuestionsResponse> call, Response<QuestionsResponse> response) {
QuestionsResponse questionsResponse = response.body();
if (questionsResponse != null) {
mQuestions.postValue(questionsResponse.getItems());
//shouldShowData = true;
} else {
Log.d("SearchRepository", "No matching question");
//shouldShowData = false;
}
}
#Override
public void onFailure(Call<QuestionsResponse> call, Throwable t) {
//shouldShowData = false;
t.printStackTrace();
}
});
}
public LiveData<List<Question>> getQuestions(String inTitle) {
getQuestionsWithTextInTitle(inTitle);
return mQuestions;
}
}
Your approach of passing the search input in through your CustomSearchViewModelFactory and into the constructor for the ViewModel and into the constructor for your SearchRepository isn't going to work in any case. While the first time you search your CustomSearchViewModelFactory creates the ViewModel, the second time you hit search, your SearchViewModel is already created and your factory is not invoked a second time, meaning you never get the second query.
Instead, you should file the ViewModel Overview documentation, and use Transformations.switchMap() to convert your input (the search string) into a new LiveData<List<Question>> for that given query.
This means that your ViewModel would look something like
public class SearchViewModel extends ViewModel {
private SearchRepository mSearchRepository;
private MutableLiveData<String> mSearchLiveData = new MutableLiveData<String>();
private LiveData<List<Question>> mQuestionLiveData =
Transformations.switchMap(mSearchLiveData, (query) -> {
return mSearchRepository.getQuestions(query);
});
public SearchViewModel() {
mSearchRepository = new SearchRepository();
}
public void setQuery(String query) {
mSearchLiveData.setValue(query);
}
public LiveData<List<Question>> getQuestionLiveData() {
return mQuestionLiveData;
}
}
You'd then update your Activity to:
Always observe the getQuestionLiveData() (note that you won't get a callback to your Observer until you actually set the first query)
Call setQuery() on your SearchViewModel in your makeSearch()
Remove your CustomSearchViewModelFactory entirely (it would no longer be needed).
I have a Login Fragment which uses API call to login. I use mvvm and databinding to bind views with viewmodel. In viewmodel Login Response via retrofit is observed in viewmodel which uses RxJava.
I need to observe the retrofit response in the loginFragment, which is not get observed when retrofit response came. Following are the fragment and viewmodel code. I need retrofit response to pass to fragment or fragment get automatically observe response.
public class LoginFragment extends Fragment {
private LoginViewModel mLoginViewModel;
private Observable<LoginResult> dataObservable;
public static String TAG = LoginFragment.class.getSimpleName();
public Disposable disposable;
public static Fragment LoginFragmentInstance() {
Log.e(TAG, "LoginFragmentInstance: " );
Fragment fragment = new LoginFragment();
return fragment;
}
public LoginFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
FragmentLoginBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_login, container, false);
mLoginViewModel = new LoginViewModel(getActivity());
//setViewModel method name changes based on variable name declared in XML
//mLoginViewModel.loginResult.observeO
dataObservable= mLoginViewModel.loginResult;
disposable = dataObservable.subscribe(new Consumer<LoginResult>() {
#Override
public void accept(LoginResult result) throws Exception {
Log.d("TAG", result.toString());
}
});
binding.setViewModel(mLoginViewModel);
return binding.getRoot();
}
#Override
public void onDestroy() {
mLoginViewModel.destroy();
disposable.dispose();
super.onDestroy();
}
}
ViewModel File
public class LoginViewModel {
private static final String TAG = "LoginViewModel";
public ObservableField<String> userName = new ObservableField<>();
public ObservableField<String> password = new ObservableField<>();
public ObservableField<String> email = new ObservableField<>();
public ObservableField<String> userNameErr = new ObservableField<>();
public ObservableField<String> passwordErr = new ObservableField<>();
public ObservableField<String> emailErr = new ObservableField<>();
public Observable<LoginResult> loginResult = new Observable<LoginResult>() {
#Override
protected void subscribeActual(Observer<? super LoginResult> observer) {
}
};
public ObservableField<Boolean> enableLogin;
private CompositeDisposable myCompositeDisposable = new CompositeDisposable();
private HashMap<String, String> loginApiParams;
public Action signIn;
public Context context;
public LoginViewModel(final Context context) {
this.context = context;
Observable result = Observable.combineLatest(FieldUtils.toObservable(userName), FieldUtils.toObservable(password),
new BiFunction() {
#Override
public Object apply(Object userName, Object password) throws Exception {
int failCount = 0;
if (!InputValidator.validateMobileno(userName.toString())) {
++failCount;
userNameErr.set(context.getResources().getString(R.string.mobileno_incorrect));
} else {
userNameErr.set("");
}
if (!InputValidator.validatePassword(password.toString())) {
++failCount;
passwordErr.set(context.getResources().getString(R.string.password_incorrect));
} else {
passwordErr.set("");
}
return failCount == 0;
}
});
enableLogin = FieldUtils.toField(result);
signIn = new Action() {
#Override
public void run() throws Exception {
Log.d(TAG, "signIn button clicked");
loginCall();
}
};
}
private void loginCall() {
loginApiParams = new HashMap<>();
// loginApiParams.put(, paymentType.toString())
loginApiParams.put(ApiParameterKeyConstants.MOBILE,userName.get());
loginApiParams.put(ApiParameterKeyConstants.PASSWORD, password.get());
UserApi usersService = ApiService.INSTANCE.apiCall();
Disposable disposable = usersService.getLogin(loginApiParams)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<LoginResult>() {
#Override
public void accept(LoginResult result) throws Exception {
loginResult = Observable.just(result);
//loginResult.subscribe()
//loginResult = result ;
//Log.d(TAG, "Login Successfull");
}
}, new Consumer<Throwable>()
{
#Override
public void accept(Throwable throwable) throws Exception {
Log.d(TAG, "Login Failed");
}
});
myCompositeDisposable.add(disposable);
}
}
It seems like you are re-assigning the loginResult an Observable in your loginCall method of the ViewModel instead of passing the result to its Observers.
You should try calling loginResult.onNext(result) or loginResult.onComplete(result) instead of loginResult = Observable.just(result);
I am trying to do the MvP Pattern and I am separating firebase queries in the model part. I get null object reference exception when I call it on the presenter.
Model:
public class LoginModelz implements LoginContract.LoginModelz {
private LoginPresenter loginPresenter;
FirebaseAuth mAuth; //= FirebaseAuth.getInstance();
FirebaseUser currentuser;// = mAuth.getCurrentUser();
public LoginModelz(LoginPresenter loginPresenter) {
this.loginPresenter = loginPresenter;
}
public void FirebaseQuery(){
mAuth = FirebaseAuth.getInstance();
currentuser = mAuth.getCurrentUser();
}
}
Presenter:
public class LoginPresenter implements LoginContract.LoginPresenter {
private LoginView loginView;
private LoginModelz loginModelz;
public LoginPresenter(LoginView loginView) {
this.loginView = loginView;
}
public LoginPresenter(LoginModelz loginModelz) {
this.loginModelz = loginModelz;
}
#Override
public void OnLogin(String email, String password,final Context context) {
loginModelz.FirebaseQuery();
Log.e("asda",context.toString());
if(email.isEmpty() && password.isEmpty()){
loginView.OnLoginFail("Email and Password is empty");
return;
}
else if(email.isEmpty()){
loginView.OnLoginFail("Email is empty");
return;
}else if(password.isEmpty()){
loginView.OnLoginFail("Password is empty");
return;
}else {
loginView.ProgressShow("ASDASDASDASDSAd");
Log.e("emailpass", email+password);
**loginModelz.mAuth.signInWithEmailAndPassword(email,password)**
.addOnCompleteListener( new OnCompleteListener<AuthResult>()
I was getting an error from the loginmodelz.mAuth. If i put the initializations in the method in the presenter class it works just fine, but when i separated it is getting a null object reference.
Interfaces
public interface LoginContract {
interface LoginView{
void OnLoginSuccess();
void OnLoginFail(String message);
void Onregister();
void ProgressShow(String messsagge);
void ProgressDismiss(String message);
void Alert();
}
interface LoginPresenter{
void OnLogin(String email,String password,final Context context);
void OnVerify(Context context);
}
interface LoginModelz{
void FirebaseQuery();
}
}
This looks suspicious:
public LoginPresenter(LoginView loginView) {
this.loginView = loginView;
}
public LoginPresenter(LoginModelz loginModelz) {
this.loginModelz = loginModelz;
}
You have to decide on your constructor, if you call the first one you don't have a model and so on.
You could create your model inside the first constructor and pass it as an argument:
public LoginPresenter(LoginView loginView) {
this.loginView = loginView;
this.loginModel = new LoginModel(this);
}
At least that's what I can suggest from the code you provided.
You have not initialized loginModelz.
Change your presenter constructor implement
public LoginPresenter(LoginView loginView) {
this.loginView = loginView;
this.loginModelz = new LoginModelz(this);
}
I want to re-use ViewModel and LiveData for reading nodes from Firebase. This is my code in Fragment
FirebaseDatabaseViewModel test = ViewModelProviders.of(this, new FirebaseDatabaseViewModel.Factory(getActivity().getApplication(),"node1")).get(FirebaseDatabaseViewModel.class);
LiveData<DataSnapshot> ldTest = test.getDataSnapshotLiveData();
ldTest.observe(this, new Observer<DataSnapshot>() {
#Override
public void onChanged(#Nullable DataSnapshot dataSnapshot) {
Log.d("MyTag", "liveData.observe TEST dataSnapshot = " + dataSnapshot);
}
});
FirebaseDatabaseViewModel test2 = ViewModelProviders.of(this, new FirebaseDatabaseViewModel.Factory(getActivity().getApplication(),"node2")).get(FirebaseDatabaseViewModel.class);
LiveData<DataSnapshot> ldTest2 = test2.getDataSnapshotLiveData();
ldTest2.observe(this, new Observer<DataSnapshot>() {
#Override
public void onChanged(#Nullable DataSnapshot dataSnapshot) {
Log.d("MyTag", "liveData.observe TEST2 dataSnapshot = " + dataSnapshot);
}
});
}
Here is ViewModel
public class FirebaseDatabaseViewModel extends AndroidViewModel {
private final String mRef;
private final FirebaseQueryLiveData liveData;
public FirebaseDatabaseViewModel(Application application, String ref) {
super(application);
this.mRef = ref;
this.liveData = new FirebaseQueryLiveData(FirebaseDatabase.getInstance().getReference(mRef));
}
#NonNull
public LiveData<DataSnapshot> getDataSnapshotLiveData() {
return liveData;
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
private final String mRef;
public Factory(#NonNull Application application, String ref) {
mApplication = application;
this.mRef = ref;
}
#NonNull
#Override
public <T extends ViewModel> T create(#NonNull Class<T> modelClass) {
return (T) new FirebaseDatabaseViewModel(mApplication, mRef);
}
}
}
Here is LiveData
public class FirebaseQueryLiveData extends LiveData<DataSnapshot> {
private final Query query;
private final MyValueEventListener listener = new MyValueEventListener();
public FirebaseQueryLiveData(Query query) {
this.query = query;
}
public FirebaseQueryLiveData(DatabaseReference ref) {
this.query = ref;
}
#Override
protected void onActive() {
Log.d("MyTag", "onActive");
query.addValueEventListener(listener);
}
#Override
protected void onInactive() {
Log.d("MyTag", "onInactive");
query.removeEventListener(listener);
}
private class MyValueEventListener implements ValueEventListener{
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
setValue(dataSnapshot);
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.e("MyTag", "Can't listen to query " + query, databaseError.toException());
}
}
}
Problem is reading same node from FirebaseDatabase
D/MyTag: liveData.observe TEST dataSnapshot = DataSnapshot { key = node1, value = {.....
D/MyTag: liveData.observe TEST2 dataSnapshot = DataSnapshot { key = node1, value = {....
Second time I expected node2
The default ViewModelProvider only keeps a single ViewModel for a given class name. The only time your Factory is invoked is when there's no already existing ViewModel - in your case, you're using the same class name for both calls, so your second factory is never used.
Generally, you should consider only having a single ViewModel and have it return multiple different LiveData instances based on the key passed into it, keeping a HashMap<String,LiveData> to avoid recreating LiveData objects it already has:
public class FirebaseDatabaseViewModel extends AndroidViewModel {
private HashMap<String, LiveData<DataSnapshot>> mLiveDataMap = new HashMap<>();
public FirebaseDatabaseViewModel(#NonNull final Application application) {
super(application);
}
public LiveData<DataSnapshot> getDataSnapshotLiveData(String ref) {
if (!mLiveDataMap.containsKey(ref)) {
// We don't have an existing LiveData for this ref
// so create a new one
mLiveDataMap.put(ref, new FirebaseQueryLiveData(
FirebaseDatabase.getInstance().getReference(ref)));
}
return mLiveDataMap.get(ref);
}
}
I am following this tutorial to learn ViewModel and LiveData. In my case, instead of getting data from network, I am simply generating random string on button click and trying to update a textview. The problem is that the textview does not get updated when the data is changed by button click, but only gets updated when orientation is toggled.
Activity Class (extends LifecycleActivity)
public class PScreen extends BaseActivity {
#Override protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.places_screen);
final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
viewModel.init();
viewModel.getUser().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
final MutableLiveData<User> data = new MutableLiveData<>();
User user = new User();
user.setName(String.valueOf(Math.random() * 1000));
data.postValue(user);
viewModel.setUser(data); // Why it does not call observe()
}
});
}
}
ViewModel Class
package timsina.prabin.tripoptimizer.model;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.ViewModel;
public class UserModel extends ViewModel {
private LiveData<User> user;
public void init() {
if (this.getUser() != null) {
return;
}
this.user = new LiveData<User>() {
#Override protected void setValue(User value) {
value.setName("Fresh New Name");
super.setValue(value);
}
};
}
public LiveData<User> getUser() {
return user;
}
public void setUser(LiveData<User> user) {
this.user = user;
}
}
You are creating a new LiveData instance each time! You are not supposed to do that. If you do that all previous observers will be ignored.
In this case you could replace your setUSer(LiveData<User>) method on your ViewModel to setUser(User u) (taking a User instead of a LiveData) and then do user.setValue(u) inside it.
Of course, will have to initialize the LiveData member in your ViewModel class, like this:
final private LiveData<User> user = new MutableLiveData<>();
It will work then because it will notify the existing observers.
I was somehow able to resolve this by using MutableLiveData instead of LiveData.
Model class
private MutableLiveData<User> user2;
public void init() {
if (user2 == null) {
user2 = new MutableLiveData<>();
}
}
public MutableLiveData<User> getUser2() {
return user2;
}
public void setUser2(final User user) {
user2.postValue(user);
}
Activity
viewModel.getUser2().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
User user = new User();
viewModel.getUser().postValue(user);
}
});
You replace the reference to the object inside UserModel, try to swap the lines of code
data.postValue(user);
viewModel.setUser(data); // Why it does not call observe()
replace on
viewModel.setUser(data); // Why it does not call observe()
data.postValue(user);
Try to modify your code as #niqueco mentioned, set your updated method inside setUser() method and change your onclick() listener in the activity to send the new user data info only. Other works the LiveData will help u.
public class UserModel extends ViewModel {
private LiveData<User> user;
public void init() {
if (this.getUser() != null) {
return;
}
this.user = new LiveData<User>() {
#Override protected void setValue(User value) {
value.setName("Fresh New Name");
super.setValue(value);
}
};
}
public LiveData<User> getUser() {
return user;
}
public void setUser(LiveData<User> user) {
this.user.setValue(user); //the live data will help u push data
}
}
Activity Class
public class PScreen extends BaseActivity {
#Override protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.places_screen);
final UserModel viewModel = ViewModelProviders.of(this).get(UserModel.class);
viewModel.init();
viewModel.getUser().observe(this, new Observer<User>() {
#Override public void onChanged(#Nullable User user) {
((TextView) findViewById(R.id.name)).setText(user.getName());
}
});
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
#Override public void onClick(View v) {
//final MutableLiveData<User> data = new MutableLiveData<>();
User user = new User();
user.setName(String.valueOf(Math.random() * 1000));
//data.postValue(user);
viewModel.setUser(user); // Why it does not call observe()
}
});
}
}