Este artículo es un conjunto de pequeños cubos de azúcar para un proyecto de Android, que encontré a su debido tiempo y que fue útil. Algunas cosas pueden no ser soluciones ideales, pero pueden serle útiles de la misma manera que me fueron útiles alguna vez.
Aplicación y tostadas
Lo primero que siempre puede ser útil y que a veces se necesita en cualquier punto del programa es un enlace a la Aplicación. Esto se resuelve mediante una clase simple, cuyo enlace está registrado en AndroidManifest.
class App : Application() { init { APP = this } companion object { lateinit var APP: App } }
Gracias a esto, siempre hay acceso al contexto de toda la aplicación y puede obtener líneas / recursos desde cualquier lugar. Y al menos esto es necesario para el siguiente grano de azúcar:
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() }
Un poco, pero gracias a Kotlin y al hecho de que tenemos acceso al contexto, ahora puede llamar a Toast de forma breve y concisa desde cualquier lugar de la aplicación. Para que el método sea accesible en todas partes, se puede marcar en un archivo sin especificar la clase raíz.
¿Quién está en la pantalla?
Los programas con una actividad están ganando popularidad. Pero para nuestra arquitectura, se decidió utilizar varias actividades. Al menos para separar la lógica de autorización y la parte principal de la aplicación. Con el tiempo, era necesario comprender si la pantalla es visible y qué parte de la aplicación es. Y en el futuro, también era necesario obtener cadenas en la configuración regional de la aplicación. Pero lo primero es lo primero. Para que nuestro valor de APP no se aburra solo, la compañía lo hará:
var screenActivity: AppCompatActivity? = null var onScreen: Boolean = false
Y luego, creamos nuestra clase base, heredada de 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 } }
Sí, para las nuevas guías, puede suscribirse a Lifecycle with Activity en los lugares necesarios. Pero tenemos lo que tenemos.
Cadenas localizadas
Existen funcionalidades de dudosa utilidad, pero TK es TK. A dicha funcionalidad, atribuiría la elección del idioma en la aplicación en lugar del sistema. Durante mucho tiempo, hay un código que le permite reemplazar el lenguaje mediante programación. Pero nos enfrentamos a un error, que probablemente solo se repite con nosotros. La esencia del error es que si lleva la línea a través del contexto de la aplicación, y no a través del contexto de Actividad, la línea vuelve a las configuraciones regionales del sistema. Y no siempre es conveniente lanzar el contexto de una Actividad. Los siguientes métodos vinieron al rescate:
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)
Y ahora, desde cualquier lugar de la aplicación, podemos obtener la cadena en la configuración regional correcta.
Preferencias compartidas
Como con todas las aplicaciones, la nuestra tiene que almacenar algunas configuraciones en SharedPreferences. Y para simplificar la vida, se inventó una clase que oculta un poco de lógica en sí misma. Para comenzar, se agregó un nuevo amigo para la variable APP:
lateinit var settings: SharedPreferences
Se inicializa cuando se inicia la aplicación y siempre está disponible para nosotros.
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() {} }
Por supuesto, tendrá que generar una clase de este tipo para cada tipo de variable, pero puede reemplazar singleton con todas las configuraciones necesarias, por ejemplo:
val PREF_LANGUAGE = PreferenceString("pref_language", "ru")
Y ahora siempre puede referirse a la configuración como un campo, y la carga / guardado y la comunicación a través del oyente estarán ocultos.
Orientación y Tableta
¿Has tenido que apoyar ambas orientaciones? ¿Y cómo determinaste en qué orientación estás ahora? Tenemos un método conveniente para esto, que nuevamente se puede llamar desde cualquier lugar y no importa el contexto:
fun isLandscape(): Boolean { return getRes().configuration.orientation == Configuration.ORIENTATION_LANDSCAPE }
Si pones valores / dimen.xml:
<bool name="isTablet">false</bool>
Y en valores-large / dimen.xml:
<bool name="isTablet">true</bool>
Entonces también puedes crear un método:
fun isTablet(): Boolean { return getRes().getBoolean(R.bool.isTablet) }
Formato de fecha
Multithreading. A veces es una palabra aterradora. Una vez atrapamos un error muy extraño cuando formamos líneas de fechas en el fondo. Resultó que SimpleDateFormat no es seguro para subprocesos. Por lo tanto, nació lo siguiente:
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 } }
Y un ejemplo de uso (sí, esto se usa nuevamente dentro del singleton):
private val utcDateSendSafeFormat = ThreadSafeDateFormat("yyyy-MM-dd", true) val utcDateSendFormat: SimpleDateFormat get() = utcDateSendSafeFormat.formatter
Para toda la aplicación, nada ha cambiado y el problema con los hilos ha sido resuelto.
Textwatcher
Y nunca se molestó en que si necesita capturar el texto que se ingresa en EditText, entonces debe usar TextWatcher e implementar 3 (!) Métodos. No es crítico, pero no conveniente. Y todo lo decide la clase:
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) {} }
Teclado
Lo que siempre se necesita. Debe mostrar inmediatamente el teclado, luego, en algún momento, debe ocultarlo. Y luego se necesitan los siguientes dos métodos. En el segundo caso, debe pasar la vista raíz.
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) }
Y, tal vez alguien sea útil, una función para copiar cualquier línea en un portapapeles con tostadas mostrando:
fun String.toClipboard(toast: Int) { val clip = ClipData.newPlainText(this, this) (App.APP.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)?.setPrimaryClip(clip) Toast(toast) }
RecyclerView y TableLayout
Y al final de este pequeño artículo, quiero compartir lo que recientemente tuve que resolver. Tal vez alguien sea útil.
Los datos iniciales son los siguientes:
- 1k + datos para mostrar.
- Cada "celda" consta de aproximadamente 10 campos.
- es necesario capturar deslizar, hacer clic, doble clic, clic largo.
- y ... los datos se actualizan cada 300 a 500 milisegundos.
Si te olvidas del primer punto. entonces la solución más funcional es TableLayout. ¿Por qué no RecyclerView? Y por 3 y 4 puntos. Hay optimizaciones dentro de la hoja y reutiliza la vista, pero no siempre. Y al momento de crear una nueva vista, no existen controladores táctiles. Y bueno, si solo afectó el deslizamiento, pero de vez en cuando el problema se reproduce con el toque habitual. Incluso actualizar los datos directamente en la Vista no ayuda, y no a través de notificar. Por lo tanto, se decidió usar TableLayout. Y todo estuvo bien, hasta que los datos no fueron más de 100. Y luego, bienvenidos al mundo de las congelaciones.
Vi 2 formas de resolver, o enseñarle a TableLayout a reutilizar celdas y hacer magia al desplazarse. O intente hacer amigos RecyclerView y actualizaciones frecuentes. Y fui por el segundo camino. Dado que los toques y deslizamientos (principalmente debido a deslizamientos) fueron procesados por una clase autoescrita basada en View.OnTouchListener, resultó ser una solución efectiva para mover el procesamiento táctil al nivel RecyclerView al anular el método dispatchTouchEvent.
El algoritmo es simple:
- coger toque
- determinar qué niño vuela el toque con findChildViewUnder (x, y)
- obtener la posición del elemento del LayoutManager
- si es MotionEvent.ACTION_MOVE, entonces verificamos con la misma posición en la que trabajamos que antes o no
- realizar la lógica incrustada para tocar
Quizás en el futuro todavía habrá problemas con este método, pero en este momento todo está funcionando y esto es bueno.