我正在编写一个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内容的唯一地方是视图,但下载逻辑显然不属于那里。
答案 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
}
}