diff --git a/README.md b/README.md index 6e9bfd3..0130ec4 100644 --- a/README.md +++ b/README.md @@ -1 +1,82 @@ -# RoboSwag \ No newline at end of file +# RoboSwag +Roboswag - библиотека решений, ускоряющих разработку Android приложений. Она включает в себя архитектурные решения для построения приложения, утилитарные классы и общие инструменты, которые используются в компании Touch Instinct. +Библиотека состоит из gradle модулей. Каждый модуль отвечает за свой функционал. В проектах используются только те модули, которые нужны. Такая модульность позволяет сохранять размер приложения небольшим и ускорять сборку проекта. + +## Минимальные требования + +* Andoroid Api: 19 +* Kotlin: 1.3.11 +* Gradle: 3.2.1 +* Gradle CPD Plugin: 1.1 +* Detekt Plugin: 1.0.0-RC12 + +## Основная архитектура +За основу архитектуры взят подход от Google - MVVM на основе [Android Architecture Components](https://developer.android.com/jetpack/docs/guide). Данный подход популярен в сообществе Android разработки, позволяет разбивать код на мелкие и независимые части, что ускоряет разработку и последующую поддержку приложения. +Для организации многопоточности используется фреймворк [RxJava2](https://github.com/ReactiveX/RxJava). RxJava - обширный инструмент, реализующий концепции реактивного программирования. Сочетание этой концепции с возможностью выносить задачи на другой поток позволяет легко писать многопоточное асинхронное приложение. +В качестве Di-фреймворка выбран [Dagger 2](https://github.com/google/dagger). Он позволяет сделать код приложения менее связным, более гибким и позволяет легко настроить автотестирование. +Roboswag позволяет сочетать эти три решения в одну гибкую и удобную архитектуру. Разработка становится быстрее, проще и надежнее. За архитектуру отвечают модули [lifecycle](/lifecycle) и [lifecycle-rx](/lifecycle-rx). + +## Основные инструменты библиотеки +### Работа с RecyclerView +RecyclerView - один из самых часто используемых инструментов Android разработчика. Модуль [recyclerview-adapters](/recyclerview-adapters) позволяет сделать работу с RecyclerView более гибкой и делает работу самого элемента быстрее. +### BuildScripts +[BuildScrpts](https://github.com/TouchInstinct/BuildScripts) - набор скриптов, автоматизирующих разработку. Один из главных скриптов - staticAnalysis - инструмент для автоматической проверки кода на соответствие правилам компании. +### Api Generator +Внутренний инструмент компании Touch Instinct для генерации общего кода на разные платформы - Android, iOS и Server. Описанные в одном месте общие классы и Http методы используются на разных платформах. Данный инструмент позволяет сократить время разработки в два раза. +### Работа с SharedPreferences +Чтобы сохранять простые данные в память смартфона, используются SharedPreferences. Модуль [storable](/storable) разработан для облегчения работы с SharedPreferences. +### Утилиты и extension функции +В Roboswag также есть много [утилитарных](/utils) классов и [extension](/kotlin-extensions) функций, которые позволяют писать часто используемый код в одну строку. + +### Подключение + +#### .gitmodules + +``` +[submodule "RoboSwag"] + path = RoboSwag + url = git@github.com:TouchInstinct/RoboSwag.git +``` + +#### build.gradle (Module: app) + +```gradle +dependencies { + implementation project(':utils') + implementation project(':views') + implementation project(':storable') + implementation project(':logging') + implementation project(':api-logansquare') + implementation project(':lifecycle') + implementation project(':lifecycle-rx') + implementation project(':navigation') + implementation project(':templates') + implementation project(':recyclerview-adapters') + implementation project(':recyclerview-calendar') + implementation project(':kotlin-extensions') + implementation project(':livedata-location') +} +``` +Можно подключать только те модули, которые вам необходимы. + +### R8/Proguard + +``` +-keep class ** extends ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController { *; } +``` + +### Лицензия + +``` +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/kotlin-extensions/README.md b/kotlin-extensions/README.md new file mode 100644 index 0000000..7e23a7a --- /dev/null +++ b/kotlin-extensions/README.md @@ -0,0 +1,67 @@ +kotlin-extensions +===== + +Модуль содержит extension-функции для `Activity`, `Context`, `Delegates`, `TextView`, `View` и `ViewHolder`. + +### Основные интерфейсы и классы + +##### Расширения для `Activity`: + +* *safeStartActivityForResult* - функция для запуска нового активити с `requestCode`, который будет передан в *onActivityResult* при завершении работы данного активити. Находит наиболее подходящий активити для выполнения действия. Если не будет найден ни один активити для выполнения действия, то функция ничего не сделает и вернет `false`. + +##### Расширения для `Context`: + +* *safeStartActivity* - функция запуска активити, аналогична *safeStartActivityForResult*, но не позволяет передать `requestCode`. + +* *openBrowser* - функция для открытия ссылки в браузере через *safeStartActivity*. + +* *callToPhoneNumber* - функция для открытия программы "Телефон" с переданным номером телефона через *safeStartActivity*. + +##### Расширения для `TextView`: + +* *drawableStart*, *drawableTop*, *drawableEnd*, *drawableBottom* - функции для установки и получения `Drawable` на соответсвующих позициях. + +##### Расширения для `View`: + +* *setOnRippleClickListener* - функция для добавления Ripple-эффекта и действия, которое будет выполняться при нажатии на `View`. + +##### Расширения для `ViewHolder`: + +* *ViewHolder.findViewById* - функция для поиска `View`, расположеного внутри *itemView*. + +* *ViewHolder.getText* - функция для получения текста из ресурсов. + +* *ViewHolder.getString* - функция для получения строк из ресурсов. Может также принимать вторым аргументом и далее - строки, которые будут подставлены вместо специальных символов в строку из ресурсов. + +* *ViewHolder.getColor* - получить цвет в виде `Int` из ресурсов. + +* *ViewHolder.getColorStateList* - получить `ColorStateList`, который ассоциируется с переданным цветом. + +* *ViewHolder.getDrawable* - получить `Drawable` из ресурсов. + +##### Расширения для `Delegates`: + +* *observable* - подписка на изменения свойства, принимает `initialValue` - начальное значение и `onChange` - действие, которое будет выполняться после каждой установки свойства. + +* *distinctUntilChanged* - тоже самое, что и предыдущее расширение, только `onChange` будет выполняться лишь в том случае, когда свойство принимает новое значение не равное `null` и отличное от предыдущего. + +### Примеры + +```kotlin +class LinkViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + private val linkView: TextView = findViewById(R.id.item_link) + private val linkColor = getColor(R.color.global_action) + + fun bind(link: String) { + linkView.text = link + linkView.setOnRippleClickListener { context.openBrowser(link) } + } +} +``` + +### Подключение + +```gradle +implementation project(':kotlin-extensions') +``` diff --git a/lifecycle-rx/README.md b/lifecycle-rx/README.md new file mode 100644 index 0000000..e527153 --- /dev/null +++ b/lifecycle-rx/README.md @@ -0,0 +1,56 @@ +lifecycle-rx +===== + +Модуль для преобразования событий из `Observable` в `LiveData`. Нужен для передачи событий из `ViewModel` во `ViewController` с автоматическим управлением подписками во `ViewController`. + +### Основный интерфейсы и классы +`Destroyable` - интерфейс, который содержит extansion-функцию *untilDestroy* для `Flowable`, `Observable`, `Single`, `Completable`, `Maybe`. Данная функция гарантирует, что подписка на события "умрет" после *onDestroy*. + +`LifeDataDispatcher` - интерфейс, описывающий функцию *dispatchTo* для преобразования `Observable` в `MutableLiveData`. + +`BaseDestroyable` и `BaseLifeDataDispatcher` - базовые реализации `Destroyable` и `LifeDataDispatcher` соответсвенно. + +`RxViewModel` - базовый класс, от которого должны наследоваться все `ViewModel`. Обеспечивает отписку всех подписчиков при возникновении *onCleared*. Реализует `BaseDestroyable` и `LiveDataDispatcher`. По умолчанию использует базовые реализации данных интерфейсов, при желании можно передать свои `Destroyable` и `LiveDataDispatcher` через конструктор. + +### Примеры + +Простой пример `ViewModel`, через который можно получить список элементов и добавить один элемент. + +```kotlin +class SomeViewModel ( + private val someRepository: SomeRepository +) : RxViewModel() { + + val itemsList = MutableLiveData>>() + + fun getItemsList() { + someRepository + .getItems() + .dispatchTo(itemsList) + } + + fun addItem(item: Item) { + someRepository + .addItem(item) + .untilDestroy() + } +} +``` + +Подписка на события во `ViewController`. `ContentEvent` описан в модуле [lifecycle](https://github.com/TouchInstinct/RoboSwag/tree/master/lifecycle). + +```kotlin +someViewModel.itemsList.observe(this, Observer { event -> + when (event) { + is ContentEvent.Loading -> // do something + is ContentEvent.Success -> // do something + is ContentEvent.Error -> // do something + } +}) +``` + +### Подключение + +``` gradle +implementation project(':lifecycle-rx') +``` diff --git a/lifecycle/README.md b/lifecycle/README.md new file mode 100644 index 0000000..1c265cf --- /dev/null +++ b/lifecycle/README.md @@ -0,0 +1,49 @@ +lifecycle +===== + +Модуль содержит обертку над `ViewModelProviders` для работы с `ViewController` и обертки для передачи событий из `ViewModel` во `ViewController`. + +### Основные интерфейсы и классы + +`LifecycleViewModelProviders` - объект для получения `ViewModelProvider`. Содержит функцию *of*, которая принимает `LifecycleOwner` и возвращает специфичный для него `ViewModelProvider`. + +`SingleLiveEvent` - событие - одиночка. Посылает события только один раз. Наследуется от `MutableLiveData` и переопределяет методы `observe` и `setValue`. + +`ContentEvent` - событие, обертка над данными. +Дочерние классы: +* `Loading` - символизирует состояние загрузки, +* `Success` - символизирует успешное событие, +* `Error` - символизирует ошибку, +* `Complete` - символизирует завершение события. + +`Event` - аналогичен `ContentEvent`, только не содержит никакой информации о данных. Нужен для оповещения о наступлении одного из следующих событий: `Loading`, `Complete` или `Error`. + +### Примеры + +Получение `ViewModel` во `ViewController`. + +```kotlin +private val viewModel = LifecycleViewModelProviders.of(this).get(SomeViewModel::class.java) +``` + +Подписка на `SingleLiveEvent`. + +```kotlin +// во ViewModel +val event = SingleLiveEvent() + +// во ViewController +event.observe(this, Observer { event -> + when (event) { + is Event.Loading -> ::onEventLoading + is Event.Complete -> ::onEventComplete + is Event.Error -> ::onEventError + } +}) +``` + +### Подключение + +```gradle +implementation project(':lifecycle') +``` diff --git a/livedata-location/README.md b/livedata-location/README.md new file mode 100644 index 0000000..5789566 --- /dev/null +++ b/livedata-location/README.md @@ -0,0 +1,33 @@ +livedata-location +===== + +Модуль позволяющий получать местоположение пользователя в виде потока данных `LiveData`. + +### Основный интерфейсы и классы + +Класс `LocationLiveData`. В конструкторе принимает `Context` и `LocationRequest`. Посылает `Location` подписчикам через указанные в `LocationRequest` интервалы времени. Метод *observe* позволяет подписаться на эти обновления. Данный метод принимает `LifecycleOwner` и `Observer`. Стоит учесть, что для использования данного класса нужно одно из следующих разрешений `ACCESS_COARSE_LOCATION` или `ACCESS_FINE_LOCATION`. +### Примеры + +Во `ViewModel`. + +```kotlin +val locationWithInterval = LocationLiveData( + context, + LocationRequest + .create() + .setInterval(5000) + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) +) +``` + +Во `ViewController`. + +```kotlin +viewModel.locationWithInterval.observe(this, Observer(::onLocationChanged)) +``` + +### Подключение + +``` gradle +implementation project(':livedata-location') +``` \ No newline at end of file diff --git a/navigation/README.md b/navigation/README.md new file mode 100644 index 0000000..097ac0d --- /dev/null +++ b/navigation/README.md @@ -0,0 +1,129 @@ +navigation +==== + +Модуль содержит классы для организации навигации в приложении. + +### Основные интерфейсы и классы + +#### Пакет `activities` + +`BaseActivity` - абстрактный класс, в котором выполняется логгирование с помощью модуля [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging) при выполнении некоторых методов. Класс позволяет добавлять новые `OnBackPressedListener` и удалять их с помощью методов *addOnBackPressedListener* и *removeOnBackPressedListener* (*removeAllOnBackPressedListeners*) соответственно. + +Интерфейс `OnBackPressedListener` - интерфейс с одним методом *onBackPressed*. Используется в `BaseActivity`. + +#### Пакет `fragments` + +Класс `ViewControllerFragment` наследуется от `Fragment`. Через статический метод *args* получается `Bundle` с классом `ViewController`(а) и состоянием, которое наследуется от `Parcelable`. В методе *onCreate* инициализируются поля *state* и *viewControllerClass* используя данные из `Bundle`. В методе *onCreateView* создается `ViewController`. + +#### Пакет `viewcontrollers` + +`ViewController` - обертка над Fragment. Один ViewController - один экран. К моменту инициализации вашего класса уже будут доступны следующие поля из `ViewController`: *state*, *activity*, *fragment*, *view*. Это означает, что можно выполнять всю настройку экрана в `init { }`. + +У класса есть два параметра `TActivity: FragmentActivity` и `TState: Parcelable`, которые нужно указывать при инициализации класса `ViewController`. В конструкторе данный класс принимает `CreationContext` и идентификатор layout-ресурса. + +`ViewControllerNavigation` отвечает за навигацию по `ViewController`(ам). В конструкторе принимает `Context`, `FragmentManager` и идентификатор ресурса, который является контейнером для других фрагментов. Имеет параметр `TActivity : FragmentActivity`. + +`EmptyState` - пустое состояние. Использутся, когда при переходе к новому `ViewController` не нужно передавать никаких инициализирующих данных. + +`LifecycleLoggingObserver` подписывается на вызовы методов жизненного цикла и логгирует номер строки, из которой был вызваны эти методы. + +Методы для навигации: + +* *pushViewController* добавляет `ViewController` в стек. Имеет два обязательных параметра *viewControllerClass* - класс, унаследованный от `ViewController` и *state* - объект описывающий состояние. + +* *pushViewControllerForResult* аналогичен предыдущему методу, используется, когда необходимо запустить какой-то фрагмент и при его завершении получить код. Для этого передаются еще два параметра: *requestCode* - код, который нужно получить при закрытии фрагмента и *targetFragment* - фрагмент, который должен получить этот код. + +* *setViewControllerAsTop* работает так же как и *pushViewController* но еще добавляет в качестве *backStackName* тег `TOP_FRAGMENT_TAG_MARK`. При выполнении возврата с помощью метода `up` будет выполнен возврат данному фрагменту. + +* *setInitialViewController* очищает стек и добавляет туда переданный `ViewController`. + +`ViewControllerNavigation` является наследником класса `FragmentNavigation` и для возвратов необходимо использовать методы из родительского класса: + +* *back* - вернуться к фрагменту, который лежит ниже в стеке. +* *up* - вернуться к самому низу стека, если в стеке нет фрагментов, помеченных тегом `TOP_FRAGMENT_TAG_MARK`. Если есть, то выполнить возврат к нему. Имеет два необязательных параметра: *name* - имя класса до которого нужно сделать возврат, если он не будет найден, то будет произведен возврат к самому низу стека; *inclusive* - если установить этот флаг, то будет произведен возврат к самому низу стека несмотря на фрагменты с тегом `TOP_FRAGMENT_TAG_MARK`. Если будет установлен и *name* и *inclusive*, то будет произведен возврат к фрагменту, который стоит ниже фрагмента с переданным *name*. + +### Примеры + +Файл `MainActivity.kt` +```Kotlin +class MainActivity : BaseActivity() { + + private val screenNavigation by lazy { + ViewControllerNavigation( + this, + supportFragmentManager, + R.id.fragment_container + ) + } + + fun getNavigation() = screenNavigation + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + screenNavigation.setInitialViewController( + MainViewController::class.java, + MainScreenState(true) + ) + } + +} +``` + +Файл `MainViewController.kt` +```Kotlin +class MainViewController( + creationContext: CreationContext +) : ViewController( + creationContext, + R.layout.view_controller_main +) { + + private val button: View = findViewById(R.id.view_controller_main_button) + + init { + button.setOnClickListener { + activity.getNavigation().pushViewController( + TutorialViewController::class.java, + EmptyState + ) + } + } + +} +``` + +Файл `activity_main.xml` +```xml + + + + + + +``` + +### Рекомендации + +Рекомендуется делать состояния, которые передаются во `ViewController` неизменяемыми, чтобы при навигации обратно `ViewController` корректно восстанавливались с изначально заданным состоянием. + +### Зависимости + +Для работы с данным модулем необходимо так же подключить модуль [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging). + +```gradle +implementation project(':logging') +``` + +### Подключение + +```gradle +implementation project(':navigation') +``` diff --git a/recyclerview-adapters/README.md b/recyclerview-adapters/README.md new file mode 100644 index 0000000..3328fe7 --- /dev/null +++ b/recyclerview-adapters/README.md @@ -0,0 +1,71 @@ +# recyclerview-adapters + +Модуль, расширяющий возможности работы со стандартным `RecyclerView.Adapter`. + +### Основные интерфейсы и классы + +`DelegationListAdapter` - базовый класс, наследник от `RecyclerView.Adapter`. + +Конструктор принимает `DiffUtil.ItemCallback` - интерфейс, описывающий как различать элементы в адаптере. Он содержит два абстрактных метода: *areItemsTheSame* - метод, сравнивающий элементы, и *areContentsTheSame* - метод, сравнивающий визуальную составляющую элементов. + +Например, возьмем список товаров, у которых есть уникальный *id* и *title*, который может повторяться. В `RecyclerView` отображается только название товара, т.е. *title*. В такой ситуации в методе *areItemsTheSame* нужно будет написать `oldItem.id == newItem.id`, а в методе *areContentsTheSame* - `oldItem.title == newItem.title`. Оба эти метода вычисляются в бэкграунд потоке. + +Методы `getHeadersCount` и `getFootersCount` нужны, когда в списке есть элементы, которые всегда должны быть наверху или внизу. Например, если нужно добавить кнопку после всех элементов. + +Управлением элементами списка занимаются делегаты. Они добавляются с помощью метода *addDelegate*. + +`ItemAdapterDelegate` - делегат, который управляет созданием и прикреплением элементов в зависимости от типа элемента. + +`PositionAdapterDelegate` - делегат, который управляет созданием и прикреплением элементов, основываясь на позиции элемента в `DelegationListAdapter`. + +При реализации делегатов, необходимо описать два метода: +* *isForViewType* - метод, который говорит делегату, должен ли он управлять соответсвующим элементом; +* *onBindViewHolder* - метод, который описывает действия при прикреплении элемента к `ViewHolder`. + +### Примеры + +Создание адаптера. + +```Kotlin +class SomeAdapter : DelegationListAdapter(CALLBACK) { + + companion object { + private val CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Item, newItem: Item) = newItem.id == oldItem.id + override fun areContentsTheSame(oldItem: Item, newItem: Item) = newItem == oldItem + } + } + + init { + addDelegate(HeaderDelegate()) + addDelegate(ItemDelegate()) + addDelegate(BottomDelegate()) + } + + // Some logic in your adapter +} +``` + +Создание делегата. +```Kotlin +class HeaderDelegate( + private val addAction: () -> Unit +) : PositionAdapterDelegate() { + + override fun isForViewType(adapterPosition: Int): Boolean = adapterPosition == 0 + + override fun onCreateViewHolder(parent: ViewGroup) = + object : RecyclerView.ViewHolder(HeaderItemView()) {} + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + adapterPosition: Int, + payloads: MutableList + ) = holder.itemView.setOnClickListener { addAction.invoke() } +} +``` + +### Подключение +```gralde +implementation project(':recyclerview-adapters') +```