Hola a todos! Mi nombre es Anatoly Varivonchik. Llevo más de un año trabajando en Badoo y mi experiencia total de desarrollo de Android es de más de cinco años.
En mi práctica, mis colegas y yo a menudo enfrentamos la necesidad de probar ideas de la manera más rápida y sencilla posible. No queremos gastar mucho esfuerzo en la implementación, porque sabemos que si el experimento no tiene éxito, desecharemos el código.
En este artículo mostraré con ejemplos reales cómo actuamos en tales situaciones y qué principios nos ayudan a tomar una decisión a favor de una solución particular al problema. El análisis de ejemplos debería ayudar a comprender nuestro patrón de pensamiento: ¿cómo puede a veces cortar atajos, acelerando el desarrollo?

El plan es este:
- Principios del enfoque de desarrollo en Badoo.
- Estudios de caso
- Sistema de diseño.
- Cuándo aplicar los principios descritos.
Este artículo es una versión de texto de mi informe en AppsConf, el video se puede ver
aquí .
Principios del enfoque de desarrollo
Cientos de millones de personas usan Badoo, por lo que no podemos implementar nuevas funciones si no estamos seguros de que a los usuarios les gustará y demuestren ser útiles.
Nuestro enfoque de desarrollo está influenciado por varios factores.
Usando pruebas A / B
Hoy tenemos docenas de pruebas A / B activas en plataformas móviles, mientras que se completan varios cientos. En consecuencia, si toma la aplicación Badoo en dos dispositivos diferentes, entonces con un alto grado de probabilidad habrá algunas diferencias entre ellos, posiblemente imperceptibles a primera vista.
¿Por qué necesitamos pruebas A / B? Es importante comprender que lo que los gerentes de producto consideran necesario, e incluso lo que nos parece obvio, no siempre es útil en realidad. A veces tenemos que eliminar el código que escribimos hace solo un mes o dos. A veces tiene sentido probar la idea de un nuevo funcional para comprender si es adecuado o no. Y si a los usuarios les gustó la funcionalidad, entonces ya podemos invertir tiempo en su desarrollo.
Reduce los costos de desarrollo
Por supuesto, queremos que todo funcione rápidamente y sea hermoso. Sin embargo, no siempre es posible lograr esto en poco tiempo. A veces lleva muchos días. Para evitar estos problemas, tratamos de ayudar a los gerentes de producto evaluando previamente el costo de las tareas e indicando qué es difícil para nosotros y qué es fácil.
La mayoría de las reglas de usuario
Imagine que tiene una función que funciona perfectamente en todos los escenarios en todos los dispositivos, pero al mismo tiempo hay un grupo de usuarios con dispositivos chinos, donde no funciona exactamente como se esperaba. En este caso, puede que no valga la pena solucionar el problema lo más rápido posible, ya que lo más probable es que tenga tareas más importantes.
¿Cómo aceleramos el desarrollo?
Veamos algunos ejemplos que ilustran cómo funcionan estos principios. Aquí presentaremos casos reales que encontramos en nuestro trabajo, así como opciones de solución consideradas.
Para empezar, te sugiero que pienses por ti mismo cómo resolver este caso. Y luego consideraré cada una de las opciones con una explicación de por qué surgió / no encajaba en nuestro caso.
Ejemplo 1. El botón de acumulación de progreso
Necesitamos mostrarle al usuario el proceso de acumulación de préstamos con el progreso de la batería con esquinas redondeadas de 0 a 1.

¿Cuáles son las opciones de solución?

Opción A. No necesitamos este icono. Debemos pedir a los diseñadores que rehagan la funcionalidad. Deje que solo se muestre algo de texto.
Opción B. Use máscaras de mapa de bits. Con la combinación correcta, obtenemos exactamente lo que necesitamos.

Opción C: solo tome algunos iconos, codifíquelos en el cliente y muestre uno de ellos.

En nuestro caso, llegamos a las soluciones B y C. Discutiremos con más detalle.

¿Por qué no la
opción A ? Podemos resolver este problema en particular, no es complicado. Usamos el mismo diseño en iOS y en la web móvil. En consecuencia, no hay razón para rechazar y decir que no hacemos esto y que necesitamos un diseño diferente.

Las máscaras de mapa de bits (
opción B ) son una solución ideal para este problema. Podemos dibujar fácilmente un rectángulo redondeado. Podemos dibujar fácilmente un rectángulo regular sobre el porcentaje de relleno que necesitamos. Queda por mezclarlos y establecer la configuración correcta. Después de eso, ambas esquinas a la izquierda desaparecerán.

En código, se parece a esto:
data class GoalInProgress(val progress: Float) private val unchargedPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) } private fun mixChargedAndUncharged(canvas: Canvas) { drawFullyCharged(canvas) drawUnchargedPart(canvas) }
Eliminé la mayor parte del código. Lea más sobre las máscaras de mapa de bits en el artículo:
https://habr.com/en/company/badoo/blog/310618/ . También aprenderá de él cómo mezclar máscaras, qué efectos lograr y cómo funciona en términos de rendimiento.
Esta solución cumple al 100% con nuestros requisitos, es decir, permite mostrar el progreso de 0 a 1.
Lo único negativo: si nunca ha hecho esto antes, tendrá que dedicar tiempo a descubrir máscaras de mapas de bits. Además, todavía tienes que jugar con ellos, ver casos extremos y probar. Creo que, en general, llevará unas cuatro horas.
Opción C. Solo tomamos algunos tipos fijos de iconos y, según el progreso, mostramos uno de ellos. Por ejemplo, si el progreso del usuario es inferior a 0,5, se mostrará un icono vacío. Obviamente, esta solución no satisface el 100% de los requisitos. Pero para su implementación, solo necesita escribir cinco líneas de código y obtener tres iconos del diseñador.
fun getBackground(goal: GoalInProgress) = when (goal.progress) { in 0.0..0.5 -> R.drawable.ic_not_filled in 0.5..0.99 -> R.drawable.ic_half_filled else -> R.drawable.ic_full_filled }
Además, esta solución es óptima en condiciones de grave falta de tiempo (como teníamos cuando lanzamos la funcionalidad de transmisión en vivo). No lleva mucho tiempo: simplemente despliégalo y reemplázalo con la hermosa solución correcta en la próxima versión. En realidad, como lo hicimos en nuestro tiempo.
Ejemplo 2. Una línea para ingresar un número de teléfono
El siguiente ejemplo es ingresar un número de teléfono. Características distintivas:
- el prefijo del país está a la izquierda;
- el prefijo no se puede eliminar;
- hay una sangría;
- el prefijo no se puede hacer clic.

Pensemos cómo se puede implementar esto.

Opción A: escriba un TextWatcher personalizado que implemente la lógica deseada. Mantendrá este prefijo, mantendrá un espacio, controlará la posición del cursor.
Opción B: dividir este componente en dos campos independientes. Desde el punto de vista de la IU, será el mismo componente.
Opción C: solicite un diseño diferente para que sea más fácil para nosotros.

Decidimos implementar la opción B. Considerar con más detalle.

Pedir un diseño diferente (opción C) es lo primero que intentamos hacer. Sin embargo, los productos insistieron en la idea inicial. Y si la empresa insiste en algún tipo de funcionalidad, nuestra tarea es implementarla.
TextWatcher personalizado (opción A) solo a primera vista parece una solución simple, pero de hecho hay muchos casos extremos que deben manejarse. Por ejemplo, necesitas:
- intercepte clics en el prefijo de alguna manera, luego para cambiar la posición del cursor;
- sangría adicional;
- prohibir la eliminación para que el usuario no pueda eliminar ni el espacio ni el prefijo del país.
Hacer todo esto, por supuesto, es posible, pero bastante difícil. Parece que hay una opción más fácil.
Y realmente fue encontrado:
<merge xmlns:android="http://schemas.android.com/apk/res/android" tools:parentTag="android.widget.LinearLayout"> <TextView android:id="@+id/country_code" /> <EditText android:id="@+id/phone_number" /> </merge>
Simplemente dividimos este componente en dos partes: TextView y EditText. Programáticamente, en TextView, establecemos el fondo de tal manera que obtengamos exactamente el diseño que los productos esperan.
Lo único que vale la pena considerar es que en Android, por defecto, el ancho de la línea de fondo aumenta cuando EditText está enfocado. Pero nos suscribimos fácilmente a un cambio de enfoque y cambiamos el fondo en el prefijo. Nada complicado:
phoneNumber.setOnFocusChangeListener { _, hasFocus -> countryCode.setBackgroundResource(background(hasFocus)) } private fun background(hasFocus: Boolean) = when (hasFocus) { true -> R.drawable.phone_input_active false -> R.drawable.phone_input_inactive }
Esta solución tiene varias ventajas:
- no es necesario manejar clics en el prefijo;
- no es necesario trabajar con la posición del cursor; siempre está en un campo separado.
- Mucho menos casos extremos y problemas surgen con tal implementación.
Ejemplo 3. Problema con autocompletar
Como puede ver en la animación de la izquierda, el autocompletado no funciona como nos gustaría. Nos gustaría que todo se parezca a la animación de la derecha.


Pensemos en lo que podemos hacer al respecto.

Opción A: Parece que este es un caso raro que nadie soluciona. ¿Por qué no hacemos lo mismo?
Opción B: TextWatcher personalizado funcionará mucho mejor y resolverá todos nuestros problemas.
Opción C: eliminar el límite en el número de caracteres (como se ve en la animación, tenemos un cierto número de caracteres en este componente). Enviaremos el número de teléfono completo con el prefijo al servidor y luego dejaremos que el servidor decida si el número es válido o no.
Opción D: tomar N caracteres del final.
Nos decidimos por la opción D.

Opción A. Busqué en varias aplicaciones grandes. Nadie parece arreglarlo.
Sin embargo, en el futuro, más y más campos se llenarán con un autófilo. Cuanto antes resuelva este problema, más leales serán sus usuarios y más agradable será usar la aplicación usted mismo. Por ejemplo, estoy muy satisfecho cuando paso por toda la pantalla con dos clics.
Opción B. Es realmente más fácil implementar TextWatcher personalizado, ya que no hay tantos scripts de borde como en el ejemplo anterior. Puede interceptar fácilmente el texto insertado. Solo hay un pequeño problema: en algunos países hay alias locales. Por ejemplo, +44 y 0 significan lo mismo.
TextWatcher personalizado no puede ayudar aquí. En este caso, debe escribir una lógica adicional y también pedirle al servidor que devuelva todos los posibles alias locales para este país. Para resolver este problema, deberá realizar cambios en el protocolo de comunicación con el servidor y luego implementar esta funcionalidad en el servidor. Esto llevará más tiempo que hacer algo en el cliente. Parece que hay una solución más simple (y llegaremos a ella).
Opción C. Eliminamos el límite en el número de caracteres, y luego el servidor lo valida. Esta es una gran opción. Está bien que el prefijo se muestre dos veces. Si el usuario continúa con el siguiente paso y el número de teléfono se determina de manera válida, entonces, en principio, no hay problemas.
Pero aún hay un inconveniente. Imagine que el usuario no usa el autocompletado, sino que simplemente ingresa su número de teléfono. En este caso, si hay un límite en el número de caracteres, será mucho más difícil para él duplicar accidentalmente un dígito; al final verá que el último dígito no se ha impreso. Por lo tanto, decidimos no usar este método.
Opción D. Usar N caracteres desde el final nos pareció una solución adecuada.
class DigitsTrimStartFilter(private val max: Int) : InputFilter { override fun filter(...): CharSequence? { val s = source.subSequence(start, end).filter { it.isDigit() } val keep = max - (dest.length - (dend - dstart)) return when { keep <= 0 -> "" keep >= s.length -> null
Tenemos la longitud máxima de un número de teléfono que se puede insertar. Estamos escribiendo una clase simple, está encapsulada y puede reutilizarse en otros lugares. Además, cuando cualquier otro desarrollador vea el código, rápidamente descubrirá qué es qué. Pero hay otros dos problemas.
En primer lugar, hay países con diferentes números de teléfono. En tales casos, nuestra solución mostrará un dígito adicional del prefijo. En segundo lugar, si un usuario inserta un prefijo para otro país con un archivo automático, puede ocurrir la misma situación. El segundo caso nos parece raro, porque el servidor inicialmente devuelve el número de teléfono dependiendo del país donde se encuentre el usuario. Sin embargo, si entendemos que esto es un problema, tendremos que cambiar el protocolo en el servidor para que devuelva una lista de todos los números a la vez y escribir una lógica adicional (ahora no consideramos que sea necesario).
Ejemplo 4. Componente de entrada de fecha
Los diseñadores y productos desean ver una máscara para ingresar una fecha de la siguiente manera:

Pensemos cómo se puede implementar esto.

Opción A: solo hazlo. La tarea parece simple, es fácil de resolver, no deben surgir problemas.
Opción B: use la biblioteca de máscaras. Ella nos conviene en esta situación.
Opción C: deshabilitar el control de la posición del cursor. Por lo tanto, simplificamos un poco los requisitos y nos será más fácil implementar esta funcionalidad.
Opción D: use el componente de entrada de fecha estándar que está en Android y que todos vimos.
Llegamos a la opción C.

Opción A. La tarea parece simple. Seguramente no somos los primeros en implementar esta funcionalidad. ¿Por qué no ver si hay una solución adecuada en Internet?

Tomamos esta solución, la agregamos al código, la ejecutamos. Comenzamos a probar:
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (edited) { edited = false return } var working = getEditText() working = manageDateDivider(working, 2, start, before) working = manageDateDivider(working, 5, start, before) edited = true input.setText(working) input.setSelection(input.text.length) }
A primera vista parece que más o menos nos conviene. Es cierto que hay problemas con el hecho de que la posición del cursor salta al final después de cada cambio. Luego comenzamos a probar con más cuidado y entendemos que no todo es tan bueno y que hay escenarios no contabilizados.

Todos necesitamos refinar esto. Me gustaría evitar esto, porque entonces este es el trabajo de los probadores, y de nuevo el nuestro es corregir errores, etc.
Opción B. ¿Por qué no utilizar una biblioteca preparada para máscaras de
decoro o
input-mask-android ? Probaron todos los escenarios, puedes reutilizar todo y disfrutar de la vida. Si su proyecto tiene una biblioteca para máscaras o está listo para agregarlo, entonces esta es una gran solución.
No teníamos una biblioteca. Y arrastrarlo al proyecto por el bien de un componente pequeño, que no se usa en ningún otro lado, parecía superfluo.
Opción D. Use el componente de entrada de fecha estándar.

Esta parece ser la solución más inteligente. Todo está bien con él, excepto un pequeño defecto. Cuando abre este componente, ya tiene un valor predefinido, una fecha válida. Si establece una fecha válida para ir al siguiente paso, por ejemplo, el 1 de enero de 1980, recibirá millones de usuarios que nacieron ese día. De lo contrario, obtendrá muchos errores idénticos: el usuario no puede registrarse porque es demasiado viejo o demasiado joven.
Por esta razón, en un momento abandonamos el diálogo estándar para ingresar fechas en el formulario de registro en Badoo. El número de errores sobre una fecha no válida se ha reducido tres veces.

Y una pequeña desventaja más. Parece que solo los usuarios avanzados pueden pasar del primer estado al segundo:

Si no solo los usuarios avanzados usan su aplicación, se clasificarán mes tras mes en busca de la fecha deseada.
Por lo tanto, decidimos que la opción A no es tan mala. Solo necesita refinarlo y simplificarlo un poco:
class DateEditText : AppCompatEditText(context, attrs, defStyleAttr) { private var canChange: Boolean = false private var actualText: StringBuilder = StringBuilder() override fun onSelectionChanged(selStart: Int, selEnd: Int) { super.onSelectionChanged(selStart, selEnd) if (!canChange) return canChange = false setSelection(actualText.length) canChange = true } }
Las desventajas de la opción A comenzaron a aparecer cuando el usuario cambió la posición del cursor. Y pensamos: "¿Por qué dar la oportunidad de mover este cursor?" y simplemente prohibido hacer esto.

Entonces resolvimos todos los problemas. Los productos tienen una implementación que les conviene. Y si en el futuro deciden que aún necesitas dar la oportunidad de eliminar personajes del medio, lo haremos.
Ejemplo 5. Herramientas en la pantalla de transmisión de video
Al iniciar la transmisión de video, los productos querían mostrar información sobre herramientas para enseñar a los usuarios cómo usar la funcionalidad.
En el momento de la implementación de la función, teníamos seis tipos de información sobre herramientas. Al mismo tiempo, no debe haber más de uno en la pantalla. La información sobre herramientas llegó dinámicamente en momentos aleatorios desde el servidor. Algunos tuvieron que repetirse. Si apareció la información sobre herramientas, pero el usuario no hizo clic en ella, luego de N minutos debería haber aparecido nuevamente.

Todo esto parecía bastante difícil de implementar. Le pedimos al producto algunas cosas.
Primero, agregue un clasificador, priorizando la información sobre herramientas. En cualquier caso, surgirán situaciones en las que tanto la información sobre herramientas como la otra desean aparecer al mismo tiempo y se debe elegir una de ellas. En consecuencia, necesitamos prioridades. En segundo lugar, pedimos una pequeña simplificación: mantener un temporizador solo para la información sobre herramientas de mayor prioridad.
Anteriormente, los temporizadores de repetición de información sobre herramientas eran independientes:

Pedimos que admitiera el temporizador solo para la información sobre herramientas de mayor prioridad:

En consecuencia, el temporizador funcionó para nosotros solo para la información sobre herramientas 1. Tan pronto como apareció la información sobre herramientas 1, se eliminó y comenzó el siguiente proceso.

Por lo tanto, simplificamos los requisitos: nos resultó mucho más fácil implementar la función, y para los evaluadores fue más fácil probarla. Al final, nos dimos cuenta de que esta decisión se adapta a todos.
Ejemplo 6. Reordenando fotos
Este diseño nos llegó:

Llegamos a la conclusión de que es bastante difícil de implementar, tendremos que pasar tres días en el desarrollo y pensamos: "¿Por qué deberíamos hacer esto si no sabemos si el usuario lo necesita?" Sugerimos comenzar con una versión simplificada y evaluar cuánto demanda esta característica.

Resultó que los usuarios están interesados en esta funcionalidad. Después de eso, mejoramos la representación al estado que tenía el diseño original.
Total:
- nos hemos protegido a nosotros mismos y a la compañía del riesgo de gastar demasiado tiempo de trabajo en una función que puede ser inútil;
- los requisitos del producto como resultado se implementaron completamente.
Ejemplo 7. Componente de entrada de PIN
Estamos desarrollando no solo la aplicación Badoo, también tenemos otras aplicaciones con un diseño completamente diferente. Y en las tres aplicaciones usamos el mismo componente para ingresar el código PIN:

Desde una perspectiva UX, un componente debe comportarse igual. Sin embargo, en diferentes aplicaciones, diferentes fuentes, sangrías, incluso diferentes fondos. Me gustaría no copiar esto en cada aplicación, sino reutilizarlo. El sistema de diseño nos puede ayudar con esto.
Un sistema de diseño es un conjunto de reglas de UX sobre cómo deben comportarse ciertos componentes. Por ejemplo, hemos declarado claramente que cada botón debe tener ciertos estados y que debe comportarse de cierta manera.



Puede obtener más información sobre el sistema de diseño del informe de
Rudy Artyom .
Mientras tanto, regrese al componente de entrada de PIN. Que nos gustaria
- Corregir el comportamiento del teclado;
- la capacidad de personalizar completamente la interfaz de usuario para que se vea diferente en diferentes aplicaciones;
- recibir un flujo estándar de datos de este componente, como de un EditText normal.

¿Cuáles fueron nuestras opciones de solución?

Opción A: use cuatro EditText separados, donde cada elemento PIN será un EditText separado.
Opción B: use un EditText, agregue un poco de creatividad y obtenga lo que necesita.
Elegimos la opción B.

Opción A. Hay problemas con cuatro EditTexts separados. Android agrega relleno adicional en todos los lados, que tendremos que manejar correctamente. Además, deberá implementar un toque largo para que el usuario pueda eliminar todo el código PIN. Tendremos que trabajar manualmente con foco y manejar la eliminación de caracteres. Parece bastante complicado.
Por lo tanto, decidimos hacer un poco de trampa y creamos un EditText invisible de tamaño 0 por 0, que será la fuente de datos:
private fun createActualInput(lengthCount: Int) = EditText(context) .apply { inputType = InputType.TYPE_CLASS_NUMBER isClickable = false maxHeight = 0 maxWidth = 0 alpha = 0F addOrUpdateFilter(InputFilter.LengthFilter(lengthCount)) } private fun createPinItems(count: Int) { actualText = createActualInput(count) actualText.textChanges() .subscribe { updatePins(it.toString()) pinChangesRelay.accept(it) } overlay.clicks().subscribe { focus() } }
Cada dígito del código PIN se agregará mediante programación. Debido a esto, podemos dibujar cualquier tipo de interfaz de usuario, poner cualquier sangría, etc. Después de que el usuario hace clic en el componente, ponemos el foco en nuestro EditText. Por lo tanto, obtenemos un teclado que funciona correctamente.
Además, nos suscribimos para cambiar el texto del EditText invisible y mostrarlo en la interfaz de usuario. Después de eso, es fácil para nosotros obtener el flujo de datos de este componente. De hecho, reutilizamos el Android EditText estándar, solo agregamos ligeramente la lógica necesaria.
Resumen
Estos principios no siempre son aplicables. Te daré las condiciones bajo las cuales funcionarán bien.
- El desarrollador tiene la capacidad de influir en la funcionalidad . De lo contrario, solo necesita completar la tarea.
- El desarrollador trabaja para una compañía de productos , donde las características se comparten activamente y se lanzan rápidamente , y las hipótesis sobre estas características se verifican rápidamente . En tales condiciones, estos principios se manifiestan con toda su fuerza, ya que nuevamente, desde el principio, no podemos estar 100% seguros de qué actualizaciones complacerán a los usuarios y cuáles no.
- El desarrollador tiene la capacidad de descomponer tareas . Estos principios son una solución lógica en una situación en la que los gerentes de producto y los desarrolladores tienen comunicación bidireccional, lo que permite a ambas partes encontrar lo que se puede y se debe rehacer.
- Outsourcing En casos excepcionales, el cliente puede estar interesado en una propuesta, por ejemplo, para reducir el tiempo que lleva completar una tarea simplificando parte de la funcionalidad.
¿Cómo usar estos principios? Desafortunadamente, fuera de contexto, es difícil hacer recomendaciones. Sin embargo, puedo aconsejarle que preste atención a las siguientes cosas.
Puede tener problemas con UI / UX, como en la mayoría de los ejemplos, o puede tener problemas con la lógica empresarial, como en el ejemplo de información sobre herramientas. Debe intentar descomponer su tarea en varias subtareas pequeñas y luego evaluarlas.
Después de eso, puede averiguar exactamente dónde estarán los problemas. A continuación, discute con sus colegas cómo resolverlos. Quizás algo se pueda simplificar. O tal vez simplemente no conozca alguna solución simple que sus colegas ya conozcan. En la siguiente etapa, coordina con los productos una solución alternativa. Si están satisfechos, implemente su oferta.
Quiero agregar que todas las personas a veces cometen errores.
Quizás los productos han establecido una tarea que no resuelve su verdadero problema. Quizás los diseñadores te enviaron un diseño para iOS. Quizás el protocolo de comunicación entre el servidor y el cliente es absolutamente inconveniente para el cliente. Necesitamos hablar sobre todas estas cosas, necesitamos discutirlas y dar retroalimentación. Por lo tanto, aumentará su valor como desarrollador y su utilidad para la empresa. Es decir, es Win-Win para ambas partes.Enlaces para contactarme:PS , . , , . — , . ? .