Подготовка
Конечно, писать его я начал не сразу, перед этим я несколько месяцев проходил всякие уроки по Android разработке, читал книги, статьи и активно впитывал информацию из других полезных источников. Всех кто собирается (или уже) заняться разработкой под Android хочу обратить внимание на наиболее полезные для меня:
- startandroid.ru
- developer.alexanderklimov.ru
- androidweekly.net
- androiddev.apptractor.ru
- t.me/android_ru
- t.me/Android_Architecture
- github.com/googlesamples
Первые две, вы, скорее всего уже встречали, если интересовались тем, как начать разрабатывать мобильные приложения, тут можно познакомиться с самыми основами разработки под android. Дальше интереснее — androidweekly еженедельная e-mail рассылка с новостями и статьями обо всем интересном, что происходит в мире Android. И далее два очень полезных чата, в первом можно получить ответы на свои вопросы по разработке под android, во втором конкретно про архитектуру. И последняя, но не по важность, ссылка сэмплы от Гугла.
О приложении
Еще задолго до начала разработки я уже определился с тематикой приложения и взял очень насущную для себя проблему хранение большого количества логинов, паролей и тд.
В Play Market’е куча подобных приложений и я выделил для себя основные недостатки, учел их, и составил список основных особенностей, которые хотел реализовать:
- Несколько независимых хранилищ (Аккаунтов). Каждое со своим мастер паролем;
- Возможность создания неограниченного количества своих категорий (папок) для группировки записей;
- Возможность создания неограниченного количества полей (Логин, пароль и т.д) для каждой записи, настройка видимости поля и их произвольной сортировки;
- Большой набор иконок для записей;
- Возможность быстрого создания новой записи и набора полей для нее;
- Не имеет доступа в интернет, все данные хранятся только оффлайн в зашифрованном виде на устройстве. Отсюда вытекает, что и речи не идет о бэкэнде и синхронизации в облаке (что я и так не потяну).
Разработка
А теперь собственно о моем приложении, чтобы вы могли приблизительно понять уровень приложения, о котором будет идти речь вот структура проекта в Android Studio:
Есть несколько вариантов разделения проекта по пакетам мне ближе всего деление по экранам (фичам). То есть практически все пакеты содержат классы для реализации одного экрана. За исключением нескольких:
- base — базовые классы для активити, фрагментов, презентеров
- data – классы репозиториев
- global _di – основной ApplicationComponent для инъекций зависимостей, скоупы
- model – pojo классы бизнес-сущностей (Категория, записть, поле и т.д)
- util – ну тут просто вспомогательные классы, которые не вписались в другие пакеты
Ах да забыл перечислить основные инструменты, и подходы которые я использовал при разработке. Уже упомянутый MVP. Dependency Injection (DI), реализованный с помощью Dagger 2 наверно тоже стандарт для Android разработки. Где MVP и DI там, конечно же, и юнит тестирование. Юнит тестами покрыта вся логика в презентерах пакет model и частично data. Система контроля версий (VCS) еще один must have, опять-таки даже pet project’ы я бы не стал разрабатывать без нее. Я использую Git, сначала использовал GitHub но когда пришло время задуматься о приватном репозитории мигрировал на Bitbucket, происходит все в несколько кликов с сохранением истории всех коммитов.
Для тех кто жаждет хоть какого то кода, ниже вкратце о том как реализован экран списка паролей в приложении. Вот как он выглядит в итоге:

Классы пакета passwords:
В качестве разметки используется практически стандартный scrolling activity который Android Studio создает сама.
На весь размер CollapsingToolbarLayout расположена картинка категории, в которой находится запись, ниже заголовок.
public class PasswordsActivity extends AppCompatActivity implements PasswordsFragment.OnPasswordListFragmentInteractionListener, PasswordFieldsFragment.OnPasswordFragmentInteractionListener { PasswordsContract.Presenter mPasswordsPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityUtils.enableScreenshots(this); setContentView(R.layout.activity_scrolling_passwords_list); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mPasswordsPresenter.onAddNewPasswordButtonClick(); } }); //Open passwords of category with that id String categoryId = getIntent().getStringExtra(PasswordsFragment.ARG_EXTRA_CATEGORY_ID); //In tablet mode open details of password with id String passwordId = getIntent().getStringExtra(PasswordFieldsFragment.ARG_EXTRA_PASSWORD_ID); PasswordsMvpController.createPasswordsViews(this, categoryId, passwordId); } @Override protected void onStop() { super.onStop(); if (isFinishing()) { Injector injector = Injector.getInstance(); injector.destroyPasswordsComponent(); if (ActivityUtils.isTablet(this)) { injector.destroyPasswordFieldsComponent(); } } } public void setPasswordsPresenter(PasswordsContract.Presenter passwordsPresenter) { mPasswordsPresenter = passwordsPresenter; } /** * Methods implemented from {@link PasswordsFragment.OnPasswordListFragmentInteractionListener} * * @param parentCategory category that must be displayed */ @Override public void showCategoryInfo(Category parentCategory) { setTitle(parentCategory.getTitle()); CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout); VectorDrawableCompat vectorDrawableCompat = VectorDrawableCompat.create(getResources(), parentCategory.getPicture().getResId(), getTheme()); collapsingToolbarLayout.setContentScrim(vectorDrawableCompat); ImageView categoryImageView = (ImageView) findViewById(R.id.category_picture_imageView); categoryImageView.setImageResource(parentCategory.getPicture().getResId()); }
Первой же строкой а onCreate() вызывается метод который в зависимости от пользовательских настроек запрещает или разрешает делать скриншоты экрана.
Через intent получаем id категории, в которой хранится вся информация которую необходимо отобразить на это экране. PasswordMvpController определяет смартфон у нас или планшет и создает соответствующие фрагменты.
В onStop() проверяем, закрывается ли Activity навсегда, и если да то удаляем компонент этого экрана. Еще из интересного callback из фрагмента (о самом фрагменте ниже), в котором устанавливаем картинку и название категории.
В моем случае фрагмент является View и соответственно предоставляет Presenter’у необходимые методы для отображения данных, кроме них во фрагменте ничего интересного нет, да и сами методы достаточно просты.
Список записей реализован с помощью RecyclerView, по статье, которая как раз вовремя появилась в Android Weekly. Благодаря этому подходу адаптер получается очень легким и не перегружен лишней логикой, которая в свою очередь находится в ViewHolder’е.
Теперь о самом интересном – Presenter. В том самом классе PasswordMvpController происходит создание и Inject презентера. Для самого презентара нам нужны: passwordView наш фрагмент, categoryId уникальный идентификатор категории, записи из которой мы хотим показать и dataRepository откуда мы эту категорию по идентификатору и будем получать.
public class PasswordsPresenter implements PasswordsContract.Presenter { private final DataRepository mDataRepository; private PasswordsContract.View mPasswordsView; // On tablets we can have null categoryId when @Nullable String mCategoryId; Category mCurrentCategory; PasswordsPresenter(DataRepository dataRepository, PasswordsContract.View passwordsView, @CategoryId @Nullable String categoryId) { mDataRepository = dataRepository; mPasswordsView = passwordsView; mCategoryId = categoryId; } public void setupListeners(PasswordsContract.View passwordsView) { mPasswordsView = passwordsView; mPasswordsView.setPresenter(this); } /* * Methods implemented from {@Link PasswordsContract.Presenter} * */ @Override public void start() { if (Strings.isNullOrEmpty(mCategoryId)) { // TODO: 25.02.2017 on tablets we can have null category id when don't pick any category, need to show message about it return; } mDataRepository.getCategory(mCategoryId, new DataSource.LoadCategoryCallback() { @Override public void onCategoryLoaded(Category category) { mCurrentCategory = category; mPasswordsView.showPasswordsInList(category.mPasswords); mPasswordsView.showCategoryInfo(mCurrentCategory); } @Override public void onDataNotAvailable() { //RecycleView show empty data message by itself } }); } @Override public void onPasswordClick(Password password) { mPasswordsView.showDetailPasswordUi(password); } @Override public void onAddNewPasswordButtonClick() { mPasswordsView.showNewPasswordUi(mCategoryId); } @Override public void onSwapPasswords(int firstPosition, int secondPosition) { mDataRepository.swapPasswords(mCurrentCategory, firstPosition, secondPosition); mPasswordsView.swapPasswordInList(firstPosition, secondPosition); } @Override public void loadCategory(Category category) { mCurrentCategory = category; mPasswordsView.showCategoryInfo(category); mPasswordsView.showPasswordsInList(category.mPasswords); } /* * Helper methods that can be called from {@Link CategoriesTabletPresenter} to manipulate view in tablet mode * */ public void onCurrentCategoryDeleted() { mPasswordsView.showEmptyUi(); } }
В onResume() фрагмента вызывается метод start() презентера, а уже в нем мы обращаемся к DataRepository и запрашиваем нашу категорию при получении которой говорим view показать ее и записи которые в ней хранятся. В принципе все методы достаточно просты (опять отдадим честь MVP) не вижу смысла их пояснять.
Заключение
В одну статью уместить все совсем как-то не получилось, совсем мало времени было уделено непосредственно коду. Поэтому если вам интересно пишите свои мнения в комментариях, а я учту их при написании статьи о том, каким образом хранятся данные в приложении, как они шифруются и, в общем, обо всем, что связано с безопасностью (будет много кода).
