При разработке сложных приложений можно столкнуться с проблемами, которые, вероятно, возникали раньше и уже имеют большое количество решений. Такие решения называются паттернами (шаблонами). Как правило, говорят о паттернах дизайна и паттернах архитектуры. Они упрощают разработку приложений, поэтому целесообразно их использовать, если такая возможность есть.
Дизайн проекта должен быть приоритетной задачей с самого начала разработки. Одна из задач, которые нужно решить в первую очередь — это архитектура проекта, определяющая, как разные элементы нашего приложения будут соотноситься друг с другом.
Хотя Android предлагает отличный SDK, его архитектурные паттерны довольно необычны и могут мешать во время разработки, особенно при создании сложных приложений, которые необходимо тестировать и поддерживать в течение долгого времени. К счастью, мы можем сами выбирать, как хотим проектировать приложение.
Инструменты, предлагаемые Android, предлагают нам использовать модель MVC (Model View Controller). MVC представляет собой паттерн, который направлен на разделение ролей в приложении.
Архитектура в нём делится на три слоя:
- Model;
- View;
- Controller.
Каждый слой отвечает за отдельный аспект приложения. Model отвечает за бизнес-логику, View — за пользовательский интерфейс, а Controller обеспечивает доступ к Model.
Однако, если проанализировать архитектуру Android, особенно отношение между View (Activity, Fragment и т.д.) и Model (структуры данных), то можно сделать вывод, что это нельзя считать MVC. Если мы подумает о симбиотической связи между загрузчиками и активностями с фрагментами, то то вы сможете понять, почему нужно уделять пристальное внимание архитектуре Android. Активность или фрагмент отвечает за вызов загрузчика, который должен извлекать данные и возвращать их родителям. Его существование полностью привязано к его родителям и нет разделение между ролью View и бизнес-логикой, выполняемой загрузчиком.
Отсюда возникает проблемы:
- Как можно использовать модульное тестирование в приложении, в котором данные тесно связаны с представлениями (активностями или фрагментами)?
- Как найти проблему в чужом коде, если проект не придерживается какого-то паттерна и код может находиться буквально в любом месте?
Чтобы решить эти проблемы, нужно выбрать для разработки какую-то свою архитектуру. Примером хорошего паттерна может являться MVP (Model View Presenter). MVP является аналогом MVC, но с более современной парадигмой, которая создаёт лучшее разделение ролей и максимизирует тестируемость приложения.
MVP разделяет приложение на следующие уровни:
- Model;
- View;
- Presenter.
У каждого из них есть свои обязанности и связь между ними происходит через Presenter.
Model содержит бизнес-логику приложения. Он контролирует как данные создаются, сохраняются и изменяются.
View это интерфейс, который отображает данные и направляет действия пользователя в Presenter.
Presenter выступает в роли посредника. Он извлекает данные из Model и показывает их в View. Он также обрабатывает действия пользователя, переданные ему из View.
Основным отличием от MVC здесь является то, что Presenter контролирует взаимодействие между View и Moder более строго, чем это делает Controller, в MVC слой View может сам извлекать данные из Model. Кроме того, MVP обладает и другими отличиями, делающими его более подходящим для построения архитектуры приложения:
- View не имеет доступа к Model;
- Presenter привязан к одному View;
- View полностью пассивен;
Такое разделение слоёв позволяет лучше тестировать приложение, поскольку заранее известно, на каком уровне нужно искать код.
Реализуем паттерн MVP в одном из наших приложений (почитать о его реализации вы можете, перейдя по этой ссылке или по этой). Исходный код MainActivity в приложении выглядит следующим образом:
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, GetAppsTask.AsyncResponse, AppsAdapter.ClickListener { int REQUEST_UNINSTALL = 1; private SearchView mSearchView; private AppsAdapter mAppsAdapter; private List<AppInfo> mAppsList; public static List<AppInfo> mSelectedList; private int currentList = R.string.menu_installed; private LinearLayout footer; ProgressBar progressBar; private RecyclerView recyclerView; private GetIconThread<AppsAdapter.AppViewHolder> mGetIconTask; private IconCache iconCache; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); progressBar = findViewById(R.id.progress_spinner); recyclerView = findViewById(R.id.recyclerView); int memClass = ((ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass(); int cacheSize = 1024 * 1024 * memClass / 8; iconCache = new IconCache(cacheSize); Handler responseHandler = new Handler(); mGetIconTask = new GetIconThread<>(responseHandler, getApplicationContext(), iconCache); mGetIconTask.setIconDownloadListener( new GetIconThread.GetIconTaskListener<AppsAdapter.AppViewHolder>() { @Override public void onIconDownloaded(AppsAdapter.AppViewHolder target, Drawable icon) { target.bindDrawable(icon); } }); mGetIconTask.start(); mGetIconTask.getLooper(); mSelectedList = new ArrayList<>(); DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) { ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.addDrawerListener(toggle); toggle.syncState(); } NavigationView navigationView = findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); setTitle(R.string.loading); new GetAppsTask(this, this).execute(); footer = findViewById(R.id.footer); Button delete = findViewById(R.id.delete); delete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { delete(); } }); Button clear = findViewById(R.id.clear); clear.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { clearSelection(); } }); } private void clearSelection() { footer.setVisibility(View.GONE); mAppsAdapter.updateView(mSelectedList); mSelectedList.clear(); // mAppsAdapter.notifyDataSetChanged(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_UNINSTALL) { // 0 means success, other means failed. Log.d("TAG", "got result of uninstall: " + resultCode); updateAppList(); } } private void updateAppList() { for (AppInfo ai : mSelectedList) { if (!Tools.isPackageInstalled(this, ai.packageName)) mAppsAdapter.remove(ai); } setTitle(getString(currentList) + " (" + mAppsAdapter.getItemCount() + ")"); clearSelection(); } private void delete() { if (currentList == R.string.menu_installed) { for (AppInfo ai : mSelectedList) { Uri packageURI = Uri.parse("package:" + ai.packageName); Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI); startActivityForResult(uninstallIntent, REQUEST_UNINSTALL); // mAppsAdapter.remove(ai); } } else { setTitle(R.string.deleting); progressBar.setVisibility(View.VISIBLE); mSearchView.setVisibility(View.GONE); for (AppInfo ai : mSelectedList) { if (Tools.m521a(ai.publicSourceDir)) mAppsAdapter.remove(ai); } progressBar.setVisibility(View.GONE); mSearchView.setVisibility(View.VISIBLE); updateAppList(); if (SP.getBoolean(this, SHOW_UNINSTALL_DIALOG, true)) uninstallDialog(); } } @Override public void onBackPressed() { DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) { if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { if (footer.getVisibility() == View.VISIBLE) { clearSelection(); } else { super.onBackPressed(); } } } else { if (footer.getVisibility() == View.VISIBLE) { clearSelection(); } else { super.onBackPressed(); } } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); mSearchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search)); mSearchView.setQueryHint(getString(R.string.search_hint)); mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { search(query); return true; } @Override public boolean onQueryTextChange(String query) { search(query); return true; } }); return true; } private void search(String query) { if (mAppsAdapter != null) mAppsAdapter.filter(query); } @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_installed) { currentList = R.string.menu_installed; if (mAppsList != null) { setAdapter(filterAppList()); } } else if (id == R.id.nav_system) { currentList = R.string.menu_system; if (mAppsList != null) { setAdapter(filterAppList()); } checkRootDialog(); } else if (id == R.id.nav_settings) { Toast.makeText(this, R.string.settings_empty, Toast.LENGTH_LONG).show(); // startActivity(new Intent(this, RootHelpActivity.class)); } else if (id == R.id.nav_wiki) { openWikiPage(); } else if (id == R.id.nav_share) { ShareCompat.IntentBuilder.from(this) // getActivity() or activity field if within Fragment .setText(getString(R.string.send_message) + " " + getString(R.string.share_text)) .setType("text/plain") // most general text sharing MIME type .setChooserTitle(R.string.share_title) .startChooser(); } else if (id == R.id.nav_send) { ShareCompat.IntentBuilder.from(this) .setType("message/rfc822") // .addEmailTo(email) .setSubject(getString(R.string.app_name)) .setText(getString(R.string.send_message) + " " + getString(R.string.share_text)) //.setHtmlText(body) //If you are using HTML in your body text .setChooserTitle(R.string.send_title) .startChooser(); // startActivity(new Intent(this, AboutActivity.class)); } else if (id == R.id.nav_apps) { openMarket(); } DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) drawer.closeDrawer(GravityCompat.START); return true; } private void openMarket() { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pub:Android Tools (ru)")); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } else { Toast.makeText(this, R.string.error_missing_market, Toast.LENGTH_LONG).show(); } } private void openWikiPage() { Intent intentBrowser = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.wiki_url))); if (intentBrowser.resolveActivity(getPackageManager()) != null) { startActivity(intentBrowser); } else { Toast.makeText(this, R.string.error_missing_browser, Toast.LENGTH_LONG).show(); } } private void checkRootDialog() { if (!Tools.checkRooted()) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.system_app_no_root_access).setTitle(R.string.common_warning); builder.setPositiveButton(R.string.root_help_title, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { openWikiPage(); } }); builder.setNegativeButton(R.string.common_i_know, null); AlertDialog dialog = builder.create(); dialog.show(); } } private void uninstallDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); LayoutInflater inflater = this.getLayoutInflater(); View dialogView = inflater.inflate(R.layout.uninstall_dialog, null); dialogBuilder.setView(dialogView); CheckBox checkBox = dialogView.findViewById(R.id.checkBox); checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) SP.setBoolean(MainActivity.this, SHOW_UNINSTALL_DIALOG, false); } }); dialogBuilder.setPositiveButton(android.R.string.ok, null); dialogBuilder.create().show(); } @Override public void onTaskComplete(List<AppInfo> result) { mAppsList = result; setAdapter(filterAppList()); } private List<AppInfo> filterAppList() { List<AppInfo> list = new ArrayList<>(); if (currentList == R.string.menu_installed) { for (AppInfo appInfo : mAppsList) if (!appInfo.isSystem) list.add(appInfo); } else { for (AppInfo appInfo : mAppsList) if (appInfo.isSystem) list.add(appInfo); } return list; } public void setAdapter(List<AppInfo> list) { sort(list); mAppsAdapter = new AppsAdapter(list, this, mGetIconTask); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL)); recyclerView.setAdapter(mAppsAdapter); setTitle(getString(currentList) + " (" + mAppsAdapter.getItemCount() + ")"); } public static void sort(List<AppInfo> apps) { Comparator<AppInfo> myComparator = new Comparator<AppInfo>() { public int compare(AppInfo obj1, AppInfo obj2) { return obj1.title.compareToIgnoreCase(obj2.title); } }; Collections.sort(apps, myComparator); } @Override public void onItemClicked(AppInfo ai) { if (!mSelectedList.contains(ai)) { mSelectedList.add(ai); } else { mSelectedList.remove(ai); } if (mSelectedList.isEmpty()) { footer.setVisibility(View.GONE); } else { footer.setVisibility(View.VISIBLE); } } @Override public boolean onItemLongClicked(int position) { return false; } @Override protected void onStop() { super.onStop(); mGetIconTask.clearQueue(); } @Override public void onDestroy() { super.onDestroy(); mGetIconTask.quit(); } }
На уровне Model в нашем приложении будет использоваться класс AppInfo, отвечающий за создание, хранение и изменение данных о приложениях.
public class AppInfo implements Serializable {
public AppInfo(String title, String packageName, String sourceDir, String publicSourceDir, String versionName, int versionCode, boolean isSystem, String size, String dataDir, String nativeLibraryDir) {
this.title = title;
this.packageName = packageName;
this.sourceDir = sourceDir;
this.publicSourceDir = publicSourceDir;
this.versionName = versionName;
this.versionCode = versionCode;
this.isSystem = isSystem;
this.size = size;
this.dataDir = dataDir;
this.nativeLibraryDir = nativeLibraryDir;
}
public boolean isSystem;
public String title;
public String packageName;
public String sourceDir;
public String publicSourceDir;
public String versionName;
public String size;
public int versionCode;
public String dataDir, nativeLibraryDir;
@Override
public boolean equals(Object obj) {
if (obj instanceof AppInfo) {
AppInfo temp = (AppInfo) obj;
if (this.title.equals(temp.title) && this.packageName.equals(temp.packageName))
return true;
}
return false;
}
@Override
public int hashCode() {
return (this.title.hashCode() + this.packageName.hashCode());
}
}
Для того, чтобы работать с данными из модели, создадим на этом же уровне специальный класс-интерактор, который будет сортировать списки приложений по категориям и возвращать итоговый результат в Presenter. Интерфейс класса будет состоять из одного метода filterApps(int currentList, List<AppInfo> mAppsList), который реализует сортировку.
public interface AppsInteractor {
List<AppInfo> filterApps(int currentList, List<AppInfo> mAppsList);
}
Сама реализация выглядит следующим образом.
public class AppsInteractorImpl implements AppsInteractor {
@Override
public List<AppInfo> filterApps(int currentList, List<AppInfo> mAppsList) {
List<AppInfo> list = new ArrayList<>();
if (currentList == R.string.menu_installed) {
for (AppInfo appInfo : mAppsList)
if (!appInfo.isSystem) list.add(appInfo);
} else {
for (AppInfo appInfo : mAppsList)
if (appInfo.isSystem) list.add(appInfo);
}
sortAppList(list);
return list;
}
private static void sortAppList(List<AppInfo> apps) {
Comparator<AppInfo> myComparator = new Comparator<AppInfo>() {
public int compare(AppInfo obj1, AppInfo obj2) {
return obj1.title.compareToIgnoreCase(obj2.title);
}
};
Collections.sort(apps, myComparator);
}
}
Преимущества использования интерактора в отдельном классе заключается в том, что он разбивает Presenter, делая его код более чистым и проверяемым, а также служит для абстракции логики данной модели. Класс-интерактор будет передаваться в Presenter при запуске приложения, поэтому у Presenter всегда будет к нему доступ.
Теперь нам нужно создать Presenter. Для этого необходимо перенести в отдельный класс обработку всех действий пользователя, а также работу с данными. Интерфейс получившегося презентера выглядит так:
public interface MainPresenter {
void detachView();
void attachView(MainView mainView);
void downloadApps(GetAppsTask mGetAppsTask);
void changeList(int change);
void clearQueue();
void deleteApp();
void addToSelected(AppInfo ai);
void clearSelection();
void updateAppList();
void prepareRootDialog(AlertDialog.Builder builder);
void onWikiClick();
void onMarketClick();
void onSettingsClick();
void onShareClick(ShareCompat.IntentBuilder intentBuilder);
void onSendClick(ShareCompat.IntentBuilder intentBuilder);
void showUninstallDialog(AlertDialog.Builder builder);
}
При запуске приложения в методе onCreate() активности мы создаём экземпляр Presenter и передаём ему интерактор.
mainPresenter = new MainPresenterImpl(new AppsInteractorImpl());
Таким образом, имеем следующий конструктор.
public MainPresenterImpl(AppsInteractor appsInteractor) {
this.appsInteractor = appsInteractor;
mAppsList = new ArrayList<>();
mSelectedList = new ArrayList<>();
}
Затем необходимо передать в презентер родительскую View, для этого вызывается метод презентера attachView(MainView mainView).
@Override
public void attachView(MainView mainView) {
this.mainView = mainView;
}
При завершении работы с активностью крайне важно затем откреплять View от Presenter, поскольку нам не нужно хранить View всё время. Как правило, экземпляр View следует очищать в onDestroy() или onPause() вашей активности, в зависимости от требований приложения. В данном случае удаление будет происходит в методе onDestroy().
@Override
public void onDestroy() {
mainPresenter.detachView();
super.onDestroy();
}
И собственно код выставление значения null экземпляру.
@Override
public void detachView() {
mGetIconThread.quit();
mainView = null;
}
После этого уже можно приступить к реализации методов, отвечающих за обработку действий пользователя и работу с данными в приложении. В результате получился следующий класс Presenter, связывающий уровни View и Model в нашем приложении.
public class MainPresenterImpl implements MainPresenter {
private MainView mainView;
private AppsInteractor appsInteractor;
private List<AppInfo> mAppsList;
private GetIconThread<AppsAdapter.AppViewHolder> mGetIconThread;
private int currentList = R.string.menu_installed;
private List<AppInfo> mSelectedList;
public MainPresenterImpl(AppsInteractor appsInteractor) {
this.appsInteractor = appsInteractor;
mAppsList = new ArrayList<>();
mSelectedList = new ArrayList<>();
}
@Override
public void attachView(MainView mainView) {
this.mainView = mainView;
}
@Override
public void detachView() {
mGetIconThread.quit();
mainView = null;
}
@Override
public void clearQueue() {
mGetIconThread.clearQueue();
}
@Override
public void downloadApps(GetAppsTask mGetAppTask) {
mainView.onSetToolbarTitle(mainView.getStringFromResources(R.string.loading));
setIconTask();
mGetAppTask.setAsyncResponce(new GetAppsTask.AsyncResponse() {
@Override
public void onTaskComplete(List<AppInfo> result) {
mAppsList = result;
initAppsAdapter();
}
});
mGetAppTask.execute();
}
@Override
public void changeList(int change) {
currentList = change;
initAppsAdapter();
}
@Override
public void addToSelected(AppInfo ai) {
Log.d("TAG", "addToSelected");
if (!mSelectedList.contains(ai)) {
mSelectedList.add(ai);
} else {
mSelectedList.remove(ai);
}
mainView.onSelectionChanged(mSelectedList);
if (mSelectedList.isEmpty()) {
mainView.onGoneFooter();
} else {
mainView.onVisibleFooter();
}
}
@Override
public void clearSelection() {
mSelectedList.clear();
mainView.onSelectionClear();
}
@Override
public void deleteApp() {
Log.d("TAG", "deleteApp");
if (currentList == R.string.menu_installed) {
for (AppInfo ai : mSelectedList) {
Uri packageURI = Uri.parse("package:" + ai.packageName);
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI);
mainView.onReadyActivityStartForResult(uninstallIntent);
mAppsList.remove(ai);
}
} else {
mainView.onSetToolbarTitle(mainView.getStringFromResources(R.string.deleting));
mainView.onPrepareDeleteDialog(mSelectedList);
}
}
@Override
public void updateAppList() {
Log.d("TAG", "updateAppList");
for (AppInfo ai : mSelectedList) {
if (!mainView.isPackageInstalled(ai.packageName)) {
mainView.onUpdatedList(ai);
}
}
List<AppInfo> list = appsInteractor.filterApps(currentList, mAppsList);
mainView.onSetToolbarTitle(mainView.getStringFromResources(currentList) + " (" + list.size() + ")");
clearSelection();
}
@Override
public void prepareRootDialog(AlertDialog.Builder builder) {
builder.setMessage(R.string.system_app_no_root_access).setTitle(R.string.common_warning);
builder.setPositiveButton(R.string.root_help_title, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
onWikiClick();
}
});
builder.setNegativeButton(R.string.common_i_know, null);
builder.create().show();
}
@Override
public void onWikiClick() {
Intent intentBrowser = new Intent(Intent.ACTION_VIEW, Uri.parse(mainView.getStringFromResources(R.string.wiki_url)));
if (intentBrowser.resolveActivity(mainView.getPackageManagerFromActivity()) != null) {
mainView.onReadyActivityStart(intentBrowser);
} else {
mainView.onToastReady(mainView.getStringFromResources(R.string.error_missing_browser));
}
}
@Override
public void onMarketClick() {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pub:Android Tools (ru)"));
if (intent.resolveActivity(mainView.getPackageManagerFromActivity()) != null) {
mainView.onReadyActivityStart(intent);
} else {
mainView.onToastReady(mainView.getStringFromResources(R.string.error_missing_market));
}
}
@Override
public void onSettingsClick() {
mainView.onToastReady(mainView.getStringFromResources(R.string.settings_empty));
}
@Override
public void onShareClick(ShareCompat.IntentBuilder intentBuilder) {
intentBuilder.setText(mainView.getStringFromResources(R.string.send_message) + " " + mainView.getStringFromResources(R.string.share_text));
intentBuilder.setType("text/plain"); // most general text sharing MIME type
intentBuilder.setChooserTitle(R.string.share_title);
intentBuilder.startChooser();
}
@Override
public void onSendClick(ShareCompat.IntentBuilder intentBuilder) {
intentBuilder.setType("message/rfc822");
intentBuilder.setSubject(mainView.getStringFromResources(R.string.app_name));
intentBuilder.setText(mainView.getStringFromResources(R.string.send_message) + " " + mainView.getStringFromResources(R.string.share_text));
intentBuilder.setChooserTitle(R.string.send_title);
intentBuilder.startChooser();
}
@Override
public void showUninstallDialog(AlertDialog.Builder builder) {
View dialogView = mainView.getViewForDialog(R.layout.uninstall_dialog);
builder.setView(dialogView);
CheckBox checkBox = dialogView.findViewById(R.id.checkBox);
checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
mainView.saveCheckedUninstallDialog();
}
}
});
builder.setPositiveButton(android.R.string.ok, null);
builder.create().show();
}
private void initAppsAdapter() {
List<AppInfo> list = appsInteractor.filterApps(currentList, mAppsList);
mainView.onSetToolbarTitle(mainView.getStringFromResources(currentList) + " (" + list.size() + ")");
mainView.onDownloadApps(list, mGetIconThread, currentList);
}
private void setIconTask() {
int memClass = mainView.getMemoryClassFromActivity();
int cacheSize = 1024 * 1024 * memClass / 8;
IconCache iconCache = new IconCache(cacheSize);
Handler responseHandler = new Handler();
mGetIconThread = new GetIconThread<>(responseHandler, mainView.getPackageManagerFromActivity(), iconCache);
mGetIconThread.setIconDownloadListener(
new GetIconThread.GetIconThreadListener<AppsAdapter.AppViewHolder>() {
@Override
public void onIconDownloaded(AppsAdapter.AppViewHolder target, Drawable icon) {
target.bindDrawable(icon);
}
});
mGetIconThread.start();
mGetIconThread.getLooper();
}
}
Для связи презентера с активностью реализуем интерфейс, содержащий в себе методы для возврата результатов работы на View или для запроса необходимых данных.
public interface MainView {
void onDownloadApps(List<AppInfo> mAppsList, GetIconThread<AppsAdapter.AppViewHolder> getIconThread, int currentList);
PackageManager getPackageManagerFromActivity();
int getMemoryClassFromActivity();
void onToastReady(String message);
void onGoneFooter();
void onVisibleFooter();
void onSelectionChanged(List<AppInfo> selectedList);
void onSelectionClear();
void onPrepareDeleteDialog(List<AppInfo> selectedList);
void onUpdatedList(AppInfo ai);
void onSetToolbarTitle(String title);
void onReadyActivityStart(Intent intent);
void onReadyActivityStartForResult(Intent intent);
String getStringFromResources(int id);
boolean isPackageInstalled(String packageName);
View getViewForDialog(int layout);
void saveCheckedUninstallDialog();
}
Запрос различных объектов, таких как PackageManager, происходит на уровне View потому, что Presenter не должен ничего знать о контексте приложения и тем более хранить его у себя. Некоторые разработчики для этих целей советуют хранить статический контекст в отдельном классе Application и запрашивать его оттуда, однако анализатор кода Android Studio настоятельно не рекомендует использовать такой способ, поскольку он приводит к утечкам памяти.
Перенеся логику работы приложения и оставив только пользовательский интерфейс, мы разгрузили код главной активности, сделав его более удобным и читаемым. Итоговый класс MainActivity после преобразований выглядит следующим образом:
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener,
AppsAdapter.ClickListener,
MainView {
private int REQUEST_UNINSTALL = 1;
private SearchView mSearchView;
private AppsAdapter mAppsAdapter;
private LinearLayout footer;
private ProgressBar progressBar;
private RecyclerView recyclerView;
private MainPresenter mainPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
progressBar = findViewById(R.id.progress_spinner);
recyclerView = findViewById(R.id.recyclerView);
mainPresenter = new MainPresenterImpl(new AppsInteractorImpl());
mainPresenter.attachView(this);
mainPresenter.downloadApps(new GetAppsTask(this));
DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer != null) {
ActionBarDrawerToggle toggle =
new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open,
R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();
}
NavigationView navigationView = findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
footer = findViewById(R.id.footer);
Button delete = findViewById(R.id.delete);
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mainPresenter.deleteApp();
}
});
Button clear = findViewById(R.id.clear);
clear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mainPresenter.clearSelection();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
mSearchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
mSearchView.setQueryHint(getString(R.string.search_hint));
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
search(query);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
search(query);
return true;
}
});
return true;
}
@Override
public void onBackPressed() {
DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer != null) {
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
if (footer.getVisibility() == View.VISIBLE) {
mainPresenter.clearSelection();
} else {
super.onBackPressed();
}
}
} else {
if (footer.getVisibility() == View.VISIBLE) {
mainPresenter.clearSelection();
} else {
super.onBackPressed();
}
}
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
mainPresenter.clearSelection();
if (id == R.id.nav_installed) {
mainPresenter.changeList(R.string.menu_installed);
} else if (id == R.id.nav_system) {
mainPresenter.changeList(R.string.menu_system);
checkRootDialog();
} else if (id == R.id.nav_settings) {
mainPresenter.onSettingsClick();
} else if (id == R.id.nav_wiki) {
mainPresenter.onWikiClick();
} else if (id == R.id.nav_share) {
mainPresenter.onShareClick(ShareCompat.IntentBuilder.from(this));
} else if (id == R.id.nav_send) {
mainPresenter.onSendClick(ShareCompat.IntentBuilder.from(this));
} else if (id == R.id.nav_apps) {
mainPresenter.onMarketClick();
}
DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer != null) drawer.closeDrawer(GravityCompat.START);
return true;
}
@Override
public void onDownloadApps(List<AppInfo> mAppsList, GetIconThread<AppsAdapter.AppViewHolder> getIconThread, int currentList) {
setAdapter(getIconThread);
for (AppInfo app : mAppsList) {
mAppsAdapter.addApp(app);
}
mAppsAdapter.notifyDataSetChanged();
}
@Override
public void onSetToolbarTitle(String title) {
setTitle(title);
}
@Override
public boolean isPackageInstalled(String packageName) {
return (Tools.isPackageInstalled(this, packageName));
}
@Override
public void onSelectionClear() {
footer.setVisibility(View.GONE);
mAppsAdapter.clearSelection();
}
@Override
public void onToastReady(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
@Override
public int getMemoryClassFromActivity() {
return ((ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
}
@Override
public PackageManager getPackageManagerFromActivity() {
return getPackageManager();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_UNINSTALL) {
// 0 means success, other means failed.
Log.d("TAG", "got result of uninstall: " + resultCode);
mainPresenter.updateAppList();
}
}
@Override
public void onReadyActivityStart(Intent intent) {
startActivity(intent);
}
@Override
public void onReadyActivityStartForResult(Intent intent) {
startActivityForResult(intent, REQUEST_UNINSTALL);
}
@Override
public void onUpdatedList(AppInfo ai) {
mAppsAdapter.remove(ai);
}
@Override
public void onPrepareDeleteDialog(List<AppInfo> selectedList) {
progressBar.setVisibility(View.VISIBLE);
mSearchView.setVisibility(View.GONE);
for (AppInfo ai : selectedList) {
if (Tools.m521a(ai.publicSourceDir)) mAppsAdapter.remove(ai);
}
progressBar.setVisibility(View.GONE);
mSearchView.setVisibility(View.VISIBLE);
mainPresenter.updateAppList();
if (SP.getBoolean(this, SHOW_UNINSTALL_DIALOG, true)) {
mainPresenter.showUninstallDialog(new AlertDialog.Builder(this));
}
}
@Override
public String getStringFromResources(int id) {
return getString(id);
}
@Override
public View getViewForDialog(int layout) {
LayoutInflater inflater = this.getLayoutInflater();
return inflater.inflate(layout, null);
}
@Override
public void saveCheckedUninstallDialog() {
SP.setBoolean(MainActivity.this, SHOW_UNINSTALL_DIALOG, false);
}
@Override
public void onItemClicked(AppInfo ai) {
mainPresenter.addToSelected(ai);
}
@Override
public void onGoneFooter() {
footer.setVisibility(View.GONE);
}
@Override
public void onVisibleFooter() {
footer.setVisibility(View.VISIBLE);
}
@Override
public void onSelectionChanged(List<AppInfo> selectedList) {
mAppsAdapter.updateSelected(selectedList);
}
@Override
protected void onStop() {
mainPresenter.clearQueue();
super.onStop();
}
@Override
public void onDestroy() {
mainPresenter.detachView();
super.onDestroy();
}
private void checkRootDialog() {
if (!Tools.checkRooted()) {
mainPresenter.prepareRootDialog(new AlertDialog.Builder(this));
}
}
public void setAdapter(GetIconThread<AppsAdapter.AppViewHolder> getIconThread) {
mAppsAdapter = new AppsAdapter(this, getIconThread);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
recyclerView.setAdapter(mAppsAdapter);
}
private void search(String query) {
if (mAppsAdapter != null) mAppsAdapter.filter(query);
}
}
После всех проделанных операций мы разделили код на три уровня, каждый из которых отвечает выполняет определённые задачи. Подводя итоги, можно сделать следующие выводы:
- MainActivity не знает ничего об уровне Model;
- MainActivity не заботится о результате запроса от Presenter;
- Логика callback-ов остаётся в Presenter. Поэтому если вам захочется в будущем её изменить, вам не нужно будет затрагивать View.
Таким образом, используя в своих приложениях паттерн MVP, мы можем сделать разработку намного проще и понятнее.
Исходный код приложения вы можете посмотреть на GitHub, перейдя по следующей ссылке. Само приложение «Менеджер системных приложений» доступно в Google Play здесь.




