كان الحصان يحرث أكثر ، لكن رئيس المزرعة الجماعية لم يصبح



في الآونة الأخيرة ، في مجتمع الجوال ، يمكن للمرء أن يسمع في كثير من الأحيان عن Flutter ، React Native. كنت مهتمًا بفهم الربح من هذه القطع. وإلى أي مدى سيغيرون حياتهم حقًا عند تطوير التطبيقات. ونتيجة لذلك ، تم إنشاء 4 تطبيقات (متطابقة من وجهة نظر الوظائف المنجزة): Android الأصلي و iOS الأصلي و Flutter و React Native. في هذه المقالة ، وصفت ما تعلمته من تجربتي وكيف يتم تنفيذ عناصر تطبيقات مماثلة في الحلول قيد النظر.

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

كتطبيق مطور ، تقرر إنشاء "مؤقت رياضي" ، والذي سيساعد الأشخاص المشاركين في الرياضة عند أداء التدريب الفاصل.

يتكون التطبيق من 3 شاشات.


شاشة تشغيل المؤقت


شاشة محفوظات التمرين


شاشة إعدادات المؤقت

هذا التطبيق مثير للاهتمام بالنسبة لي كمطور ، لأن المكونات التالية التي تهمني ستتأثر بإنشائه:
- التخطيط
- عرض مخصص
- العمل مع قوائم واجهة المستخدم
- multithreading
- قاعدة بيانات
- شبكة
- تخزين القيمة الرئيسية

من المهم ملاحظة أنه بالنسبة لـ Flutter و React Native ، يمكننا إنشاء جسر (قناة) إلى الجزء الأصلي من التطبيق واستخدامه لتنفيذ كل ما يوفره نظام التشغيل. لكنني كنت أتساءل ما هي الأطر التي تقدم من الصندوق.

اختيار أدوات التطوير


بالنسبة للتطبيق الأصلي لنظام iOS - اخترت بيئة تطوير Xcode ولغة برمجة Swift. بالنسبة إلى Android الأصلي - Android Studio و Kotlin. تم تطوير React Native في WebStorm ، لغة برمجة JS. Flutter - Android Studio و Dart.

حقيقة مثيرة للاهتمام عند التطوير على Flutter بدت لي أنه من Android Studio مباشرة (IDE الرئيسي لتطوير Android) يمكنك تشغيل التطبيق على جهاز iOS.



هيكل المشروع


هياكل مشاريع iOS و Android الأصلية متشابهة للغاية. هذا ملف تخطيط يحتوي على الامتدادات .storyboard (iOS) و. xml (Android) ، ومديري التبعيات Podfile (iOS) و Gradle (Android) ، وملفات التعليمات البرمجية المصدر مع الامتدادات. swift (iOS) و .kt (Android).


هيكل مشروع أندرويد


هيكل مشروع IOS

تحتوي هياكل Flutter و React Native على مجلدات Android و iOS ، والتي تحتوي على المشاريع الأصلية المعتادة لنظامي Android و iOS. يتصل Flutter و React Native بالمشاريع الأصلية كمكتبة. في الواقع ، عند تشغيل Flutter على جهاز iOS الخاص بك ، يتم تشغيل تطبيق iOS الأصلي المعتاد مع توصيل مكتبة Flutter. بالنسبة إلى React Native و Android ، كل شيء هو نفسه.

يحتوي Flutter و React Native أيضًا على مديري التبعية pub.json (React Native) و pubspec.yaml (Flutter) وملفات المصدر ذات امتدادات .js (React Native) و .dart (Flutter) ، والتي تحتوي أيضًا على التخطيط.


هيكل مشروع رفرفة


تفاعل هيكل المشروع الأصلي

التخطيط


بالنسبة لنظامي iOS و Android الأصليين ، هناك برامج تحرير مرئية. هذا يبسط إلى حد كبير إنشاء شاشات.


محرر مرئي للأندرويد الأصلي


محرر مرئي لنظام iOS الأصلي

لا توجد برامج تحرير مرئية لـ React Native و Flutter ، ولكن هناك دعم لوظيفة إعادة التحميل الساخنة ، والتي تبسط على الأقل بطريقة ما العمل مع واجهة المستخدم.


إعادة التشغيل الساخن في Flutter


إعادة التشغيل السريع في React Native

في Android و iOS ، يتم تخزين التخطيط في ملفات منفصلة بالملحقين xml و. storybord ، على التوالي. في React Native و Flutter ، يتم اشتقاق التخطيط مباشرةً من الشفرة. يجب ملاحظة نقطة مهمة في وصف سرعة واجهة المستخدم أن Flutter له آليات العرض الخاصة به ، والتي يعد منشئو إطار العمل باستخدامها 60 إطارًا في الثانية. ويستخدم React Native عناصر واجهة المستخدم الأصلية التي تم إنشاؤها باستخدام js ، مما يؤدي إلى تعشيشها المفرط.

في Android و iOS ، لتغيير خاصية العرض ، نستخدم رابطًا لها من الرمز ، وعلى سبيل المثال ، لتغيير لون الخلفية ، نتسبب في تغييرات في الكائن مباشرة. في حالة React Native and Flutter ، هناك فلسفة أخرى: نقوم بتغيير الخصائص داخل استدعاء setState ، ويتم إعادة رسم العرض نفسه وفقًا للحالة المتغيرة.

أمثلة على إنشاء شاشة عداد لكل من الحلول المحددة:


تخطيط شاشة المؤقت على Android


تخطيط شاشة المؤقت على iOS

تخطيط شاشة مؤقت Flutter

@override Widget build(BuildContext context) { return Scaffold( body: Stack( children: <Widget>[ new Container( color: color, child: new Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( " ${getTextByType(trainingModel.type)}", style: new TextStyle( fontWeight: FontWeight.bold, fontSize: 24.0), ), new Text( "${trainingModel.timeSec}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 56.0), ), new Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( " ${trainingModel.setCount}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 24.0), ), Text( " ${trainingModel.cycleCount}", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 24.0), ), ], ), ], ), padding: const EdgeInsets.all(20.0), ), new Center( child: CustomPaint( painter: MyCustomPainter( //0.0 trainingModel.getPercent()), size: Size.infinite, ), ) ], )); } 

تفاعل تخطيط شاشة المؤقت الأصلي

  render() { return ( <View style={{ flex: 20, flexDirection: 'column', justifyContent: 'space-between', alignItems: 'stretch', }}> <View style={{height: 100}}> <Text style={{textAlign: 'center', fontSize: 24}}> {this.state.value.type} </Text> </View> <View style={styles.container}> <CanvasTest data={this.state.value} style={styles.center}/> </View> <View style={{height: 120}}> <View style={{flex: 1}}> <View style={{flex: 1, padding: 20,}}> <Text style={{fontSize: 24}}>  {this.state.value.setCount} </Text> </View> <View style={{flex: 1, padding: 20,}}> <Text style={{textAlign: 'right', fontSize: 24}}>  {this.state.value.cycleCount} </Text> </View> </View> </View> </View> ); } 

عرض مخصص


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



لم تكن هناك مشكلة لنظام iOS الأصلي ، حيث يوجد وصول إلى الطبقة ، حيث يمكنك رسم أي شيء تريده.

  let shapeLayer = CAShapeLayer() var angle = (-Double.pi / 2 - 0.000001 + (Double.pi * 2) * percent) let circlePath = UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), radius: CGFloat(95), startAngle: CGFloat(-Double.pi / 2), endAngle: CGFloat(angle), clockwise: true) shapeLayer.path = circlePath.cgPa 

بالنسبة إلى Android الأصلي ، يمكنك إنشاء فئة ترث من View. وأعد تعريف طريقة onDraw (كانفاس كانفاس) ، في المعلمة التي يرسم عليها كائن كانفاس.

  @Override protected void onDraw(Canvas canvas) { pathCircleOne = new Path(); pathCircleOne.addArc(rectForCircle, -90, value * 3.6F); canvas.drawPath(pathCircleBackground, paintCircleBackground); } 

بالنسبة إلى Flutter ، يمكنك إنشاء فئة ترث من CustomPainter. وتجاوز طريقة الطلاء (قماش كانفاس ، حجم الحجم) ، التي تمر في المعلمة كائن قماش - أي تنفيذ مشابه جدًا كما في Android.

  @override void paint(Canvas canvas, Size size) { Path path = Path() ..addArc( Rect.fromCircle( radius: size.width / 3.0, center: Offset(size.width / 2, size.height / 2), ), -pi * 2 / 4, pi * 2 * _percent / 100); canvas.drawPath(path, paint); } 

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

  handleCanvas = (canvas) => { if (canvas) { var modelTimer = this.state.value; const context = canvas.getContext('2d'); context.arc(75, 75, 70, -Math.PI / 2, -Math.PI / 2 - 0.000001 - (Math.PI * 2) * (modelTimer.timeSec / modelTimer.maxValue), false); } } 

العمل مع قوائم واجهة المستخدم




خوارزمية Android و iOS و Flutter - الحلول متشابهة جدًا. نحتاج إلى الإشارة إلى عدد العناصر الموجودة في القائمة. ومن خلال رقم العنصر الخلية التي تريد رسمها.
يستخدم IOS UITableView لرسم القوائم ، حيث تحتاج إلى تنفيذ أساليب DataSource.

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return countCell } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return cell } 

بالنسبة إلى Android ، يستخدمون RecyclerView ، في المحول الذي نطبق فيه طرق iOS مماثلة.

 class MyAdapter(private val myDataset: Array<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() { override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.textView.text = myDataset[position] } override fun getItemCount() = myDataset.size } 

بالنسبة للرفرفة ، يستخدمون ListView ، حيث يتم تنفيذ طرق مماثلة في المنشئ.

 new ListView.builder( itemCount: getCount() * 2, itemBuilder: (BuildContext context, int i) { return new HistoryWidget( Key("a ${models[index].workTime}"), models[index]); }, ) 

يستخدم التفاعل الأصلي ListView. التنفيذ مشابه للحلول السابقة. ولكن لا يوجد مرجع لعدد وعدد العناصر في القائمة ، في DataSource قمنا بتعيين قائمة العناصر. وفي RenderRow ، نقوم بتنفيذ إنشاء خلية اعتمادًا على العنصر الذي جاء.

 <ListView dataSource={this.state.dataSource} renderRow={(data) => <HistoryItem data={data}/>} /> 

تعدد مؤشرات الترابط


عندما بدأت في التعامل مع تعدد مؤشرات الترابط وعدم التزامن ، شعرت بالرعب من مجموعة متنوعة من الحلول. في iOS - هذا هو GCD ، التشغيل ، في Android - AsyncTask ، Loader ، Coroutine ، in React Native - Promise ، Async / Await ، في Flutter-Future ، Stream. مبادئ بعض الحلول متشابهة ، لكن التنفيذ لا يزال مختلفًا.
جاء Rx الحبيب لإنقاذ. إذا كنت لا تحبه بالفعل ، أنصحك بالدراسة. إنه في جميع الحلول التي أعتبرها في الشكل: RxDart ، RxJava ، RxJs ، RxSwift.

Rxjava

  Observable.interval(1, TimeUnit.SECONDS) .subscribe(object : Subscriber<Long>() { fun onCompleted() { println("onCompleted") } fun onError(e: Throwable) { println("onError -> " + e.message) } fun onNext(l: Long?) { println("onNext -> " + l!!) } }) 

Rxswift

 Observable<Int>.interval(1.0, scheduler: MainScheduler.instance) .subscribe(onNext: { print($0) }) 

Rxdart

  Stream.fromIterable([1, 2, 3]) .transform(new IntervalStreamTransformer(seconds: 1)) .listen((i) => print("$i sec"); 

Rxjs

 Rx.Observable .interval(500 /* ms */) .timeInterval() .take(3) .subscribe( function (x) { console.log('Next: ' + x); }, function (err) { console.log('Error: ' + err); }, function () { console.log('Completed'); }) 

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

قاعدة البيانات


في تطبيقات الجوال ، المعيار هو قاعدة بيانات SQLite. في كل من الحلول المدروسة ، يتم كتابة غلاف للعمل معه. يستخدم Android عادةً غرفة ORM.

على iOS ، البيانات الأساسية. يستطيع Flutter استخدام المكوّن الإضافي sqflite.

In React Native - تفاعل التخزين الأصلي مع sqlite. تم تصميم كل هذه الحلول بشكل مختلف. ولجعل التطبيقات تبدو ، يجب عليك كتابة استعلامات Sqlite يدويًا ، دون استخدام الأغلفة.

ربما من الأفضل التطلع إلى مكتبة تخزين بيانات Realm ، التي تستخدم نواة خاصة بها لتخزين البيانات. وهو مدعوم على iOS و Android و React Native. Flutter ليس لديه دعم حاليًا ، لكن مهندسي Realm يعملون في هذا الاتجاه.

عالم على Android

 RealmResults<Item> item = realm.where(Item.class) .lessThan("id", 2) .findAll(); 

عالم على iOS

 let item = realm.objects(Item.self).filter("id < 2") 

عالم في رد الفعل الأم

 let item = realm.objects('Item').filtered('id < 2'); 

تخزين القيمة الرئيسية


يستخدم نظام iOS الأصلي UserDefaults. في Android الأصلي ، التفضيلات. في React Native و Flutter ، يمكنك استخدام المكتبات التي هي مجمّع لمتاجر قيمة المفتاح الأصلية (SharedPreference (Android) و UserDefaults (iOS)).

Android

 SharedPreferences sPref = getPreferences(MODE_PRIVATE); Editor ed = sPref.edit(); ed.putString("my key'", myValue); ed.commit(); 

iOS

 let defaults = UserDefaults.standard defaults.integer(forKey: "my key'") defaults.set(myValue, forKey: "my key") 

رفرفة

 SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.getInt(my key') prefs.setInt(my key', myValue) 

رد فعل الأم

 DefaultPreference.get('my key').then(function(value) {console.log(value)}); DefaultPreference.set('my key', myValue).then(function() {console.log('done')}); 

الشبكة


للعمل مع الشبكة في نظامي iOS و Android الأصليين ، هناك عدد كبير من الحلول. الأكثر شعبية هي Alamofire (iOS) و Retrofit (Android). لدى React Native و Flutter عملاء مستقلين عن النظام الأساسي الخاص بهم للانتقال عبر الإنترنت. تم تصميم جميع العملاء بشكل مشابه للغاية.

Android

 Retrofit.Builder() .baseUrl("https://timerble-8665b.firebaseio.com") .build() @GET("/messages.json") fun getData(): Observable<Map<String,RealtimeModel>> 

iOS

 let url = URL(string: "https://timerble-8665b.firebaseio.com/messages.json") Alamofire.request(url, method: .get) .responseJSON { response in … 

رفرفة

 http.Response response = await http.get('https://timerble-8665b.firebaseio.com/messages.json') 

رد فعل الأم

 fetch('https://timerble-8665b.firebaseio.com/messages.json') .then((response) => response.json()) 

وقت التطوير




ربما يكون من غير الصحيح استخلاص أي استنتاجات بناءً على وقت التطوير ، حيث إنني مطور Android. ولكن أعتقد أنه بالنسبة لمطور iOS ، فإن دخول Flutter وتقنية Android سيبدو أسهل من React Native.

الخلاصة


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

فيما يتعلق بتحرير المقالة ، يرجى الكتابة في بريد إلكتروني شخصي ، وسوف أصلح كل شيء بسرور.

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


All Articles