Куда мне поместить логику загрузки изображений на Android по шаблону MVP?

Я пишу приложение для Android, и хотя я уже читал о MVP и видел несколько примеров в Android, я сомневаюсь, как мне структурировать эту часть приложения.

ПРИМЕЧАНИЕ. Мое приложение имеет структуру, очень похожую на: https://github.com/googlesamples/android-architecture/tree/todo-mvp.

В этом приложении Модель должна получать данные 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. Мне бы очень хотелось, если бы я мог провести модульное тестирование этой части приложения (Mocking the DownloadManager). Таким образом, передача контекста в модель не является вариантом, поскольку она нарушает MVP (IMHO) и будет намного сложнее ее модульное тестирование.

Любая информированная помощь будет оценена!

Обновления

Спасибо за ответ @ amadeu-cavalcante-filho. Позвольте мне рассмотреть каждую проблему. Во-первых, проблема контекста: мне нужен контекст, если я использую Glade (библиотеку загрузки изображений) или DownloadManager для загрузки изображений, поэтому, если я загружаю изображения в модели (репозиторий), мне придется передать их в Смоделируйте экземпляр Context, и это явно нарушит MVP.

Во-вторых, MVVM, я мало что знаю о MVVM, но мне кажется, что модель в MVP должна знать, как получать данные (https://medium.com/@cervonefrancesco/model-view-presenter-android-Guidelines-94970b430ddf) с использованием шаблона репозитория или чего-то еще как это.

В-третьих, я склонен согласиться с тем, что Presenter действительно может загружать изображения (это именно тот пример, который я построил в своем вопросе). Но моя проблема в следующем: должен ли Presenter знать о материалах Android (в данном случае о контексте)? Это огромная часть моего вопроса, где Android должен быть в MVP? Единственное место, где можно узнать об Android, - это представление, но логика загрузки явно не принадлежит ему.


person gpedote    schedule 10.04.2018    source источник
comment
Задача Asynk имеет метод onPostExecute, в котором вы можете реализовать собственный обратный вызов, чтобы сообщить, что вы хотите о результате загрузки.   -  person Giulio Pettenuzzo    schedule 11.04.2018
comment
Спасибо, я это знаю, @GiulioPettenuzzo. Если я использую задачу Async для модели (репозитория), возникает проблема: как сообщить докладчику об окончании загрузки, не нарушая MVP? Должен ли я использовать события для информирования ведущего? Достаточно ли вызова метода для Presenter, если да, должна ли модель знать о Presenter?   -  person gpedote    schedule 11.04.2018
comment
@gpedote использует обратные вызовы для обратной связи с докладчиком, таким образом модель не знает, кто это вызвал   -  person Tim    schedule 11.04.2018
comment
Единственное место, где можно узнать об Android, - это представление, но логика загрузки явно не принадлежит ему. Вы можете использовать утилиту, которая знает про Android и создать уровень абстракции для управления чем-то вроде уровня Presenter. Hava посмотрите github.com/googlesamples/android-architecture/tree/todo-mvp   -  person Amadeu Cavalcante Filho    schedule 11.04.2018


Ответы (2)


После обновления вопрос, похоже, сильно отличается от того, что я думал вначале,

@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 имеет контекст действия, вы можете протестировать его, передав имитационный объект, который реализует те же методы, но не нуждается в контексте приложения, чтобы вы могли протестировать. Нравится:

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
    }
}
person Amadeu Cavalcante Filho    schedule 11.04.2018
comment
Еще раз спасибо за ответ @AmadeuCavalcanteFilho. Ваш ответ выглядит великолепно. Я обдумываю это, и как только выясню, решит ли это мою проблему, я отмечу правильный ответ. - person gpedote; 12.04.2018

Вы можете иметь Presenter без необходимости иметь представление, явно связанное с этим Presenter. Другими словами, у вас может быть докладчик только для инкапсуляции некоторой логики. В вашем случае у вас может быть докладчик, который знает только, как получать и предоставлять некоторые изображения. И ваше представление может использовать этого конкретного ведущего.

Я не понял, зачем вам передавать контекст модели.

Прямо сейчас моя идея состоит в том, чтобы добавить логику запроса веб-службы в модель (я также использую шаблон репозитория) и логику загрузки в презентаторе. Примерно так (код - всего лишь пример):

Вы могли бы это сделать. Однако это больше похоже на MVVM, где вы помещаете логику в модель, а модель знает, как получать данные.

В вашем случае вы хотите следовать MVP, поэтому модель содержит только данные (фрагменты информации / данных). Таким образом, у вас может быть один Presenter, который знает, как загружать ваши изображения. И у вас может быть несколько Utils, которые могут помочь вам с частью запроса. У вас может быть другая модель для этого Presenter, которая загружает изображения для сохранения ваших изображений, например, кэш. И, опять же, если вы хотите создать какую-то логику кеширования, вы должны сделать это на том же презентаторе, который знает, как загружать изображение. Или, если он становится слишком большим и сложным, вы можете сделать Presenter, который умеет только кэшировать.

Когда ваш Presenter знает только, как загрузить изображение, или только знает, как сохранить изображение. Вы можете легко это проверить, просто передайте ссылку на свой Presenter метод и проверьте, сможет ли он загрузить изображение.

Примечание: я не понимал, как может быть удобно или важно передавать контекст для вашей модели, если она не знает какой-то кеш с использованием настроек Android?

ПРИМЕЧАНИЕ 2. Мне бы очень хотелось, если бы я мог провести модульное тестирование этой части приложения (Mocking the DownloadManager). Таким образом, передача контекста в модель не является вариантом, поскольку она нарушает MVP (IMHO) и будет намного сложнее ее модульное тестирование.

person Amadeu Cavalcante Filho    schedule 11.04.2018
comment
Спасибо за ответ @AmadeuCavalcanteFilho, я обновил свой вопрос, чтобы решить эти проблемы. - person gpedote; 11.04.2018
comment
У вас может быть Presenter без необходимости иметь представление, явно связанное с этим Presenter - в этом нет смысла. Что это будет представлять? - person Tim; 11.04.2018
comment
@TimCastelijns У вас может быть служба, которая выполняет какие-то функции в фоновом режиме, но вы не хотите, чтобы ваша логика была подчиненной API Android, однако вы можете поместить это на одного докладчика, который не имеет представления, но обрабатывает логика для некоторого сервиса и тестируема. - person Amadeu Cavalcante Filho; 11.04.2018