根据MVP模式,我应该在哪里将图像下载逻辑放在Android上?

时间:2018-04-10 21:18:19

标签: android mvp android-mvp

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

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

在这个应用程序中,Model应该从Web服务获取JSON数据。此类数据包含应用程序应异步下载的图像链接。并且,下载后,这些图像应呈现给用户。

我该如何处理?

现在,我的想法是在模型上添加Web服务请求逻辑(我也使用存储库模式)和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需要一个上下文(顺便说一下,Glide也需要)。或者,我知道我可以使用模型上的AsyncTask使用HttpURLConnection进行下载,但是如何将下载结果通知给Presenter?在后者中,我应该使用事件吗?

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

任何明智的帮助都将不胜感激!

更新

感谢您回复@ amadeu-cavalcante-filho。让我解决每个问题。首先,Context问题:我需要一个Context,如果我使用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内容的唯一地方是视图,但下载逻辑显然不属于那里。

2 个答案:

答案 0 :(得分:0)

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

我没理解为什么你必须将上下文传递给模型。

  

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

你可以这样做。但是,它似乎更像MVVM,您将逻辑放入模型,模型知道如何获取数据。

在您的情况下,您希望遵循MVP,因此模型仅保存数据(信息/数据)。因此,您可以让一位知道如何下载图像的Presenter。您可以使用一些Utils来帮助您处理请求部分。您可以使用另一个模型Presenter来下载用于保存图像的图像,例如缓存。而且,如果你想制作某种缓存逻辑,你应该在知道如何下载图像的同一个演示者上进行。或者,如果它变得太大而复杂,你可以制作一个Presenter,只知道热点来缓存内容。

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

注意:我不明白为模型传递上下文有多方便或重要,除非它知道某些使用Android偏好的缓存?

  

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

答案 1 :(得分:-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

的示例

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

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