РАЗРАБОТКА МОБИЛЬНОГО ПРИЛОЖЕНИЯ, ВЗАИМОДЕЙСТВУЮЩЕГО С СЕРВЕРОМ, С ИСПОЛЬЗОВАНИЕМ АРХИТЕКТУРНЫХ КОМПОНЕНТОВ ОПЕРАЦИОННОЙ СИСТЕМЫ ANDORID И CUSTOM VIEW

Научная статья
DOI:
https://doi.org/10.23670/IRJ.2020.93.3.002
Выпуск: № 3 (93), 2020
Опубликована:
2020/03/17
PDF

РАЗРАБОТКА МОБИЛЬНОГО ПРИЛОЖЕНИЯ, ВЗАИМОДЕЙСТВУЮЩЕГО С СЕРВЕРОМ, С ИСПОЛЬЗОВАНИЕМ АРХИТЕКТУРНЫХ КОМПОНЕНТОВ ОПЕРАЦИОННОЙ СИСТЕМЫ ANDORID И CUSTOM VIEW

Обзор

Ахмаров И.О. *

ORCID: 0000-0001-7413-9919,

ПАО Сбербанк, Москва, Россия

* Корреспондирующий автор (akhmarov9774[at]gmail.com)

Аннотация

В текущей статье описываются правила и методы применения инструментов разработки в процессе создания мобильного приложения на операционной системе Android. Приложение разработано по принципу «чистой архитектуры» с использованием архитектурных компонентов (Android Architecture Components) – группы библиотек, представленных Google. Такой подход помогает создавать надежные, тестируемые и легкие в поддержке приложения, начиная от управления жизненными циклами элементов приложения до управления внутренними данными. Данное приложение предназначено для перевода текстов и хранения сохраненных переводов в локальной базе данных устройства. Для перевода введенного текста приложение должно уметь выполнять запросы к серверу, используя API (Application Programming Interface) «Яндекс.Переводчика», с возможностью выбора языков перевода.

Ключевые слова: мобильное приложение, Android, чистая архитектура, Android Architecture Components, Яндекс.Переводчик.

DEVELOPING MOBILE APPLICATION INTERACTING WITH SERVER WITH THE USE OF ARCHITECTURAL COMPONENTS OF ANDROID AND CUSTOM VIEW OPERATING SYSTEM

Review

Akhmarov I.O. *

ORCID: 0000-0001-7413-9919,

Sberbank PJSC, Moscow, Russia

* Corresponding author (akhmarov9774[at]gmail.com)

Abstract

This paper describes the rules and methods for the usage of tools in the mobile application development process on the Android operating system. The application was developed on the clean architecture principle using architectural components (Android Architecture Components) – a group of libraries provided by Google. This approach helps to create robust, testable, and easy-to-maintain applications, ranging from managing the life cycles of application elements to managing internal data. This application is intended to translate texts and store saved translations in the local database of a device. To translate the entered text, the application must be able to fulfill the requests to the server using the Yandex.Translator API (Application Programming Interface), with the ability to select the translation languages.

Keywords: mobile application, Android, clean architecture, Android Architecture Components, Yandex.Translator.

Введение

В настоящее время мобильные устройства играют невероятно большую роль в жизни людей: они не ограничиваются функциями приема и совершения вызовов и стали больше чем просто инструментами связи. С их помощью люди слушают музыку, смотрят видео, делают снимки. Телефон стал неотъемлемой частью жизни людей, и именно с этим связано увеличение темпов развития в разработке программ под мобильные платформы. В связи с этим также увеличивается и количество инструментов разработки, которые облегчают разработку и поддержку программ, однако не всегда применяются так, как было задумано. В данной статье были описаны методы применения наиболее востребованных инструментов в создании мобильных приложений. Самые популярные мобильные операционные системы это Android и iOS. Так как Android занимает большую долю мирового рынка – 74,13% против 24,79% у iOS [1], то в статье будет рассматриваться разработка приложения под эту ОС.

В связи с увеличением количества инструментов для разработки, становится трудно уследить за всеми ними и тем, как правильно их применять в реальных проектах

В качестве разрабатываемого приложения будет выступать Language Cards, идея которого заключается в переводе текста и сохранении ранее переведенных слов в локальную базу данных устройства. Для перевода текста приложение используется API «Яндекс.Переводчик».

Архитектурные компоненты

Архитектурные компоненты Android – группа библиотек, помогающие писать тестируемые и поддерживаемые приложения, начиная от классов управления данными UI компонентов до обработки данных. В архитектурные компоненты входят:

· Lifecycles Жизненный цикл – состояния компонента (активити или фрагмента) от момента его создания до его уничтожения. Lifecycles позволяет создавать объекты, знающие о жизненном цикле компоненты-владельца. Это знание позволяет им управлять их собственным жизненным циклом, снижая вероятность утечек или сбоев. Lifecycles позволяет избежать лишнего кода и является основой для других архитектурных компонентов [2]. · LiveData Сущность-наблюдатель, обладающая знаниями про жизненный цикл, которая также может хранить данные и уведомляет слушателей о изменениях. Визуальный интерфейс подписывается на эти изменения и предоставляет LiveData ссылку на свой жизненный цикл. Так как LiveData знает про жизненный цикл, она уведомляет подписчиков пока он запускается или возобновляется, но перестает если жизненный цикл был уничтожен. LiveData простой способ для создания реактивного UI, который безопасней и более производителен [3]. · ViewModel

Библиотека для поддержки шаблона проектирования архитектуры MVVM (Model-View-ViewModel), суть которого в разделении отображения данных от бизнес-логики путем вынесения её в отдельный класс для более четкого разграничения.  ViewModel сохраняется до тех пор, пока связанная с ним активити или фрагмент не была уничтожена – это позволяет данным отображения переживать события как, например, пересоздание фрагмента из-за поворота экрана. ViewModel помогает устранить не только проблемы связанные с жизненным циклом приложения, но и создавать более поддерживаемое и простое в тестировании приложение [4].

· Rooм

Библиотека является высокоуровневой оболочкой над SQLite, что позволяет писать меньше шаблонного кода. Room довольно простой мощный и надежный инструмент для управления локальным хранилищем [5].

Разработка приложения

Применение архитектурных компонентов

Lifecycles

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

Данный класс описывает общую логику работы фрагментов, расширяет класс Fragment и реализует интерфейс LifecycleOwner. Реализация данного интерфейса помечает класс, как класс, обладающий жизненным циклом и содержит всего один метод getLifecycle(), который возвращает жизненный цикл реализующего класса типа Lifecycle. Метод уже определен в базовых классах ComponentActivity и Fragment и не должен быть переопределен в собственных активити или фрагментах, для этого планируется сделать его final в будущем.

Другой интерфейс – LifecycleObserver помечает реализующие его классы, как классы, наблюдающие за жизненным циклом. Этот интерфейс не содержит никаких методов, но полагается на методы аннотированные @OnLifecycleEvent.

Для применения архитектурного компонента Lifecycles необходимо зарегистрировать реализацию подписчика в классе-владельце жизненного цикла с помощью метода addObserver() у объекта Lifecycle, который можно получить с помощью метода getLifecycle().

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

@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { CreatingCardDataBinding binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, false); readArguments(); mViewModel = ViewModelProviders.of(this, new ViewModelFactory<>( () -> new CreatingCardViewModel( new TranslateInteractor(), mCardInteractor, new TranslateSettingInteractor(requireActivity())) )) .get(CreatingCardViewModel.class); binding.setVariable(com.igor.langugecards.BR.viewModel, mViewModel); binding.setLifecycleOwner(getViewLifecycleOwner()); getLifecycle().addObserver(mViewModel);    //добавление подписчика return binding.getRoot(); } Листинг 1 – Добавление подписчика на изменение жизненного цикла фрагмента В листинге 2 представлен метод ViewModel, который должен быть вызван. @OnLifecycleEvent(Lifecycle.Event.ON_START) public void readTranslateSettings() { TranslateSettings translateSettings = mTranslateSettingInteractor.readTranslateSettings(); mFromLanguageCode = translateSettings.getLanguageCodeFrom(); mFromLanguage.postValue(translateSettings.getLanguageFrom()); mToLanguageCode = translateSettings.getLanguageCodeTo(); mToLanguage.postValue(translateSettings.getLanguageTo()); mTranslateSettings.postValue(translateSettings); } Листинг 2 – Метод ViewModel, вызываемый при событии onStart жизненного цикла фрагмента Параметр аннотации метода означает что метод будет вызван при срабатывании метода onStart() у владельца жизненного цикла. При этом класс подписчик должен реализовывать интерфейс LifecycleObserver.

LiveData & ViewModel

Эти архитектурные компоненты используются вместе: класс, расширяющий ViewModel описывает некоторую логику и результат операций записывает в переменную типа LiveData, являющейся оберткой над нужным типом. Результат записывается с помощью методов postValue() и setValue(). Отличия этих методов в том, что setValue() устанавливает значение и должен быть вызван из главного потока, в то время как postValue() передает главному потоку задачу на установку значения. Код в активити или фрагменте подписывается на изменения значения внутри LiveData, как результат ViewModel неявно управляет View.

В приложении этот механизм подписок присутствует на каждом экране. Во ViewModel создаются LiveData по типу нужного параметра. В листинге 3 приведен пример событий обновлений настроек перевода и статуса операции, обернутых в LiveData.

private MutableLiveData<TranslateSettings> mTranslateSettings = new MutableLiveData<>(); private MutableLiveData<Integer> mOperationStatusEvent = new MutableLiveData<>();

Листинг 3 – Объявления событий ViewModel изменения настроек перевода и статуса операции

Фрагмент получает события в LiveData и подписывается на них. К примеру, в листинге 4 при изменении настроек перевода будет вызван метод, обновляющий отображение этих настроек с передачей данных, лежащих внутри LiveData (листинг 5).

@Override protected void setUpViews() { mViewModel.getTranslateSettings().observe(this, this::showSettings); mViewModel.getOperationStatusEvent().observe(this, this::showMessage); mFromLanguageTextView.setOnClickListener(v -> openLanguagesFragment()); mToLanguageTextView.setOnClickListener(v -> openLanguagesFragment()); mTranslateArrow.setOnClickListener(v -> openLanguagesFragment()); } Листинг 4 – Подписка на события ViewModel во фрагменте private void showSettings(TranslateSettings settings) { mFromLanguageTextView.setText(settings.getLanguageFrom()); mToLanguageTextView.setText(settings.getLanguageTo()); } Листинг 5 – Применение изменений настроек перевода Инициализация класса ViewModel осуществляется с помощью метода of() класса ViewModelProviders (листинг 6). mViewModel = ViewModelProviders.of(this, new ViewModelFactory<>( () -> new CreatingCardViewModel( new TranslateInteractor(), mCardInteractor, new TranslateSettingInteractor(requireActivity())) )) .get(CreatingCardViewModel.class);

Листинг 6 – Инициализация ViewModel фрагмента

Данный метод возвращает экземпляр ViewModelProvider, класса, предоставляющего ViewModel для требуемой области видимости (масштаба фрагмента). Первый параметр метода of() – фрагмент, в чьей области видимости будет удержана ViewModel, второй параметр – фабрика для создания ViewModel. В качестве такой фабрики выступает класс ViewModelFactory, позволяющий получать экземпляры класса с помощью лямбда выражений (листинг 7).

public class ViewModelFactory<VM extends ViewModel> implements ViewModelProvider.Factory { private Supplier<VM> mViewModelCreator; public ViewModelFactory(@NonNull Supplier<VM> viewModelCreator) { mViewModelCreator = viewModelCreator; } @NonNull @Override public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { VM viewModel = mViewModelCreator.get(); //noinspection unchecked return (T) viewModel; } } Листинг 7 – Класс ViewModelFactory

Класс реализует интерфейс Factory и содержит единственное поле типа Supplier с типизированным параметром, наследующимся от класса ViewModel. Интерфейс Supplier является простой оберткой для хранения данных, который содержит только один метод get(), который возвращает хранимый экземпляр. Метод create() является частью интерфейса Factory и создает экземпляр переданного класса.

Room

Для того чтобы использовать этот компонент в своем проекте нужно написать абстрактный класс базы данных, унаследовав его от RoomDatabase (листинг 8). Класс помечается аннотацией @Database В параметрах аннотации прописывается сущности, которые будут храниться в базе данных, и версия базы данных. Параметр exportSchema означает, что историю версий базы данных сохранять не нужно. Для получения экземпляра базы данных в метод databaseBuilder() необходимо передать контекст, класс базы данных и название файла базы данных, далее нужно вызвать метод build().

@Database(entities = {Card.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public static AppDatabase getInstance(@NonNull Context context) { return Room .databaseBuilder(context, AppDatabase.class, "AppDatabase") .build(); } public abstract CardInteractor getCardInteractor(); } Листинг 8 – Класс базы данных приложения

Для сформирования методов доступа к базе данных, нужно определить DAO (Data Access Object) интерфейс. В текущем проекте этот интерфейс определяется классом CardInteractor (листинг 9).

@Dao interface CardInteractor { @Insert(onConflict = OnConflictStrategy.REPLACE) fun addCard(card: Card) @Delete fun deleteCard(card: Card) @Query(value = "DELETE FROM Card WHERE mId == :cardId") fun deleteCardById(cardId: Long) @Query(value = "SELECT * FROM Card") fun getAllCards(): Observable<MutableList<Card>> @Query(value = "SELECT * FROM Card WHERE mTheme == :theme") fun getCardsByTheme(theme: String): Observable<MutableList<Card>> @Query(value = "SELECT * FROM Card WHERE mId == :id") fun getCardById(id: Long): Observable<Card> } Листинг 9 – DAO сущность для взаимодействия с базой данных

Интерфейс помечается аннотацией @Dao и содержит методы для добавления перевода в базу данных, получению и удалению карточки из базы данных и 2 метода для получения списка карточек: получить все карточки и получить карточки, соответствующие данной теме. Над каждым методом присутствует аннотации библиотеки Room:

· @Insert

Аннотация помечает метод, который добавляет элемент в базу данных. В параметрах аннотации может быть указана стратегия обработки конфликтов. В данном коде, в случае существования добавляемого элемента в базе данных, старый элемент будет замещен новым.

· @Delete

Аннотация ставится над методом, который добавляет элемент в базу данных.

· @Query

Аннотация означает, что в случае вызова текущего метода будет выполнен запрос к базе данных. Тело запроса в формате SQLite указывается в параметрах аннотации.

Возвращаемое значение всех методов получения карточек с переводом обернуто в Observable. Это позволяет использовать Room совместно с RxJava [6], что позволяет оперировать запросами к базе данных асинхронно.

Взаимодействие с сервером Перевод текста в приложении разбит на несколько классов в соответствии с идеей чистой архитектурой [7], запросы к серверу осуществляются с помощью библиотеки Retrofit. Всего к серверу будут отправляться 2 запроса: перевод текста и получение списка языков, на которые возможен перевод. Описание этих запросов представлено в интерфейсе TranslatorApi (листинг 10). public interface TranslatorApi { @POST("tr.json/translate") Observable<Translate> getTranslate(@Query("key") String key, @Query("text") String text, @Query("lang") String lang); @POST("tr.json/getLangs") Observable<TranslateLanguages> getLangs(@Query("key") String key, @Query("ui") String languageCode); } Листинг 10 – Интерфейс TranslatorApi

Оба метода содержат аннотацию @POST, который обозначает метод как метод, делающий запрос типа POST [8]. В параметрах аннотации передается адрес запроса. Каждый из методов принимает аргументы для формирования запроса, так, например, первый метод принимает на вход ключ (уникальный идентификатор для каждого клиента), переводимый текст и направление перевода (с какого языка на какой будет переведен текст). Подробнее о структуре запросов к Яндекс API, можно ознакомиться в документации Яндекс Переводчика для разработчиков [9].

Класс NetworkService построен по паттерну «Одиночка» (листинг 11) и формирует общую структуру запроса с помощью Retrofit: подставляет в запрос базовый адрес, фабрику конвертации (для сериализации и десериализации) объектов и фабрику для оборачивания полученного от сервера результата в тип RxJava (для работы с сервером асинхронно), а также класс содержит поле уникального ключа для работы с Яндекс API.

public class NetworkService { public static final String KEY = "ЗДЕСЬ ДОЛЖЕН БЫТЬ УНИКАЛЬНЫЙ КЛЮЧ"; private static NetworkService mInstance; private static final String BASE_URL = "https://translate.yandex.net/api/v1.5/"; private Retrofit mRetrofit; private NetworkService() { mRetrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) .build(); } public static NetworkService getInstance() { if (mInstance == null) { mInstance = new NetworkService(); } return mInstance; } public TranslatorApi getJSONApi() { return mRetrofit.create(TranslatorApi.class); } } Листинг 11 – Класс NetworkService Метод getJSONApi() возвращает с помощью Retrofit реализацию интерфейса TranslatorApi. Вызов данного метода происходит из репозитория (листинг 12). public class TranslateRepositoryImpl implements TranslateRepository { private TranslatorApi mTranslatorApi = NetworkService.getInstance().getJSONApi(); private TranslateLanguages mLanguages; @Override public Observable<Translate> getTranslate(@NonNull TranslatorRequest request) { return mTranslatorApi .getTranslate(NetworkService.KEY, request.getText(), request.getTranslationDirection()); } @Override public Observable<TranslateLanguages> getLanguages() { if (mLanguages == null) { return mTranslatorApi .getLangs(NetworkService.KEY, "ru") .map(langs -> mLanguages = langs); } else { return Observable.just(mLanguages); } } } Листинг 12 – Репозиторий для запросов к серверу Класс TranslateRepositoryImpl реализует интерфейс TranslateRepository и является репозиторием для запросов к серверу. Этот класс определяет реализацию 2-методов на получение перевода и списка языков, на которые доступен перевод. Данные методы вызываются в соответствующих классах-интеракторах и возвращают результаты запросов, обернутые в тип Observable для интеграции с библиотекой RxJava. public class TranslateInteractor { private TranslateRepository mRepository = new TranslateRepositoryImpl(); public Observable<Translate> translate(@NonNull String text, @NonNull String from, @NonNull String to) { if (!text.isEmpty() && !to.isEmpty()) { TranslatorRequest request = TranslatorRequest.createRequest(text, from, to); return mRepository.getTranslate(request); } else { return Observable.error(new Throwable("Text or target language must not be empty")); } } }

Листинг 13 – Интерактор для запроса перевода текста

Класс TranslateInteractor (листинг 13) – интерактор для перевода текста, хранит в себе ссылку на репозиторий и содержит всего один метод. Метод translate() принимает 3 параметра: переводимый текст, начальный язык (язык с которого осуществляется перевод) и конечный язык (язык на который осуществляется перевод). На основе этих данных формируется запрос: преобразование информации в формат, требуемый сервером, однако если языки перевода содержат пустую строку, то будет выброшен объект Throwable, сигнализирующий об ошибке.

Данный метод вызывается не при каждом изменении значения текстового поля ввода текста для перевода, так как в случае, если пользователь печатает длинное слово, то запрос будет ходить каждый раз, при вводе новой буквы. Это значит, что ресурсы будут тратиться на операции, результат которых не важен. Для предотвращения этой проблемы было введено новое поле типа BehaviorSubject во ViewModel экрана перевода с типовым параметром String (листинг 14).

private BehaviorSubject<String> mUserInputSubject = BehaviorSubject.create();

Листинг 14 – Объявление поля для подписки на изменение переводимого текста

Далее необходимо задать цепочку действий для данного объекта при получении значения для перевода текста (листинг 15). С помощью первого параметра метода debounce() задаем время между срабатываниями запроса на перевод текста, а вторым – единицу времени исчисления. Метод distictUntilChanged() пропускает только те элементы, которые отличаются от своих предшественников (отсеивает идентичные строки в поле ввода текста для перевода), что тоже позволяет экономить ресурсы, не формируя ненужные запросы.

mDisposable.add(mUserInputSubject .debounce(TRANSLATE_DEBOUNCE_TIME, TimeUnit.MILLISECONDS) .distinctUntilChanged() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::translate) );

Листинг 15 – Цепочка действий, выполняемых перед формированием запроса к серверу

При изменении текстового поля ввода текста для перевода, в случае если новая строка пуста, то текстовому полю, содержащему переведенный текст, присваивается значение пустой строки, иначе – метод onNext() у поля BehaviorSubject, который провоцирует перевод текста (листинг 16).

public void onTranslatedWordChanged(@NonNull CharSequence text) { if (text.toString().isEmpty()) { mTranslate.postValue(EMPTY_STRING); } else { mUserInputSubject.onNext(text.toString()); } } Листинг 16 – Метод, вызываемый при изменении текста для перевода Custom View

Экран запоминания слов содержит уникальный элемент отображения – LanguageCardView (рисунок 1). Данный элемент содержит в себе распознавание и обработку применяемых жестов, анимацию и отображение. Текстовые поля и индикатор прогресса не являются частью элемента – они отображены поверх него. Изначально планировалось сделать этот элемент полностью уникальным (содержащим отображения всех дочерних элементов, за исключением индикатора прогресса) и наследовать класс представления от CardView, однако позже выявились проблемы с анимацией для элемента CardView – при повороте элемента на 90 градусов начиналась утечка памяти, связанная с нативными ресурсами (ресурсами операционной системы). При решении этой проблемы все уникальные дочерние элементы были заменены на аналогичные элементы, предоставляемые системой Андроид, и был изменен класс родитель. Для соблюдения связи «элемент-дочерний элемент» между представлениями экрана, в качестве класса родителя LanguageCardView должен выступать класс типа ViewGroup [10], и в роли такого класса был выбран ConstraintLayout из-за гибкости размещения дочерних элементов внутри родительского.

24-03-2020 12-06-03

Рис. 1 – Название рисунка

 

Распознавание жестов и анимация

Обработка жестов работает за счет класса GestureListener, реализующего интерфейс OnGestureListener и использующего класс GestureDetectorCompat. Для этого в классе LanguageCardView был переопределен метод onTouchEvent() (листинг 17), который запускает одноименный метод в классе GestureListener (листинг 18), в котором происходит вызов одноменного метода класса GestureDetectorCompat.

override fun onTouchEvent(event: MotionEvent): Boolean { performClick() return if (gestureListener.onTouchEvent(event)) { true } else { super.onTouchEvent(event) } }

Листинг 17 – Метод, вызываемый при касании LanguageCardView fun onTouchEvent(event: MotionEvent): Boolean { return detector.onTouchEvent(event) } Листинг 18 – Метод GestureListener, вызываемый при касании При определении жеста экземпляр класса GestureDetectorCompat выполняет вызов метода, который должен обработать данный жест. override fun onShowPress(e: MotionEvent?) { Log.d("CardView", "onShowPress") } override fun onSingleTapUp(e: MotionEvent?): Boolean { Log.d("CardView", "onSingleTapUp") return false } override fun onDown(e: MotionEvent?): Boolean { Log.d("CardView", "onDown") return true } override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { Log.d("CardView", "onFling") return false } override fun onLongPress(e: MotionEvent?) { Log.d("CardView", "onLongPress") } override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean { return cardView.onScroll(e1, e2) }

Листинг 19 – Переопределение методов интерфейса OnGestureListener в классе GestureListener

Как видно из листинга 19, класс «глушит» все методы, за исключением методов onDown() и onScroll(). Метод onDown() сигнализирует о событии когда произошел жест «вниз» (пользователь коснулся элемента на экране) и необходим для возможности обработки других жестов. Метод onScroll() вызывается в том случае, если жест был распознан как «смахивание» (пользователь коснулся элемента и провел по нему). При распознавании этого типа жеста будет вызван одноименный метод в классе LanguageCardView (листинг 20).

fun onScroll(firstMotionEvent: MotionEvent?, secondMotionEvent: MotionEvent?): Boolean { Log.d("CardView", "onScroll") if (animationIsRunning) { return false } val direction = gestureListener.getSwipeDirection(firstMotionEvent, secondMotionEvent) Log.d("Direction", "Direction: $direction") return when (direction) { SwipeDirection.LEFT -> { animate().setDuration(FLIPPING_ANIMATION_DURATION) .rotationYBy(SWIPE_TO_LEFT_DEGREE) .start() flipAnimation = true flipRTL = true true } SwipeDirection.RIGHT -> { animate().setDuration(FLIPPING_ANIMATION_DURATION) .rotationYBy(SWIPE_TO_RIGHT_DEGREE) .start() flipAnimation = true flipLTR = true true } SwipeDirection.UP -> { animate() .yBy(-requiredBottomPosition) .setDuration(SCROLLING_ANIMATION_DURATION) .start() scrollAnimation = true scrollUp = true true } SwipeDirection.DOWN -> { animate() .yBy(requiredBottomPosition) .setDuration(SCROLLING_ANIMATION_DURATION) .start() scrollAnimation = true scrollDown = true true } else -> false } } Листинг 20 – Метод LanguageCardView, вызываемый при смахивании

Данный метод проверяет запущена ли в данный момент анимация, и если анимация запущена, то метод завершает работу, возвращая значение false, иначе вызывает метод getSwipeDirection() класса GestureListener, и в зависимости от результата стартует один из видов анимации. Проверка на работающую анимацию нужна для того, чтобы анимации не накладывались друг на друга и к объекту не были применены одновременно несколько видов анимаций. В данном методе, к элементу LanguageCardView могут быть применены 2 вида анимаций:

  • анимация поворота карточки при жесте смахивания вбок (карточка переворачивается в сторону совершения жеста);
  • анимация смещения карточки вверх или вниз при вертикальном жесте смахивания (карточка уезжает вверх или вниз в зависимости от стороны совершения жеста).

При каждом старте анимации включаются флаги, сигнализирующие о типе анимации и ее направлении. Карточки при смахивании вбок должны поворачиваться на 180 градусов, однако в таком случае отображение текстов будет полностью зеркальными. Для того чтобы избежать этого было решено создать видимость поворота: анимация поворота на 90 градусов, применение поворота на противоположный угол без анимации и снова анимация поворота на 90 градусов. Для достижения этого эффекта класс LanguageCardView реализует интерфейс AnimatorListener, в котором есть метод onAnimationEnd(), вызывающийся при окончании анимации (листинг 21).

override fun onAnimationEnd(animation: Animator?) { animationIsRunning = false if (scrollAnimation) { alpha = 0f y = requiredTopPosition if (scrollUp) { scrollUp = false gestureManagerListener.onScrollUp() } else if (scrollDown) { scrollDown = false gestureManagerListener.onScrollDown() } scrollAnimation = false animate().setDuration(APPEARANCE_ANIMATION_DURATION) .alpha(1f) .start() } else if (flipAnimation) { if (flipLTR) { rotationY = SWIPE_TO_LEFT_DEGREE flipLTR = false animate().setDuration(FLIPPING_ANIMATION_DURATION) .rotationYBy(SWIPE_TO_RIGHT_DEGREE) .start() } else if (flipRTL) { rotationY = SWIPE_TO_RIGHT_DEGREE flipRTL = false animate().setDuration(FLIPPING_ANIMATION_DURATION) .rotationYBy(SWIPE_TO_LEFT_DEGREE) .start() } flipAnimation = false flipped = !flipped gestureManagerListener.onFlip(flipped) } }

Листинг 21– Метод, вызываемый при окончании анимации LanguageCardView

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

Так же стоит отметить что в этом методе будет вызван один из методов: onScrollUp(), onScrollDown() или onFlip() в зависимости от типа анимации и направления смахивания. Данные методы реализованы во ViewModel экрана запоминания слов и уведомляют о том, что к элементу было применено некое действие. Это нужно для того, чтоб изменить данные, отображаемые на дочерних элементах LanguageCardView.

Отображение элемента может быть настроено с помощью двух параметров – радиуса округления угла и цвета заливки (листинг 22). Параметры, позволяющие это сделать, объявлены в атрибутах проекта и применяются к LanguageCardView в разметке экрана.

<declare-styleable name="LanguageCardView"> <attr name="corner_radius" format="dimension" /> <attr name="card_tint_color" format="color" /> </declare-styleable>

Листинг 22 – Атрибуты LanguageCardView, которые можно задать в разметке экрана

Переданные параметры можно получить при инициализации класса LanguageCardView с помощью экземпляра класса TypedArray (листинг 23). После инициализации полей (радиуса и цвета заливки) необходимо вызвать метод recycle() для переработки класса. После вызова данного метода обращаться к классу повторно запрещается.

if (attrs != null) { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LanguageCardView) cornerRadius = typedArray.getDimension( R.styleable.LanguageCardView_corner_radius, context.dpToPx(cornerRadius.toInt()) ) cardTintColor = typedArray.getColor( R.styleable.LanguageCardView_card_tint_color, NO_COLOR ) typedArray.recycle() } Листинг 23 – Получение параметров отображения LanguageCardView, заданных в разметке экрана Значение цвета заливки применяется в методе setupPaint() (листинг 24), который будет вызван только в том случае, если цвет заливки был передан. private fun setupPaint() { background = context.getDrawable(R.drawable.empty_view) with(maskPaint) { color = cardTintColor style = Paint.Style.FILL } }

Листинг 24– Метод настройки кисти для заливки отображения LanguageCardView

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

<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" />

Листинг 25 – Ресурс, применяемый к фону LanguageCardView

Полученное значение радиуса используется в методе onDraw(). На канвасе (холсте отображения) с помощью метода drawRoundRect() рисуется прямоугольник, содержащий закругления углов с заданным радиусом (листинг 26) и кисть, содержащая цвет заливки и стиль отрисовки.

override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(viewRect, cornerRadius, cornerRadius, maskPaint) } Листинг 26 – Метод onDraw() класса LanguageCardView Основные результаты

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

  • Экран перевода текста (рисунок 2);
  • Экран просмотра сохраненных карточек (слов) (рисунок 3);
  • Экран запоминания сохраненных карточек (слов) (рисунок 4);
  • Экран выбора языков для перевода текста (рисунок 5).

24-03-2020 12-08-22

Рис. 2 – Экран перевода текста

24-03-2020 12-08-32

Рис. 3 – Экран вывода списка сохраненных карточек перевода

24-03-2020 12-08-42

Рис. 4 – Экран запоминания сохраненных переводов

24-03-2020 12-08-53

Рис. 5 – Экран выбора языков перевода

  Заключение

В данной статье были рассмотрены архитектурные компоненты и методы их применения для решения задач в проекте. Вместе тем было описано использование API Яндекс.Переводчик в мобильном приложении с использованием библиотек Retrofit и RxJava. Также в статье было уделено внимание созданию уникального представления с обработкой применяемых к нему жестов и запуску анимаций с участием данного представления. 

Конфликт интересов Не указан. Conflict of Interest None declared.

Список литературы / References

  1. Mobile Operating System Market Share Worldwide [Electronic resource]. Jan 2019 - Jan 2020. – URL: https://gs.statcounter.com/os-market-share/mobile/worldwide (accessed:04.02.2020)
  2. Android Developers: Handling Lifecycles with Lifecycle-Aware Components [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/lifecycle (accessed:04.02.2020)
  3. Android Developers: LiveData Overview [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/livedata (accessed:04.02.2020)
  4. Android Developers: ViewModel Overview [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/viewmodel (accessed:04.02.2020)
  5. Android Developers: Room Persistence Library [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/room (accessed:04.02.2020)
  6. Бен Кристенсен, Томаш Нуркевич Реактивное программирование с применением RxJava [Текст] : [пер. с англ.] / ДМК. Пресс, 2017, гл. 8, стр. 135-148.
  7. Роберт Мартин Чистая Архитектура: Чистая архитектура [Текст], 1-ое издание, Нью Йорк: Pearson Education, 2018, гл. 22, стр. 204-205.
  8. Square Open Source: Retrofit. [Electronic resource] – URL: https://square.github.io/retrofit/ (accessed:04.02.2020)
  9. Яндекс API Переводчика. [Электронный ресурс] – URL: https://yandex.ru/dev/translate/.( дата обращения:04.02.2020)
  10. Android Developers: Custom View Components. [Electronic resource] – URL: https://developer.android.com/guide/topics/ui/custom-components (accessed:04.02.2020)

Список литературы на английском языке / References in English

  1. Mobile Operating System Market Share Worldwide [Electronic resource]. Jan 2019 - Jan 2020. – URL: https://gs.statcounter.com/os-market-share/mobile/worldwide (accessed:04.02.2020)
  2. Android Developers: Handling Lifecycles with Lifecycle-Aware Components [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/lifecycle (accessed:04.02.2020)
  3. Android Developers: LiveData Overview [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/livedata (accessed:04.02.2020)
  4. Android Developers: ViewModel Overview [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/viewmodel (accessed:04.02.2020)
  5. Android Developers: Room Persistence Library [Electronic resource] – URL: https://developer.android.com/topic/libraries/architecture/room (accessed:04.02.2020)
  6. Ben Christensen, Tomas Nurkevich Reaktivnoye programmirovaniye s primeneniyem RxJava [Reactive programming using RxJava] [Text]: [Trans. from English] / DMK. Press, – 2017, – Ch. 8, – P. 135-148. [in Russian]
  7. Robert Martin Chistaya Arkhitektura: Chistaya arkhitektura [Pure Architecture: Pure Architecture] [Text], 1st Edition, New York: Pearson Education, – 2018, – Ch. 22, – P. 204-205. [in Russian]
  8. Square Open Source: Retrofit. [Electronic resource] – URL: https://square.github.io/retrofit/ (accessed:04.02.2020)
  9. Yandex Translator API. [Electronic resource] – URL: https://yandex.ru/dev/translate/ (accessed:04.02.2020) [in Russian]
  10. Android Developers: Custom View Components. [Electronic resource] – URL: https://developer.android.com/guide/topics/ui/custom-components (accessed:04.02.2020)