Android MVP - 应该避免在演示者中使用 R.string 引用吗?

Posted

技术标签:

【中文标题】Android MVP - 应该避免在演示者中使用 R.string 引用吗?【英文标题】:Android MVP - Should avoid using R.string references in presenter? 【发布时间】:2014-11-05 19:05:48 【问题描述】:

为了将 android SDK 与我的演示者类完全分离,我试图找出避免访问我们通常使用 R 的资源 ID 的最佳方法。我以为我可以创建一个接口来访问字符串资源之类的东西,但我仍然需要 ID 来引用字符串。如果我要做类似...

public class Presenter 
    private MyView view = ...;
    private MyResources resources = ...;

    public void initializeView() 
        view.setLabel(resources.getString(LABEL_RES_ID);
    

我仍然必须拥有LABEL_RES_ID,然后将其映射到我的资源桥中的R.string.label。这很酷,因为我可以在用其他东西进行单元测试时将其换掉,但我不想管理另一个到字符串值的映射。

如果我放弃并只使用 R.string 值,我的演示者将再次绑定到我的视图。这不理想吗?人们是否有更简单的解决方案来解决这个问题,以使他们远离演示者。我不想以 Android 提供的方式之外的方式管理字符串,因为我仍然想将它们放入布局文件并获得国际化等好处。我想做一个可以与这个演示者一起工作的愚蠢单元测试无需让 Android SDK 生成 R.java 文件。这要求太多了吗?

【问题讨论】:

我在 Android 上使用 MVP 的次数越多,我发现自己问这些问题的次数就越多。资源和上下文对象真的很难根除。您最终找到了满意的解决方案吗? 看看这篇文章和可能有帮助的示例项目:medium.com/@m_mirhoseini/… 【参考方案1】:

您的presenter 应该需要了解如何显示 UI 的详细信息,以及 R.string 引用。

假设您遇到网络问题,并且想要向用户显示网络错误消息。

第一件事(错误的 IMO)是从 view 获取上下文并在您的 presenter 中调用类似这样的方法:

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

您在其中使用 view 中的 context - 这是 ActivityFragment

现在,如果您被告知将复制内容从 R.string.res1 更改为 R.string.res2,该怎么办?您应该更改哪个组件?

view。但这有必要吗?

我不相信,因为对于presenter 来说重要的是view 显示有关网络错误的消息,无论是“网络错误!请重试”或“网络错误。请稍后再试” 。”

那么更好的方法是什么?

将您的 presenter 更改为以下内容:

public void showNetworkError()
    view.showNetworkErrorMessage();

并将实现细节留给view

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

我已经写了一篇完整的关于MVPhere的文章,以防万一。

【讨论】:

【参考方案2】:

最好不要在presenter中使用上下文和依赖于android sdk的所有对象。我发送字符串的 id 并查看将其转换为字符串。像这样->

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

并像这样展示它

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

通过这种方法,您可以在 Presenter 中测试您的方法。它取决于 R 对象,但没关系。所有 MVP 类都放置在 uncle bob clean architecture 的表示层中,因此您可以使用像 R 类这样的 android 对象。但在域层中,您必须只使用常规的 java 对象

更新

对于那些想在其他平台上重用代码的人,您可以使用包装类将 id 或 enum 类型映射到资源并获取字符串。

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

字符串解析器方法是这样的,类可以由View和DI提供给presenters。

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

但在大多数情况下,我不建议这样做,因为过度工程!在大多数情况下,您永远不需要在其他平台上使用精确的演示代码,所以: 更好的解决方案是在其他平台上模拟 R 类,因为 R 类已经像一个包装器。您应该在其他平台上编写自己的 R。

【讨论】:

似乎还是不对。您仍然依赖于 Android SDK 生成的代码。我认为 PaNaVTEC 上面有一个答案,虽然这似乎是“正确”的做法,但要从演示者那里获得这些参考资料似乎确实需要做很多工作。如果您引用的是 R 文件,那么您的演示者似乎现在依赖于 Android 功能(至少是从 Android SDK 生成的代码)。 @zoonsf 您无法对活动中的逻辑做出决定。使用资源没有问题,甚至可以在单元测试中使用资源。 MVP 是在 Bob 大叔干净架构的外部布局上,并且依赖于 android,所以在 Presenter 中使用资源是正常且正确的 你是如何做出“资源使用没有问题”这个假设的? 以更好的方式提出您的问题,我确实解释了使用干净的架构方法,并且根据我的经验,每个平台的表示层应该是不同的,我介绍了一种方法,甚至可以将其重用于以后的解决方案。如果您真的想知道原因,请在 LinkedIn 上给我发短信,以便我用波斯语解释。另外,这可不是黑客:D【参考方案3】:

这将是一篇关于如何在我最后回答您的问题之前构建 MVP 项目的长文。

我只是根据我自己的回答在这里how to structure MVP project 报告 MVP 结构。

我经常将业务逻辑代码放在模型层中(不要与数据库中的模型混淆)。我经常重命名为XManager 以避免混淆(例如ProductManagerMediaManager ...),因此演示者类仅用于保持工作流程。

经验法则是在presenter类中没有或至少限制import android package。这个最佳实践支持您更轻松地测试演示者类,因为演示者现在只是一个普通的 java 类,所以我们不需要 android 框架来测试这些东西。

例如,这是我的 mvp 工作流程。

View 类:这是您存储所有视图(例如按钮、文本视图...)的地方,您可以在该层上为这些视图组件设置所有侦听器。同样在此视图上,您​​稍后为演示者实现定义一个侦听器类。您的视图组件将调用此侦听器类的方法。

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 类:这是您在其中存储视图和模型以供以后调用的地方。演示者类也将实现上面定义的 ViewListener 接口。主讲人的重点是控制逻辑工作流。

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类:这里是核心业务逻辑代码。也许一个演示者会有很多经理(取决于视图的复杂程度)。我们经常通过Dagger等一些注入框架得到Context类。

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();
   

最后:把这些东西放在Activities、Fragments中……这里是你初始化view、manager并将所有东西分配给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();
   

您会看到每个演示者、模型、视图都由一个接口包装。这些组件将通过接口调用。这种设计将使您的代码更加健壮,并且更易于以后修改。

简而言之,在您的情况下,我提出了这样的设计:

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

如果逻辑视图复杂,例如设置值的一些条件。所以我会将逻辑放入DataManager 以获取文本。例如:

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

因此,您永远不会将与 android 相关的东西放入演示者类。您应该将其移至 View 类或 DataManager 类,具体取决于上下文。

这是一篇很长的帖子,详细讨论了 MVP 以及如何解决您的具体问题。希望对您有所帮助:)

【讨论】:

除了演示者不应该有 android 回调这一事实(测试它很奇怪,它正在将 Android 意大利面条转移到演示者)我喜欢你的解决方案。但是,如果这些值没有任何逻辑并且数量很多,该怎么办?在 Manager 或 Presenter 中使用 R.string.*?我宁愿将它们保留在 Presenter 中,以避免不必要的抽象层 AKA 代码嫉妒。但另一方面,它们是由 Android 生成的……编码准则彼此对立。【参考方案4】:

我认为没有理由在 Presenter 中调用任何 android 代码(但您始终可以这样做)。

所以在你的情况下:

查看/活动 onCreate() 调用 -> presenter.onCreate();

Presenter onCreate() 调用 -> view.setTextLabel() 或任何你想要的视图。

始终将 Android SDK 与演示者分离。

在 Github 中,您可以找到一些关于 MVP 的示例:

https://github.com/pedrovgs/EffectiveAndroidUI

https://github.com/android10/Android-CleanArchitecture

https://github.com/PaNaVTEC/Clean-Contacts/tree/develop

【讨论】:

您能否指定view.setTextLabel() 中的参数?因为如果你这样做,那将回答这个问题。基本上想象一下,如果您的演示者有逻辑来显示消息R.string.aR.string.b。您将如何传递此字符串以供视图显示? 您的 View 需要被动,并且您的 Presenter 处于活动状态,因此,Presenter 知道要呈现什么,而 View 知道如何呈现它。如果您想在演示者的视图中显示错误,您需要执行如下调用:view.showContactLoadingError();并且您的视图可以根据视图的上下文设置消息。我已经用我制作的另一个 repo 更新了示例。 感谢@PaNaVTEC,这正是我一直在寻找的答案,而且很有意义。 如果你想记录错误并使用资源来获取错误字符串呢? 根据不同的视图,你可能会显示不同的错误信息,同样显示错误。所以它可以是视图责任。如果这不是您的情况,您可以包装 R.string 并将协作者发送给您的演示者。

以上是关于Android MVP - 应该避免在演示者中使用 R.string 引用吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android - MVP 的上下文

应该将演示者(mvP)注入(dagger2)到android中的视图吗?

在 MVP 模式中,Presenter 应该是 Android 应用程序的活动方式还是功能方式

android MVP - 我可以有多个演示者用于自定义视图和片段

如何在演示者中使用 Enumerable mixin?

从单元测试的角度来看:视图应该指定演示者还是 GWT MVP 中的其他方式?