Parte 1Saludos!
Hoy, como siempre, hablemos sobre la creación de aplicaciones móviles con el framework Kivy y Python. En particular, se centrará en crear un cliente móvil para un recurso de Internet y publicarlo en Google Play. Te diré qué problemas puede tener un desarrollador novato y experimentado, que decidió probarse en el desarrollo multiplataforma con Kivy, qué se puede y qué no se debe hacer al programar con Python para Android.
Una mañana encontré en mi correo sobre Habré una carta preguntando si podía usar Python y Kivy para "recrear el sitio svyatye.com en una aplicación móvil para que las personas puedan leerlo y usarlo sin conexión" con la publicación posterior del cliente en Tienda de aplicaciones Google Play. Siguiendo el enlace y navegando por el recurso, que resultó ser una gran biblioteca de citas, imaginé cómo se vería en una presentación móvil y cómo crearía listas de "más de 30,236 dichos de los santos padres y maestros de la iglesia" a pesar de la extensión de las citas, a veces , alcanzó más de 10,000 caracteres (5-6 páginas de texto impreso). Como he estado trabajando con Kivy durante mucho tiempo, rápidamente me di cuenta de cómo y qué haría. Por lo tanto, respondió al cliente que no sería difícil realizar dicha solicitud. Sin embargo, surgieron dificultades, que analizaré a continuación, sin embargo ...
No se proporcionaron especificaciones técnicas. El único requisito es que la aplicación funcione como un reloj. No hay plazos. Tampoco hubo diseños de interfaz. "Todo debería ser lo más simple posible, sin animaciones, transformaciones y otras cáscaras, en una palabra, lo más austero posible". Bueno, mucho mejor. Además, mi decisión ya está madura: la aplicación utilizará un objeto RecycleView, que mostrará categorías, subcategorías, listas de autores de citas y citas.
Listas
Sin embargo, RecycleView, que le permite abrir enormes listas de muchos miles en una fracción de segundo, se comportó de manera bastante diferente de lo deseado. No, no hubo problemas para abrir listas de citas, todo funcionó rápidamente, ni siquiera cargué nuevas citas con la ventana "Esperar" en el sitio, porque la lista de citas de la categoría seleccionada se procesó de forma instantánea y completa. El problema era diferente: el cliente insistió en que el texto de la cotización en la lista debería mostrarse en su totalidad y que RecycleView no era del todo apropiado aquí. El hecho es que el principio de funcionamiento de este widget es el siguiente: se crea un objeto en toda la lista, que luego se clona simplemente, como resultado de lo cual tenemos una velocidad increíble de renderización de la lista, sin importar cuán grande sea. Pero hay una cosa: la altura del elemento de la lista debe ser fija y conocida de antemano. Pero si necesita calcular dinámicamente la altura del siguiente elemento de la lista al desplazarse, como en mi caso, entonces hay un retraso notable: la lista se congela por una fracción de segundo, que, como verá, no está lista para la producción.
Con la pena a la mitad, logré persuadir al cliente a una lista con una vista previa de citas, cuyo texto se abriría completamente por tapu al texto de la vista previa, como se hizo en casi cualquier foro, no porque RecycleView no pudiera hacer frente a la tarea, sino porque era el más lógico: desplazar el texto de varias páginas de la cita, especialmente si la cita no le interesaba al usuario, desde mi punto de vista, no era correcta.
Fig. 1
Vista previa y texto completo al grabar en la vista previa de cotizaciónEsta opción funcionó muy rápidamente, pero ... al cliente no le gustó ... Tuve que usar un ScrollView lento, que muestra la lista ANTES de que se muestre en la pantalla, lo que significa que no congela el desplazamiento de la lista de citas, ya que calcula y muestra todos los parámetros de los elementos de la lista por adelantado, lo que, Naturalmente, afectará la velocidad de listar la pantalla. Por lo general, el rendimiento se pone en primer lugar, y aquí me dicen: "que sea más lento".
Bueno, ya no discutí, y aunque realmente no me gustó esta solución, tuve que transferir todo a ScrollView. Como, como dije, ScrollView es muy lento, se decidió mostrar las cotizaciones en porciones de diez cada una con una carga automática adicional de las siguientes diez.
Avanzando un poco más adelante, diré que cuando los primeros comentarios de los usuarios llegaron con una solicitud, dijeron que los marcadores realmente no dolerían, como me parece, el cliente todavía dudaba de la decisión de usar ScrollView, como si dejáramos de lado las previsualizaciones de citas y RecycleView, luego, sin problemas, podrían restaurar instantáneamente a partir de los marcadores vistos previamente por el usuario en las listas de citas de la sesión anterior, sin importar cuánto tiempo duraron. Y con ScrollView, el usuario simplemente envejecerá mientras espera que la lista se muestre al menos por el espíritu de las comillas.
Buildozer y servicios
A medida que se desarrolló la aplicación, hubo una propuesta para presentar un servicio que enviaría una cotización aleatoria de la base de datos al usuario una vez al día. Nunca antes me había ocupado de tareas similares en Kivy, pero recordando que hay
un artículo sobre Habré sobre este tema, decidí probarlo.
Después de matar una semana entera, romper cinco teclados y dos monitores, no pude compilar el paquete de acuerdo con las instrucciones del artículo anterior; durante la compilación no se encontró la clase requerida. Habiendo escrito al autor del artículo, sugerí que, aparentemente, solo dos razones por las cuales fallé eran ciertas: o era un idiota o los desarrolladores rompieron Buildozer, una herramienta para construir paquetes APK para Android. Mis suposiciones resultaron ser ciertas: "Por supuesto, lo rompieron, después de la versión 0.33 qué demonios recogerán".
Sí, la mayor parte de las preguntas en el
foro de Kivy están relacionadas con varios problemas que surgen precisamente con Buildozer. Ahora, cada versión de esta herramienta requiere su propia versión de Cython, que seleccionará experimentalmente durante mucho tiempo, utilizando las últimas versiones de Buildozer, no podrá agregar una biblioteca a su proyecto JAR, porque aunque el proyecto está ensamblado, la biblioteca no se agregará y tendrá una semana más. , como yo, siéntate a buscar un problema. Y ... no la encontrarás. Por lo tanto, para principiantes y personas con una mentalidad débil, trabajar con Buildozer puede llevar a la clínica.
Así que escupí en este tractor muerto, fui al infierno, fui a github, descargué python-for-android, tomé Crystax-NDK en el sitio, instalé Python 3.5 y preparé tranquilamente un APK del proyecto con la tercera rama de Python, que resultó ser mucho más fácil, que con el notorio Buildozer.
¿Qué hay de los servicios? Pero nada No funcionan Más precisamente, el servicio creado en su proyecto no comenzará con un reinicio del teléfono inteligente, sin importar lo que el autor del artículo diga sobre los servicios en Kivy. Después de encontrar en Google Play e instalar su proyecto, descubrí que no se están iniciando servicios al reiniciar el programa. El 100% de los servicios en Kivy comienzan solo con el lanzamiento de la aplicación en sí. Más tarde, si cierra la aplicación, el servicio continuará funcionando en silencio hasta que apague el dispositivo.
Acerca de Python 2 y Python 3
En febrero de este año, Moscow Python se celebró en la oficina de Yandex en Moscú, donde Vladislav Shashkov hizo una presentación sobre el tema: "La aplicación móvil Python con kivy / buildozer es la clave del éxito". Entonces tuvo la estupidez de decir que Python 2 en el ensamblado de APK es más rápido que Python 3. No le creas a nadie, esto no es cierto. ¡Python 3 es más rápido que Python 2 en principio! Cuando desarrollé "Quotes of the Saints" (entonces también se asumió que la segunda rama de Python se usaría en el ensamblaje), me horroricé al encontrar que la base de cotización de 20 MB, que se usa en la aplicación cuando no hay conexión de red, se lee usando json. ¡Carga hasta 13-16 segundos en un dispositivo móvil! ¡Y la misma base, pero ya con Python 3, se procesa en el dispositivo en 1-2 segundos! Saca tus propias conclusiones ...
Sobre React Native
Sí, en mis artículos, decidí establecer paralelismos entre Kivy y otros marcos para el desarrollo multiplataforma. Aquí solo necesita abrir el spoiler y ver cómo se crean aplicaciones simples, rápidas y elegantes en React Native ...
EjemploIntentemos dibujar una interfaz completa. Reescribimos App.js usando los componentes de la 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 el nuevo componente AppFooter que tenemos que crear. Vamos a la carpeta ./components/ y creamos el archivo AppFooter.js con el siguiente contenido:
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;
¡Todo está listo para intentar construir nuestra aplicación!
Nuestros botones aún no saben cómo cambiar. Es hora de enseñarles. Para hacer esto, debe hacer dos cosas: aprender a manejar el evento de clic y aprender a almacenar el estado. Comencemos con el estado. Como nos negamos a almacenar el estado en el componente, después de haber optado por componentes puros y una tienda global, utilizaremos Redux.
En primer lugar, debemos crear nuestro lado.
import {createStore} from 'redux'; const initialState = {}; const store = createStore(reducers, initialState);
Creemos un espacio en blanco para reductores. En la carpeta reductores, cree el archivo index.js con el siguiente contenido:
export default (state = [], action) => { switch (action.type) { default: return state } };
Conecte los reductores a App.js:
import reducers from './reducers';
Ahora necesitamos distribuir nuestra historia en los componentes. Esto se hace utilizando el componente Proveedor específicamente. Lo conectamos al proyecto:
import {Provider} from 'react-redux';
Y envolver todos los componentes en un proveedor. App.js actualizado se ve así:
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;
Ahora nuestra aplicación puede almacenar su estado. Aprovechemos esto. Agregamos el estado del modo, por defecto establecido en ARTÍCULOS. Esto significa que en el primer render, nuestra aplicación se configurará para mostrar la lista de artículos.
const initialState = { mode: 'ARTICLES' };
No está mal, pero escribir valores de cadena manualmente conduce a posibles errores. Consigamos constantes. Cree un archivo ./constants/index.js con el siguiente contenido:
export const MODES = { ARTICLES: 'ARTICLES', PODCAST: 'PODCAST' };
Y reescribe App.js:
import {MODES} from './constants'; const initialState = { mode: MODES.ARTICLES };
Bueno, hay un estado, es hora de pasarlo al componente de pie de página. Echemos otro vistazo a nuestros ./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, el estado del interruptor se determina utilizando la propiedad activa del componente Button. Empujemos el estado actual de la aplicación a Button. Esto no es difícil, el componente principal de la campana es el componente Proveedor que conectamos anteriormente. Solo resta tomar el estado actual y colocar los componentes de AppFooter en las propiedades (accesorios). En primer lugar, modificamos nuestro AppFooter para que el estado de los botones se pueda controlar pasando el modo a través de accesorios:
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;
Ahora comencemos a crear un contenedor. Cree el archivo ./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;
Y conecte el contenedor AppFooterContainer en App.js en lugar del componente AppFooter. Hasta ahora, nuestro contenedor no es diferente del componente, pero todo cambiará tan pronto como lo conectemos al estado de la aplicación. ¡Hagámoslo!
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);
Muy funcional! Todas las características se han vuelto limpias. ¿Qué está pasando aquí? Conectamos nuestro contenedor al estado usando la función de conexión y conectamos sus accesorios al contenido del estado global usando la función mapStateToProps. Muy limpio y hermoso.
Entonces, hemos aprendido a distribuir datos de arriba a abajo. Ahora necesitamos aprender a cambiar nuestro estado global de abajo hacia arriba. Las acciones están diseñadas para generar eventos sobre la necesidad de cambiar el estado global. Creemos una acción que ocurre cuando se hace clic en un botón.
Cree el archivo ./actions/index.js:
import { SET_MODE } from './actionTypes'; export const setMode = (mode) => ({type: SET_MODE, mode});
Y el archivo ./actions/actionTypes, en el que almacenaremos las constantes con los nombres de acción:
export const SET_MODE = 'SET_MODE';
La acción crea un objeto con el nombre del evento y el conjunto de datos que acompañan a este evento, y nada más. Ahora aprenderemos cómo generar este evento. Regresamos al contenedor AppFooterContainer y conectamos la función mapDispatchToProps que conectará los despachadores de eventos a los accesorios del contenedor.
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);
Bueno, tenemos una función que genera el evento SET_MODE y lo saltamos al componente AppFooter. Quedan dos problemas:
Nadie llama a esta función.
Nadie está escuchando el evento.
Nos ocuparemos del primer problema. Vamos al componente AppFooter y conectamos la llamada a la función 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;
Ahora, cuando se presiona el botón, se generará el evento SET_MODE. Queda por aprender cómo cambiar el estado global a medida que surja. Vamos al ./reducers/index.js creado anteriormente y creamos un reductor 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 } };
Genial Ahora, al hacer clic en el botón, se genera un evento que cambia el estado global, y el pie de página, después de recibir estos cambios, vuelve a dibujar los botones.
Artículo originalEs cierto, increíblemente simple? Da miedo imaginar cuántos programadores mueren de vejez en proyectos de React Native y cuánto dinero se paga por toda esta desgracia. El resultado de todo esto es un pequeño ejemplo, un poco más complicado que Hello World.
Una vez después del programa de conciertos del álbum "... y justicia para todos" en 1988, el líder de Metallica, James Hetfield, dijo: "Esto es así ... pero es imposible tocar con vida". Entonces, después de escribir el código de muestra en React Native, me solidaricé con James, eso es ... ¡pero es imposible escribir con vida!
Y así es como se hace lo mismo usando el marco 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()
Es tan simple que incluso los comentarios son redundantes aquí.
Sí, es posible que no haya sabido sobre esto, pero todo está escrito en 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=VotPQafL7NwEn conclusión, doy un video de la aplicación:
Escriba en los comentarios qué artículos le gustaría ver sobre Kivy en las páginas de Habr. Si es posible, todos los deseos se realizarán. Hasta pronto, dzzya!