简体   繁体   中英

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

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?

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 :

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.

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 :)

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM