При разработке сложных приложений можно столкнуться с проблемами, которые, вероятно, возникали раньше и уже имеют большое количество решений. Такие решения называются паттернами (шаблонами). Как правило, говорят о паттернах дизайна и паттернах архитектуры. Они упрощают разработку приложений, поэтому целесообразно их использовать, если такая возможность есть.
Дизайн проекта должен быть приоритетной задачей с самого начала разработки. Одна из задач, которые нужно решить в первую очередь – это архитектура проекта, определяющая, как разные элементы нашего приложения будут соотноситься друг с другом.
Хотя 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 здесь.