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

Where should I put the image download logic on Android, according to the MVP pattern?

我正在编写一个 Android 应用程序,虽然我已经阅读了有关 MVP 的内容并在 Android 中看到了一些示例,但我不确定我应该如何构建应用程序的这一部分.

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

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

我该如何处理?

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

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 需要一个上下文(顺便说一句,Glide 也需要)。或者,我知道我可以在模型上使用 AsyncTask 来使用 HttpURLConnection 下载,但是我应该如何将下载结果通知给 Presenter?在后者中,我应该使用事件吗?

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

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

更新

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

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

第三,我倾向于接受 Presenter 确实可以下载图像(这是我在问题中构建的具体示例)。但是,我的问题是:Presenter 应该知道 Android 的东西(在这种情况下是 Context)吗?这是我问题的很大一部分,Android 应该放在 MVP 中的什么地方?唯一可以了解 Android 东西的地方是视图,但下载逻辑显然不属于那里。

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

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

Right now, my idea is to add the web service request logic on the Model (I'm also using the Repository pattern) and the download logic on the Presenter. Something like this (the code is just an example):

你可以做到。但是,它看起来更像是 MVVM,您将逻辑放入模型中,模型知道如何获取数据。

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

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

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

NOTE 2: I would love if I could unit test this part of the app (Mocking the DownloadManager). So, passing the Context to the Model is not an option, as it breaks the MVP (IMHO) and would be much harder to unit test it.

更新后的问题好像和我一开始想的不太一样,

@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

的示例

您可以看到存储库(获取数据)被传递给已经注入了应用程序上下文的演示者。因此,您将存储库传递给您的演示者,这是您处理数据的抽象,然后您具有可测试性,因为您可以控制这两个环境,并且可以将 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 具有 activity 的上下文,您可以通过一个实现相同方法的模拟对象来测试它,但是不需要应用上下文,所以你可以测试。 喜欢:

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
    }
}