
Hace aproximadamente un año, comencé a usar Kotlin en mis proyectos de Android. Quería probar algo nuevo que sería interesante aprender. Entonces me encontré con Anko . En ese momento, escribir la interfaz de usuario en xml era bastante aburrido. Siempre me gustó escribir la interfaz con mis manos, sin recurrir a WYSIWYG y al marcado xml utilizado en Android Studio. Lo único negativo es que tendrá que reiniciar la aplicación para verificar cualquier cambio. Puede usar un complemento que muestra cómo se verá ui sin iniciar la aplicación, pero me pareció bastante extraño. También tiene la genial habilidad de convertir xml a Anko Layouts DSL.
El mayor inconveniente de la biblioteca es la falta casi completa de documentación. Para descubrir cómo usarlo correctamente, a menudo tenía que buscar en la fuente. En este artículo, se discutirá en detalle el desarrollo de aplicaciones usando Anko Layouts y Anko Coroutines.
La propia biblioteca de Anko se divide en 4 partes independientes:
- Anko Layouts: construcción de una interfaz de usuario.
- Anko Commons - Herramientas y características útiles.
- Anko SQLite: trabaje con la base de datos SQLite.
- Anko Coroutines son herramientas útiles para trabajar con corutinas.
Para agregar una biblioteca al proyecto, solo agregue una línea dependiendo del proyecto:
implementation "org.jetbrains.anko:anko:$anko_version"
donde anko_version
es la versión actual de la biblioteca, registrada en el archivo build.gradle en el nivel del proyecto:
ext.anko_version='0.10.8'
Diseños de Anko
Anko Layouts le permite desarrollar aplicaciones de UI para Android de manera más eficiente de lo que estaba usando Java.
El jugador principal en el campo es la AnkoComponent<T>
con un único método createView que acepta AnkoContext<T>
y devuelve una Vista. Es en este método que se crea toda la IU. La AnkoContext<T>
es un contenedor sobre el ViewManager . Más sobre esto será más tarde.
Al comprender un poco cómo funciona AnkoComponent<T>
, intentemos crear una interfaz de usuario simple en la clase de nuestra Actividad. Vale la pena aclarar que dicha ortografía "directa" de la interfaz de usuario solo es posible en Actividad, ya que se escribe una función de extensión separada ankoView para ella, en la que se llama al método addView , y AnkoContextImpl<T>
se crea en el propio método con el parámetro setContentView = true
.
class AppActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) verticalLayout { lparams(matchParent, matchParent) gravity = Gravity.CENTER textView("Cool Anko TextView") { gravity = Gravity.CENTER } } }
Obviamente, para algo más de un TextView, el método onCreate se convertirá rápidamente en un volcado. Intentemos separar la clase Actividad de la IU. Para hacer esto, cree otra clase en la que se describirá.
class AppActivityUi: AnkoComponent<AppActivity> { override fun createView(ui: AnkoContext<AppActivity>): View = with(ui) { verticalLayout { lparams(matchParent, matchParent) gravity = Gravity.CENTER textView("Cool Anko TextView") { gravity = Gravity.CENTER } } } }
Ahora, para pasar nuestra IU a nuestra Actividad, puede usar
AppActivityUi().setContentView(this)
Ok, pero ¿qué pasa si queremos crear una interfaz de usuario para el fragmento? Para hacer esto, puede usar el método createView directamente llamándolo desde el método de fragmento onCreateView. Se ve así:
class AppFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return AppFragmentUi().createView(AnkoContext.create(requireContext(), this)) } }
Como ya se mencionó, AnkoContext<T>
es un contenedor sobre el ViewManager. Su objeto complementario tiene tres métodos principales que devuelven AnkoContext<T>
. Los analizaremos con más detalle.
crear
fun <T> create(ctx: Context, owner: T, setContentView: Boolean = false): AnkoContext<T>
y su hermano gemelo
fun create(ctx: Context, setContentView: Boolean = false): AnkoContext<Context>
devuelve AnkoContextImpl<T>
.
Los métodos se utilizan en todos los casos estándar, como, por ejemplo, en los ejemplos anteriores con Actividad y Fragmento. Los más interesantes aquí son las opciones propietario y setContentView. El primero le permite pasar un Fragmento, Actividad u otra cosa específica al método de instancia createView.
MyComponent().createView(AnkoContext.create(context, myVew)) class MyComponent: AnkoComponent<View> { override fun createView(ui: AnkoContext<View>): View = with(ui) { val myView: View= ui.owner
El segundo parámetro, setContentView, intentará agregar automáticamente la vista resultante si Context es una instancia de una Activity o ContextWrapper . Si no tiene éxito, lanzará una IllegalStateException.
createDelegate
Este método puede ser muy útil, pero la documentación oficial sobre el gihab no dice nada al respecto. Lo encontré en la fuente cuando resolví el problema de hinchar las clases de IU:
fun <T: ViewGroup> createDelegate(owner: T): AnkoContext<T> = DelegatingAnkoContext(owner)
Le permite agregar el resultado del componente createView al propietario.
Considere su uso como un ejemplo. Supongamos que tenemos una clase grande que describe una de las pantallas de aplicación: AppFragmentUi.
verticalLayout { relativeLayout { id = R.id.toolbar
Lógicamente, se puede dividir en dos partes: la barra de herramientas y el contenido, AppFragmentUiToolbar y AppFragmentUiContent, respectivamente. Entonces nuestra clase principal AppFragmentUi será mucho más simple:
class AppFragmentUi: AnkoComponent<AppFragment> { override fun createView(ui: AnkoContext<AppFragment>) = with(ui) { verticalLayout { AppFragmentUiToolbar().createView(AnkoContext.createDelegate(this)) AppFragmentUiContent().createView(AnkoContext.createDelegate(this)) } } } class AppFragmentUiToolbar : AnkoComponent<_LinearLayout> { override fun createView(ui: AnkoContext<_LinearLayout>): View = with(ui.owner) { relativeLayout { id = R.id.toolbar
Tenga en cuenta que no ui, pero ui.owner se pasa a la función with
como un objeto.
Por lo tanto, tenemos el siguiente algoritmo:
- Se crea la instancia del componente.
- El método createView crea la Vista que se agregará.
- La vista resultante se agrega al propietario.
Una aproximación más cercana: this.addView(AppFragmentUiToolbar().createView(...))
Como puede ver, la opción con createDelegate es más legible.
createReusable
Parece el AnkoContext.create estándar, pero con una pequeña adición: la última vista se considera la vista raíz:
class MyComponent: AnkoComponent<MyObject> { override fun createView(ui: AnkoContext<MyObject>): View = with(ui) { textView("Some text")
En la implementación estándar, si se establece la vista raíz, un intento de establecer la segunda vista en paralelo arrojará una excepción .
El método createReusable devuelve la clase ReusableAnkoContext , que hereda de AnkoContextImpl y anula el método alreadyHasView()
.
Customview
Afortunadamente, Anko Layouts no se limita a esta funcionalidad. Si necesitamos mostrar nuestro propio CustomView, no tenemos que escribir
verticalLayout { val view = CustomView(context)
Para hacer esto, puede agregar su propio contenedor, que hará lo mismo.
Los componentes principales aquí son el método de extensión <T: View>ankoView(factory: (Context) -> T, theme: Int, init: T.() -> Unit)
desde ViewManager, Context o Activity.
- factory - una función, a la entrada de la cual se pasa el Contexto y se devuelve la Vista. De hecho, es la fábrica donde tiene lugar la creación de la Vista.
- El tema es un recurso de estilo que se aplicará a la vista actual.
- init es una función en la que se establecerán los parámetros necesarios para la Vista creada.
Agregue su implementación para nuestro CustomView
inline fun ViewManager.customView(theme: Int = 0, init: (CustomView).() -> Unit): CustomView { return ankoView({ CustomView(it) }, theme, init) }
Ahora nuestro CustomView se crea de manera muy simple:
customView { id = R.id.customview
Puede usar lparams para aplicar LayoutParams a la Vista.
textView("text") { textSize = 12f }.lparams(width = matchParent, height = wrapContent) { centerInParent() }
Vale la pena señalar que esto no es aplicable a todas las vistas: todos los métodos lparams generalmente se declaran en contenedores. Por ejemplo, _RelativeLayout es un contenedor sobre RelativeLayout . Y así para todos.
Afortunadamente, se han escrito varios contenedores para la Biblioteca de soporte de Android, por lo que solo puede incluir las dependencias en el archivo gradle.
// Appcompat-v7 (Anko Layouts) implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version" implementation "org.jetbrains.anko:anko-coroutines:$anko_version" // CardView-v7 implementation "org.jetbrains.anko:anko-cardview-v7:$anko_version" // Design implementation "org.jetbrains.anko:anko-design:$anko_version" implementation "org.jetbrains.anko:anko-design-coroutines:$anko_version" // GridLayout-v7 implementation "org.jetbrains.anko:anko-gridlayout-v7:$anko_version" // Percent implementation "org.jetbrains.anko:anko-percent:$anko_version" // RecyclerView-v7 implementation "org.jetbrains.anko:anko-recyclerview-v7:$anko_version" implementation "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version" // Support-v4 (Anko Layouts) implementation "org.jetbrains.anko:anko-support-v4:$anko_version" // ConstraintLayout implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"
Entre otras cosas, la biblioteca permite una implementación más conveniente de varios oyentes. Un pequeño ejemplo del repositorio:
seekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStopTrackingTouch(seekBar: SeekBar) = Unit })
y ahora usando Anko
seekBar { onSeekBarChangeListener { onProgressChanged { seekBar, progress, fromUser -> // do something } } }
Además, algunos oyentes admiten corutinas:
verticalLayout{ val anyCoroutineContext = GlobalScope.coroutineContext onClick(anyCoroutineContext) {
Anko coroutines
Para transferir de forma segura objetos sensibles a fugas de memoria, se asReference
método asReference
. Se basa en WeakReference y devuelve un objeto Ref.
verticalLayout{ val activity = ui.owner val activityReference: Ref<AppActivity> = activity.asReference() onClick(anyCoroutineContext) { ref().doSomething() } }
Suponga que desea agregar soporte para corutina en ViewPager.OnPageChangeListener estándar. Hagámoslo tan genial como el ejemplo de la barra de búsqueda.
Primero, cree una clase separada y herede de ViewPager.OnPageChangeListener .
class CoroutineOnPageChangeListener( private val coroutineContext: CoroutineContext = Dispatchers.Main ) : ViewPager.OnPageChangeListener { }
Almacenaremos lambdas en variables que serán llamadas por ViewPager.OnPageChangeListener .
private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null
Implementamos la inicialización de una de estas variables (el resto se realiza de la misma manera)
fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) { onPageScrollStateChanged = action }
Y al final implementamos una función con el mismo nombre.
override fun onPageScrollStateChanged(state: Int) { GlobalScope.launch(coroutineContext) { onPageScrollStateChanged?.invoke(state, coroutineContext) } }
Queda por agregar una función de extensión para que funcione
fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) { val listener = CoroutineOnPageChangerListener() listener.init() addOnPageChangeListener(listener) }
Y pegue todo esto en ViewPager
viewPager { onPageChangeListenerCoroutines { onPageScrolled { position, offset, pixels, coroutineContext ->
Código completo aquí class CoroutineOnPageChangeListener( private val coroutineContext: CoroutineContext = Dispatchers.Main ) : ViewPager.OnPageChangeListener { private var onPageScrollStateChanged: ((Int, CoroutineContext) -> Unit)? = null private var onPageScrolled: ((Int, Float, Int, CoroutineContext) -> Unit)? = null private var onPageSelected: ((Int, CoroutineContext) -> Unit)? = null fun onPageScrollStateChanged(action: ((Int, CoroutineContext) -> Unit)?) { onPageScrollStateChanged = action } fun onPageScrolled(action: ((Int, Float, Int, CoroutineContext) -> Unit)?) { onPageScrolled = action } fun onPageSelected(action: ((Int, CoroutineContext) -> Unit)?) { onPageSelected = action } override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { GlobalScope.launch(coroutineContext) { onPageScrolled?.invoke(position, positionOffset, positionOffsetPixels, coroutineContext) } } override fun onPageSelected(position: Int) { GlobalScope.launch(coroutineContext) { onPageSelected?.invoke(position, coroutineContext) } } override fun onPageScrollStateChanged(state: Int) { GlobalScope.launch(coroutineContext) { onPageScrollStateChanged?.invoke(state, coroutineContext) } } } fun ViewPager.onPageChangeListenerCoroutines(init: CoroutineOnPageChangerListener.() -> Unit) { val listener = CoroutineOnPageChangerListener() listener.init() addOnPageChangeListener(listener) }
También en la biblioteca Anko Layouts hay muchos métodos útiles, como las traducciones a varias métricas .