I created the instance of View Model in onCreate method of an activity.
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
Then i have a method, AddTicket, which uses viewModel to hit a service and on response from viewModel i dismiss loading animation.
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, response ->{
dismissLoadingAnimation();
}
Now after adding a ticket, user can repress the Add Ticket button, and the addTicket() method will be called again.
but this time observer defined in ViewModel gets called 2 times, resulting in 2 network calls, and 2 dismissLoadingAnimation execution.
And if i keep pressing addTicket button, the number of executing observer defined inside ViewModel keep increases.
This is my View Model code.
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public MutableLiveData AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
return mObservableResponse;
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
The number of executing observer defined inside ViewModel keep increases becasue with every click You're registering new observers. You're not supposed to register observer with onClick() method.
You should do it in onCreate() method of your Activity or in onViewCreated method of your fragment. If You'll do that, there won't be a need to removeObserver when You'll finish work. Lifecycle mechanism will cover it for you.
But if you really want answer for you question, this is how you can do it
yourViewModel.yourList.removeObservers(this)
Passing this means passing your Activity, or there is a second way:
yourViewModel.yourList.removeObserver(observer)
val observer = object : Observer<YourObject> {
override fun onChanged(t: YourObject?) {
//todo
}
}
The purpose of Viewmodel is to expose observables (Livedata)
The purpose of View(Activity/Fragment) is to get these observables and observe them
Whenever there is a change in these observables(Livedata) the change is automatically posted to the active subscribed owners(Activity/Fragment), so you need not remove them in onPause/onStop as it is not mandatory
I can suggest few changes to your code to solve the problem with the above mentioned pointers
ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
MutableLiveData<String> mObservableResponse = new MutableLiveData<String>();
public LiveData<String> getResponseLiveData(){
return mObservableResponse;
}
public void AddTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
mObservableResponse.setValue("success");
}
}
}
View
onCreate(){
ticketViewModel = ViewModelProviders.of(this).get(TicketViewModel.class);
observeForResponse();
}
private void observeForResponse(){
ticketViewModel.getResponseLiveData().observe(this, response ->{
//do what has to be updated in UI
}
}
public void addTicket(View view){
ticketViewModel.AddTicket(id);
}
Hope this is of help :)
You only need to call the observe once, I prefer to do it in onResume and then call removeObserver in onPause:
Adds the given observer to the observers list
You keep adding listeners to the data so you get multiple callbacks.
Edit:
I took an existing code sample of mine for a Fragment and renamed everything (I hope), there's no example here for setting the data into the ViewModel but it should be ticketViewModel.AddTicket(id); in your case.
public class ListFragment extends Fragment {
private MyViewModel viewModel;
private MyRecyclerViewAdapter recyclerViewAdapter;
private Observer<List<DatabaseObject>> dataObserver;
private RecyclerView recyclerView;
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
initRecyclerView(rootView, getContext());
initObservers();
return rootView;
}
private void initRecyclerView(View rootView, Context context) {
recyclerViewAdapter = new MyRecyclerViewAdapter(context);
recyclerView = rootView.findViewById(R.id.recycler_view);
recyclerView.setAdapter(recyclerViewAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addItemDecoration(new DividerNoLastItemDecoration());
}
private void initObservers() {
dataObserver = new Observer<List<DatabaseObject>>() {
#Override
public void onChanged(#Nullable final List<DatabaseObject> data) {
recyclerViewAdapter.setData(data);
}
};
}
#Override
public void onResume() {
super.onResume();
initViewModel();
}
private void initViewModel() {
FragmentActivity activity = getActivity();
if (activity != null) {
viewModel = ViewModelProviders.of(activity).get(MyViewModel.class);
viewModel.getData().observe(activity, dataObserver);
}
}
#Override
public void onPause() {
super.onPause();
if (viewModel != null) {
viewModel.getData().removeObserver(dataObserver);
viewModel = null;
}
}
}
I had similar problem. You could try to use SingleLiveEvent
Or, in my, more complicated case, i had to use custom observer. It would looks like this:
public class CustomObserver implements Observer<YourType> {
private MyViewModel mViewModel;
public CustomObserver (){}
public void setViewModel(MyViewModel model) {
mViewModel = model;
}
#Override
public void onChanged(#Nullable YourType object) {
mViewModel.AddTicket(id).removeObserver(this); // removing previous
mmViewModel.refreshTickets(); // refreshing Data/UI
// ... do the job here
// in your case it`s: dismissLoadingAnimation();
}
}
And using it like:
public void addTicket(View view){
ticketViewModel.AddTicket(id).observe(this, myCustomObserver);
}
If you are willing to do some changes, i think we can handle it in much cleaner way
LiveData is meant to be used to contain a property value of a view
In ViewModel
public class TicketViewModel extends AndroidViewModel implements IServiceResponse {
private MutableLiveData<Boolean> showLoadingAnimationLiveData = new MutableLiveData<String>();
public LiveData<Boolean> getShowLoadingAnimationLiveData(){
return showLoadingAnimationLiveData;
}
public void addTicket(String id){
JsonObject jsonObject= new JsonObject();
jsonObject.addProperty("id", id);
NetworkUtility networkUtility= new NetworkUtility(this, ADD_TICKET);
networkUtility.hitService(URL, jsonObject, RequestMethods.POST);
showLoadingAnimationLiveData.setValue(true);
}
#Override
public void onServiceResponse(String response, String callType){
if(serviceTag.equalsIgnoreCase(ADD_TICKET)){
showLoadingAnimationLiveData.setValue(false);
}
}
}
In 'onCreate' of your Activity/Fragment
ticketViewModel.getShowLoadingAnimationLiveData().observe(this,showLoadingAnimation->{
if(showLoadingAnimation != null && showLoadingAnimation){
startLoadingAnimation();
}else{
dismissLoadingAnimation();
}
})
The main concept is to divide the responsibilities,
Activity/Fragment doesn't need to know which process is going on, they only need to know what are the current properties/state of there child views.
We need to maintain a LiveData in ViewModels for each changing property/state depending on Views. ViewModel needs to handle the view states depending on whats happening.
Only responsibility the Activity/Fragment has about a process is to trigger it and forget and ViewModel needs handle everything(like informing Repositories to do the work and changing View Properties).
In your Case,
'addTicket' is a process about which Activity/Fragment doesn't need to know about there status.
The only responsibility of Activity/Fragment about that process is to trigger it.
ViewModel is one who needs to analyze the state of process(in-progress/success/failed) and give appropriate values to the LiveDatas to inform the respective Views
Related
In my ViewModel class I have a method that fetches data from the internet and sets them on LiveData via setValue() (I've used the approach from these docs):
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
if (mDataList == null) {
mDataList = new MutableLiveData<>();
}
Handler handler = new Handler(Looper.getMainLooper());
// getData() connects to the internet and fetches the online data
mInternetConnection.getData(new InternetConnection.ConnectionCallback<List<?>>() {
#Override
public void onComplete(Result<List<?>> result) {
if (result instanceof Result.Success) {
mDataList.setValue(((Result.Success<List<?>>) result).data);
} else {
Log.e(LOG_TAG, "ViewModel could not obtain data list");
}
}
}, handler);
return mDataList;
}
But mDataList.getValue() is null. The setValue() method is called on the main thread.
onComplete gets called for sure (checked).
When I check whether the value of mDataList is null in the onComplete method right after setValue(), the logs show that it is not null.
Why is it null and how should you get the value of the modified mDataList?
I've spent hours looking for a solution on the web, but couldn't find anything that could help. The related questions on this site don't help to solve the problem either.
EDIT:
From the docs:
In this example, the callback passed into the Repository's makeLoginRequest call is executed on the main thread. That means you can directly modify the UI from the callback or use LiveData.setValue() to communicate with the UI.
So I've followed that, but this part: "or use LiveData.setValue() to communicate with the UI" doesn't work.
The observer is set on it in the Fragment, but LiveData still don't get updated - list in onChanged is null, the screen remains blank, no errors.
Fragment code:
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
View rootView = binding.getRoot();
binding.listRecyclv.setLayoutManager(new LinearLayoutManager(rootView.getContext()));
mAdapter = createAdapter(rootView.getContext(), pageViewModel.getList().getValue());
binding.listRecyclv.setAdapter(mAdapter);
pageViewModel.getList().observe(getViewLifecycleOwner(), new Observer<List<?>>() {
#Override
public void onChanged(#Nullable List<?> list) {
mAdapter.setList((ArrayList<MyObj>) list);
binding.listRecyclv.setAdapter(mAdapter);
}
}
}
});
Getter in ViewModel:
public LiveData<List<?>> getList() {
return getDataList();
}
Code in Adapter:
public class AppAdapter extends RecyclerView.Adapter<AppAdapter.AppViewHolder> {
...
public void setList(ArrayList<MyObj> list) {
mAppList = list;
notifyDataSetChanged();
}
Thanks in advance.
mDataList is null because the call you're making to get data is asynchronous. This means that the function will return before your api call (and its callbacks) does.
Read more here https://www.bmc.com/blogs/asynchronous-programming/
To get the list you need to observe data changes.
So something like this:
From the activity:
Kotlin:
viewModel.getDataList().observe(this) { dataList ->
//TODO: Do something with the list
}
Java:
final Observer<String> listObserver = new Observer<List<>>() {
#Override
public void onChanged(#Nullable final List<> newName) {
//TODO: Use the list
}
};
viewModel.getDataList().observe(this, listObserver);
The logic inside the observer will be called every time the list changes.
I solved it this way:
Changed this line in Fragment's onCreate method
from:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pageViewModel = new ViewModelProvider(this).get(PageViewModel.class);
to
pageViewModel = new ViewModelProvider(requireActivity()).get(PageViewModel.class);
Thanks to this answer
Changed the data type of getDataList() method in the PageViewModel class from LiveData to MutableLiveData (without it the above step doesn't help):
from:
public class PageViewModel extends ViewModel {
private MutableLiveData<List<?>> mDataList;
...
private LiveData<List<?> getDataList() {
to:
private MutableLiveData<List<?> getDataList() {
and now it works! The observer gets triggered now and the data are updated.
I'm using Retrofit with RxJava2 to obtain some data from a Rest API. I want to use a SwipeRefreshLayout to update the view and I'm using a ViewModel to handle the API call, so I want to implement a method in there to refresh the data programmatically.
I want to obtain something like this https://stackoverflow.com/a/34276564/6787552 but instead of having a periodic trigger, I want to do that programmatically when the user pull to refresh.
That's the ViewModel:
public class DashboardViewModel extends ViewModel {
public final Single<Dashboard> dashboard;
public DashboardViewModel() {
dashboard = Api.getDashboard();
refresh();
}
public void refresh() {
// Refresh data
}
}
And in the DashboardFragment:
#Override
public View onCreateView(...) {
...
viewModel.dashboard
.observeOn(AndroidSchedulers.mainThread())
.subscribe(dashboard -> {
binding.setDashboard(dashboard);
binding.swipeRefreshLayout.setRefreshing(false);
});
binding.swipeRefreshLayout.setOnRefreshListener(() -> viewModel.refresh());
...
}
Thank you in advance!
EDIT:
That's what I ended up doing:
public class DashboardViewModel extends ViewModel {
private final BehaviorSubject<Dashboard> dashboard;
public DashboardViewModel() {
dashboard = BehaviorSubject.createDefault(Api.getDashboard());
}
public void refresh() {
// I use a Object because null values are not supported
dashboard.onNext(Api.getDashboard());
}
public Observable<Dashboard> getDashboard(){
return dashboard;
}
}
And then in the DashboardFragment just subscribe to viewModel.getDashbaord()
I'm not 100% sure that I understood what you want to do but if I got the question right, you can do something like this:
put a subject inside the model (probably a BehaviorSubject?)
expose it as an observable to the
view and subscribe to it (instead of subscribing to the single)
in the model, when you
receive a new call to refresh() from the ui, do something like
subject.onNext(Api.getDashboard())
in this way, each call to refresh will cause the emission of a new dashboard, and that will be properly bound by the subscription in the view.
So I'm fairly new to MVVM. So I'm fetching my data in my VM and I'm passing in the Activity/fragment as a listener in the method call.
The reason I'm doing this is because I'm going to have a callback if there was to be an error. So I'd handle it in the activity/fragment with a dialog.
I'm not sure if I'm breaking MVVM here? If i'm making any other errors with this pattern, please let me know.
Thanks
In my view, fragment/activity
/*creating and using my VM inside my fragment*/
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState){
super.onActivityCreated(savedInstanceState);
//Create and observe data for any changes throughout the lifecycle
final OverviewViewModel viewModel = ViewModelProviders.of(this).get(OverviewViewModel.class);
//get info
viewModel.getUserInfo(this);
observeViewModel(viewModel);
}
//Listener in the activity/fragment that will handle an error in the request
#Override
public void onTokenExpired() {
ExpiredTokenDialogFragment dialogFragment = new ExpiredTokenDialogFragment();
dialogFragment.show(getFragmentManager(), EXPIRED_DIALOG);
}
My View model where i make request.
public void getUserInfo(AuthenticationListener listener){
mUserInformationObservable = mRepository.getUserInfo(listener);
}
My retrofit request
public LiveData<UserInformation> getUserInfo(final AuthenticationListener authenticationListener){
final MutableLiveData<UserInformation> data = new MutableLiveData<>();
mService.fetchFollowers().enqueue(new Callback<UserInformation>() {
#Override
public void onResponse(Call<UserInformation> call, retrofit2.Response<UserInformation> response) {
//note, the result is in data. Calling response.body.string twice results in an empty string
if(response.body()!=null) data.setValue(response.body());
}
#Override
public void onFailure(Call<UserInformation> call, Throwable t) {
if(t instanceof UnauthorizedException){
data.setValue(null);
mToken.setAccessToken(null);
authenticationListener.onTokenExpired();
}
}
});
return data;
}
Using a listener is not recommended. The android-architecture project uses a SingleLiveEvent class for events like navigation or displaying a Snackbar. You can use the same class for showing a dialog.
In your OverviewViewModel you can add another field:
final SingleLiveEvent<Void> tokenLiveData = SingleLiveEvent<Void>();
in your onFaliure callback you can use:
tokenLiveData.call()
instead of the callback.
In your activity subscribe to tokenLiveData and show a dialog when it emits a value.
I understand there are a lot of information about it out there, but I haven't found one that matches my case yet.
I have a recycleview on a fragment that is always open, so the fragment basically never re-creates itself.
This is my code to load the adapter.
reLoad(); //method shown below
mRecycler.setAdapter(new SolicitationAdapter(myRealm.where(SolicitationDatabase.class).findAllAsync()));
And this is the logic I came up with:
public void reLoad() {
if (!myRealm.where(SolicitationDatabase.class).findAll().isEmpty()) {
mNothingHere.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
} else {
mNothingHere.setVisibility(View.VISIBLE);
mRecycler.setVisibility(View.GONE);
}
}
It works great the first time the user opens the app.
The trouble starts when the user creates a record, since the fragment doesn't re-create itself it never reloads.
The reason I haven't been able to reload after user adds something is because the method to add a new record is on a singleton being called from a different activity. Which means when I try to do it I get a nullpointerexception when declaring the the recycleview and the textview.
Edit - What I tried (reloading views from another place)
I have a class called PostHelper, this class is in charge of posting a new record.
This is the constructor:
public PostHelper(Context context, Activity activity) {
this.mContext = context;
this.mActivity = activity; //I call this in order to use "findViewById"
This is where the post happens:
public String addSolicitation(File _file, boolean fromQueue) {
//loading view
TextView nothingHere = (TextView) mActivity.findViewById(R.id.nothing_here);
RecyclerView recycler = (RecyclerView) mActivity.findViewById(R.id.recycler);
...so on until after the post:
SolicitationAdapter n = new SolicitationAdapter(myRealm.where(SolicitationDatabase.class).findAll());
n.notifyDataSetChanged();
nothingHere.setVisibility(View.GONE);
recycler.setVisibility(View.VISIBLE);
And this is the stacktrace:
06-01 21:43:37.511 9122-9122/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.ga.realm3, PID: 9122
io.reactivex.exceptions.OnErrorNotImplementedException: Attempt to invoke virtual method 'void android.widget.TextView.setVisibility(int)' on a null object reference
Edit 2 - I load PostHelper class using the following:
mPostHelper = new PostHelper(this, PostSolicitationActivity.this);
You're supposed to make sure that SolicitationAdapter is a RealmRecyclerViewAdapter, like so:
public class SolicitationAdapter extends RealmRecyclerViewAdapter<SolicitationDatabase, SolicitationViewHolder> {
public SolicitationAdapter(OrderedRealmCollection<SolicitationDatabase> results) {
super(results, true);
}
...
}
And then what you need to do is that you put the RealmResults as a field reference in your Activity:
public class PostSoliticiationActivity extends AppCompatActivity {
RealmResults<Solicitation> results;
Realm realm;
RealmChangeListener<RealmResults<Solicitiation> realmChangeListener = (results) -> {
if(results.isLoaded() && results.isValid()) {
if(results.isEmpty()) {
mNothingHere.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
} else {
mNothingHere.setVisibility(View.VISIBLE);
mRecycler.setVisibility(View.GONE);
}
}
}
SolicitationAdapter adapter;
#Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.soliticiation_activity);
// bind views
realm = Realm.getDefaultInstance();
results = realm.where(SolicitationDatabase.class).findAllSortedAsync("id");
// .sort("id").findAllAsync(); in 4.3.0+
results.addChangeListener(realmChangeListener);
adapter = new SoliticiationAdapter(results);
mRecycler.setAdapter(adapter);
// layout manager as well
}
#Override
public void onDestroy() {
results.removeChangeListener(realmChangeListener);
realm.close();
super.onDestroy();
}
}
So things you don't need:
1.) reLoad() method
2.) onPostAdded callback
3.) PostActionListener
As long as you just add the SoliticiationDatabase to the Realm in a transaction, it'll all work without manually syncing ui.
If I understand correctly, you want to be notified in another view when an action happens elsewhere.
The way to do that is usually interfaces.
public class PostHelper {
// Define these
public interface PostActionListener {
void onPostAdded();
}
private PostActionListener postListener;
public void setPostActionListener(PostActionListener listener) throws ClassCastException {
this.postListener = (PostActionListener) context;
}
public PostHelper(Context context) {
this.mContext = context;
// Cast the passed context as a listener
if (mContext instanceof PostActionListener) {
this.postListener = (PostActionListener) mContext;
}
}
public String addSolicitation(File _file, boolean fromQueue) {
// Do something
// Callback to the UI to update
if (this.postListener != null) {
this.postListener.onPostAdded();
}
}
Then, in your initial Activity
public class YourActivity extends AppCompatActivity
implements PostHandler.PostActionListener {
// ... fields
#Override
public void onPostAdded() {
reLoad();
}
public void reLoad() {
boolean emptyList = myRealm.where(SolicitationDatabase.class).findAll().isEmpty();
mNothingHere.setVisibility(emptyList ? View.VISIBLE : View.GONE);
mRecycler.setVisibility(emptyList ? View.GONE : View.VISIBLE);
}
#Override
public void onCreate(Bundle b) {
...
mPostHelper = new PostHelper(this);
}
However, since you are using Realm, and there really is no data that you need to "return" here, then you can simply let the Android Activity lifecycle refresh the data for you.
I'm new to Android and MVP in-general, and I've been doing iOS programming for the last 1.5 years, so I find delegate patterns easy to digest. I've implemented MVP in such a way that the view conforms to a presenter's protocol, which lets the presenter disregard the view's specific type, but lets it know that certain methods are a given and thus okay to call on the "view." I've been reading various MVP guides, and all of the Mosby tutorials, and I'm not sure I agree with some of it. Is the pattern I've implemented kosher? I'd like some feedback so I don't keep heading in a bad direction, if that is indeed what I'm doing...
For example,
Base Presenter:
public abstract class Presenter<V, S> implements BasePresenterInterface<V, S> {
public interface PresenterProtocol extends BasePresenterProtocol {
}
private WeakReference<V> mAttachedView = null;
private S mService = null;
/**
* Interface Overrides
*/
#Override
public void attachView(V view) {
boolean viewDoesNotConform = !viewDoesConform(view);
if (viewDoesNotConform) {
Log.d("DEBUG", "Cannot attach View that does not conform to PresenterProtocol");
return;
}
mAttachedView = new WeakReference<>(view);
((BasePresenterProtocol) getAttachedView()).onViewAttached();
}
#Override
public void detachView() {
mAttachedView = null;
}
#Override
public boolean viewDoesConform(V view) {
Class<?> klass = view.getClass();
boolean conforms = BasePresenterInterface.BasePresenterProtocol.class.isAssignableFrom(klass);
return conforms;
}
#Override
public boolean viewIsAttached() {
return mAttachedView != null;
}
#Override
public V getAttachedView() {
return mAttachedView.get();
}
#Override
public S getService() {
return mService;
}
#Override
public void setService(S service) {
mService = service;
}
}
I then subclass this into the following:
PhotoRecyclerPresenter:
public class PhotoRecyclerPresenter extends Presenter<PhotoRecyclerPresenter.PhotoRecyclerPresenterProtocol, PhotoService> {
public interface PhotoRecyclerPresenterProtocol extends Presenter.PresenterProtocol {
void onPhotosLoaded(List<TestPhoto> photoList);
void onItemSelected(TestPhoto photo);
void onShowDetail(TestPhoto photo);
}
private static PhotoRecyclerPresenter mSharedInstance;
private PhotoRecyclerPresenter() {
setService(new PhotoService());
}
/**
* External Methods
*/
public void getPhotos() {
boolean noAttachedView = !viewIsAttached();
if (noAttachedView) {
Log.d("DEBUG", "No view attached");
return;
}
getService().getAPI()
.getPhotos()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(photoList -> getAttachedView().onPhotosLoaded(photoList));
}
/**
* Internal Methods
*/
public static PhotoRecyclerPresenter getSharedInstance() {
boolean firstInstance = mSharedInstance == null;
if (firstInstance) {
setSharedInstance(new PhotoRecyclerPresenter());
}
return mSharedInstance;
}
public static void setSharedInstance(PhotoRecyclerPresenter instance) {
mSharedInstance = instance;
}
public void didSelectItem(TestPhoto photo) {
getAttachedView().showDetail(photo);
}
}
And it communicates with the view:
PhotoRecyclerFragment:
public class PhotoRecyclerFragment extends Fragment implements PhotoRecyclerPresenter.PhotoRecyclerPresenterProtocol {
private RecyclerView mRecyclerView;
private RecyclerView.LayoutManager mLayoutManager;
private Activity mParentActivity;
private PhotoRecyclerPresenter mPresenter;
private PhotoRecyclerAdapter mAdapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Nullable
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_recycler, container, false);
mParentActivity = getActivity();
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerView);
mLayoutManager = new LinearLayoutManager(mParentActivity);
mAdapter = new PhotoRecyclerAdapter(this);
mRecyclerView.setLayoutManager(mLayoutManager);
mRecyclerView.setAdapter(mAdapter);
mPresenter = PhotoRecyclerPresenter.getSharedInstance();
mPresenter.attachView(this);
return rootView;
}
#Override
public void onDestroyView() {
super.onDestroyView();
mPresenter.detachView();
mAdapter.clear();
}
/**
* PhotoRecyclerPresenterProtocol Methods
*/
#Override
public void onItemSelected(TestPhoto photo) {
mPresenter.didSelectItem(photo);
}
#Override
public void onPhotosLoaded(List<TestPhoto> photoList) {
mAdapter.loadPhotos(photoList);
}
#Override
public void onViewAttached() {
mPresenter.getPhotos();
}
#Override
public void onViewDetached() {
}
#Override
public void onShowDetail(TestPhoto photo) {
Intent detailIntent = new Intent(mParentActivity, PhotoDetailActivity.class);
mParentActivity.startActivity(detailIntent.putExtra(Intent.EXTRA_UID, photo.getPhotoId()));
}
}
This lets me define a set of requirements a view needs to conform to in order to utilize the singleton presenter, while keeping the presenter agnostic about what views use it, as long as they conform to its protocol. So far in my practice project it seems to work fine, but I can't seem to find any resources where what I'm doing is recommended as far as MVP goes, and I have enough self-doubt that I figured I'd ask my first StackOverflow question. Can anyone who has experience with MVP shed some light on this?
Also, if I'm asking in the wrong place, feel free to point me to the correct place to post this.
Thanks :)
From my point of view you are doing the same thing that Mosby does. The only difference is the name of the interface (or protocol in objective-c) world. You call it PresenterProtocol while Mosby call it MvpView. Both are doing the same job: Offering the Presenter an Api of methods the presenter can call to manipulate the view.
The only thing that doesn't make sense is to have a method viewDoesConform(). In Java you have type safety. You can use the generics type V of your Presenter to ensure that your fragment is implementing the Presenter's protocol. just change it to V extends BasePresentersProtocol
Furthermore I think that it doesn't make sense to have a "shared instance" (a.k.a Singleton pattern) of the presenter. I think it would make more sense to have a "shared instance" of the PhotoService. But But please note also that by doing so your code is not testable (unit tests) anymore. You should google for Dependency injection or Inverse of Control to understand how to write modular, reusable and testable code. I'm not talking about dependency injection frameworks like Dagger , spring or guice. You just should understand the idea behind dependency injection. You can write classes following this principle completely without dependency injection frameworks (i.e. using constructor parameters).
Side note: you never unsubscribe your presenter from PhotoService. Depending on how PhotoService is implemented you may have a memory leak because PhotoService observable has a reference to the presenter which prevents the presenter and PhotoService (depending on your concrete implementation) from being garbage collected.
Edit: Mosby defines the protocol for the View. Have a look at the getting started section on the project website. The HelloWorldView defines two methods: showHello() and showGoodbye() (implented by the HelloWorldActivity) and HelloWorldPresenter calls these two methods to manipulate the View. The HelloWorldPresenter also cancels the async requests to avoid memory leaks. You should do that too. Otherwise your presenter can only be garbage collected after the retrofit httpcall has completed.