I've been struggling for many hours on how to do this... So I have an Activity which creates a fragment.
mAddCommentButton.setOnClickListener((View v) ->{
BottomSheetAddComment bottomSheetAddComment = new BottomSheetAddComment();
bottomSheetAddComment.show(getSupportFragmentManager(), null);
});
In that fragment, it makes a network call and I want to send the results of that network call back to the Activity's Presenter, but I can't seem to understand how to do it...
private void makeNetworkCall(Comment comment){
RetrofitInterfaces.IPostNewComment service = RetrofitClientInstance.getRetrofitInstance().create(RetrofitInterfaces.IPostNewComment.class);
Call<EventCommentsDao> call = service.listRepos(comment);
call.enqueue(new Callback<EventCommentsDao>() {
#Override
public void onResponse(Call<EventCommentsDao> call, Response<EventCommentsDao> response) {
// Send response back to Activity Presenter
}
#Override
public void onFailure(Call<EventCommentsDao> call, Throwable t) {
}
});
}
Presenter:
public class EventPresenter implements EventContract.Presenter{
private EventContract.View eventView;
private EventContract.Model eventModel;
public EventPresenter(EventContract.View eventView) {
this.eventView = eventView;
eventModel = new EventModel();
}
#Override
public void onDestroy() {
this.eventView = null;
}
#Override
public void requestDataFromServer() {
if(eventView != null){
eventView.hideProgress();
}
eventModel.getEventInfo(this);
}
}
How do I get reference to the Activity Presenter so I can send the results back?
Add a method in your Activity to return event presenter:
public EventPresenter getPresenter() {
return this.eventPresenter;
}
And in your Fragment:
private void makeNetworkCall(Comment comment){
RetrofitInterfaces.IPostNewComment service = RetrofitClientInstance.getRetrofitInstance().create(RetrofitInterfaces.IPostNewComment.class);
Call<EventCommentsDao> call = service.listRepos(comment);
call.enqueue(new Callback<EventCommentsDao>() {
#Override
public void onResponse(Call<EventCommentsDao> call, Response<EventCommentsDao> response) {
// get your presenter by:
EventPresenter mPresenter = ((MyActivity) getActivity()).getPresenter();
}
#Override
public void onFailure(Call<EventCommentsDao> call, Throwable t) {
}
});
}
Different alternatives in terms of communication between fragments would be to create callback interfaces or use event bus. See this post for more details Android MVP : One Activity with Multiple Fragments
Related
hello i have two problems regarding live data with view model and navigation component first one is when i go from fragment A with live data to fragment B and then from B to A the data in my list gets duplicated,
the other problem is one i re call viewModel.loadList() in my fragment after making and event to filter the data also gets duplicated
here is my view model
public class HomeViewModel extends ViewModel {
MutableLiveData<ArrayList<HomeResponseModel>> homeLiveData = new MutableLiveData<>();
ArrayList<HomeResponseModel> homeList = new ArrayList<>();
public MutableLiveData<ArrayList<HomeResponseModel>> geHomeList(HomeRequestModel homeRequestModel, Context context, ApiInterface apiInterface, LottieAnimationView lottieAnimationView) {
if (homeLiveData == null) {
homeLiveData = new MutableLiveData<ArrayList<HomeResponseModel>>();
loadHomeList(homeRequestModel);
}
return homeLiveData;
}
public void loadHomeList(HomeRequestModel homeRequestModel) {
Call<List<HomeResponseModel>> call = apiInterface.getHomeList(homeRequestModel, );
call.enqueue(new Callback<List<HomeResponseModel>>() {
#Override
public void onResponse(Call<List<HomeResponseModel>> call, Response<List<HomeResponseModel>> response) {
if (response.isSuccessful()) {
homeList.addAll(response.body());
homeLiveData.setValue(homeList);
}
}
#Override
public void onFailure(Call<List<HomeResponseModel>> call, Throwable t) {
}
});
}
my observer in onCreateView
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
how i recall load method after a filter event
viewModel.loadHomeList(homeRequestModel);
Clear the list before adding the new models:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeResponseModels.clear();
homeResponseModels.addAll(homeResponse);
homeAdapter.notifyDataSetChanged();
}
});
}
Or, even better:
In case your adapter holds a List or HomeResponseModel you could create a method to update it:
public update(List<HomeResponseModel> homeResponse) {
this.homeResponseModels = homeResponse;
notifydatasetchanged();
}
and then change the observe method to call it:
viewModel.geHomeList(homeRequestModel).observe(getViewLifecycleOwner(), new Observer<ArrayList<HomeResponseModel>>() {
#Override
public void onChanged(ArrayList<HomeResponseModel> homeResponse) {
homeAdapter.update(homeResponse);
}
});
}
Besides that, in your ViewModel in the loadHomeList method in the onResponse callback you could assign the data received to the liveData:
if (response.isSuccessful()) {
homeLiveData.setValue(response.body());
}
no need to save it in the homeList var, you can get rid of that var. Otherwise, perform homeList.clear(); before adding to it all the received data to avoid duplicates.
I am new to ReactiveX and I have a case where I want my observable to emit data to a late subscriber(whenever the observer subscribes, observable should emit the same data that it emitted previously). I made this Observable class that provide ReplaySubject's same instance to all observers (it is singleton class).
public class AccountsObservable {
private static ConnectableObservable<String> hotObservable;
private static AccountsObservable accountsObservable;
public static AccountsObservable getObject() {
if (accountsObservable == null) {
accountsObservable = new AccountsObservable();
}
return accountsObservable;
}
public ConnectableObservable<String> getObservable() {
if (hotObservable == null) {
Observable<String> observable = ReplaySubject.create(new ObservableOnSubscribe<String>() {
#Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception {
emitter.onNext("XYZ");
emitter.onComplete();
}
});
hotObservable = observable.replay();//publish
}
return hotObservable;
}
}
Similarly, this is the observer class that creates new observer instance.
public class AccountsObserver {
AccountsFetchListener listener;
public AccountsObserver(AccountsFetchListener listener) {
this.listener = listener;
}
public Observer<String> getObserver() {
return new Observer<String>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(String accounts) {
listener.onSuccess(accounts);
}
#Override
public void onError(Throwable e) {
listener.onFailure();
}
#Override
public void onComplete() {
}
};
}
public interface AccountsFetchListener {
void onSuccess(String accounts);
void onFailure();
}
}
Here is the function where I test these observables
private void testObs() {
ConnectableObservable<String> observable = AccountsObservable.getObject().getObservable();
Observer<String> observer = new AccountsObserver(new AccountsObserver.AccountsFetchListener() {
#Override
public void onSuccess(String accounts) {
Log.e("DATA -> ", accounts);
}
#Override
public void onFailure() {
}
}).getObserver();
observable.subscribe(observer);
observable.connect();
}
I called this function "testObs()" 5 times but it emitted data only 2 times. The problem seems to be in AccountsObservable class where I provide ReplaySUbject's instance. Thanks
Your code runs fine as it is, your logs are being suppressed in logcat as per this:
We declared an application as too chatty once it logs more than 5 lines a second. Please file a bug against the application's owner that is producing this developer-verbose-debug-level class logging spam. The logs are 256KB, that means the application is creating a DOS attack and shortening the logs timepan to 6 seconds(!) making it useless for all others.
You can avoid this behaviour by whitelisting your app for logcat:
adb logcat -P '<pid or uid of your app>'
I'm new to android and this is the first time trying to use MVP pattern in my code ... as I understand so far is that my view should talk to the presenter and the presenter to the model > then the presenter will talk again to view.. I wish I'm right with that !!! as shown below in my simple code example I'm trying to return a result value from the model to the presenter and then I will use this result value in the presenter to decide which method should I call in the view..I have 2 questions and I hope some helps.
1) The enqueue method is working async and the result value will always return empty or fail or whatever.. because it works alone... and when I try to use the execute method instead I'm facing a NetworkOnMainThreadException error... so how can I make the right way ?
2) Is this a right way in using MVP pattern ?
This is the SignupContract class
public class SignupContract {
public interface IView{
void signupSuccess();
void signupFailed(String message);
}
public interface IPresenter{
void signup(UserProfile userProfile);
}
public interface IModel{
String signup(UserProfile userProfile);
}
}
This is the View code..
public class SignupActivity extends AppCompatActivity implements SignupContract.IView {
//some code
#Override
protected void onCreate(Bundle savedInstanceState) {
//some code
createAccountBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//some code
presenter.signup(userProfile);
}
});
}
#Override
public void signupSuccess() {
/*AppUtils.dismissLoadingDialog(SignupActivity.this,"","");*/
Intent intent = new Intent(SignupActivity.this, SigninActivity.class);
startActivity(intent);
finish();
}
#Override
public void signupFailed(String message) {
/*AppUtils.dismissLoadingDialog(SignupActivity.this,"","");*/
AppUtils.showErrorMessage(SignupActivity.this, message);
}
and this is the presenter
public class SignupPresenter implements SignupContract.IPresenter {
SignupContract.IView view;
SignupContract.IModel model;
public SignupPresenter(SignupContract.IView view){
model = new SignupModel();
this.view = view;
}
#Override
public void signup(UserProfile userProfile) {
userProfile = UserProfileCleaner.clean(userProfile, "signup");
UserProfileDTO dto = new UserProfileDTO();
String validationMessage = dto.validateUserProfile(userProfile, "signup");
if(validationMessage != null && !validationMessage.equals("")){
view.signupFailed(validationMessage);
}else{
String signupResult = model.signup(userProfile);
if(signupResult.equals("success")){
view.signupSuccess();
}else {
view.signupFailed(signupResult);
}
}
}
}
and this is the model class
public class SignupModel implements SignupContract.IModel {
private String TAG = "SignupModel";
private String result = "";
#Override
public String signup(UserProfile userProfile) {
final Context context = DKApp.getContext();
ServiceWrapper serviceWrapper = new ServiceWrapper(null);
Call<SignupResponse> userSignUpCall = serviceWrapper.userSignUpCall(userProfile.getUser().getUsername(),
userProfile.getUser().getPassword(),userProfile.getPhoneNumber(), userProfile.getEmailAddress(),
userProfile.getFullName());
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if( response.body() != null && response.isSuccessful() ){
Log.e(TAG,response.body().toString());
if(response.body().getStatus() == 1){
//some code
result = "success";
}else{
result = response.body().getMessage();
}
}else{
result = context.getResources().getString(R.string.request_failed);
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
result = context.getResources().getString(R.string.request_failed);
}
});
return result;
}
}
You are making a asynchronous call in the model which may take 100ms or 2-4sec so getting signupResult from it like String signupResult = model.signup(userProfile);this is wrong.
Changes you will require :
1) Add onComplete method to IPresenter and change IModel
public interface IPresenter{
void signup(UserProfile userProfile);
//add
void onComplete(String signUpresult);
}
public interface IModel{
//changed
void signup(UserProfile userProfile);
}
2) In your SignupPresenter pass the instance of presenter to model
public class SignupPresenter implements SignupContract.IPresenter {
..
public SignupPresenter(SignupContract.IView view){
model = new SignupModel(this);
this.view = view;
}
...
#Overrides
public void onComplete(String signupResult){
if(signupResult.equals("success")){
view.signupSuccess();
}else {
view.signupFailed(signupResult);
}
}
...
}
3) In your SignupModel once the result is aquired call onComplete(//result) from the presenter
public class SignupModel implements SignupContract.IModel {
SignupPresenter presenter;
public SignupModel(SignupPresenter presenter){
this.presenter = presenter
}
#Override
public void signup(UserProfile userProfile) {
...
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if(response.body() != null && response.isSuccessful() ){
if(response.body().getStatus() == 1){
//some code
presenter.onComplete("success");
}else{
presenter.onComplete(response.body().getMessage());
}
}else{
presenter.onComplete(context.getResources().getString(R.string.request_failed));
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
presenter.onComplete(context.getResources().getString(R.string.request_failed));
}
});
}
}
Needful : Show progress dialogue when signup is called and dismiss the same on onComplete in SignupPresenter.
Your understanding is quite good also know that the model should talk with presenter as well. To know more about MVP design pattern read this
In MVP pattern you should make view dummy. Presenter always tells view what to do.
Here is an example:
In View =>
presenter.login(userName, password)
In Presenter =>
fun login(userName: String, password: String) {
//login logic
if(success) view.showLoginSuccess()
else view.showLoginError()
}
This is a very short explanation of the MVP pattern. Regarding to your question you can not make requests on Main Thread. Luckily Retrofit has a thread management system. But you shouldn't use it in a model like this you can use it in presenter or search for clean architecture articles for the more right way. If you use it in presenter you should do something like
userSignUpCall.enqueue(new Callback<SignupResponse>() {
#Override
public void onResponse(Call<SignupResponse> call, Response<SignupResponse> response) {
if( response.body() != null && response.isSuccessful() ){
Log.e(TAG,response.body().toString());
if(response.body().getStatus() == 1){
//some code
view.showMessage("success");
}else{
view.showMessage(response.body().getMessage());
}
}else{
view.showError(context.getResources().getString(R.string.request_failed));
}
}
#Override
public void onFailure(Call<SignupResponse> call, Throwable t) {
Log.e(TAG, "Failure : " + t.toString());
view.showError(context.getResources().getString(R.string.request_failed));
}
});
This is my first time using MVVM architecture.I am also using LiveData. I simply retrieve data from server using Retrofit.So upon clicking a button in the View(MainActivity.class) I invoke the ViewModel class's method(handleRetrofitcall()) to take up the duty of Api calling from the Model class(Retrofit Handler.class).The Model class upon retrieving the data informs the ViewModel of the data(which is actually the size of items).I set the size to LiveData and try to listen for it.Unfortunately I couldn't.For detailed analysis please go through the code.
Model...
RetrofitHandler.class:
public class RetrofitHandler {
private ApiInterface apiInterface;
private SimpleViewModel viewModel;
public void getData(){
apiInterface= ApiClient.getClient().create(ApiInterface.class);
Call<Unknownapi> call=apiInterface.doGetListResources();
call.enqueue(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
List<Unknownapi.Data> list;
Unknownapi unknownapi=response.body();
list=unknownapi.getData();
viewModel=new SimpleViewModel();
viewModel.postValue(list.size());
Log.e("Size",Integer.toString(list.size()));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
}
});
}
}
ViewModel....
SimpleViewModel.class:
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler;
private int size;
private MutableLiveData<Integer> mutablesize=new MutableLiveData<>();
public SimpleViewModel() {
super();
}
#Override
protected void onCleared() {
super.onCleared();
}
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData();
}
public void postValue(int size){
this.size=size;
mutablesize.postValue(this.size);
Log.e("lk","f");
}
public MutableLiveData<Integer> getObject() {
return mutablesize;
}
}
View.....
MainActivity.class:
public class MainActivity extends AppCompatActivity {
private TextView status;
private SimpleViewModel viewModel;
private Observer<Integer> observer;
private MutableLiveData<Integer> mutableLiveData;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status=findViewById(R.id.status);
viewModel=ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
observer=new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
};
viewModel.getObject().observe(MainActivity.this,observer);
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
viewModel.handleRetrofitcall();
}
});
}
#Override
protected void onDestroy() {
if (observer!=null){
viewModel.getObject().removeObserver(observer);
}
super.onDestroy();
}
}
You're creating a new ViewModel in the RetrofitHandler, so nothing is observing that viewmodel. Instead of having the RetrofitHandler rely on a ViewModel internally, it's probably safer to handle the Retrofit callback inself, and post data there.
public void handleRetrofitcall(){
retrofitHandler=new RetrofitHandler();
retrofitHandler.getData(new Callback<List<Unknownapi.Data>> {
// add actual callback implementation here
); // add a callback here, so that the data is available in the view model. Then post the results from here.
}
Edit: More clarification.
In the Activity, you're correctly creating a ViewModel and observing it (we'll call that ViewModel A). ViewModel A is then creating a RetrofitHandler and calling getData on that Retrofithandler. The issue is that RetrofitHandler is creating a new ViewModel in getData (which I'm going to call ViewModel B).
The issue is that the results are being posted to ViewModel B, which nothing is observing, so it seems like nothing is working.
Easy way to avoid this issue is to make sure that only an Activity/Fragment is relying on (and creating) ViewModels. Nothing else should know about the ViewModel.
Edit 2: Here's a simple implementation. I haven't tested it, but it should be more or less correct.
// shouldn't know anything about the view model or the view
public class RetrofitHandler {
private ApiInterface apiInterface;
// this should probably pass in a different type of callback that doesn't require retrofit
public void getData(Callback<Unknownapi> callback) {
// only create the apiInterface once
if (apiInterface == null) {
apiInterface = ApiClient.getClient().create(ApiInterface.class);
}
// allow the calling function to handle the result
apiInterface.doGetListResources().enqueue(callback);
}
}
// shouldn't know how retrofit handler parses the data
public class SimpleViewModel extends ViewModel {
private RetrofitHandler retrofitHandler = new RetrofitHandler();
// store data in mutableSize, not with a backing field.
private MutableLiveData<Integer> mutableSize = new MutableLiveData<>();
public void handleRetrofitCall() {
// handle the data parsing here
retrofitHandler.getData(new Callback<Unknownapi>() {
#Override
public void onResponse(Call<Unknownapi> call, Response<Unknownapi> response) {
Unknownapi unknownapi = response.body();
int listSize = unknownapi.getData().size;
// set the value of the LiveData. Observers will be notified
mutableSize.setValue(listSize); // Note that we're using setValue because retrofit callbacks come back on the main thread.
Log.e("Size", Integer.toString(listSize));
}
#Override
public void onFailure(Call<Unknownapi> call, Throwable t) {
// error handling should be added here
}
});
}
// this should probably return an immutable copy of the object
public MutableLiveData<Integer> getObject() {
return mutableSize;
}
}
public class MainActivity extends AppCompatActivity {
private TextView status;
// initialize the view model only once
private SimpleViewModel viewModel = ViewModelProviders.of(MainActivity.this).get(SimpleViewModel.class);
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
status = findViewById(R.id.status);
// observe the view model's changes
viewModel.getObject().observe(this, new Observer<Integer>() {
#Override
public void onChanged(#Nullable Integer integer) {
// you should handle possibility of interger being null
Log.e("lk","f");
status.setText(Integer.toString(integer));
}
});
findViewById(R.id.retrofit).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// call the view model's function
viewModel.handleRetrofitCall();
}
});
}
}
I have a fragment with a nested retrofit call inside an on response callback. I am cancelling both requests on the onStop method of the fragment, which according to the logs and debugging it's being call thus the cancel() for both retrofit calls are being called too.
Here is the code for the nested calls
serviceRequestTypesResponseCall.enqueue(new RetrofitCallback<ServiceRequestTypesResponse>(retrofit) {
#Override
public void onResponse(RetrofitResult<ServiceRequestTypesResponse> result) {
// get url from response and enqueue nested call
detailedServiceRequestTypeCall = scfServiceV2.getRequestType(result.getRequestTypeUrl());
detailedServiceRequestTypeCall.enqueue(new RetrofitCallback<DetailedServiceRequestType>(retrofit) {
#Override
public void onResponse(RetrofitResult<DetailedServiceRequestType> result) {
// this is being reached when cancelled
}
#Override
public void onFailure(RetrofitResult<DetailedServiceRequestType> error) {
// do something
}
});
}
#Override
public void onFailure(RetrofitResult<ServiceRequestTypesResponse> error) {
// do something
}
});
public void onStop() {
super.onStop();
// nested call
if(detailedServiceRequestTypeCall != null) {
detailedServiceRequestTypeCall.cancel();
}
// outer call
if(serviceRequestTypesResponseCall != null) {
serviceRequestTypesResponseCall.cancel();
}
}
The nested call gets a dynamic url from the outer call response, thus this is how it's defined in it's retrofit interface
#GET
Call<DetailedServiceRequestType> getRequestType(#Url String url);
also, this is how I am handling Retrofit cancellation in my custom callback class
public abstract class RetrofitCallback<T> implements Callback<T> {
private static final String TAG = "RetrofitCallback";
protected final Retrofit retrofit;
public RetrofitCallback(Retrofit retrofit) {
this.retrofit = retrofit;
}
public abstract void onResponse(RetrofitResult<T> result);
public abstract void onFailure(RetrofitResult<T> error);
#Override
public final void onResponse(Call<T> call, Response<T> response) {
onResponse(new RetrofitResult<>(retrofit, response));
}
#Override
public final void onFailure(Call<T> call, Throwable t) {
if (call.isCanceled()) {
Log.d(TAG, "Cancelled => " + call.request().toString());
} else {
onFailure(new RetrofitResult<T>(t));
}
}
}
The fragment is inside a bottombar navigation (https://github.com/roughike/BottomBarbottombar) which basically replaces the fragments upon tab selection. The requests are happening when the fragment gets displayed so switching the fragments quickly so to not give time for the nested call to finish reproduces this issue.
The logcat only shows the cancel log for the outer call. Not sure why I am not getting the same for the nested call despite being explicitly cancelled . This causes the request to finish and try to do some ui related logic and reference the activity which is no longer available because the fragment has been replaced when switching fragments