Merge pull request #42 from TouchInstinct/feature/documentation

First part of documentation
This commit is contained in:
Maxim Bachinsky 2019-08-02 17:14:35 +03:00 committed by GitHub
commit 9249db7b0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 487 additions and 1 deletions

View File

@ -1 +1,82 @@
# RoboSwag
# 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.
```

View File

@ -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')
```

56
lifecycle-rx/README.md Normal file
View File

@ -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<ContentEvent<List<Item>>>()
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')
```

49
lifecycle/README.md Normal file
View File

@ -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<Event>()
// во ViewController
event.observe(this, Observer { event ->
when (event) {
is Event.Loading -> ::onEventLoading
is Event.Complete -> ::onEventComplete
is Event.Error -> ::onEventError
}
})
```
### Подключение
```gradle
implementation project(':lifecycle')
```

View File

@ -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')
```

129
navigation/README.md Normal file
View File

@ -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<MainActivity>(
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<MainActivity, MainScreenState>(
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
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
```
### Рекомендации
Рекомендуется делать состояния, которые передаются во `ViewController` неизменяемыми, чтобы при навигации обратно `ViewController` корректно восстанавливались с изначально заданным состоянием.
### Зависимости
Для работы с данным модулем необходимо так же подключить модуль [logging](https://github.com/TouchInstinct/RoboSwag/tree/master/logging).
```gradle
implementation project(':logging')
```
### Подключение
```gradle
implementation project(':navigation')
```

View File

@ -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<Item>(CALLBACK) {
companion object {
private val CALLBACK = object : DiffUtil.ItemCallback<Item>() {
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<RecyclerView.ViewHolder>() {
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<Any>
) = holder.itemView.setOnClickListener { addAction.invoke() }
}
```
### Подключение
```gralde
implementation project(':recyclerview-adapters')
```