Приветствую всех. Хочу рассказать вам историю разработки своего первого мобильного приложения под Android и поделиться различными её деталями. В частности будет рассказано об архитектуре, и используемых инструментах.



Подготовка


Конечно, писать его я начал не сразу, перед этим я несколько месяцев проходил всякие уроки по Android разработке, читал книги, статьи и активно впитывал информацию из других полезных источников. Всех кто собирается (или уже) заняться разработкой под Android хочу обратить внимание на наиболее полезные для меня:


Первые две, вы, скорее всего уже встречали, если интересовались тем, как начать разрабатывать мобильные приложения, тут можно познакомиться с самыми основами разработки под android. Дальше интереснее — androidweekly еженедельная e-mail рассылка с новостями и статьями обо всем интересном, что происходит в мире Android. И далее два очень полезных чата, в первом можно получить ответы на свои вопросы по разработке под android, во втором конкретно про архитектуру. И последняя, но не по важность, ссылка сэмплы от Гугла.

О приложении


Еще задолго до начала разработки я уже определился с тематикой приложения и взял очень насущную для себя проблему хранение большого количества логинов, паролей и тд.

В Play Market’е куча подобных приложений и я выделил для себя основные недостатки, учел их, и составил список основных особенностей, которые хотел реализовать:

  • Несколько независимых хранилищ (Аккаунтов). Каждое со своим мастер паролем;
  • Возможность создания неограниченного количества своих категорий (папок) для группировки записей;
  • Возможность создания неограниченного количества полей (Логин, пароль и т.д) для каждой записи, настройка видимости поля и их произвольной сортировки;
  • Большой набор иконок для записей;
  • Возможность быстрого создания новой записи и набора полей для нее;
  • Не имеет доступа в интернет, все данные хранятся только оффлайн в зашифрованном виде на устройстве. Отсюда вытекает, что и речи не идет о бэкэнде и синхронизации в облаке (что я и так не потяну).

Разработка


А теперь собственно о моем приложении, чтобы вы могли приблизительно понять уровень приложения, о котором будет идти речь вот структура проекта в Android Studio:

image alt В качестве архитектуры я выбрал 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) не вижу смысла их пояснять.

Заключение


В одну статью уместить все совсем как-то не получилось, совсем мало времени было уделено непосредственно коду. Поэтому если вам интересно пишите свои мнения в комментариях, а я учту их при написании статьи о том, каким образом хранятся данные в приложении, как они шифруются и, в общем, обо всем, что связано с безопасностью (будет много кода).
Поделиться с друзьями
-->

Комментарии (8)


  1. Sleuthhound
    15.05.2017 20:48
    +3

    Поэтому если вам интересно пишите свои мнения в комментариях, а я учту их при написании статьи о том


    Код в статье лучше прятать за спойлер, не все хотят видеть эту портянку.

    В Play Market’е куча подобных приложений


    Чем не угодил KeePassDroid кроме скучных иконок?

    Вообще редактировать и каталогизировать пароли на самом телефоне по мне — это ад, самое правильное делать это на ПК, т.к. именно на ПК я провожу большую часть времени и там мне база паролей нужнее, а на телефон сливать эту базу и там её просто смотреть по мере надобности, собственно я так и делаю в KeePassDroid.


    1. alekciy
      16.05.2017 07:50

      Поддерживаю. И дополню. Файл базы закидываем в облако в духе дропбокса и имеем всегда актуальную базу на всех своих устройствах.


    1. tridetch
      16.05.2017 11:21

      Спрятал код под спойлеры.

      Чем не угодил KeePassDroid кроме скучных иконок?

      Ну там и весь остальной UI просто ужасен. Я хотел дать пользователю полную свободу по настройке записей и категорий. В KeePassDroid, как и во многих других приложения, например нельзя стандартные поля удалять, нельзя по своему усмотрению поля скрывать (только пароль), перекинуть уже созданные записи в другую группу.

      Вообще редактировать и каталогизировать пароли на самом телефоне по мне — это ад

      Полностью согласен, вот я и хотел сделать это более удобный менеджер паролей именно для смартфона.


      1. Sleuthhound
        16.05.2017 11:58

        Полностью согласен, вот я и хотел сделать это более удобный менеджер паролей именно для смартфона.


        Тогда ждемс ваш вариант менеджера.


        1. tridetch
          16.05.2017 14:07

          Так о нем же и статья! Тут ссылку кинуть не могу, но на первом скриншоте по имени пакета (и окна AndroidStudio) можешь найти в плэй маркете.


          1. QDeathNick
            16.05.2017 17:04

            Мне вот хотелось бы более удобный менеджер, но открытость кода и правильная синхронизация — первое что необходимо.
            Хранить пароли только на смартфоне не только маловостребовано, но и по моему просто опасно.


            1. tridetch
              16.05.2017 21:04
              -1

              На счет открытости кода — я подумываю в будущем выложить его в open source.
              С синхронизацией сложнее, разрабатывать десктопное приложение точно не вариант (нет времени/опыта), максимум планирую сделать выгрузку/загрузку в csv.


  1. SparkleMan
    16.05.2017 14:40

    Весьма интересно увидеть следующую статью про безопасность)