Parte 1Saudações!
Hoje, como sempre, vamos falar sobre a criação de aplicativos móveis com a estrutura Kivy e Python. Em particular, o foco será criar um cliente móvel para um recurso da Internet e publicá-lo no Google Play. Vou falar sobre os problemas que um novato e um desenvolvedor experiente podem ter, que decidiram se dedicar ao desenvolvimento de plataforma cruzada com o Kivy, o que pode e não deve ser feito na programação com o Python para Android.
Certa manhã, encontrei no meu e-mail no Habré uma carta perguntando se eu poderia usar o Python e o Kivy para “recriar o site svyatye.com em um aplicativo móvel, para que as pessoas possam lê-lo e usá-lo offline” com a publicação subsequente do cliente em Loja de aplicativos do Google Play. Seguindo o link e navegando no recurso, que acabou sendo uma grande biblioteca de citações, imaginei como ficaria em uma apresentação móvel e como criaria listas de "mais de 30.236 ditados dos santos pais e professores da igreja", apesar do tamanho das citações, às vezes , atingiu mais de 10.000 caracteres (5-6 páginas de texto impresso). Desde que trabalho com Kivy há muito tempo, percebi rapidamente como e o que faria. Portanto, ele respondeu ao cliente que não seria difícil fazer esse pedido. No entanto, surgiram dificuldades que discutirei abaixo ...
Nenhuma especificação técnica foi fornecida. O único requisito é que o aplicativo funcione como um relógio. Sem prazos. Também não havia layouts de interface. "Tudo deve ser o mais simples possível, sem animações, transformações e outras cascas, em uma palavra, o mais austero possível." Bem, melhor ainda. Além disso, minha decisão já está madura - o aplicativo usará um objeto RecycleView, que exibirá categorias, subcategorias, listas de autores de citações e as próprias citações.
Listas
No entanto, o RecycleView, que permite abrir enormes listas de milhares em uma fração de segundo, comportou-se de maneira bem diferente do desejado. Não, não houve problemas com a abertura de listas de cotações, tudo funcionou rapidamente, eu nem carreguei novas cotações com a janela "Aguardar", como no site, porque a lista de cotações da categoria selecionada foi renderizada instantânea e completamente. O problema era diferente - o cliente insistiu que o texto da cotação na lista fosse exibido por inteiro e que o RecycleView não era totalmente apropriado aqui. O fato é que o princípio de operação desse widget é o seguinte: um objeto é criado em toda a lista, que é simplesmente clonada e, como resultado, temos uma velocidade incrível de renderizar a lista, por maior que seja. Mas há uma coisa: a altura do item da lista deve ser fixada e conhecida com antecedência. Mas se você precisar calcular dinamicamente a altura do próximo item da lista ao rolar, como no meu caso, haverá um atraso perceptível - a lista é congelada por uma fração de segundo, o que, veja você, não está pronto para a produção.
Com o luto pela metade, consegui convencer o cliente a uma lista com uma visualização de cotações, cujo texto seria aberto inteiramente por tapu ao texto da visualização, como foi feito em quase todos os fóruns, não porque o RecycleView não pudesse lidar com a tarefa, mas porque era o mais lógico: rolar o texto de várias páginas da citação, especialmente se a citação não interessava ao usuário, do meu ponto de vista, não estava correto.
Fig. 1
Visualização e texto completo ao gravar na visualização de cotaçãoEssa opção funcionou muito rapidamente, mas ... o cliente não gostou ... Eu tive que usar um ScrollView lento, que renderiza a lista ANTES de ser exibida na tela, o que significa que não congela a rolagem da lista de cotações, pois calcula e renderiza todos os parâmetros dos elementos da lista antecipadamente, o que, Naturalmente, isso afetará a velocidade com que a lista é exibida na tela. Normalmente, o desempenho é colocado em primeiro lugar, e aqui eles me dizem: "seja mais lento".
Bem, não discuti mais e, embora realmente não gostasse dessa solução, tive que transferir tudo para o ScrollView. Como, como eu disse, o ScrollView é muito lento, foi decidido exibir cotações em partes de dez cada uma com carregamento automático adicional das próximas dez.
Indo um pouco à frente, direi que, quando os primeiros feedbacks dos usuários vieram com uma solicitação, eles disseram que os favoritos não machucariam, como me parece, o cliente ainda duvidava da decisão de usar o ScrollView, como se deixássemos as visualizações de cotações e o RecycleView, sem problemas, eles poderiam restaurar instantaneamente a partir dos marcadores visualizados anteriormente pelo usuário nas listas de cotações da sessão anterior, independentemente de quanto tempo durassem. E com o ScrollView, o usuário simplesmente envelhece enquanto aguarda a lista ser exibida pelo menos a partir do espírito de aspas.
Buildozer e serviços
À medida que o aplicativo se desenvolvia, havia uma proposta para arquivar um serviço que enviaria uma cotação aleatória do banco de dados ao usuário uma vez por dia. Eu nunca havia lidado com tarefas semelhantes em Kivy antes, mas lembrando que há
um artigo sobre Habré sobre esse assunto, decidi tentar.
Depois de matar uma semana inteira, quebrando cinco teclados e dois monitores, não consegui compilar o pacote de acordo com as instruções do artigo acima - durante a compilação, a classe necessária não foi encontrada. Depois de escrever para o autor do artigo, sugeri que, aparentemente, apenas duas razões pelas quais falhei eram verdadeiras: eu era um idiota ou os desenvolvedores quebraram o Buildozer, uma ferramenta para criar pacotes APK para Android. Minhas suposições acabaram sendo verdadeiras - "É claro que elas quebraram, depois da versão 0,33, que diabos elas coletariam".
Sim, a maior parte das perguntas no
fórum Kivy está conectada a vários problemas que surgem precisamente com o Buildozer. Agora, cada versão desta ferramenta requer sua própria versão do Cython, que você selecionará experimentalmente por um longo tempo. Usando as versões mais recentes do Buildozer, você não poderá adicionar uma biblioteca ao seu projeto JAR, porque, embora o projeto seja montado, a biblioteca não será adicionada a ele e você terá mais uma semana , como eu, sente-se procurando um problema. E ... você não a encontrará. Portanto, para iniciantes e pessoas com uma mentalidade fraca, trabalhar com Buildozer pode trazer para a clínica.
Então cuspi nesse trator morto, fui para o inferno, fui para o github, baixei python-para-android, peguei o Crystax-NDK fora do local, instalei o Python 3.5 e calmamente montei um APK do projeto com o terceiro ramo do Python, que acabou sendo muito mais simples, do que com o notório Buildozer.
E os serviços? Mas nada. Eles não funcionam. Mais precisamente, o serviço criado no seu projeto não começará com uma reinicialização do smartphone, independentemente do que o autor do artigo afirme sobre serviços no Kivy. Tendo encontrado no Google Play e instalado seu projeto, descobri que nenhum serviço com a reinicialização do programa está sendo iniciado. 100% dos serviços no Kivy iniciam apenas com o lançamento do próprio aplicativo. Mais tarde, se você fechar o aplicativo, o serviço continuará funcionando silenciosamente até você desligar o dispositivo.
Sobre o Python 2 e Python 3
Em fevereiro deste ano, o Moscow Python foi realizado no escritório da Yandex em Moscou, no qual Vladislav Shashkov fez uma apresentação sobre o tema: “Aplicativo móvel Python com kivy / buildozer é a chave do sucesso”. Então, ele teve a estupidez de dizer que o Python 2 no assembly APK é mais rápido que o Python 3. Não acredite em ninguém, isso não é verdade. O Python 3 é mais rápido que o Python 2 em princípio! Quando desenvolvi “Quotes of the Saints” (então também assumi que a segunda ramificação Python seria usada na montagem), fiquei horrorizado ao descobrir que a base de cotação de 20 MB, usada no aplicativo quando não há conexão de rede, é lida usando json. carrega de 13 a 16 segundos em um dispositivo móvel! E a mesma base, mas já com o Python 3, ele é processado no dispositivo em 1-2 segundos! Tire suas próprias conclusões ...
Sobre o React Native
Sim, em meus artigos, decidi traçar paralelos entre o Kivy e outras estruturas para o desenvolvimento de plataforma cruzada. Aqui você só precisa abrir o spoiler e ver como aplicativos simples, rápidos e elegantes são criados no React Native ...
ExemploVamos tentar desenhar uma interface completa. Reescrevemos App.js usando os componentes da biblioteca de base nativa:
import React from 'react'; import {Container, Content} from 'native-base'; import {StyleSheet, Text, View} from 'react-native'; import AppFooter from './components/AppFooter.js'; const styles = StyleSheet.create({ container: { padding: 20 }, }); const App = () => ( <Container> <Content> <View style={styles.container}> <Text> Lorem ipsum... </Text> </View> </Content> <AppFooter/> </Container> ); export default App;
Vemos o novo componente do AppFooter que precisamos criar. Vamos para a pasta ./components/ e criamos o arquivo AppFooter.js com o seguinte conteúdo:
import React from 'react'; import {Footer, FooterTab, Button, Text} from 'native-base'; const AppFooter = () => ( <Footer> <FooterTab> <Button active> <Text></Text> </Button> <Button> <Text></Text> </Button> </FooterTab> </Footer> ); export default AppFooter;
Tudo está pronto para tentar criar nosso aplicativo!
Nossos botões ainda não sabem como alternar. É hora de ensiná-los. Para fazer isso, você precisa fazer duas coisas: saiba como lidar com o evento click e como armazenar o estado. Vamos começar com o estado. Como nos recusamos a armazenar o estado no componente, optando por componentes puros e uma loja global, usaremos o Redux.
Antes de tudo, devemos criar nosso lado.
import {createStore} from 'redux'; const initialState = {}; const store = createStore(reducers, initialState);
Vamos criar um espaço em branco para redutores. Na pasta redutores, crie o arquivo index.js com o seguinte conteúdo:
export default (state = [], action) => { switch (action.type) { default: return state } };
Conecte redutores ao App.js:
import reducers from './reducers';
Agora precisamos distribuir nosso armazenamento em componentes. Isso é feito usando o componente Provedor especificamente. Nós o conectamos ao projeto:
import {Provider} from 'react-redux';
E envolva todos os componentes em um provedor. App.js atualizado é assim:
import React from 'react'; import {Container, Content} from 'native-base'; import {StyleSheet, Text, View} from 'react-native'; import AppFooter from './components/AppFooter.js'; import {createStore} from 'redux'; import {Provider} from 'react-redux'; import reducers from './reducers'; const initialState = {}; const store = createStore(reducers, initialState); const styles = StyleSheet.create({ container: { padding: 20 }, }); const App = () => ( <Provider store={store}> <Container> <Content> <View style={styles.container}> <Text> Lorem ipsum... </Text> </View> </Content> <AppFooter/> </Container> </Provider> ); export default App;
Agora nosso aplicativo pode armazenar seu estado. Vamos aproveitar isso. Nós adicionamos o estado de modo, por padrão definido como ARTICLES. Isso significa que, na primeira renderização, nosso aplicativo será configurado para mostrar a lista de artigos.
const initialState = { mode: 'ARTICLES' };
Nada mal, mas escrever manualmente os valores das strings leva a possíveis erros. Vamos obter constantes. Crie um arquivo ./constants/index.js com o seguinte conteúdo:
export const MODES = { ARTICLES: 'ARTICLES', PODCAST: 'PODCAST' };
E reescreva App.js:
import {MODES} from './constants'; const initialState = { mode: MODES.ARTICLES };
Bem, há um estado, é hora de passá-lo para o componente do rodapé. Vamos dar uma outra olhada em nosso ./components/AppFooter.js:
import React from 'react'; import {Footer, FooterTab, Button, Text} from 'native-base'; const AppFooter = () => ( <Footer> <FooterTab> <Button active> <Text></Text> </Button> <Button> <Text></Text> </Button> </FooterTab> </Footer> ); export default AppFooter;
Como podemos ver, o estado do comutador é determinado usando a propriedade ativa do componente Button. Vamos enviar o estado atual do aplicativo para Button. Isso não é difícil, o principal componente do capô é o componente Provider que conectamos anteriormente. Resta apenas tirar o estado atual e colocar os componentes do AppFooter nas propriedades (props). Antes de tudo, modificamos nosso AppFooter para que o estado dos botões possa ser controlado pelo modo de passagem através de objetos:
import React from 'react'; import {Footer, FooterTab, Button, Text} from 'native-base'; import {MODES} from "../constants"; const AppFooter = ({mode = MODES.ARTICLES}) => ( <Footer> <FooterTab> <Button active={mode === MODES.ARTICLES}> <Text></Text> </Button> <Button active={mode === MODES.PODCAST}> <Text></Text> </Button> </FooterTab> </Footer> ); export default AppFooter;
Agora vamos começar a criar um contêiner. Crie o arquivo ./containers/AppFooterContainer.js.
import React from 'react'; import AppFooter from '../components/AppFooter.js'; import {MODES} from "../constants"; const AppFooterContainer = () => ( <AppFooter mode={MODES.ARTICLES} /> ); export default AppFooterContainer;
E conecte o contêiner AppFooterContainer no App.js em vez do componente AppFooter. Até o momento, nosso contêiner não é diferente do componente, mas tudo mudará assim que o conectarmos ao estado do aplicativo. Vamos fazer isso!
import React from 'react'; import AppFooter from '../components/AppFooter.js'; import {connect} from 'react-redux'; const mapStateToProps = (state) => ({ mode: state.mode }); const AppFooterContainer = ({mode}) => ( <AppFooter mode={mode} /> ); export default connect( mapStateToProps )(AppFooterContainer);
Muito funcional! Todos os recursos ficaram limpos. O que está acontecendo aqui? Conectamos nosso contêiner ao estado usando a função connect e conectamos seus objetos ao conteúdo do estado global usando a função mapStateToProps. Muito limpo e bonito.
Então, aprendemos a distribuir dados de cima para baixo. Agora precisamos aprender como mudar nosso estado global de baixo para cima. As ações são projetadas para gerar eventos sobre a necessidade de alterar o estado global. Vamos criar uma ação que ocorre quando um botão é clicado.
Crie o arquivo ./actions/index.js:
import { SET_MODE } from './actionTypes'; export const setMode = (mode) => ({type: SET_MODE, mode});
E o arquivo ./actions/actionTypes, no qual armazenaremos as constantes com os nomes das ações:
export const SET_MODE = 'SET_MODE';
A ação cria um objeto com o nome do evento e o conjunto de dados que acompanham esse evento, e nada mais. Agora vamos aprender como gerar esse evento. Retornamos ao contêiner AppFooterContainer e conectamos a função mapDispatchToProps, que conectará os expedidores de eventos aos objetos do contêiner.
import React from 'react'; import AppFooter from '../components/AppFooter.js'; import {connect} from 'react-redux'; import {setMode} from '../actions'; const mapStateToProps = (state) => ({ mode: state.mode }); const mapDispatchToProps = (dispatch) => ({ setMode(mode) { dispatch(setMode(mode)); } }); const AppFooterContainer = ({mode, setMode}) => ( <AppFooter mode={mode} setMode={setMode} /> ); export default connect( mapStateToProps, mapDispatchToProps )(AppFooterContainer);
Bem, temos uma função que gera o evento SET_MODE e pulamos para o componente AppFooter. Dois problemas permanecem:
Ninguém chama essa função.
Ninguém está ouvindo o evento.
Nós vamos lidar com o primeiro problema. Vamos ao componente AppFooter e conectamos a chamada à função setMode.
import React from 'react'; import {Footer, FooterTab, Button, Text} from 'native-base'; import {MODES} from "../constants"; const AppFooter = ({mode = MODES.ARTICLES, setMode = () => {}}) => ( <Footer> <FooterTab> <Button active={mode === MODES.ARTICLES} onPress={ () => setMode(MODES.ARTICLES)}> <Text></Text> </Button> <Button active={mode === MODES.PODCAST} onPress={ () => setMode(MODES.PODCAST)}> <Text></Text> </Button> </FooterTab> </Footer> ); export default AppFooter;
Agora, quando o botão é pressionado, o evento SET_MODE será gerado. Resta aprender a mudar o estado global à medida que ele surge. Vamos para o arquivo ./reducers/index.js criado anteriormente e criamos um redutor para este evento:
import { SET_MODE } from '../actions/actionTypes'; export default (state = [], action) => { switch (action.type) { case SET_MODE: { return Object.assign({}, state, { mode: action.mode }); } default: return state } };
Ótimo! Agora, clicar no botão gera um evento que altera o estado global e o rodapé, após receber essas alterações, redesenha os botões.
Artigo originalVerdade, incrivelmente simples? É assustador imaginar quantos programadores morrem de velhice em projetos React Native e quanto dinheiro é pago por toda essa desgraça. O resultado de tudo isso é um pequeno exemplo, um pouco mais complicado que o Hello World.
Uma vez após o programa de concertos do álbum "... And Justice for All" em 1988, o líder do Metallica, James Hetfield, disse: "Isso é verdade ... mas é impossível tocar ao vivo". Então, depois que escrevi o código de exemplo no React Native, fiquei solidário com James - isto é ... mas é impossível escrever vivo!
E aqui está como a mesma coisa é feita usando a estrutura Kivy:
from kivy.app import App from kivy.factory import Factory from kivy.lang import Builder Builder.load_string(""" <MyButton@Button>: background_down: 'button_down.png' background_normal: 'button_normal.png' color: 0, 0, 0, 1 bold: True on_press: self.parent.parent.ids.textEdit.text = self.text; \ self.color = [.10980392156862745, .5372549019607843, .996078431372549, 1] on_release: self.color = [0, 0, 0, 1] <MyActivity@BoxLayout>: orientation: 'vertical' TextInput: id: textEdit BoxLayout: size_hint_y: None height: dp(45) MyButton: text: '' MyButton: text: '' """) class Program(App): def build(self): my_activity = Factory.MyActivity() return my_activity Program().run()
É tão simples que até os comentários são redundantes aqui.
Sim, você pode não saber disso, mas está tudo escrito em Kivy:
vimeo.com/29348760vimeo.com/206290310vimeo.com/25680681www.youtube.com/watch?v=u4NRu7mBXtAwww.youtube.com/watch?v=9rk9OQLSoJwwww.youtube.com/watch?v=aa9LXpg_gd0www.youtube.com/watch?v=FhRXAD8-UkEwww.youtube.com/watch?v=GJ3f88ebDqc&t=111swww.youtube.com/watch?v=D_M1I9GvpYswww.youtube.com/watch?v=VotPQafL7NwConcluindo, dou um vídeo do aplicativo:
Escreva nos comentários os artigos que você gostaria de ver sobre Kivy nas páginas de Habr. Se possível, todos os desejos serão realizados. Até breve, dzzya!