基维 从创造到生产是第一步。 第二部分

第一部分

问候!

今天,和往常一样,让我们​​谈谈使用Kivy和Python框架创建移动应用程序。 特别是,它将专注于为一种Internet资源创建移动客户端并将其发布到Google Play上。 我将告诉您,新手和经验丰富的开发人员可能会遇到什么问题,他们决定尝试使用Kivy进行跨平台开发,以及使用Python for Android编程可以做什么,不应该做什么。

一天早上,我在哈布雷(Habré)的邮件中发现一封信,询问我是否可以使用Python和Kivy在移动应用程序中“重新创建网站svyatye.com,以便人们可以离线阅读和使用它”,以及随后发布的客户端。 Google Play应用商店。 跟随链接并浏览该资源(原来是一个大型的报价库),我想像它会在移动演示中的外观以及如何创建“包含30,236多个教会的圣父和老师的名言”的列表,尽管有时报价很长。 ,超过10,000个字符(5-6页印刷文字)。 由于我已经与Kivy合作了很长时间,所以我很快意识到自己将如何做以及将要做什么。 因此,他回答客户说,进行这样的申请并不困难。 但是,困难仍然存在,我将在下面讨论...

没有提供技术规格。 唯一的要求是应用程序应像时钟一样工作。 没有截止日期。 也没有接口布局。 “一切都应该尽可能简单,一言以蔽之,尽量避免动画,变换和其他外壳。” 好吧,更好。 而且,我的决定已经成熟-该应用程序将使用一个RecycleView对象,该对象将显示类别,子类别,引号的作者列表和引文本身。

清单


但是,RecycleView允许您在一瞬间打开成千上万的巨大列表,其行为与期望的完全不同。 不,打开报价单没有问题,一切都很快进行,我什至没有像在网站上那样在“等待”窗口中加载新的报价,因为所选类别的报价列表可以立即完整显示。 问题是不同的-客户坚持要完整显示列表中报价的文本,并且RecycleView在这里并不完全合适。 事实是,此小部件的操作原理如下:在整个列表上创建一个对象,然后简单地对其进行克隆,结果,无论列表多大,我们都能以惊人的速度呈现该列表。 但是有一件事-列表项的高度必须是固定的,并且必须事先知道。 但是,如果您需要在滚动时动态计算下一个列表项的高度(如我的情况),则存在明显的滞后-列表冻结了一秒钟,您看,这绝对不是准备工作。

悲痛欲绝,我设法说服了客户一个带有报价预览的列表,就像在几乎所有论坛中所做的那样,完全可以通过点击预览文本来打开其文本,这并不是因为RecycleView无法应付任务,而是因为最合乎逻辑的:滚动报价的多页文本,尤其是如果报价对用户不感兴趣时​​,从我的角度来看,这是不正确的。

1个
点击报价预览时预览和全文

这个选项很快就起作用了,但是...客户不喜欢它...我不得不使用慢速的ScrollView,它会在列表显示在屏幕上之前渲染列表,这意味着它不会冻结报价列表的滚动,因为它会预先计算并渲染列表元素的所有参数,因此,自然,它将影响列表在屏幕上显示的速度。 通常,性能放在首位,在这里他们对我说:“放慢一点。”


好吧,我不再争论了,尽管我真的不喜欢这种解决方案,但我不得不将所有内容都转移到了ScrollView。 正如我说的那样,由于ScrollView非常慢,因此决定以十个为十进制显示报价,并自动显示下十个。


向前走一点,我要说的是,当用户收到第一个反馈请求时,他们说书签并没有真正受到损害,在我看来,客户仍然怀疑使用ScrollView的决定的正确性,因为如果我们留下引号和RecycleView的缩略图,那么,无论它们有多长时间,它们都可以立即从用户先前在报价单会话列表中查看的书签中恢复。 借助ScrollView,用户将至少在等待引用显示的前提下变老。

建筑设备与服务


随着应用程序的开发,有人提出了在其中提交服务的建议,该服务将每天从数据库向用户发送一次随机报价。 我以前从未在Kivy中处理过类似的任务,但是我记得有一篇关于哈布雷的文章 ,我决定尝试一下。

在消磨了整整一周的时间后,打破了五个键盘和两个显示器,我无法按照以上文章的说明来编译软件包-在编译过程中找不到所需的类。 给文章作者写信后,我建议,显然,我失败的原因只有两个:我是个白痴,或者开发人员破坏了Buildozer,后者是一个用于为Android构建APK软件包的工具。 我的假设是正确的-“当然,在版本0.33之后,他们打破了规则,他们到底会收集到什么。”


是的,在Kivy论坛上的大部分问题与Buildozer恰恰引起的各种问题有关。 现在,此工具的每个版本都需要自己的Cython版本,您将长期尝试使用最新的Buildozer版本选择该版本的Cython,因此您将无法向您的JAR项目添加库,因为尽管该项目已组装完毕,但仍不会添加该库,因此您将需要一个多星期的时间和我一样,四处寻找问题。 而且...您将找不到她。 因此,对于初学者和心态较弱的人,与Buildozer合作可以带到诊所。

所以我吐槽了这辆破烂的拖拉机,下了地狱,去了github,下载了python-for-android,在非现场使用Crystax-NDK,安装了Python 3.5,并冷静地将项目的APK与第三个Python分支放在一起,事实证明这要容易得多。而不是臭名昭著的Buildozer。

服务如何? 但是什么都没有。 他们不工作。 更准确地说,无论文章的作者对Kivy中的服务有何要求,在项目中创建的服务都不会从重新启动智能手机开始。 在Google Play上找到并安装了它的项目后,我发现没有任何重启程序的服务正在启动。 Kivy中100%的服务仅始于应用程序本身的启动。 以后,如果您关闭应用程序,该服务将继续安静地工作,直到您关闭设备。

关于Python 2和Python 3


今年2月,Moscow Python在Yandex的莫斯科办公室举行,弗拉迪斯拉夫·沙什科夫(Vladislav Shashkov)在会上作了主题演讲:“具有kivy / buildozer的Python移动应用程序是成功的关键”。 因此,他愚蠢地说APK程序集中的Python 2比Python 3快。别相信任何人,这是不对的。 原则上,Python 3比Python 2快! 当我开发“圣徒的语录”(当时还假定第二个Python分支将在程序集中使用)时,我很震惊地发现使用json读取20 MB引用库,该库在没有网络连接时在应用程序中使用。最多可在移动设备上加载13-16秒! 和相同的基础,但是已经使用Python 3在1-2秒内在设备上处理了! 得出自己的结论...

关于React Native



是的,在我的文章中,我决定在Kivy和其他框架之间进行跨平台开发。 在这里,您只需打开扰流板,看看如何在React Native上创建简单,快速且优雅的应用程序...

例子
让我们尝试绘制一个完整的界面。 我们使用本机库中的组件重写App.js:

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; 


我们看到了必须创建的新AppFooter组件。 我们转到./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; 

一切准备就绪,可以尝试构建我们的应用程序!

我们的按钮尚不知道如何切换。 是时候教他们了。 为此,您需要做两件事:学习如何处理click事件以及如何存储状态。 让我们从状态开始。 由于我们选择了纯组件和全局存储,因此拒绝将状态存储在组件中,因此我们将使用Redux。

首先,我们必须创造自己的一面。

 import {createStore} from 'redux'; const initialState = {}; const store = createStore(reducers, initialState); 

让我们为减速器创建一个空白。 在reducers文件夹中,创建具有以下内容的index.js文件:

 export default (state = [], action) => { switch (action.type) { default: return state } }; 

将reducer连接到App.js:

 import reducers from './reducers'; 

现在,我们需要在组件上分发我们的存储。 这是专门使用提供程序组件完成的。 我们将其连接到项目:

 import {Provider} from 'react-redux'; 

并将所有组件包装在提供程序中。 更新后的App.js如下所示:

 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; 

现在,我们的应用程序可以存储其状态。 让我们利用这一点。 我们添加了模式状态,默认情况下设置为ARTICLES。 这意味着在第一个渲染中,我们的应用程序将设置为显示文章列表。

 const initialState = { mode: 'ARTICLES' }; 

不错,但是手动写入字符串值会导致潜在的错误。 让我们获取常量。 创建具有以下内容的./constants/index.js文件:

 export const MODES = { ARTICLES: 'ARTICLES', PODCAST: 'PODCAST' }; 

并重写App.js:

 import {MODES} from './constants'; const initialState = { mode: MODES.ARTICLES }; 

好吧,有一个状态,是时候将其传递给页脚组件了。 让我们再看看我们的./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; 

如我们所见,开关的状态是使用Button组件的active属性确定的。 让我们将应用程序的当前状态推送到Button。 这并不困难,引擎盖的主要组件是我们之前连接的Provider组件。 仅保留其中的当前状态并将AppFooter组件放入属性(props)中。 首先,我们修改AppFooter,以便可以通过将模式传递给props来控制按钮的状态:

 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; 

现在让我们开始创建一个容器。 创建文件./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; 

并在App.js中而不是AppFooter组件中连接AppFooterContainer容器。 到目前为止,我们的容器与组件没有什么不同,但是一旦我们将其连接到应用程序的状态,一切都会改变。 开始吧!

 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); 

很实用! 所有功能都变得干净了。 这是怎么回事 我们使用connect函数将容器连接到状态,并使用mapStateToProps函数将其道具连接到全局状态的内容。 很干净漂亮。

因此,我们已经学会了从上到下分发数据。 现在我们需要学习如何从下至上改变我们的全球状态。 动作旨在生成有关需要更改全局状态的事件。 让我们创建一个单击按钮时发生的动作。

创建文件./actions/index.js:

 import { SET_MODE } from './actionTypes'; export const setMode = (mode) => ({type: SET_MODE, mode}); 

还有./actions/actionTypes文件,我们将在其中存储带有动作名称的常量:

 export const SET_MODE = 'SET_MODE'; 

该操作将创建一个具有事件名称和该事件附带的数据集的对象,仅此而已。 现在,我们将学习如何生成此事件。 我们返回到AppFooterContainer容器并连接mapDispatchToProps函数,该函数会将事件分发程序连接到容器的prop。

 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); 

好吧,我们有一个引发SET_MODE事件的函数,我们将其跳过了AppFooter组件。 仍然存在两个问题:

没有人调用此功能。
没有人在听事件。

我们将处理第一个问题。 我们转到AppFooter组件,并将调用连接到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; 

现在,当按下按钮时,将引发SET_MODE事件。 有待学习如何在出现时改变全球状态。 我们转到先前创建的./reducers/index.js并为此事件创建一个reducer:

 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 } }; 

太好了! 现在,单击按钮将生成一个更改全局状态的事件,并且页脚在收到这些更改后将重新绘制按钮。


原始文章
是的,非常简单吗? 很难想象有多少程序员死于React Native项目,以及为这些耻辱付出了多少钱。 所有这些的结果只是一个小例子,比Hello World稍微复杂一点。


在1988年发行专辑“ ... and Justice for All”的音乐会节目后,Metallica乐队的负责人James Hetfield说:“是的。 因此,当我在React Native上编写了示例代码之后,我声援James了-那就是...但是写活着是不可能的!

这是使用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() 

它是如此简单,以至于评论在这里都是多余的。

是的,您可能对此一无所知,但全部都是用Kivy编写的:

vimeo.com/29348760
vimeo.com/206290310
vimeo.com/25680681
www.youtube.com/watch?v=u4NRu7mBXtA
www.youtube.com/watch?v=9rk9OQLSoJw
www.youtube.com/watch?v=aa9LXpg_gd0
www.youtube.com/watch?v=FhRXAD8-UkE
www.youtube.com/watch?v=GJ3f88ebDqc&t=111s
www.youtube.com/watch?v=D_M1I9GvpYs
www.youtube.com/watch?v=VotPQafL7Nw

最后,我给出了该应用程序的视频:


在评论中写下您希望在Habr页面上看到的有关Kivy的文章。 如果可能,所有愿望都会实现。 很快见,dzzya!

Source: https://habr.com/ru/post/zh-CN414353/


All Articles