Android MVP - Should avoid using R.string references in presenter?

11,774

Solution 1

I consider that there's no reason to call any android code in Presenter (But you always can do it).

So in your case:

View / activity onCreate() calls -> presenter.onCreate();

Presenter onCreate() calls -> view.setTextLabel() or whatever you want in the view.

Always decouple Android SDK from presenters.

In Github, you can find some examples about MVP:

Solution 2

it's better to not use context and all object that depends on android sdk in presenter. I send id of the String and view cast it into string. like this->

getview().setTitle(R.string.hello);

and get this on view like this

@Override
public void setTitle(int id){
String text=context.getString(id);
//do what you want to do
}

With this approach you can test your method in presenter. It depends on R object but it's okay. all MVP classes placed in presentation layer in uncle bob clean architecture so you can use android objects like R class. but in domain layer you have to use only regular java objects

Update

For those who want to reuse their code in other platforms you can use a wrapper class for mapping the id or enum types to the resources and get the string.

getView().setTitle(myStringTools.resolve(HELLO));

The string resolver method is like this and the class can provided by View and DI into presenters.

Public String resolve(int ourID){
return context.getString(resourceMap.getValue(ourID));
}

But I do not recommend this in most of cases because of over engineering! you never need exact presentation code in other platforms in most of the times so: Better solution would be something like mocking that R class in other platforms because R class is already like a wrapper. You Should write your own R in other platform.

Solution 3

Your presenter should NOT need to know about how to show the details of showing the UI, and as such the R.string references.

Let's suppose you encounter a network issue and you want to show the user a network error message.

The first (wrong IMO) thing would be to get the context from the view and call some method like this in your presenter:

public void showNetworkError(){
    presenter.showMessage(view.getResources().getString(R.string.res1));
}

In which you're using the context from your view -- which is either an Activity or a Fragment.

Now what if you're told to change the copy content from R.string.res1 to R.string.res2? which component should you change?

The view. But is that necessary?

I believe not, because what is important for the presenter is that the view shows a message regarding network error, be it "Network error! please try again" or "There is a network error. Please try later."

So what is the better way?

Change your presenter to the following:

public void showNetworkError(){
    view.showNetworkErrorMessage();
}

and leave the implementation details to the view:

public void showNetworkErrorMessage(){
    textView.setText(R.string.resX)
}

I have written a complete article on MVP here, just in case.

Solution 4

This will be a long post about how to structure MVP project before getting into solving your problem at very last of my answer.

I just report MVP structure here how to structure MVP project from my own answer.

I often put business logic code in Model Layer (don't make confusion with model in database). I often rename as XManager for avoiding confusion (such as ProductManager, MediaManager ...) so presenter class just uses for keeping workflow.

The rule of thumb is no or at least limit import android package in presenter class. This best practice supports you easier in testing presenter class because presenter now is just a plain java class, so we don't need android framework for testing those things.

For example here is my mvp workflow.

View class: This is a place you store all your view such as button, textview ... and you set all listeners for those view components on this layer. Also on this View, you define a Listener class for presenter implements later. Your view components will call methods on this listener class.

class ViewImpl implements View {
   Button playButton;
   ViewListener listener;

   public ViewImpl(ViewListener listener) {
     // find all view

     this.listener = listener;

     playButton.setOnClickListener(new View.OnClickListener() {
       listener.playSong();
     });
   }

   public interface ViewListener {
     playSong();
   }
}

Presenter class: This is where you store view and model inside for calling later. Also presenter class will implement ViewListener interface has defined above. Main point of presenter is control logic workflow.

class PresenterImpl extends Presenter implements ViewListener {
    private View view;
    private MediaManager mediaManager;

    public PresenterImpl(View, MediaManager manager) {
       this.view = view;
       this.manager = manager;
    }

    @Override
    public void playSong() {
       mediaManager.playMedia();
    }
}

Manager class: Here is the core business logic code. Maybe one presenter will have many managers (depend on how complicate the view is). Often we get Context class through some injection framework such as Dagger.

Class MediaManagerImpl extends MediaManager {
   // using Dagger for injection context if you want
   @Inject
   private Context context;
   private MediaPlayer mediaPlayer;

   // dagger solution
   public MediaPlayerManagerImpl() {
     this.mediaPlayer = new MediaPlayer(context);
   }

   // no dagger solution
   public MediaPlayerManagerImpl(Context context) {
     this.context = context;
     this.mediaPlayer = new MediaPlayer(context);
   }

   public void playMedia() {
     mediaPlayer.play();
   }

   public void stopMedia() {
      mediaPlayer.stop();
   }
}

Finally: Put those thing together in Activities, Fragments ... Here is the place you initialize view, manager and assign all to presenter.

public class MyActivity extends Activity {

   Presenter presenter;

   @Override
   public void onCreate() {
      super.onCreate();

      IView view = new ViewImpl();
      MediaManager manager = new   MediaManagerImpl(this.getApplicationContext());
      // or this. if you use Dagger
      MediaManager manager = new   MediaManagerImpl();
      presenter = new PresenterImpl(view, manager);
   }   

   @Override
   public void onStop() {
     super.onStop();
     presenter.onStop();
   }
}

You see that each presenter, model, view is wrapped by one interface. Those components will called through interface. This design will make your code more robust and easier for modifying later.

In short, in your situation, I propose this design:

class ViewImpl implements View {
       Button button;
       TextView textView;
       ViewListener listener;

       public ViewImpl(ViewListener listener) {
         // find all view

         this.listener = listener;

         button.setOnClickListener(new View.OnClickListener() {
           textView.setText(resource_id);
         });
       }
    }

In case the logic view is complicated, for example some conditions for setting value. So I will put logic into DataManager for getting back text. For example:

class Presenter {
   public void setText() {
      view.setText(dataManager.getProductName());
   }
}

class DataManager {
   public String getProductName() {
      if (some_internal_state == 1) return getResources().getString(R.string.value1);
      if (some_internal_state == 2) return getResources().getString(R.string.value2);
   }
}

So you never put android related-thing into presenter class. You should move that to View class or DataManager class depend on context.

This is a very long post discuss in detail about MVP and how to solve your concreted problem. Hope this help :)

Share:
11,774
Scott Merritt
Author by

Scott Merritt

Most interested in iOS/Android native development. Have worked mostly in the financial industry on apps with lots of users.

Updated on June 15, 2022

Comments

  • Scott Merritt
    Scott Merritt about 2 years

    In an attempt to entirely decouple the Android SDK from my presenter classes, I'm trying to figure out the best way to avoid accessing resource IDs which we normally use R for. I thought I could just create an interface to access things like string resources, but I still need IDs to reference the strings. If I were to do something like...

    public class Presenter {
        private MyView view = ...;
        private MyResources resources = ...;
    
        public void initializeView() {
            view.setLabel(resources.getString(LABEL_RES_ID);
        }
    }
    

    I still have to have LABEL_RES_ID and then map it to R.string.label in my resources bridge. It's cool because I could swap it out when unit testing with something else, but I don't want to manage another mapping to the string value.

    If I give up and just use the R.string values, my presenter is bound to my view again. That's not ideal? Is there an easier solution that people use to get around this in order to keep them out of the presenter. I don't want to manage strings in a way outside of what Android provides, because I still want to throw them in layout files and get the benefit of internationalization, etc. I want to make a dumb unit test that can work with this presenter without having to have the Android SDK generate the R.java files. Is this too much to ask?

  • Joao Sousa
    Joao Sousa about 9 years
    Could you specify please the argument that goes in view.setTextLabel()? Because if you do, that would answer the question. Basically imagine if you have logic in your presenter to display either message R.string.a or R.string.b. How would you pass this string to be displayed by the view?
  • PaNaVTEC
    PaNaVTEC about 9 years
    Your View needs to ve Passive, and your Presenter active, so, the presenter knows what to present and the view knows how to present it. if you want to show an error in your view from your presenter you need to do a call like: view.showContactLoadingError(); and your view can setup the message depending on the context of the view. I've updated the samples with another repo that I made.
  • Joao Sousa
    Joao Sousa about 9 years
    Thanks @PaNaVTEC, that's exactly the answer I was looking for and it makes sense.
  • baybora.oren
    baybora.oren about 8 years
    if you want to log for error and use resource to get error string then ?
  • PaNaVTEC
    PaNaVTEC about 8 years
    Depending on the view you maybe will display different error messages, is the same to show an error. So it can be view responsability. If this is not your case, you can wrap R.string and send a collaborator to your presenter.
  • WindRider
    WindRider about 8 years
    What if my Presenter is intended to dynamically generate a map marker popup with very simple HTML containing some localized strings. How can I generate it without access to resources or how to give access to the strings in clean way?
  • WindRider
    WindRider about 8 years
    I think to use a dedicated formatter class that is technically in the View side. The presenter will delegate most of the work to render the HTML snippet to it. It has an access to the Context. With DI it might be even cleaner.
  • Scott Merritt
    Scott Merritt almost 8 years
    Still doesn't seem right. You're still dependent on code that was generated by the Android SDK. I think PaNaVTEC has an answer above, and although it seems like the "right" way to do it, it does seem like a lot of work to get these references out of the presenter. If you're referencing the R file, it seems like you're presenter is now dependent on Android functionality (at least code that gets generated from the Android SDK).
  • Siavash Abdoli
    Siavash Abdoli almost 8 years
    @zoonsf you can't make decisions on logic in activity. There is no problem for using resources and you can even use resources in unit test. MVP is on outer layout of uncle Bob clean architecture and dependend on android so using resources in presenter is ordinary and correct
  • Przemo
    Przemo over 6 years
    Apart from the fact that presenter shouldn't have android callbacks (strange to test that, and it's moving Android spaghetti to Presenters) I like your solution. Although, what to do if those values haven't got any logic and there are many of them? Use R.string.* in Manager or Presenter? I'd rather kept them in Presenter to avoid unnecessary layer of abstraction AKA code envy. But on the other hand they're generated by Android... Coding guidelines stands in opposition to each other.
  • Farid
    Farid over 2 years
    How the hack you made this assumption "There is no problem for using resources"?
  • Siavash Abdoli
    Siavash Abdoli over 2 years
    Ask your question in a better way and I did explain that with clean architecture method and in my experience presentation layer should be different for every platform and I introduce a way to even reuse it for later solution. If you really want to know the reason, text me on LinkedIn So I can explain in Persian. also, that's heck not hack :D