Kivy. From creation to production - one step. Part 2

Part 1

Greetings

Today, as always, let's talk about creating mobile applications with the Kivy and Python framework. In particular, it will be about creating a mobile client for one Internet resource and publishing it on Google Play. I’ll tell you what problems a newbie and an experienced developer might face, who decided to try cross-platform development with Kivy, what can and shouldn’t be done in programming with Python for Android.

One morning, I found a letter in my e-mail on Habré asking if I could use Python and Kivy to “recreate the svyatye.com website in a mobile application, so that people could read and use it offline” and then publish the client in app store google play. Following the link and looking at a resource that turned out to be a large library of quotations, I mentally imagined how it would look in a mobile presentation and how I would create lists of “more than 30,236 sayings of the holy fathers and teachers of the church”, with the length of quotations sometimes , reached over 10,000 characters (5-6 pages of printed text). Since I have been working with Kivy for a long time, I quickly understood how and what I would do. Therefore, he replied to the customer that it would not be difficult to make such an application. However, the difficulties, which I will discuss below, still arose ...

No technical specifications were provided. The only requirement is that the application should work like a clock. No dates were set. Interface layouts were not there either. "Everything should be as simple as possible, without animations, transformations and other husks, in a word, as austere as possible." Well, so much the better. Especially since my solution has already matured - the application will use one RecycleView object, which will display categories, subcategories, lists of authors of quotes and quotes themselves.

Lists


However, RecycleView, which allows you to open huge thousands lists for a fraction of a second, behaved completely differently from what you wanted. No, there were no problems with opening quotation lists, everything worked quickly, I didn’t even begin uploading new quotes with the “Wait” window, as on the site, because the list of quotes of the selected category was rendered instantly and completely. The problem was concluded in another - the customer insisted that the text of the quotation in the list be displayed in its entirety, and RecycleView was not entirely appropriate here. The fact is that the principle of operation of this widget is as follows: one object is created for the entire list, which is then simply cloned in the future, with the result that we have an amazing list rendering speed, no matter how large it is. But there is one thing - the height of the list item should be fixed and known in advance. But if you need to dynamically calculate the height of the next element of the list when scrolling, as in my case, then a noticeable lag occurs - the list is frazzled for a split second, which you will agree is not prodaction ready.

With a grief in half, I managed to persuade the customer to a list with previews of quotes, the text of which would open entirely on tapu on the text of the preview, as it was done in almost any forum, not because RecycleView could not cope with the task, but because it was most logical: to scroll a multi-page quotation text, especially if the quotation did not interest the user, from my point of view it was not correct.

Fig. one
Preview and full text with a quote preview

This option worked very quickly, but ... the customer didn’t like it ... I had to use a slow ScrollView, which renders the list BEFORE it is displayed on the screen, which means it will not freeze the scrolling of the list of quotes, since it will calculate and render all the parameters of the list elements in advance, Naturally, it will affect the speed of displaying the list on the screen. Usually, performance is put in the first place, and here they say to me, "let it be slower."


Well, I didn’t argue anymore, and although I didn’t like this solution terribly, I had to transfer everything under ScrollView. Since, as I said, ScrollView is very slow, it was decided to display quotes in portions of ten pieces with further automatic loading of the next ten.


Looking ahead a little bit, I’ll say that when the first feedbacks from users came with a request, they say, it would not hurt the bookmarks, I think the customer still doubted the correctness of the decision to use ScrollView, as if we had left the preview quotes and RecycleView, without problems, they could instantly restore the lists of quotes previously viewed by the user in the previous session, no matter how long they were. And with ScrollView, the user will simply grow old until they wait for the list to be displayed at least from the spirit of the quotes.

Buildozer and services


As the application was developed, it was proposed to file a service in it, which would send a random quote from the database to the user once a day. I have never dealt with similar tasks in Kivy before, but remembering that there is an article on Habré on this issue, I decided to try.

After killing a whole week, breaking five keyboards and two monitors, I did not manage to assemble the package according to the instructions from the above article - the required class was not found when compiling. Having written to the author of the article, I assumed that, apparently, there are only two reasons why I failed: either I'm an idiot or the developers broke Buildozer, a tool for building APK packages for Android. My assumptions turned out to be true - “Of course, they broke it, after the 0.33 version of the hell you’ll collect them.”


Yes, the lion's share of questions on the Kivy forum is related to various problems that arise precisely with Buildozer. Now, each version of this tool requires its own version of Cython, which you will be experimentally selecting for a long time, using the latest versions of Buildozer, you will not be able to add a library to your JAR project, because even though the project is assembled, the library will not be added to it and you will have one more week Just like me, sit in search of a problem. And ... you will not find it. Therefore, for beginners and people with a weak psyche, working with Buildozer can bring to the clinic.

So I spat on this dead tractor, dropping it to hell, went to github, downloaded python-for-android, took Crystax-NDK off site, put Python 3.5 and calmly assembled the project's APK with the third branch of Python, which turned out to be much easier than with the notorious Buildozer.

What about services? And nothing. They do not work. More precisely, the service created in your project will not start with the restart of the smartphone, no matter what the author of the article claims, about the services in Kivy. Having found in Google Play and installing its project I found that no services with the restart of the program are launched. 100% services in Kivy start only with the launch of the application itself. Subsequently, if you close the application, the service will continue to work quietly until you turn off the device.

About Python 2 and Python 3


In February of this year, Moscow Python was held in the Moscow office of Yandex, in which Vladislav Shashkov spoke on the topic “Mobile application in Python with kivy / buildozer is the key to success”. So he had the folly to say that Python 2 in the APK assembly is faster than Python 3. Don't you believe it, this is not true. Python 3 is faster than Python 2 in principle! When I was developing “Quotes of Saints” (then it was assumed that the second branch of Python would be used in the assembly), I was horrified to find that the base of quotes of 20 MB in size, which is used in the application when there is no network connection, is read by json. loads already 13-16 seconds on your mobile device! And the same base, but with Python 3 is processed on the device in 1-2 seconds! Draw your own conclusions ...

About React Native



Yes, in my articles I decided to draw parallels between Kivy and other frameworks for cross-platform development. Here you just need to open the spoiler and see how simple, fast and elegant applications are created on React Native ...

Example
Let's try to draw a full interface. Rewrite App.js using the components from the native-base library:

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; 


We see a new component AppFooter, which we have to create. Go to the folder ./components/ and create the file AppFooter.js with the following contents:

 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; 

Everything is ready to try to build our application!

Our buttons do not know how to switch. It's time to teach them. To do this, you need to do two things: learn how to handle a click event and learn how to store a state. Let's start with the state. Since we refused to store the state in the component, making a choice in favor of the pure components and the global store, we will use Redux.

First of all, we have to create our story.

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

Let's create a billet for reducer. In the reducers folder, create an index.js file with the following contents:

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

We connect reduser to App.js:

 import reducers from './reducers'; 

Now we need to extend our stor on components. This is done using the Provider components specifically. We connect it to the project:

 import {Provider} from 'react-redux'; 

And wrap all the components in Provider. The updated App.js looks like this:

 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; 

Now our application can store its state. Let's take advantage of this. Add the state mode, the default setting in ARTICLES. This means that during the first render, our application will be set to display the list of articles.

 const initialState = { mode: 'ARTICLES' }; 

Not bad, but manually typing string values ​​leads to potential errors. Let's get constants. Create a ./constants/index.js file with the following contents:

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

And rewrite App.js:

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

Great, the state is there, it's time to transfer it to the footer component. Let's look at our ./components/AppFooter.js again:

 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; 

As we can see, the state of the switch is determined using the active property of the Button component. Prokin to Button current state of the application. This is done not difficult, the Provider component, which we connected earlier, takes on the main engine compartment. It remains only to take from it the current state and put AppFooter components into the props. First of all, we modify our AppFooter so that the state of the buttons can be controlled by passing mode via 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; 

Now let's start creating the container. Create a ./containers/AppFooterContainer.js file.

 import React from 'react'; import AppFooter from '../components/AppFooter.js'; import {MODES} from "../constants"; const AppFooterContainer = () => ( <AppFooter mode={MODES.ARTICLES} /> ); export default AppFooterContainer; 

And we connect the AppFooterContainer container in App.js instead of the AppFooter component. While our container is no different from the components, but everything will change as soon as we connect it to the state of the application. Let's do it!

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

Very functional! All features have become clean. What's going on here? We connect our container to the state using the connect function and connect its props with the contents of the global state using the mapStateToProps function. Very clean and beautiful.

So, we learned to spread data from top to bottom. Now we need to learn how to change our global state from the bottom up. To generate events about the need to change the global state are actions. Let's create an action that occurs when a button is clicked.

Create a ./actions/index.js file:

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

And the file ./actions/actionTypes, in which we will store constants with action names:

 export const SET_MODE = 'SET_MODE'; 

The action creates an object with the name of the event and the data set that accompanies this event, and nothing more. Now learn how to generate this event. We return to the AppFooterContainer container and connect the mapDispatchToProps function which connects the event dispatchers to the container's props.

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

Great, we have a function that spawns the SET_MODE event and we get it up to the AppFooter component. There are two problems left:

Nobody calls this function
No one is listening to the event.

Let's deal with the first problem. Go to the AppFooter component and enable the function call 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; 

Now, clicking the button will trigger the SET_MODE event. It remains to learn how to change the global state as it arises. Go to the previously created ./reducers/index.js and create a reducer for this event:

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

Gorgeous! Now click on the button generates an event that changes the global state, and the footer, having received these changes, redraws the buttons.


Original article
True, incredibly simple? It is terrible to imagine how many programmers are dying of old age on React Native projects and how much money is paid for all this disgrace. The result of all this is a small example, a bit more complicated than “Hello World”.


Once after the concert program of the album "... And Justice for All" in 1988, Metallica leader James Hetfield said: "Such a thing ... but alive is impossible to play." So, after I wrote the sample code for React Native, I became solidary with James - this is so ... but living is impossible to write!

And here is how the same is done using the Kivy framework:

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

It is so simple that even comments are superfluous here.

Yes, maybe you did not know about it, but all this is written in 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

In conclusion, I give the video of the application:


Write in the comments, what would you like to see articles on Kivy on the pages of Habr. If possible, all wishes will be realized. Until we meet again, drzya!

Source: https://habr.com/ru/post/414353/


All Articles