Partie 1Salutations!
Aujourd'hui, comme toujours, parlons de la création d'applications mobiles avec le framework Kivy et Python. En particulier, il se concentrera sur la création d'un client mobile pour une ressource Internet et sa publication sur Google Play. Je vais vous dire quels problèmes un novice et un développeur expérimenté peuvent avoir, qui ont décidé de s’essayer au développement multiplateforme avec Kivy, ce qui peut et ne devrait pas être fait en programmation avec Python pour Android.
Un matin, j'ai trouvé dans mon courrier sur Habré une lettre me demandant si je pouvais utiliser Python et Kivy pour «recréer le site svyatye.com dans une application mobile afin que les gens puissent le lire et l'utiliser hors ligne» avec la publication ultérieure du client dans App Store de Google Play. En suivant le lien et en parcourant la ressource, qui s'est avérée être une grande bibliothèque de citations, j'ai imaginé à quoi cela ressemblerait dans une présentation mobile et comment je créerais des listes de «plus de 30 236 dictons des saints pères et enseignants de l'église» malgré la longueur des citations, parfois , atteint plus de 10 000 caractères (5 à 6 pages de texte imprimé). Depuis que je travaille avec Kivy depuis longtemps, j'ai rapidement compris comment et ce que je ferais. Par conséquent, il a répondu au client qu'il ne serait pas difficile de faire une telle demande. Cependant, des difficultés, dont je parlerai plus loin, sont néanmoins apparues ...
Aucune spécification technique n'a été fournie. La seule exigence est que l'application fonctionne comme une horloge. Pas de délais. Il n'y avait pas non plus de disposition d'interface. "Tout devrait être aussi simple que possible, sans animations, transformations et autres enveloppes, en un mot, aussi austère que possible." Et bien, tant mieux. De plus, ma décision est déjà mûre - l'application utilisera un objet RecycleView, qui affichera les catégories, sous-catégories, listes d'auteurs de citations et de citations elles-mêmes.
Listes
Cependant, RecycleView, qui vous permet d'ouvrir d'énormes listes de plusieurs milliers en une fraction de seconde, s'est comporté de manière très différente de celle souhaitée. Non, il n'y a eu aucun problème lors de l'ouverture des listes de devis, tout a fonctionné rapidement, je n'ai même pas chargé de nouveaux devis avec la fenêtre «Attendre» sur le site, car la liste des devis de la catégorie sélectionnée a été rendue instantanément et complètement. Le problème était différent - le client a insisté pour que le texte du devis dans la liste soit affiché dans son intégralité et que RecycleView n'était pas entièrement approprié ici. Le fait est que le principe de fonctionnement de ce widget est le suivant: un objet est créé sur toute la liste, qui est ensuite simplement cloné, à la suite de quoi nous avons une vitesse incroyable de rendu de la liste, quelle que soit sa taille. Mais il y a une chose - la hauteur de l'élément de liste doit être fixée et connue à l'avance. Mais si vous devez calculer dynamiquement la hauteur de l'élément de liste suivant lors du défilement, comme dans mon cas, il y a un décalage notable - la liste se bloque pendant une fraction de seconde, ce qui, vous voyez, n'est en aucun cas prêt pour la prodaction.
Avec un chagrin de moitié, j'ai réussi à persuader le client d'une liste avec un aperçu des citations, dont le texte s'ouvrirait entièrement par tapu au texte de l'aperçu, comme cela a été fait dans presque tous les forums, non pas parce que RecycleView ne pouvait pas faire face à la tâche, mais parce qu'il était le plus logique: faire défiler le texte multi-page du devis, surtout si le devis n'intéressait pas l'utilisateur, de mon point de vue ce n'était pas correct.
Fig. 1
Aperçu et texte intégral lorsque vous appuyez sur l'aperçu du devisCette option a fonctionné très rapidement, mais ... le client ne l'aimait pas ... J'ai dû utiliser un ScrollView lent, qui rend la liste AVANT qu'elle ne s'affiche à l'écran, ce qui signifie qu'elle ne gèle pas le défilement de la liste de devis, car elle calcule et restitue à l'avance tous les paramètres des éléments de la liste, ce qui, Naturellement, cela affectera la vitesse à laquelle la liste est affichée à l'écran. Habituellement, la performance est mise en premier lieu, et ici, ils me disent: «que ce soit plus lent».
Eh bien, je ne discutais plus, et même si je n'aimais vraiment pas cette solution, je devais tout transférer vers ScrollView. Comme, comme je l'ai dit, ScrollView est très lent, il a été décidé d'afficher des citations par portions de dix chacune avec un chargement automatique supplémentaire des dix suivantes.
En allant un peu plus loin, je dirai que lorsque les premiers retours des utilisateurs sont venus avec une demande, ils ont dit que les signets ne feraient pas vraiment de mal, comme il me semble, le client doutait toujours de la décision d'utiliser ScrollView, comme si nous omettions les aperçus des devis et RecycleView, puis sans problème, ils pouvaient restaurer instantanément à partir des signets précédemment consultés par l'utilisateur dans les listes de devis de la session précédente, quelle que soit leur durée. Et avec ScrollView, l'utilisateur vieillira simplement en attendant que la liste soit affichée au moins dans l'esprit des citations.
Buildozer et services
Au fur et à mesure du développement de l'application, il a été proposé d'y déposer un service qui enverrait un devis aléatoire de la base de données à l'utilisateur une fois par jour. Je n'avais jamais traité de tâches similaires à Kivy auparavant, mais en me rappelant qu'il existe
un article sur Habré à ce sujet, j'ai décidé de l'essayer.
Après avoir tué une semaine entière, cassé cinq claviers et deux moniteurs, je n'ai pas pu compiler le package selon les instructions de l'article ci-dessus - lors de la compilation, la classe requise n'a pas été trouvée. Après avoir écrit à l'auteur de l'article, j'ai suggéré que, apparemment, seules deux raisons pour lesquelles j'avais échoué étaient vraies: soit j'étais un idiot, soit les développeurs ont cassé Buildozer, un outil pour créer des packages APK pour Android. Mes hypothèses se sont avérées vraies - "Bien sûr, ils l'ont cassé, après la version 0.33 ce qu'ils vont collecter."
Oui, la part du lion des questions sur le
forum Kivy est liée à divers problèmes qui se posent précisément avec Buildozer. Maintenant, chaque version de cet outil nécessite sa propre version de Cython, que vous sélectionnerez expérimentalement pendant une longue période, en utilisant les dernières versions de Buildozer, vous ne pourrez pas ajouter de bibliothèque à votre projet JAR, car bien que le projet soit assemblé, la bibliothèque n'y sera pas ajoutée et vous serez encore une semaine , comme moi, asseyez-vous à la recherche d'un problème. Et ... vous ne la trouverez pas. Par conséquent, pour les débutants et les personnes ayant une mentalité faible, travailler avec Buildozer peut apporter à la clinique.
J'ai donc craché sur ce tracteur mort, je suis allé en enfer, je suis allé sur github, j'ai téléchargé python-pour-android, pris Crystax-NDK sur le site, installé Python 3.5 et tranquillement assemblé l'APK du projet avec la troisième branche Python, qui s'est avérée être beaucoup plus simple, qu'avec le célèbre Buildozer.
Et les services? Mais rien. Ils ne fonctionnent pas. Plus précisément, le service créé dans votre projet ne commencera pas par un redémarrage du smartphone, peu importe ce que l'auteur de l'article prétend à propos des services dans Kivy. Après avoir trouvé sur Google Play et installé son projet, j'ai constaté qu'aucun service ne redémarre avec le redémarrage du programme. 100% des services dans Kivy ne commencent qu'avec le lancement de l'application elle-même. Plus tard, si vous fermez l'application, le service continuera de fonctionner sans bruit jusqu'à ce que vous éteigniez l'appareil.
À propos de Python 2 et Python 3
En février de cette année, Moscou Python a eu lieu au bureau de Moscou de Yandex, dans lequel Vladislav Shashkov a fait une présentation sur le thème: «L'application mobile Python avec kivy / buildozer est la clé du succès». Il a donc eu la stupidité de dire que Python 2 dans l'assemblage APK est plus rapide que Python 3. Ne croyez personne, ce n'est pas vrai. Python 3 est plus rapide que Python 2 en principe! Lorsque j'ai développé «Quotes of the Saints» (on supposait également que la deuxième branche Python serait utilisée dans l'assemblage), j'ai été horrifié de constater que la base de cotation de 20 Mo, qui est utilisée dans l'application lorsqu'il n'y a pas de connexion réseau, est lue à l'aide de json. charge jusqu'à 13-16 secondes sur un appareil mobile! Et la même base, mais déjà avec Python 3, il est traité sur l'appareil en 1-2 secondes! Tirez vos propres conclusions ...
À propos de React Native
Oui, dans mes articles, j'ai décidé de faire des parallèles entre Kivy et d'autres frameworks pour le développement multiplateforme. Ici, il vous suffit d'ouvrir le spoiler et de voir comment les applications simples, rapides et élégantes sont créées sur React Native ...
ExempleEssayons de dessiner une interface complète. Nous réécrivons App.js en utilisant les composants de la bibliothèque native:
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;
Nous voyons le nouveau composant AppFooter que nous devons créer. Nous allons dans le dossier ./components/ et créons le fichier AppFooter.js avec le contenu suivant:
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;
Tout est prêt pour essayer de construire notre application!
Nos boutons ne savent pas encore comment basculer. Il est temps de leur enseigner. Pour ce faire, vous devez faire deux choses: apprendre à gérer l'événement click et apprendre à stocker l'état. Commençons par l'État. Comme nous avons refusé de stocker l'état dans le composant, ayant opté pour des composants purs et un magasin global, nous utiliserons Redux.
Tout d'abord, nous devons créer notre côté.
import {createStore} from 'redux'; const initialState = {}; const store = createStore(reducers, initialState);
Créons un blanc pour les réducteurs. Dans le dossier des réducteurs, créez le fichier index.js avec le contenu suivant:
export default (state = [], action) => { switch (action.type) { default: return state } };
Connectez les réducteurs à App.js:
import reducers from './reducers';
Maintenant, nous devons distribuer notre magasin sur les composants. Cela se fait en utilisant spécifiquement le composant Provider. Nous le connectons au projet:
import {Provider} from 'react-redux';
Et enveloppez tous les composants dans un fournisseur. App.js mis à jour ressemble à ceci:
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;
Maintenant, notre application peut stocker son état. Profitons de cela. Nous ajoutons l'état du mode, défini par défaut sur ARTICLES. Cela signifie que lors du premier rendu, notre application sera configurée pour afficher la liste des articles.
const initialState = { mode: 'ARTICLES' };
Pas mal, mais l'écriture manuelle de valeurs de chaîne entraîne des erreurs potentielles. Obtenons des constantes. Créez un fichier ./constants/index.js avec le contenu suivant:
export const MODES = { ARTICLES: 'ARTICLES', PODCAST: 'PODCAST' };
Et réécrivez App.js:
import {MODES} from './constants'; const initialState = { mode: MODES.ARTICLES };
Eh bien, il y a un état, il est temps de le passer au pied de page. Jetons un autre regard sur nos ./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;
Comme nous pouvons le voir, l'état du commutateur est déterminé à l'aide de la propriété active du composant Button. Poussons l'état actuel de l'application sur Button. Ce n'est pas difficile, le composant principal de la hotte est le composant Provider que nous avons connecté plus tôt. Il ne reste plus qu'à en prendre l'état actuel et à mettre les composants AppFooter dans les propriétés (accessoires). Tout d'abord, nous modifions notre AppFooter pour que l'état des boutons puisse être contrôlé en passant le mode via des accessoires:
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;
Commençons maintenant à créer un conteneur. Créez le fichier ./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;
Et connectez le conteneur AppFooterContainer dans App.js au lieu du composant AppFooter. Jusqu'à présent, notre conteneur n'est pas différent du composant, mais tout changera dès que nous le connecterons à l'état de l'application. Faisons-le!
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);
Très fonctionnel! Toutes les fonctionnalités sont devenues propres. Que se passe-t-il ici? Nous connectons notre conteneur à l'état à l'aide de la fonction connect et connectons ses accessoires au contenu de l'état global à l'aide de la fonction mapStateToProps. Très propre et magnifique.
Nous avons donc appris à répartir les données de haut en bas. Maintenant, nous devons apprendre à changer notre état mondial de bas en haut. Les actions sont conçues pour générer des événements sur la nécessité de changer l'état global. Créons une action qui se produit lorsqu'un bouton est cliqué.
Créez le fichier ./actions/index.js:
import { SET_MODE } from './actionTypes'; export const setMode = (mode) => ({type: SET_MODE, mode});
Et le fichier ./actions/actionTypes, dans lequel nous allons stocker les constantes avec les noms d'actions:
export const SET_MODE = 'SET_MODE';
L'action crée un objet avec le nom de l'événement et l'ensemble de données qui accompagnent cet événement, et rien de plus. Nous allons maintenant apprendre à générer cet événement. Nous revenons au conteneur AppFooterContainer et connectons la fonction mapDispatchToProps qui connectera les répartiteurs d'événements aux accessoires du conteneur.
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);
Eh bien, nous avons une fonction qui déclenche l'événement SET_MODE et nous l'avons ignoré dans le composant AppFooter. Deux problèmes demeurent:
Personne n'appelle cette fonction.
Personne n'écoute l'événement.
Nous allons traiter le premier problème. Nous allons au composant AppFooter et connectons l'appel à la fonction 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;
Maintenant, lorsque le bouton est enfoncé, l'événement SET_MODE sera déclenché. Il reste à apprendre comment changer l'état global à mesure qu'il se présente. Nous allons dans le ./reducers/index.js précédemment créé et créons un réducteur pour cet événement:
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 } };
Super! Maintenant, cliquer sur le bouton génère un événement qui modifie l'état global et le pied de page, après avoir reçu ces modifications, redessine les boutons.
Article originalVrai, incroyablement simple? Il est effrayant d'imaginer combien de programmeurs meurent de vieillesse sur des projets React Native et combien d'argent est payé pour toute cette honte. Le résultat de tout cela est un petit exemple, un peu plus compliqué que Hello World.
Une fois après le programme de concert de l'album "... Et Justice pour tous" en 1988, le leader de Metallica, James Hetfield, a déclaré: "C'est ainsi ... mais il est impossible de jouer vivant." Donc, après avoir écrit l'exemple de code sur React Native, je suis devenu solidaire de James - c'est-à-dire ... mais il est impossible d'écrire vivant!
Et voici comment la même chose se fait en utilisant le framework 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()
C'est si simple que même les commentaires sont redondants ici.
Oui, vous ne le saviez peut-être pas, mais tout est écrit 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 conclusion, je donne une vidéo de l'application:
Écrivez dans les commentaires quels articles vous aimeriez voir sur Kivy sur les pages de Habr. Si possible, tous les souhaits seront réalisés. A bientôt, dzzya!