Search code examples
androidmvpandroid-mvp

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


I'm writing an Android application and although I already have read about MVP and saw some examples in Android, I'm in doubt about how should I structure this part of the app.

NOTE: My app follows a structure very similar to: https://github.com/googlesamples/android-architecture/tree/todo-mvp

In this app, the Model should fetch JSON data from a web service. This data, among other stuff, contains links of images that the app should download asynchronously. And, after downloading, these images should be presented to the user.

How should I approach this?

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

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
    }

    ....
}

Does this makes sense? Does the download logic belongs to the Presenter? Am I putting too much logic on the Presenter?

NOTE: I' thinking about putting the download logic in the Presenter because the DownloadManager needs a Context (btw, Glide needs too). Alternatively, I know that I can use an AsyncTask on the Model to download using an HttpURLConnection, but how should I inform the download result back to the Presenter? In the latter, should I use events?

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.

Any informed help would be appreciated!

Updates

Thanks for you response @amadeu-cavalcante-filho. Let me address each issue. First, the Context problem: I need a Context, if I use Glade (an image download library) or DownloadManager, to download the images, thus, if I download the images on the Model (repository) I’ll have to give to the Model a Context instance and this clearly breaks the MVP.

Second, the MVVM, I don’t know much about the MVVM, but it seems to me that the Model in MVP should know how to fetch data (https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-94970b430ddf) using a repository pattern or something like that.

Third, I'm prone to accept that the Presenter can indeed download the images (this is specifically the example that I constructed in my question). But, my problem is: should the Presenter know about Android stuff (the Context in this case)? This is a huge part of my question, where the Android stuff should be in the MVP? The only place that can know about Android stuff is the view, but the download logic clearly doesn’t belong there.


Solution

  • After the update the question seems to be quite different from what I thought firstly,

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

    Here is an example from https://github.com/googlesamples/android-architecture

    You can see that the repository (which fetch data) is passed to the presenter already injected with the application context. So, you are passing the Repository which is your abstraction to handle data to your presenter, then you have testability because you can control those two enviroments and you can have the Context passed to your repository where you can fetch data.

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

    When you whant to test. You could do something like.

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

    And, as your Presenter does not know that your Repository has a Context of the activity, you can test it passing a mock object that implements the same methods, but does not need the application context, so you can test. Like:

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