كيفي. من الخلق إلى الإنتاج خطوة واحدة. الجزء 2

الجزء الأول

تحياتي!

اليوم ، كما هو الحال دائمًا ، دعنا نتحدث عن إنشاء تطبيقات محمولة باستخدام إطار عمل Kivy و Python. على وجه الخصوص ، سيركز على إنشاء عميل محمول لمورد إنترنت واحد ونشره على Google Play. سأخبرك بالمشكلات التي قد يواجهها المبتدئ والمطور المتمرس ، الذين قرروا تجربة أنفسهم في تطوير الأنظمة الأساسية المختلفة مع Kivy ، ما الذي يمكن وما يجب القيام به في البرمجة مع Python لنظام Android.

ذات صباح ، وجدت في بريدي على حبري رسالة تسأل عما إذا كان بإمكاني استخدام Python و Kivy "لإعادة إنشاء موقع svyatye.com في تطبيق محمول حتى يتمكن الأشخاص من قراءته واستخدامه بلا اتصال" مع النشر اللاحق للعميل في متجر تطبيقات Google Play. باتباع الرابط وتصفح المورد ، الذي تبين أنه مكتبة كبيرة من الاقتباسات ، تخيلت كيف ستبدو في عرض تقديمي متنقل وكيف يمكنني إنشاء قوائم "أكثر من 30.236 من أقوال الآباء ومعلمي الكنيسة" على الرغم من طول الاقتباسات ، وأحيانًا ، وصل إلى أكثر من 10000 حرف (5-6 صفحات من النص المطبوع). منذ أن كنت أعمل مع Kivy لفترة طويلة ، أدركت بسرعة كيف وماذا أفعل. لذلك ، أجاب الزبون أنه لن يكون من الصعب تقديم مثل هذا الطلب. ومع ذلك ، نشأت صعوبات ، والتي سأناقشها أدناه ، مع ذلك ...

لم يتم توفير المواصفات الفنية. الشرط الوحيد هو أن يعمل التطبيق مثل الساعة. لا مواعيد نهائية. لم تكن هناك تخطيطات للواجهة أيضًا. "يجب أن يكون كل شيء بسيطًا قدر الإمكان ، بدون رسوم متحركة ، وتحولات ، وقشور أخرى ، في كلمة واحدة ، قدر الإمكان." حسنا ، كل خير. علاوة على ذلك ، قراري ناضج بالفعل - سيستخدم التطبيق كائن RecycleView واحدًا ، والذي سيعرض الفئات والفئات الفرعية وقوائم مؤلفي الاقتباسات والاقتباسات نفسها.

القوائم


ومع ذلك ، فإن RecycleView ، الذي يسمح لك بفتح قوائم ضخمة من عدة آلاف في جزء من الثانية ، تصرف بشكل مختلف تمامًا عن المطلوب. لا ، لم تكن هناك مشاكل في فتح قوائم عروض الأسعار ، وعمل كل شيء بسرعة ، حتى أنني لم أحمل عروض أسعار جديدة مع نافذة "انتظر" على الموقع ، لأن قائمة عروض الأسعار للفئة المحددة تم تقديمها على الفور وبشكل كامل. كانت المشكلة مختلفة - أصر العميل على أنه يجب عرض نص الاقتباس في القائمة بالكامل وأن RecycleView لم يكن مناسبًا تمامًا هنا. والحقيقة هي أن مبدأ تشغيل هذه الأداة هو كما يلي: يتم إنشاء كائن واحد في القائمة بأكملها ، ثم يتم استنساخه ببساطة ، ونتيجة لذلك لدينا سرعة مذهلة في عرض القائمة ، بغض النظر عن حجمها. ولكن هناك شيء واحد - يجب أن يكون ارتفاع عنصر القائمة ثابتًا ومعروفًا مسبقًا. ولكن إذا كنت بحاجة إلى حساب ارتفاع عنصر القائمة التالي ديناميكيًا عند التمرير ، كما هو الحال في حالتي ، فهناك تأخر ملحوظ - تتجمد القائمة لجزء من الثانية ، وهو ، كما ترى ، جاهزًا بأي شكل من الأشكال.

مع الحزن إلى النصف ، تمكنت من إقناع العميل بقائمة بمعاينة الاقتباسات ، والتي سيتم فتح نصها بالكامل عن طريق tapu إلى نص المعاينة ، كما تم في أي منتدى تقريبًا ، ليس لأن RecycleView لم يتمكن من التعامل مع المهمة ، ولكن لأنه كان الأكثر منطقية: لتمرير النص متعدد الصفحات من الاقتباس ، خاصة إذا كان الاقتباس لا يهم المستخدم ، من وجهة نظري لم يكن صحيحًا.

التين. 1
معاينة والنص الكامل عند النقر على معاينة الاقتباس

عمل هذا الخيار بسرعة كبيرة ، ولكن ... العميل لم يعجبه ... اضطررت إلى استخدام عرض ScrollView بطيئ ، والذي يعرض القائمة قبل عرضها على الشاشة ، مما يعني أنه لا يجمد التمرير لقائمة الاقتباسات ، لأنه يحسب ويعرض جميع معلمات عناصر القائمة مقدمًا ، بطبيعة الحال ، سيؤثر ذلك على سرعة إدراج الشاشة. عادة يتم وضع الأداء في المقام الأول ، وهنا يقولون لي ، "فليكن أبطأ".


حسنًا ، لم أجادل بعد الآن ، وعلى الرغم من أنني لم يعجبني هذا الحل حقًا ، كان عليّ نقل كل شيء إلى ScrollView. نظرًا لأن ، كما قلت ، فإن ScrollView بطيء جدًا ، فقد تقرر عرض عروض الأسعار في أجزاء من عشرة لكل منها مع تحميل تلقائي إضافي للعشر التالية.


بعد قليل ، سأقول أنه عندما جاءت ردود الأفعال الأولى من المستخدمين مع طلب ، قالوا إن الإشارات المرجعية لن تضر حقًا ، كما يبدو لي ، إلا أن العميل شك في صحة قرار استخدام ScrollView ، لأنه إذا تركنا الصور المصغرة للاقتباسات و RecycleView ، ثم بدون مشاكل ، يمكنهم استعادة على الفور من الإشارات المرجعية التي شاهدها المستخدم سابقًا في قوائم جلسات العمل السابقة للاقتباسات ، بغض النظر عن مدتها. ومع ScrollView ، سيكبر المستخدم ببساطة أثناء انتظار عرض القائمة على الأقل من روح الاقتباسات.

البناء والخدمات


مع تطور التطبيق ، كان هناك اقتراح لتقديم خدمة فيه لإرسال عرض أسعار عشوائي من قاعدة البيانات إلى المستخدم مرة واحدة في اليوم. لم أكن قد تعاملت مع مهام مماثلة في Kivy من قبل ، ولكن تذكرت أن هناك مقالة عن حبري حول هذه المسألة ، قررت تجربتها.

بعد قتل أسبوع كامل ، وكسر خمس لوحات مفاتيح وجهازي عرض ، لم أتمكن من تجميع الحزمة وفقًا للتعليمات الواردة في المقالة أعلاه - أثناء التجميع لم يتم العثور على الفصل المطلوب. بعد أن كتبت إلى كاتب المقال ، اقترحت ، على ما يبدو ، سببين فقط فشلت في تحقيقهما: إما أنني كنت أحمق أو كسر المطورون Buildozer ، وهي أداة لبناء حزم APK لنظام Android. تبين أن افتراضاتي صحيحة - "بالطبع ، لقد كسروها ، بعد الإصدار 0.33 ما الذي سيجمعونه".


نعم ، ترتبط حصة الأسد من الأسئلة في منتدى Kivy بمختلف المشكلات التي تنشأ بالضبط مع Buildozer. الآن يتطلب كل إصدار من هذه الأداة إصدارًا خاصًا به من Cython ، والذي ستقوم باختياره تجريبيًا لفترة طويلة ، باستخدام أحدث إصدارات Buildozer ، لن تتمكن من إضافة مكتبة إلى مشروع JAR الخاص بك ، لأنه على الرغم من تجميع المشروع ، فلن تتم إضافة المكتبة إليه وستكون أسبوعًا آخر ، مثلي ، اجلس حولك للبحث عن مشكلة. و ... لن تجدها. لذلك ، بالنسبة للمبتدئين والأشخاص ذوي العقلية الضعيفة ، فإن العمل مع Buildozer يمكن أن يجلب إلى العيادة.

لذلك بصقت على هذا الجرار الميت ، ذهبت إلى الجحيم ، ذهبت إلى github ، تم تنزيل python-for-android ، وأخذت Crystax-NDK في الموقع خارج الموقع ، وقمت بتثبيت Python 3.5 ووضعت بهدوء ملف APK للمشروع مع فرع Python الثالث ، والذي تبين أنه أسهل بكثير. من مع Buildozer سيئ السمعة.

ماذا عن الخدمات؟ لكن لا شيء. إنهم لا يعملون. بتعبير أدق ، لن تبدأ الخدمة التي تم إنشاؤها في مشروعك بإعادة تشغيل الهاتف الذكي ، بغض النظر عما يدعي مؤلف المقالة حول الخدمات في Kivy. بعد العثور على Google Play وتثبيت مشروعه ، وجدت أنه لا توجد خدمات تبدأ بإعادة تشغيل البرنامج. 100 ٪ من الخدمات في Kivy تبدأ فقط مع إطلاق التطبيق نفسه. في وقت لاحق ، إذا قمت بإغلاق التطبيق ، فستستمر الخدمة في العمل بهدوء حتى تقوم بإيقاف تشغيل الجهاز.

حول Python 2 و Python 3


في فبراير من هذا العام ، عقدت موسكو Python في مكتب ياندكس في موسكو ، حيث قدم فلاديسلاف شاشكوف عرضًا حول موضوع: "تطبيق الهاتف المحمول Python مع kivy / buildozer هو مفتاح النجاح". لذلك كان لديه الغباء ليقول أن Python 2 في تجميع APK أسرع من Python 3. لا تصدق أي شخص ، هذا ليس صحيحًا. Python 3 أسرع من Python 2 من حيث المبدأ! عندما قمت بتطوير "اقتباسات القديسين" (ثم افترض أيضًا أن فرع Python الثاني سيتم استخدامه في التجميع) ، شعرت بالرعب عندما وجدت أن قاعدة الاقتباس 20 ميجا بايت ، والتي يتم استخدامها في التطبيق عندما لا يكون هناك اتصال بالشبكة ، تتم قراءتها باستخدام json. تحميل ما يصل إلى 13-16 ثانية على جهاز محمول! ونفس القاعدة ، ولكن مع Python 3 ، تتم معالجته على الجهاز في 1-2 ثانية! استخلاص استنتاجاتك الخاصة ...

حول رد فعل السكان الأصليين



نعم ، في مقالاتي ، قررت أن أرسم أوجه التشابه بين 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; 

كل شيء جاهز لمحاولة بناء تطبيقنا!

أزرارنا لا تعرف حتى الآن كيفية التبديل. حان الوقت لتعليمهم. للقيام بذلك ، تحتاج إلى القيام بأمرين: تعلم كيفية التعامل مع حدث النقر ومعرفة كيفية تخزين الحالة. لنبدأ بالدولة. نظرًا لأننا رفضنا تخزين الحالة في المكون ، وبعد اختيار المكونات النقية ومتجر عالمي ، سنستخدم Redux.

بادئ ذي بدء ، يجب علينا إنشاء جانبنا.

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

دعونا إنشاء فراغ للمخفضات. في مجلد المخفضات ، قم بإنشاء ملف index.js بالمحتويات التالية:

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

ربط مخفضات 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. دعنا ندفع الحالة الحالية للتطبيق إلى Button. هذا ليس صعبًا ، فالجزء الرئيسي من غطاء المحرك هو مكون المزود الذي قمنا بتوصيله مسبقًا. يبقى فقط لأخذ الحالة الحالية منه ووضع مكونات AppFooter في الخصائص (الدعائم). بادئ ذي بدء ، نقوم بتعديل AppFooter الخاص بنا بحيث يمكن التحكم في حالة الأزرار عن طريق تمرير الوضع من خلال الدعائم:

 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; 

وقم بتوصيل حاوية AppFooterContainer في App.js بدلاً من مكون AppFooter. حتى الآن ، لا تختلف حاويتنا عن المكون ، ولكن كل شيء سيتغير بمجرد توصيله بحالة التطبيق. لنفعلها!

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

وظيفية للغاية! أصبحت جميع الميزات نظيفة. ما الذي يحدث هنا؟ نقوم بتوصيل حاويتنا بالحالة باستخدام وظيفة الاتصال وربط دعائمها بمحتويات الحالة العالمية باستخدام وظيفة 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 التي ستربط مرسلات الأحداث بدعائم الحاوية.

 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 الذي تم إنشاؤه مسبقًا وإنشاء مخفض لهذا الحدث:

 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 ، قال زعيم ميتاليكا جيمس هيتفيلد ، "هذا ... ولكن من المستحيل أن تلعب على قيد الحياة". لذا ، بعد أن كتبت نموذج التعليمات البرمجية على React Native ، أصبحت متضامنًا مع جيمس - هذا ... ولكن من المستحيل الكتابة على قيد الحياة!

وإليك كيف يتم نفس الشيء باستخدام إطار عمل 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

في الختام أعطي فيديو عن التطبيق:


اكتب في التعليقات ما هي المقالات التي تود رؤيتها عن كيفي على صفحات هبر. إذا أمكن ، سيتم تحقيق جميع الرغبات. نراكم قريبا يا دزة!

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


All Articles