Приветствую всех. Хочу рассказать вам историю разработки своего первого мобильного приложения под Android и поделиться различными её деталями. В частности будет рассказано об архитектуре, и используемых инструментах.
Конечно, писать его я начал не сразу, перед этим я несколько месяцев проходил всякие уроки по Android разработке, читал книги, статьи и активно впитывал информацию из других полезных источников. Всех кто собирается (или уже) заняться разработкой под Android хочу обратить внимание на наиболее полезные для меня:
Первые две, вы, скорее всего уже встречали, если интересовались тем, как начать разрабатывать мобильные приложения, тут можно познакомиться с самыми основами разработки под android. Дальше интереснее — androidweekly еженедельная e-mail рассылка с новостями и статьями обо всем интересном, что происходит в мире Android. И далее два очень полезных чата, в первом можно получить ответы на свои вопросы по разработке под android, во втором конкретно про архитектуру. И последняя, но не по важность, ссылка сэмплы от Гугла.
Еще задолго до начала разработки я уже определился с тематикой приложения и взял очень насущную для себя проблему хранение большого количества логинов, паролей и тд.
В Play Market’е куча подобных приложений и я выделил для себя основные недостатки, учел их, и составил список основных особенностей, которые хотел реализовать:
А теперь собственно о моем приложении, чтобы вы могли приблизительно понять уровень приложения, о котором будет идти речь вот структура проекта в Android Studio:
В качестве архитектуры я выбрал MVP, по моему мнению, сейчас это стандарт в Android разработке. Есть еще Clean Architecture, более гибкая еще больше абстракций, отлично подходит для больших проектов. Но для меня это было уже слишком, так как начал я разработку весь код в Activity/Fragment, и только потом переписал под MVP (благо успел вовремя одуматься).
Есть несколько вариантов разделения проекта по пакетам мне ближе всего деление по экранам (фичам). То есть практически все пакеты содержат классы для реализации одного экрана. За исключением нескольких:
Ах да забыл перечислить основные инструменты, и подходы которые я использовал при разработке. Уже упомянутый 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 расположена картинка категории, в которой находится запись, ниже заголовок.
Первой же строкой а onCreate() вызывается метод который в зависимости от пользовательских настроек запрещает или разрешает делать скриншоты экрана.
Через intent получаем id категории, в которой хранится вся информация которую необходимо отобразить на это экране. PasswordMvpController определяет смартфон у нас или планшет и создает соответствующие фрагменты.
В onStop() проверяем, закрывается ли Activity навсегда, и если да то удаляем компонент этого экрана. Еще из интересного callback из фрагмента (о самом фрагменте ниже), в котором устанавливаем картинку и название категории.
В моем случае фрагмент является View и соответственно предоставляет Presenter’у необходимые методы для отображения данных, кроме них во фрагменте ничего интересного нет, да и сами методы достаточно просты.
Список записей реализован с помощью RecyclerView, по статье, которая как раз вовремя появилась в Android Weekly. Благодаря этому подходу адаптер получается очень легким и не перегружен лишней логикой, которая в свою очередь находится в ViewHolder’е.
Теперь о самом интересном – Presenter. В том самом классе PasswordMvpController происходит создание и Inject презентера. Для самого презентара нам нужны: passwordView наш фрагмент, categoryId уникальный идентификатор категории, записи из которой мы хотим показать и dataRepository откуда мы эту категорию по идентификатору и будем получать.
В onResume() фрагмента вызывается метод start() презентера, а уже в нем мы обращаемся к DataRepository и запрашиваем нашу категорию при получении которой говорим view показать ее и записи которые в ней хранятся. В принципе все методы достаточно просты (опять отдадим честь MVP) не вижу смысла их пояснять.
В одну статью уместить все совсем как-то не получилось, совсем мало времени было уделено непосредственно коду. Поэтому если вам интересно пишите свои мнения в комментариях, а я учту их при написании статьи о том, каким образом хранятся данные в приложении, как они шифруются и, в общем, обо всем, что связано с безопасностью (будет много кода).
Подготовка
Конечно, писать его я начал не сразу, перед этим я несколько месяцев проходил всякие уроки по 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:
В качестве архитектуры я выбрал MVP, по моему мнению, сейчас это стандарт в Android разработке. Есть еще Clean Architecture, более гибкая еще больше абстракций, отлично подходит для больших проектов. Но для меня это было уже слишком, так как начал я разработку весь код в Activity/Fragment, и только потом переписал под MVP (благо успел вовремя одуматься).
Есть несколько вариантов разделения проекта по пакетам мне ближе всего деление по экранам (фичам). То есть практически все пакеты содержат классы для реализации одного экрана. За исключением нескольких:
- 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 расположена картинка категории, в которой находится запись, ниже заголовок.
Код PasswordsActivity:
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) не вижу смысла их пояснять.
Заключение
В одну статью уместить все совсем как-то не получилось, совсем мало времени было уделено непосредственно коду. Поэтому если вам интересно пишите свои мнения в комментариях, а я учту их при написании статьи о том, каким образом хранятся данные в приложении, как они шифруются и, в общем, обо всем, что связано с безопасностью (будет много кода).
Поделиться с друзьями
Sleuthhound
Код в статье лучше прятать за спойлер, не все хотят видеть эту портянку.
Чем не угодил KeePassDroid кроме скучных иконок?
Вообще редактировать и каталогизировать пароли на самом телефоне по мне — это ад, самое правильное делать это на ПК, т.к. именно на ПК я провожу большую часть времени и там мне база паролей нужнее, а на телефон сливать эту базу и там её просто смотреть по мере надобности, собственно я так и делаю в KeePassDroid.
alekciy
Поддерживаю. И дополню. Файл базы закидываем в облако в духе дропбокса и имеем всегда актуальную базу на всех своих устройствах.
tridetch
Спрятал код под спойлеры.
Ну там и весь остальной UI просто ужасен. Я хотел дать пользователю полную свободу по настройке записей и категорий. В KeePassDroid, как и во многих других приложения, например нельзя стандартные поля удалять, нельзя по своему усмотрению поля скрывать (только пароль), перекинуть уже созданные записи в другую группу.
Полностью согласен, вот я и хотел сделать это более удобный менеджер паролей именно для смартфона.
Sleuthhound
Тогда ждемс ваш вариант менеджера.
tridetch
Так о нем же и статья! Тут ссылку кинуть не могу, но на первом скриншоте по имени пакета (и окна AndroidStudio) можешь найти в плэй маркете.
QDeathNick
Мне вот хотелось бы более удобный менеджер, но открытость кода и правильная синхронизация — первое что необходимо.
Хранить пароли только на смартфоне не только маловостребовано, но и по моему просто опасно.
tridetch
На счет открытости кода — я подумываю в будущем выложить его в open source.
С синхронизацией сложнее, разрабатывать десктопное приложение точно не вариант (нет времени/опыта), максимум планирую сделать выгрузку/загрузку в csv.