Olá pessoal! Meu nome é Anatoly Varivonchik. Trabalho no Badoo há mais de um ano e minha experiência total em desenvolvimento para Android é de mais de cinco anos.
Na minha prática, meus colegas e eu frequentemente enfrentamos a necessidade de testar idéias o mais rápido e simples possível. Não queremos gastar muito esforço na implementação, porque sabemos que, se o experimento não der certo, jogaremos fora o código.
Neste artigo, mostrarei com exemplos reais como agimos nessas situações e quais princípios nos ajudam a fazer uma escolha em favor de uma ou outra solução para o problema. A análise de exemplos deve ajudar a entender nosso padrão de pensamento: como às vezes você pode cortar custos, acelerando o desenvolvimento.

O plano é este:
- Princípios de abordagem de desenvolvimento no Badoo.
- Estudos de caso.
- Sistema de design.
- Quando aplicar os princípios descritos.
Este artigo é uma versão em texto do meu relatório no AppsConf. O vídeo pode ser visto
aqui .
Princípios da Abordagem de Desenvolvimento
Centenas de milhões de pessoas usam o Badoo, por isso não podemos lançar novas funcionalidades se não tivermos certeza de que os usuários gostarão e se mostrarem úteis.
Nossa abordagem de desenvolvimento é influenciada por vários fatores.
Usando testes A / B
Atualmente, temos dezenas de testes A / B ativos em plataformas móveis, enquanto várias centenas são concluídas. Portanto, se você usar o aplicativo Badoo em dois dispositivos diferentes, com um alto grau de probabilidade, haverá algumas diferenças entre eles, possivelmente imperceptíveis à primeira vista.
Por que precisamos de testes A / B? É importante entender: o que os gerentes de produto consideram necessário e até o que nos parece óbvio nem sempre é útil na realidade. Às vezes, temos que excluir o código que escrevemos apenas um mês ou dois atrás. Às vezes, faz sentido testar a idéia de um novo funcional para entender se é adequado ou não. E se os usuários gostaram da funcionalidade, já podemos investir tempo em seu desenvolvimento.
Reduzir custos de desenvolvimento
Obviamente, queremos que tudo funcione rapidamente e seja bonito. No entanto, nem sempre é possível conseguir isso em pouco tempo. Às vezes, leva muitos dias. Para evitar esses problemas, tentamos ajudar os gerentes de produto pré-avaliando o custo das tarefas e indicando o que é difícil para nós e o que é fácil.
Regra da maioria dos usuários
Imagine que você tenha uma funcionalidade que funcione perfeitamente em todos os cenários e em todos os dispositivos, mas, ao mesmo tempo, há um grupo de usuários com dispositivos chineses, onde não funciona exatamente como o esperado. Nesse caso, pode não valer a pena corrigir o problema o mais rápido possível, pois é provável que você tenha tarefas mais importantes.
Como aceleramos o desenvolvimento
Vejamos alguns exemplos que ilustram como esses princípios funcionam. Aqui, apresentaremos casos reais que encontramos em nosso trabalho, bem como opções de solução consideradas.
Para começar, sugiro que você pense como resolver esse caso. E então considerarei cada uma das opções com uma explicação de por que surgiu ou não se encaixou no nosso caso.
Exemplo 1. O botão de acumulação de progresso
Precisamos mostrar ao usuário o processo de acumular empréstimos com o progresso da bateria com cantos arredondados de 0 a 1.

Quais são as opções de solução?

Opção A. Não precisamos deste ícone. Devemos pedir aos designers para refazer a funcionalidade. Deixe apenas algum texto exibido.
Opção B. Use máscaras de bitmap. Com a combinação certa, obtemos exatamente o que precisamos.

Opção C: pegue alguns ícones, codifique-os no cliente e mostre um deles.

No nosso caso, chegamos às soluções B e C. Discutiremos mais detalhadamente.

Por que não a
opção A ? Podemos resolver esse problema em particular, não é complicado. Usamos o mesmo design no iOS e na web móvel. Portanto, não há razão para recusar e dizer que não fazemos isso e precisamos criar um design diferente.

Máscaras de bitmap (
opção B ) são uma solução ideal para esse problema. Podemos desenhar facilmente um retângulo arredondado. Podemos desenhar facilmente um retângulo regular sobre a porcentagem de preenchimento necessário. Resta misturá-los e definir as configurações corretas. Depois disso, os dois cantos à esquerda desaparecerão.

No código, parece algo como isto:
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) }
Eu apaguei a maior parte do código. Leia mais sobre máscaras de bitmap no artigo:
https://habr.com/en/company/badoo/blog/310618/ . Você também aprenderá como misturar máscaras, quais efeitos obter e como funciona em termos de desempenho.
Essa solução 100% atende aos nossos requisitos, ou seja, permite mostrar o progresso de 0 a 1.
O único aspecto negativo: se você nunca fez isso antes, precisará gastar tempo descobrindo máscaras de bitmap. Além disso, você ainda precisa brincar com eles, ver casos extremos e testar. Eu acho que no geral levará cerca de quatro horas.
Opção C. Apenas pegamos alguns tipos fixos de ícones e, dependendo do progresso, mostramos um deles. Por exemplo, se o progresso do usuário for menor que 0,5, um ícone vazio será exibido. Obviamente, esta solução não atende a 100% dos requisitos. Mas para sua implementação, você precisa escrever apenas cinco linhas de código e obter três ícones do designer.
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 }
Além disso, esta solução é ideal em condições de grave falta de tempo (como tínhamos quando lançamos a funcionalidade de transmissão ao vivo). Não leva muito tempo - basta lançá-lo e substituí-lo pela bela solução correta no próximo lançamento. Na verdade, como fizemos em nosso tempo.
Exemplo 2. Uma linha para inserir um número de telefone
O próximo exemplo é inserir um número de telefone. Características distintivas:
- o prefixo do país está à esquerda;
- o prefixo não pode ser excluído;
- existe um recuo;
- o prefixo não é clicável.

Vamos pensar em como isso pode ser implementado.

Opção A: escreva um TextWatcher personalizado que implemente a lógica desejada. Ele manterá esse prefixo, manterá um espaço, controlará a posição do cursor.
Opção B: divida este componente em dois campos independentes. Do ponto de vista da interface do usuário, será o mesmo componente.
Opção C: solicite um design diferente para facilitar para nós.

Decidimos implementar a opção B. Considere com mais detalhes.

Pedir um design diferente (opção C) é a primeira coisa que tentamos fazer. No entanto, os produtos insistiram na ideia inicial. E se a empresa insistir em algum tipo de funcionalidade, nossa tarefa é implementá-la.
O TextWatcher personalizado (opção A) apenas à primeira vista parece ser uma solução simples, mas, na verdade, existem muitos casos extremos que precisam ser tratados. Por exemplo, você precisa de:
- de alguma forma, intercepte cliques no prefixo e depois mude a posição do cursor;
- recuo adicional;
- proibir a exclusão para que o usuário não possa excluir o espaço ou o prefixo do país.
Fazer tudo isso, é claro, é possível, mas bastante difícil. Parece que existe uma opção mais fácil.
E ele realmente foi 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>
Simplesmente dividimos esse componente em duas partes: TextView e EditText. Programaticamente, no TextView, definimos o plano de fundo de forma a obter exatamente o design que os produtos esperam.
A única coisa que vale a pena considerar é que, no Android, por padrão, a largura da linha inferior aumenta quando o EditText está em foco. Mas nós facilmente assinamos uma mudança de foco e mudamos o plano de fundo no prefixo. 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 solução tem várias vantagens:
- não há necessidade de lidar com cliques no prefixo;
- não é necessário trabalhar com a posição do cursor - ele está sempre em um campo separado.
- Muito menos casos extremos e problemas surgem com essa implementação.
Exemplo 3. Problema com o Preenchimento Automático
Como você pode ver na animação à esquerda, o preenchimento automático não funciona como gostaríamos. Gostaríamos que tudo parecesse a animação à direita.


Vamos pensar no que podemos fazer sobre isso.

Opção A: Parece que este é um caso raro que ninguém corrige. Por que não fazemos o mesmo?
Opção B: o TextWatcher personalizado fará muito melhor e resolverá todos os nossos problemas.
Opção C: remova o limite do número de caracteres (como visto na animação, temos um certo número de caracteres neste componente). Enviaremos o número de telefone inteiro com o prefixo para o servidor e deixaremos que o servidor decida se o número é válido ou não.
Opção D: pegue N caracteres do final.
Nós decidimos pela opção D.

Opção A. Procurei em vários aplicativos grandes. Ninguém parece consertar isso.
No entanto, no futuro, mais e mais campos serão preenchidos com um autófilo. Quanto mais cedo você resolver esse problema, mais fiéis serão seus usuários e mais agradável será usar o aplicativo. Por exemplo, fico muito satisfeito ao percorrer a tela inteira com dois cliques.
Opção B. É realmente mais fácil implementar o TextWatcher personalizado, pois não existem tantos scripts de borda quanto no exemplo anterior. Você pode interceptar facilmente o texto inserido. Há apenas um pequeno problema: em alguns países, existem apelidos locais. Por exemplo, +44 e 0 significam a mesma coisa.
O TextWatcher personalizado não pode ajudar aqui. Nesse caso, você precisa escrever uma lógica adicional e também pedir ao servidor para retornar todos os aliases locais possíveis para esse país. Para resolver esse problema, você precisará fazer alterações no protocolo de comunicação com o servidor e implementar essa funcionalidade no servidor. Isso levará mais tempo do que fazer algo no cliente. Parece que existe uma solução mais simples (e vamos chegar a ela).
Opção C. Removemos o limite do número de caracteres - e o servidor valida. Esta é uma ótima opção. Tudo bem que o prefixo seja exibido duas vezes. Se o usuário prosseguir para a próxima etapa e o número de telefone for validamente determinado, então, em princípio, não haverá problemas.
Mas ainda há um problema. Imagine que o usuário não use o preenchimento automático, mas simplesmente digite seu número de telefone. Nesse caso, se houver um limite no número de caracteres, será muito mais difícil duplicar acidentalmente um dígito - no final, ele verá que o último dígito não foi impresso. Portanto, decidimos não usar esse método.
Opção D. O uso de N caracteres no final nos pareceu uma solução adequada.
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
Temos o tamanho máximo de um número de telefone que pode ser inserido. Estamos escrevendo uma classe simples, ela é encapsulada e pode ser reutilizada em outros lugares. Além disso, quando qualquer outro desenvolvedor visualizar o código, ele descobrirá rapidamente o que é o quê. Mas existem outros dois problemas.
Em primeiro lugar, existem países com diferentes números de telefone. Nesses casos, nossa solução exibirá um dígito extra do prefixo. Em segundo lugar, se um usuário inserir um prefixo para outro país com um arquivo automático, a mesma situação pode ocorrer. O segundo caso nos parece raro, porque o servidor retorna inicialmente o número de telefone, dependendo do país em que o usuário está localizado. No entanto, se entendermos que isso é um problema, teremos que alterar o protocolo no servidor para que ele retorne uma lista de todos os números de uma vez e escreva uma lógica adicional (agora não consideramos necessário).
Exemplo 4. Componente de entrada de data
Designers e produtos desejam ver uma máscara para inserir uma data da seguinte maneira:

Vamos pensar em como isso pode ser implementado.

Opção A: basta fazê-lo. A tarefa parece simples, fácil de resolver, sem problemas.
Opção B: use a biblioteca de máscaras. Ela nos convém nesta situação.
Opção C: desativa o controle da posição do cursor. Assim, simplificamos um pouco os requisitos e será mais fácil implementar essa funcionalidade.
Opção D: use o componente de entrada de data padrão que está no Android e que todos vimos.
Chegamos à opção C.

Opção A. A tarefa parece simples. Certamente não somos os primeiros a implementar essa funcionalidade. Por que não ver se há uma solução adequada na Internet.

Nós pegamos essa solução, adicionamos ao código, executamos. Começamos a testar:
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) }
À primeira vista, parece que mais ou menos nos convém. É verdade que há problemas com o fato de a posição do cursor pular para o final após cada alteração. Começamos a testar com mais cuidado e compreendemos que nem tudo é tão bom e há cenários inexplicáveis.

Todos nós precisamos refinar isso. Eu gostaria de evitar isso, porque esse é o trabalho dos testadores e, novamente, o nosso é o de corrigir bugs, etc.
Opção B. Por que não usar uma biblioteca pronta para máscaras de
decoro ou
input-mask-android ? Eles testaram todos os cenários, você pode simplesmente reutilizar tudo e aproveitar a vida. Se você tem uma biblioteca de máscaras em seu projeto ou está pronto para adicioná-lo, essa é uma ótima solução.
Não tínhamos biblioteca. E arrastá-lo para o projeto em prol de um pequeno componente, que não é usado em nenhum outro lugar, parecia supérfluo.
Opção D. Use o componente de entrada de data padrão.

Esta parece ser a solução mais inteligente. Está tudo bem com ele, exceto por uma pequena falha. Ao abrir esse componente, você já possui algum valor predefinido, alguma data válida. Se você definir uma data válida para a próxima etapa, por exemplo, 1º de janeiro de 1980, receberá milhões de usuários que nasceram naquele dia. Caso contrário, você receberá muitos erros idênticos: o usuário não pode se registrar porque é muito velho ou muito jovem.
Por esse motivo, abandonamos o diálogo padrão para inserir datas no formulário de registro no Badoo. O número de erros sobre uma data inválida foi reduzido três vezes.

E mais um pequeno sinal de menos. Parece que apenas usuários avançados podem passar do primeiro estado para o segundo:

Se não apenas usuários avançados usarem seu aplicativo, eles serão classificados mês após mês em busca da data desejada.
Portanto, decidimos que a opção A não é tão ruim. Você só precisa refinar e simplificar um pouco:
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 } }
As desvantagens da opção A começaram a aparecer quando o usuário alterou a posição do cursor. E pensamos: "Por que dar a oportunidade de mover esse cursor?" e simplesmente banido de fazer isso.

Então resolvemos todos os problemas. Os produtos têm uma implementação que lhes convém. E se no futuro eles decidirem que você ainda precisa dar a oportunidade de remover personagens do meio, nós faremos isso.
Exemplo 5. Ferramentas na tela de transmissão de vídeo
Ao iniciar o streaming de vídeo, os produtos queriam mostrar dicas de ferramentas para ensinar aos usuários como usar a funcionalidade.
No momento da implementação do recurso, tínhamos seis tipos de dicas de ferramentas. Ao mesmo tempo, não deve haver mais de um na tela. As dicas de ferramentas chegaram dinamicamente em horários aleatórios a partir do servidor. Alguns tiveram que ser repetidos. Se a dica de ferramenta apareceu, mas o usuário não clicou nela, depois de N minutos, ela deveria ter aparecido novamente.

Tudo isso parecia bastante difícil de implementar. Pedimos algumas coisas ao produto.
Primeiro, adicione um classificador, priorizando as dicas de ferramentas. Em qualquer caso, surgirão situações em que uma e a outra dica de ferramenta deseja aparecer ao mesmo tempo e uma delas deve ser escolhida. Nesse sentido, precisamos de prioridades. Em segundo lugar, pedimos uma pequena simplificação: manter um cronômetro apenas para a dica de ferramenta de maior prioridade.
Anteriormente, os temporizadores de repetição de dicas de ferramentas eram independentes:

Pedimos para dar suporte ao cronômetro apenas para a dica de ferramenta de maior prioridade:

Consequentemente, o timer funcionou para nós apenas para a dica de ferramenta 1. Assim que a dica de ferramenta 1 apareceu, ela foi removida e o próximo processo foi iniciado.

Assim, simplificamos os requisitos: ficou muito mais fácil implementar o recurso e, para os testadores, foi mais fácil testá-lo. No final, percebemos que essa decisão se adequa a todos.
Exemplo 6. Reordenando fotos
Este design chegou até nós:

Chegamos à conclusão de que é bastante difícil de implementar, teremos que passar três dias no desenvolvimento e pensamos: "Por que devemos fazer isso se não sabemos se o usuário precisa?" Sugerimos começar com uma versão simplificada e avaliar quanto esse recurso está em demanda.

Descobriu-se que os usuários estão interessados nessa funcionalidade. Depois disso, aprimoramos a nova renderização para o estado que estava no design original.
Total:
- protegemos a nós mesmos e à empresa do risco de gastar muito tempo de trabalho em um recurso que pode ser inútil;
- Como resultado, os requisitos do produto foram totalmente implementados.
Exemplo 7. Componente de entrada do PIN
Estamos desenvolvendo não apenas o aplicativo Badoo - também temos outros aplicativos com um design completamente diferente. E nos três aplicativos, usamos o mesmo componente para inserir o código PIN:

Da perspectiva do UX, um componente deve se comportar da mesma maneira. No entanto, em diferentes aplicações, diferentes fontes, recuos e até diferentes origens. Gostaria de não copiar isso em cada aplicativo, mas reutilizá-lo. O sistema de design pode nos ajudar com isso.
Um sistema de design é um conjunto de regras de UX sobre como certos componentes devem se comportar. Por exemplo, afirmamos claramente que cada botão deve ter determinados estados e que deve se comportar de uma certa maneira.



Você pode aprender mais sobre o sistema de design no relatório de
Rudy Artyom .
Enquanto isso, volte ao componente de entrada do PIN. O que gostaríamos?
- Corrigir o comportamento do teclado;
- a capacidade de personalizar completamente a interface do usuário para que pareça diferente em diferentes aplicativos;
- receba um fluxo padrão de dados desse componente, como de um EditText regular.

Quais eram as nossas opções de solução?

Opção A: Use quatro EditText separados, em que cada elemento do PIN será um EditText separado.
Opção B: use um EditText, adicione um pouco de criatividade - e obtenha o que você precisa.
Escolhemos a opção B.

Opção A. Há problemas com quatro EditTexts separados. O Android adiciona preenchimento extra por todos os lados, que precisaremos manipular corretamente. Além disso, você precisará implementar um toque longo para que o usuário possa excluir todo o código PIN. Teremos que trabalhar manualmente com foco e lidar com a exclusão de caracteres. Parece bastante complicado.
Portanto, decidimos trapacear um pouco e criamos um EditText invisível de tamanho 0 por 0, que será a fonte de dados:
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 do código PIN será adicionado programaticamente. Devido a isso, podemos desenhar qualquer tipo de interface do usuário, colocar qualquer recuo, etc. Depois que o usuário clicar no componente, colocamos o foco em nosso EditText. Assim, obtemos um teclado funcionando corretamente.
Além disso, assinamos para alterar o texto do EditText invisível e exibi-lo na interface do usuário. Depois disso, é fácil obter o fluxo de dados desse componente. De fato, reutilizamos o Android EditText padrão, apenas adicionamos um pouco a lógica necessária.
Sumário
Esses princípios nem sempre são aplicáveis. Eu darei as condições sob as quais eles funcionarão bem.
- O desenvolvedor tem a capacidade de influenciar a funcionalidade . Caso contrário, ele só precisa concluir a tarefa.
- O desenvolvedor trabalha para uma empresa de produtos , onde os recursos compartilham ativamente e são liberados rapidamente , e as hipóteses sobre esses recursos são rapidamente verificadas . Sob tais condições, esses princípios se manifestam com força total, uma vez que, desde o início, não podemos ter 100% de certeza de quais atualizações agradarão aos usuários e quais não.
- O desenvolvedor tem a capacidade de decompor tarefas . Esses princípios são uma solução lógica em uma situação em que gerentes e desenvolvedores de produtos têm comunicação bidirecional, o que permite que ambas as partes encontrem o que pode e deve ser refeito.
- Terceirização . Em casos raros, o cliente pode estar interessado em uma proposta, por exemplo, para reduzir o tempo necessário para concluir uma tarefa, simplificando parte da funcionalidade.
Como usar esses princípios? Infelizmente, fora do contexto, é difícil fazer recomendações. No entanto, posso aconselhá-lo a prestar atenção às seguintes coisas.
Você pode ter problemas com a interface do usuário / UX, como na maioria dos exemplos, ou com a lógica de negócios, como no exemplo da dica de ferramenta. Você precisa tentar decompor sua tarefa em várias subtarefas pequenas e avaliá-las.
Depois disso, você pode descobrir exatamente onde estarão os problemas. Em seguida, você discute com os colegas como resolvê-los. Talvez algo possa ser simplificado. Ou talvez você simplesmente não saiba sobre uma solução simples que seus colegas já conhecem. Na próxima etapa, você coordena com os produtos uma solução alternativa. Se eles estiverem satisfeitos, implemente sua oferta.
Quero acrescentar que todas as pessoas às vezes cometem erros.
Talvez os produtos tenham definido uma tarefa que não resolve seu problema real. Talvez os designers tenham enviado um design para iOS. Talvez o protocolo de comunicação entre o servidor e o cliente seja absolutamente inconveniente para o cliente. Precisamos conversar sobre todas essas coisas, precisamos discuti-las e dar feedback. Assim, você aumentará seu valor como desenvolvedor e sua utilidade para a empresa. Ou seja, é Win-Win para ambas as partes.Links para entrar em contato comigo:PS , . , , . — , . ? .