根据 MVP 模式,我应该将图像下载逻辑放在 Android 上的啥位置?

Posted

技术标签:

【中文标题】根据 MVP 模式,我应该将图像下载逻辑放在 Android 上的啥位置?【英文标题】:Where should I put the image download logic on Android, according to the MVP pattern?根据 MVP 模式,我应该将图像下载逻辑放在 Android 上的什么位置? 【发布时间】:2018-04-10 21:18:19 【问题描述】:

我正在编写一个 android 应用程序,虽然我已经阅读了有关 MVP 并在 Android 中看到了一些示例,但我对如何构建应用程序的这一部分存有疑问。

注意:我的应用遵循的结构非常类似于:https://github.com/googlesamples/android-architecture/tree/todo-mvp

在此应用中,模型应从 Web 服务获取 JSON 数据。除其他内容外,此数据包含应用程序应异步下载的图像链接。并且,下载后,这些图像应该呈现给用户。

我应该如何处理这个问题?

现在,我的想法是在 Model 上添加 Web 服务请求逻辑(我也在使用 Repository 模式)和在 Presenter 上添加下载逻辑。像这样(代码只是一个例子):

class MyPresenter 
    ....

    void init() 
        myRepositoryInstance.fetchDataAndSaveLocally(new MyCallback() 

            @Override
            public void success(List<Thing> listOfThings) 
                // do some other stuff with listOfThings data
                ...

                List<URL> imagesURL = getImagesURLs(listOfThings);

                // config/use Android DownloadManager to download the images
                ...

                registerReceiver(onImageDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
            

            @Override
            public void error() // logging stuff, try again...
        );
    

    void onImageDownloadComplete() 
        URL path = getWhereTheImageWasSaved();
        Thing thing = getInstanceOfThingAssociatedWithThisImage();
        myRepositoryInstance.updatePathOfThingImage(thing, path);
        viewInstance.updateTheViewPager(); // I'll probably show these images on a ViewPager
    

    ....

这有意义吗?下载逻辑是否属于 Presenter?我是否对 Presenter 施加了太多逻辑?

注意:我正在考虑将下载逻辑放在 Presenter 中,因为 DownloadManager 需要一个 Context(顺便说一句,Glide 也需要)。或者,我知道我可以使用模型上的 AsyncTask 来使用 HttpURLConnection 进行下载,但是我应该如何将下载结果通知给 Presenter?在后者中,我应该使用事件吗?

注意 2:如果我可以对应用程序的这一部分进行单元测试(模拟下载管理器),我会很高兴。因此,将上下文传递给模型不是一种选择,因为它破坏了 MVP(恕我直言)并且更难以对其进行单元测试。

任何知情的帮助将不胜感激!

更新

感谢您对@amadeu-cavalcante-filho 的回复。让我解决每个问题。首先,上下文问题:我需要一个上下文,如果我使用 Glade(一个图像下载库)或 DownloadManager 来下载图像,因此,如果我在模型(存储库)上下载图像,我将不得不给为 Context 实例建模,这显然破坏了 MVP。

其次,MVVM,我对 MVVM 了解不多,但在我看来,MVP 中的 Model 应该知道如何使用存储库模式或类似的方式获取数据 (https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf)。

第三,我倾向于接受 Presenter 确实可以下载图像(这特别是我在我的问题中构建的示例)。但是,我的问题是:Presenter 是否应该知道 Android 的东西(在这种情况下是 Context)?这是我的问题的一个重要部分,Android 的东西应该在 MVP 中的什么位置?唯一能了解Android东西的地方就是view,但是下载逻辑显然不属于那里。

【问题讨论】:

Asynk 任务有一个 onPostExecute 方法,您可以在其中实现自己的回调,以便告知您想要的下载结果 谢谢,我知道,@GiulioPettenuzzo。如果我在模型(存储库)上使用异步任务,问题就变成了:如何在不破坏 MVP 的情况下通知演示者下载结束?我应该使用事件来通知演示者吗?对 Presenter 的方法调用是否足够,如果是,Model 是否应该知道 Presenter? @gpedote 使用回调与演示者通信,这样模型不知道是谁调用它 "唯一能知道Android东西的地方就是视图,但下载逻辑显然不属于那里。"您可以使用了解 Android 内容的实用程序并创建一个抽象层来操作 Presenter 层。哈瓦看看github.com/googlesamples/android-architecture/tree/todo-mvp 【参考方案1】:

更新后的问题似乎和我最初想的很不一样,

@Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);

        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mActionBar = getSupportActionBar();
        mActionBar.setDisplayHomeAsUpEnabled(true);
        mActionBar.setDisplayShowHomeEnabled(true);

        AddEditTaskFragment addEditTaskFragment = (AddEditTaskFragment) getSupportFragmentManager()
                .findFragmentById(R.id.contentFrame);

        String taskId = getIntent().getStringExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID);

        setToolbarTitle(taskId);

        if (addEditTaskFragment == null) 
            addEditTaskFragment = AddEditTaskFragment.newInstance();

            if (getIntent().hasExtra(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID)) 
                Bundle bundle = new Bundle();
                bundle.putString(AddEditTaskFragment.ARGUMENT_EDIT_TASK_ID, taskId);
                addEditTaskFragment.setArguments(bundle);
            

            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),
                    addEditTaskFragment, R.id.contentFrame);
        

        boolean shouldLoadDataFromRepo = true;

        // Prevent the presenter from loading data from the repository if this is a config change.
        if (savedInstanceState != null) 
            // Data might not have loaded when the config change happen, so we saved the state.
            shouldLoadDataFromRepo = savedInstanceState.getBoolean(SHOULD_LOAD_DATA_FROM_REPO_KEY);
        

        // Create the presenter
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskFragment,
                shouldLoadDataFromRepo);
    

这是来自https://github.com/googlesamples/android-architecture的示例

您可以看到存储库(获取数据)已传递给已注入应用程序上下文的演示者。因此,您将 Repository 传递给您的演示者,这是您处理数据的抽象,然后您具有可测试性,因为您可以控制这两个环境,并且您可以将 Context 传递到您可以获取数据的存储库。

public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) 
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;

        mAddTaskView.setPresenter(this);
    

当您想要测试时。你可以做类似的事情。

@Rule
    public ActivityTestRule<TasksActivity> mTasksActivityTestRule =
            new ActivityTestRule<TasksActivity>(TasksActivity.class) 

                /**
                 * To avoid a long list of tasks and the need to scroll through the list to find a
                 * task, we call @link TasksDataSource#deleteAllTasks() before each test.
                 */
                @Override
                protected void beforeActivityLaunched() 
                    super.beforeActivityLaunched();
                    // Doing this in @Before generates a race condition.
                    Injection.provideTasksRepository(InstrumentationRegistry.getTargetContext())
                        .deleteAllTasks();
                
            ;

而且,由于您的Presenter 不知道您的Repository 具有活动的上下文,因此您可以通过一个实现相同方法但不需要应用程序上下文的模拟对象对其进行测试,因此您可以测试。 喜欢:

public class AddEditTaskPresenterTest 

    @Mock
    private TasksRepository mTasksRepository;

    @Mock
    private AddEditTaskContract.View mAddEditTaskView;

    /**
     * @link ArgumentCaptor is a powerful Mockito API to capture argument values and use them to
     * perform further actions or assertions on them.
     */
    @Captor
    private ArgumentCaptor<TasksDataSource.GetTaskCallback> mGetTaskCallbackCaptor;

    private AddEditTaskPresenter mAddEditTaskPresenter;

    @Before
    public void setupMocksAndView() 
        // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
        // inject the mocks in the test the initMocks method needs to be called.
        MockitoAnnotations.initMocks(this);

        // The presenter wont't update the view unless it's active.
        when(mAddEditTaskView.isActive()).thenReturn(true);
    

    @Test
    public void createPresenter_setsThePresenterToView()
        // Get a reference to the class under test
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                null, mTasksRepository, mAddEditTaskView, true);

        // Then the presenter is set to the view
        verify(mAddEditTaskView).setPresenter(mAddEditTaskPresenter);
    

    @Test
    public void saveNewTaskToRepository_showsSuccessMessageUi() 
        // Get a reference to the class under test
        mAddEditTaskPresenter = new AddEditTaskPresenter(
                null, mTasksRepository, mAddEditTaskView, true);

        // When the presenter is asked to save a task
        mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Description");

        // Then a task is saved in the repository and the view updated
        verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model
        verify(mAddEditTaskView).showTasksList(); // shown in the UI
    

【讨论】:

再次感谢您的回复@AmadeuCavalcanteFilho。你的答案看起来很棒。我正在考虑它,一旦我确定这是否解决了我的问题,我会标记正确的答案。【参考方案2】:

您可以拥有一个 Presenter,而无需将视图显式链接到该 Presenter。换句话说,你可以让一个演示者只封装一些逻辑。在您的情况下,您可以有一个只知道如何获取和提供一些图像的演示者。并且您的视图可以使用这个特定的演示者。

我不明白为什么必须将上下文传递给模型。

现在,我的想法是在 模型(我也在使用存储库模式)和下载逻辑 在演示者上。像这样(代码只是一个例子):

你可以这样做。但是,它看起来更像是 MVVM,您将逻辑放入 Model 并且 Model 知道如何获取数据。

在您的情况下,您想遵循 MVP,因此模型只保存数据(信息/数据片段)。因此,您可以拥有一位知道如何下载图像的演示者。你可以有一些Utils 可以帮助你处理请求部分。您可以为 Presenter 提供另一个模型,用于下载图像以保存图像,例如缓存。而且,如果你想创建某种缓存逻辑,你应该在知道如何下载图像的同一个演示者上做。或者,如果它变得太大太复杂,您可以创建一个只知道热缓存内容的Presenter

一旦你的Presenter 只知道如何下载图片,或者只知道如何保存图片。您可以轻松测试它,只需将链接传递给您的 Presenter 方法并检查它是否可以处理下载图像。

注意:我不明白您为模型传递上下文如何方便或重要,除非它知道使用 Android 首选项的某种缓存?

注意 2:如果我可以对应用程序的这一部分进行单元测试,我会很高兴 (模拟下载管理器)。因此,将上下文传递给模型是 不是一种选择,因为它打破了 MVP(恕我直言)并且更难 对其进行单元测试。

【讨论】:

感谢您对@AmadeuCavalcanteFilho 的回复,我更新了我的问题以解决这些问题。 您可以拥有一个 Presenter,而无需将视图明确链接到该 Presenter - 这是没有意义的。它会呈现什么? @TimCastelijns 你可以有一个服务,它在后台做一些事情,但你不想把你的逻辑放在那个服务上成为 android API 的奴隶,但是,你可以把它放在一个Presenter 没有视图,但处理某些服务的逻辑并且是可测试的。

以上是关于根据 MVP 模式,我应该将图像下载逻辑放在 Android 上的啥位置?的主要内容,如果未能解决你的问题,请参考以下文章

如何从MVP模式进阶到Clean模式

MVP模式

我应该如何对不同方向的图像进行分类?

android-samples-mvp

原子设计模式 - 业务逻辑

Android MVC模式和MVP模式的区别