Эта статья — набор небольших кубиков сахара для android-проекта, до которых я дошел в свое время и что мне пригодилось. Некоторые вещи, возможно, не будут идеальными решениями, но могут пригодиться вам так же, как в свое время пригодились и мне.
Первое, что всегда может пригодиться и бывает нужно в любой точке программы — ссылка на Application. Это решается простым классом, ссылка на который прописывается в AndroidManifest.
Благодаря этому всегда есть доступ к контексту всего приложения и можно получить строки/ресурсы из любого места. И как минимум это нужно для следующей крупицы сахара:
Мелочь, но благодаря Kotlin и тому, что у нас есть доступ к контексту — теперь из любого места приложения можно вызвать Toast коротко и лаконично. Для того, чтобы метод был доступен везде, его можно разметить в файле без указания корневого класса.
Сейчас набирают популярность программы с одним Activity. Но для нашей архитектуры было решено использовать несколько Activity. Как минимум для разделения авторизационной логики и основной части приложения. Со временем появилась необходимость понимать, виден ли экран и какая это часть приложения. А в дальнейшем это понадобилось еще и для получения строк в локали приложения. Но обо всем по порядку. Чтобы наше значение APP не скучало в одиночестве, компанию ему составят:
А дальше, мы создаем свой базовый класс, наследуемый от AppCompatActivity:
Да, по новым гайдам можно в необходимых местах подписаться на Lifecycle у Activity. Но имеем что имеем.
Бывают функционалы сомнительной полезности, но ТЗ есть ТЗ. К такому функционалу я бы отнес выбор языка в приложении взамен системному. Уже давно есть код, который позволяет программно подменять язык. Но мы столкнулись с одним багом, который возможно повторяется только у нас. Суть бага в том, что если брать строку через контекст приложения, а не через контекст Activity — то строка возвращается в локали системы. И не всегда удобно прокидывать контекст Activity. На помощь пришли следующие методы:
И теперь из любого места приложения мы можем получить строку в правильной локали.
Как и во всех приложениях, в нашем приходится хранить некоторые настройки в SharedPreferences. И для упрощения жизни был придуман класс, который скрывает в себе немного логики. Для начала был добавлен для переменной APP новый друг:
Он инициализируется при запуске приложения и всегда доступен нам.
Кончено придется генерировать такой класс для каждого типа переменных, но за то можно замести singleton со всеми нужными настройками, например:
И теперь всегда можно обратиться к настройкам как к полю, а загрузка/сохранение и связь через listener будут скрыты.
Вам приходилось поддерживать обе ориентации? А как вы определяли в какой вы ориентации сейчас? У нас для этого есть удобный метод, который опять же можно вызвать из любого места и не заботиться о контексте:
Если завести в values/dimen.xml:
А в values-large/dimen.xml:
То можно создать еще и метод:
Многопоточность. Порой это страшное слово. Как то раз мы поймали очень странный баг, когда формировали строки из дат в фоне. Оказалось, что SimpleDateFormat не потоко-безопасен. Поэтому было рождено следующее:
И пример использования (да, это опять используется внутри синглтона):
Для всего приложения ничего не изменилось, а проблема с потоками решена.
А вас никогда не напрягало, что если тебе надо отловить какой текст вводится в EditText, то надо использовать TextWatcher и реализовывать 3(!) метода. Не критично, но не удобно. А все решается классом:
То, что всегда нужно. То надо сразу показать клавиатуру, то в какой-то момент надо ее скрыть. И тогда нужны следующие два метода. Во-втором случае необходимо передавать корневую view.
И, может кому пригодится, функция для копирования любой строки в clipboard с показом toast:
А в завершение этой маленькой статьи, хочу поделиться тем, что недавно мне пришлось решать. Может кому пригодится.
Исходные данные такие:
Если забыть про первый пункт. то наиболее рабочее решение — TableLayout. Почему не RecyclerView? А из-за 3 и 4 пунктов. Внутри листа есть оптимизации и он переиспользует view, но не всегда. И в момент создания новой view обработчики касаний не существуют. И ладно, если бы это влияло только на свайпы, но периодически проблема воспроизводится и с обычным тапом. Не помогает даже обновление данных напрямую в View, а не через notify. Поэтому было решено использовать TableLayout. И все было прекрасно, пока данных было не больше 100. А дальше — добро пожаловать в мир зависаний.
Я видел 2 пути решения — или учить TableLayout переиспользовать ячейки и делать магию при скролле. Или постараться подружить RecyclerView и частое обновление. И я пошел по второму пути. Так как касания и свайпы (в большей мере из-за свайпов) обрабатывались самописным классом на основе View.OnTouchListener, то действенным решением оказалось вынести обработку касаний на уровень RecyclerView, переопределив метод dispatchTouchEvent.
Алгоритм прост:
Возможно в будущем еще будут проблемы от этого способа, но на данный момент все работает и это хорошо.
Application и Toast
Первое, что всегда может пригодиться и бывает нужно в любой точке программы — ссылка на Application. Это решается простым классом, ссылка на который прописывается в AndroidManifest.
class App : Application() {
init {
APP = this
}
companion object {
lateinit var APP: App
}
}
Благодаря этому всегда есть доступ к контексту всего приложения и можно получить строки/ресурсы из любого места. И как минимум это нужно для следующей крупицы сахара:
fun Toast(messageId: Int) {
Toast.makeText(App.APP, messageId, Toast.LENGTH_LONG).show()
}
fun Toast(message: String) {
Toast.makeText(App.APP, message, Toast.LENGTH_LONG).show()
}
Мелочь, но благодаря Kotlin и тому, что у нас есть доступ к контексту — теперь из любого места приложения можно вызвать Toast коротко и лаконично. Для того, чтобы метод был доступен везде, его можно разметить в файле без указания корневого класса.
Кто на экране?
Сейчас набирают популярность программы с одним Activity. Но для нашей архитектуры было решено использовать несколько Activity. Как минимум для разделения авторизационной логики и основной части приложения. Со временем появилась необходимость понимать, виден ли экран и какая это часть приложения. А в дальнейшем это понадобилось еще и для получения строк в локали приложения. Но обо всем по порядку. Чтобы наше значение APP не скучало в одиночестве, компанию ему составят:
var screenActivity: AppCompatActivity? = null
var onScreen: Boolean = false
А дальше, мы создаем свой базовый класс, наследуемый от AppCompatActivity:
open class BaseActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
App.onScreen = true
}
override fun onStop() {
super.onStop()
if (App.screenActivity == this) {
App.onScreen = false
if (isFinishing())
App.screenActivity = null
}
}
override fun onDestroy() {
super.onDestroy()
if (App.screenActivity == this)
App.screenActivity = null
}
override fun onCreate(savedInstanceState: Bundle?) {
App.screenActivity = this
}
override fun onRestart() {
super.onRestart()
App.screenActivity = this
}
}
Да, по новым гайдам можно в необходимых местах подписаться на Lifecycle у Activity. Но имеем что имеем.
Localized strings
Бывают функционалы сомнительной полезности, но ТЗ есть ТЗ. К такому функционалу я бы отнес выбор языка в приложении взамен системному. Уже давно есть код, который позволяет программно подменять язык. Но мы столкнулись с одним багом, который возможно повторяется только у нас. Суть бага в том, что если брать строку через контекст приложения, а не через контекст Activity — то строка возвращается в локали системы. И не всегда удобно прокидывать контекст Activity. На помощь пришли следующие методы:
fun getRes(): Resources = screenActivity?.resources ?: APP.resources
fun getLocalizedString(stringId: Int): String = getRes().getString(stringId)
fun getLocalizedString(stringId: Int, vararg formatArgs: Any?): String = getRes().getString(stringId, *formatArgs)
И теперь из любого места приложения мы можем получить строку в правильной локали.
SharedPreferences
Как и во всех приложениях, в нашем приходится хранить некоторые настройки в SharedPreferences. И для упрощения жизни был придуман класс, который скрывает в себе немного логики. Для начала был добавлен для переменной APP новый друг:
lateinit var settings: SharedPreferences
Он инициализируется при запуске приложения и всегда доступен нам.
class PreferenceString(val key: String, val def: String = "", val store: SharedPreferences = App.settings, val listener: ModifyListener? = null) {
var value: String
get() {
listener?.customGet()
return store.getString(key, def) ?: def
}
set(value) {
store.edit().putString(key, value).apply()
listener?.customSet()
}
}
interface ModifyListener {
fun customGet() {}
fun customSet() {}
}
Кончено придется генерировать такой класс для каждого типа переменных, но за то можно замести singleton со всеми нужными настройками, например:
val PREF_LANGUAGE = PreferenceString("pref_language", "ru")
И теперь всегда можно обратиться к настройкам как к полю, а загрузка/сохранение и связь через listener будут скрыты.
Orientation и Tablet
Вам приходилось поддерживать обе ориентации? А как вы определяли в какой вы ориентации сейчас? У нас для этого есть удобный метод, который опять же можно вызвать из любого места и не заботиться о контексте:
fun isLandscape(): Boolean {
return getRes().configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
}
Если завести в values/dimen.xml:
<bool name="isTablet">false</bool>
А в values-large/dimen.xml:
<bool name="isTablet">true</bool>
То можно создать еще и метод:
fun isTablet(): Boolean {
return getRes().getBoolean(R.bool.isTablet)
}
DateFormat
Многопоточность. Порой это страшное слово. Как то раз мы поймали очень странный баг, когда формировали строки из дат в фоне. Оказалось, что SimpleDateFormat не потоко-безопасен. Поэтому было рождено следующее:
class ThreadSafeDateFormat(var pattern: String, val isUTC: Boolean = false, val locale: Locale = DEFAULT_LOCALE){
val dateFormatThreadLocal = object : ThreadLocal<SimpleDateFormat>(){
override fun initialValue(): SimpleDateFormat? {
return SimpleDateFormat(pattern, locale)
}
}
val formatter: SimpleDateFormat
get() {
val dateFormat = dateFormatThreadLocal.get() ?: SimpleDateFormat(pattern, locale)
dateFormat.timeZone = if (isUTC) TimeZone.getTimeZone("UTC") else timeZone
return dateFormat
}
}
И пример использования (да, это опять используется внутри синглтона):
private val utcDateSendSafeFormat = ThreadSafeDateFormat("yyyy-MM-dd", true)
val utcDateSendFormat: SimpleDateFormat
get() = utcDateSendSafeFormat.formatter
Для всего приложения ничего не изменилось, а проблема с потоками решена.
TextWatcher
А вас никогда не напрягало, что если тебе надо отловить какой текст вводится в EditText, то надо использовать TextWatcher и реализовывать 3(!) метода. Не критично, но не удобно. А все решается классом:
open class TextWatcherObject : TextWatcher{
override fun afterTextChanged(p0: Editable?) {}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
}
Keyboard
То, что всегда нужно. То надо сразу показать клавиатуру, то в какой-то момент надо ее скрыть. И тогда нужны следующие два метода. Во-втором случае необходимо передавать корневую view.
fun showKeyboard(view: EditText){
view.requestFocus();
(App.APP.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?)
?.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
}
fun hideKeyboardFrom(view: View) {
(App.APP.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?)
?.hideSoftInputFromWindow(view.windowToken, 0)
}
И, может кому пригодится, функция для копирования любой строки в clipboard с показом toast:
fun String.toClipboard(toast: Int) {
val clip = ClipData.newPlainText(this, this)
(App.APP.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)?.setPrimaryClip(clip)
Toast(toast)
}
RecyclerView и TableLayout
А в завершение этой маленькой статьи, хочу поделиться тем, что недавно мне пришлось решать. Может кому пригодится.
Исходные данные такие:
- 1к+ данных для отображения.
- Каждая “ячейка” состоит из примерно 10 полей.
- необходимо отлавливать свайп, click, doubleClick, longClick.
- и…. данные обновляются каждые 300 — 500 миллисекунд.
Если забыть про первый пункт. то наиболее рабочее решение — TableLayout. Почему не RecyclerView? А из-за 3 и 4 пунктов. Внутри листа есть оптимизации и он переиспользует view, но не всегда. И в момент создания новой view обработчики касаний не существуют. И ладно, если бы это влияло только на свайпы, но периодически проблема воспроизводится и с обычным тапом. Не помогает даже обновление данных напрямую в View, а не через notify. Поэтому было решено использовать TableLayout. И все было прекрасно, пока данных было не больше 100. А дальше — добро пожаловать в мир зависаний.
Я видел 2 пути решения — или учить TableLayout переиспользовать ячейки и делать магию при скролле. Или постараться подружить RecyclerView и частое обновление. И я пошел по второму пути. Так как касания и свайпы (в большей мере из-за свайпов) обрабатывались самописным классом на основе View.OnTouchListener, то действенным решением оказалось вынести обработку касаний на уровень RecyclerView, переопределив метод dispatchTouchEvent.
Алгоритм прост:
- ловим касание
- определяем в какой child касание летит с помощью findChildViewUnder(x, y)
- получаем от LayoutManager позицию элемента
- если это MotionEvent.ACTION_MOVE, то проверяем с той же позицией мы работаем что и раньше или нет
- выполняем заложенную логику для касания
Возможно в будущем еще будут проблемы от этого способа, но на данный момент все работает и это хорошо.