كيفية تكوين صداقات مع RxJava مع VIPER في Android ، وأساليب التطبيق وهيكل المنظمين

صورة



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



هندسة تناسب الجميع



RxJava هو تطبيق لمفهوم ReactiveX وتم إنشاؤه بواسطة Netflix. تحتوي مدونتهم على سلسلة من المقالات حول سبب قيامهم بذلك والمشكلات التي قاموا بحلها. يمكن العثور على الروابط (1 ، 2) في نهاية المقال. استخدمت Netflix RxJava على جانب الخادم (الواجهة الخلفية) لموازاة معالجة طلب واحد كبير. على الرغم من أنهم اقترحوا طريقة لاستخدام RxJava على الواجهة الخلفية ، إلا أن هذه البنية مناسبة لكتابة أنواع مختلفة من التطبيقات (المحمول وسطح المكتب والخلفية وغيرها الكثير). استخدم مطورو Netflix RxJava في طبقة الخدمة بطريقة تجعل كل طريقة من طرق طبقة الخدمة تُرجع الملاحظة. النقطة المهمة هي أن العناصر الموجودة في Observable يمكن تسليمها بشكل متزامن وغير متزامن. يسمح هذا للطريقة بأن تقرر بنفسها ما إذا كانت ستعيد القيمة على الفور بشكل متزامن ، (على سبيل المثالإذا كانت متوفرة في ذاكرة التخزين المؤقت) أو احصل أولاً على هذه القيم (على سبيل المثال ، من قاعدة بيانات أو خدمة بعيدة) وأعدها بشكل غير متزامن. في أي حال ، سيعود التحكم فورًا بعد استدعاء الطريقة (سواء مع البيانات أو بدونها).



/**
 * ,    ,  
 * ,      ,
 *        callback `onNext()`
 */
public Observable<T> getProduct(String name) {
    if (productInCache(name)) {
        //   ,   
        return Observable.create(observer -> {
           observer.onNext(getProductFromCache(name));
           observer.onComplete();
        });
    } else {
        //     
        return Observable.<T>create(observer -> {
            try {
                //     
                T product = getProductFromRemoteService(name);
                //  
                observer.onNext(product);
                observer.onComplete();
            } catch (Exception e) {
                observer.onError(e);
            }
        })
        //  Observable   IO
        //  / 
        .subscribeOn(Schedulers.io());
    }
}


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



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



صورة



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



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



خبرة في Android و VIPER



في معظم مشاريع Android الحالية والجديدة ، نستخدم بنية VIPER. التقيت بها عندما انضممت إلى أحد المشاريع التي استخدمت فيها بالفعل. أتذكر أنني فوجئت عندما سئلت عما إذا كنت أتطلع إلى نظام iOS. فكرت "IOS في مشروع Android؟" وفي الوقت نفسه ، جاءنا VIPER من عالم iOS وهو في الواقع إصدار أكثر تنظيماً ونمطية من MVP. تمت كتابة VIPER جيدًا في هذه المقالة (3).



في البداية ، بدا كل شيء على ما يرام: طبقات مقسمة بشكل صحيح ، وليست محملة بشكل زائد ، ولكل طبقة مجال مسؤوليتها الخاصة ، ومنطق واضح. ولكن بعد مرور بعض الوقت ، بدأ يظهر عيب واحد ، ومع نمو المشروع وتغيره ، بدأ حتى في التدخل.



الحقيقة هي أننا استخدمنا Interactor بنفس طريقة زملائنا في مقالتنا. تنفذ Interactor حالة استخدام صغيرة ، على سبيل المثال ، "تنزيل المنتجات من الشبكة" أو "أخذ منتج من قاعدة البيانات حسب المعرف" ، وتنفذ إجراءات في سير العمل. داخليًا ، ينفذ المتفاعل العمليات باستخدام عنصر يمكن ملاحظته. من أجل "تشغيل" Interactor والحصول على النتيجة ، يقوم المستخدم بتنفيذ واجهة ObserverEntity جنبًا إلى جنب مع أساليب onNext و onError و onComplete ويمررها مع المعلمات إلى طريقة التنفيذ (params، ObserverEntity).



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



saveProductInteractor.execute(product, new ObserverEntity<Void>() {
    @Override
    public void onNext(Void aVoid) {
        //      ,
        //     
    }

    @Override
    public void onError(Throwable throwable) {
        //    
        // - 
    }

    @Override
    public void onComplete() {
        //     
        // - 
    }
});


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



private void checkProduct(int id, Locale locale) {
    getProductByIdInteractor.execute(new TypesUtil.Pair<>(id, locale), new ObserverEntity<Product>() {
        @Override
        public void onNext(Product product) {
            getProductInfo(product);
        }

        @Override
        public void onError(Throwable throwable) {
            // - 
        }

        @Override
        public void onComplete() {
        }
    });
}

private void getProductInfo(Product product) {
    getReviewsByProductIdInteractor.execute(product.getId(), new ObserverEntity<List<Review>>() {
        @Override
        public void onNext(List<Review> reviews) {
            product.setReviews(reviews);
            saveProduct(productInfo);
        }

        @Override
        public void onError(Throwable throwable) {
            // - 
        }

        @Override
        public void onComplete() {
            // - 
        }
    });
    getImageForProductInteractor.execute(product.getId(), new ObserverEntity<Image>() {
        @Override
        public void onNext(Image image) {
            product.setImage(image);
            saveProduct(product);
        }

        @Override
        public void onError(Throwable throwable) {
            // - 
        }

        @Override
        public void onComplete() {
        }
    });
}

private void saveProduct(Product product) {
    saveProductInteractor.execute(product, new ObserverEntity<Void>() {
        @Override
        public void onNext(Void aVoid) {
        }

        @Override
        public void onError(Throwable throwable) {
            // - 
        }

        @Override
        public void onComplete() {
            goToSomeScreen();
        }
    });
}


حسنًا ، كيف تحب هذه المعكرونة؟ في الوقت نفسه ، لدينا منطق عمل بسيط وتداخل فردي ، لكن تخيل ما يمكن أن يحدث مع كود أكثر تعقيدًا. كما أنه يجعل من الصعب إعادة استخدام الطريقة وتطبيق برامج جدولة مختلفة لـ Interactor.



الحل بسيط بشكل مدهش. هل تشعر أن هذا النهج يحاول تقليد سلوك الملاحظة ، لكن هل هو خاطئ ويخلق قيودًا غريبة بحد ذاته؟ كما قلت من قبل ، حصلنا على هذا الرمز من مشروع قائم. عند إصلاح هذا الرمز القديم ، سنستخدم النهج الذي تركه لنا شباب Netflix. بدلاً من الاضطرار إلى تنفيذ ObserverEntity في كل مرة ، دعنا نجعل Interactor يعيد Observable فقط.



private Observable<Product> getProductById(int id, Locale locale) {
    return getProductByIdInteractor.execute(new TypesUtil.Pair<>(id, locale));
}

private Observable<Product> getProductInfo(Product product) {
    return getReviewsByProductIdInteractor.execute(product.getId())
    .map(reviews -> {
        product.set(reviews);
        return product;
    })
    .flatMap(product -> {
        getImageForProductInteractor.execute(product.getId())
        .map(image -> {
            product.set(image);
            return product;
        })
    });
}

private Observable<Product> saveProduct(Product product) {
    return saveProductInteractor.execute(product);
}

private doAll(int id, Locale locale) {
    //    
    getProductById (id, locale)
    //  
    .flatMap(product -> getProductInfo(product))
    //     
    .flatMap(product -> saveProduct(product))
    //        
    .ignoreElements()
    //  
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    //    
    .subscribe(() -> goToSomeScreen(), throwable -> handleError());
}


هاهو! لذلك لم نتخلص فقط من هذا الرعب المرهق وغير العملي ، بل جلبنا أيضًا قوة RxJava إلى Presenter.



مفاهيم في القلب



رأيت كثيرًا كيف حاولوا شرح مفهوم RxJava باستخدام البرمجة التفاعلية الوظيفية (المشار إليها فيما يلي بـ FRP). في الواقع ، لا علاقة له بهذه المكتبة. يدور FRP حول القيم المتغيرة ديناميكيًا (السلوكيات) ، والوقت المستمر ، والدلالات الدلالية. في نهاية المقال ، يمكنك العثور على بعض الروابط الممتعة (4 ، 5 ، 6 ، 7).



تستخدم RxJava البرمجة التفاعلية والبرمجة الوظيفية كمفاهيمها الأساسية. يمكن وصف البرمجة التفاعلية على أنها نقل تسلسلي للمعلومات من الكائن المرصود إلى كائن المراقب بطريقة يستقبلها كائن المراقب تلقائيًا (بشكل غير متزامن) عند ظهور هذه المعلومات.



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



يتيح لك وضع هذين المفهومين معًا ، جنبًا إلى جنب مع أنماط GoF's Observer و Iterator ، إنشاء تدفقات بيانات غير متزامنة ومعالجتها بترسانة ضخمة من الوظائف سهلة الاستخدام. كما أنه يجعل من الممكن استخدام تعدد مؤشرات الترابط بكل بساطة ، والأهم من ذلك بشكل آمن ، دون التفكير في مشاكله مثل المزامنة ، وعدم تناسق الذاكرة ، وتداخل الخيط ، إلخ.



صورة



ثلاثة حيتان من RxJava



المكونات الثلاثة الرئيسية التي تم بناء RxJava عليها هي المراقبة والمشغلون والمجدولون.

يمكن ملاحظته في RxJava مسؤولة عن تنفيذ النموذج التفاعلي. غالبًا ما يشار إلى الملاحظات على أنها تدفقات لأنها تنفذ مفهوم تدفقات البيانات ونشر التغييرات. يمكن ملاحظته هو نوع يحقق تنفيذ نموذج تفاعلي من خلال الجمع بين نمطين من عصابة الأربعة: المراقب و Iterator. يضيف Observable دلالات مفقودة إلى Observer ، وهما في Iterable:

  • قدرة المنتج على إرسال إشارة للمستهلك بعدم توفر المزيد من البيانات (تنتهي حلقة foreach في النهايات القابلة للتكرار وتعود فقط ؛ وتستدعي الملاحظة في هذه الحالة طريقة onCompleate).
  • قدرة المنتج على إبلاغ المستهلك بحدوث خطأ ولم يعد بإمكان المرصد إرسال عناصر (يطرح Iterable استثناءً إذا حدث خطأ أثناء التكرار ؛ يستدعي الملاحظة onError على المراقب والمخارج).


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



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



في حين أن مفهوم البرمجة التفاعلية نفسها غير متزامن (لا ينبغي الخلط بينه وبين تعدد مؤشرات الترابط) ، بشكل افتراضي ، يتم تسليم جميع العناصر في Observable إلى المشترك بشكل متزامن ، على نفس الخيط الذي تم استدعاء طريقة الاشتراك () عليه. لإدخال هذا النوع من عدم التزامن ، تحتاج إما إلى استدعاء أساليب onNext (T) أو onError (Throwable) أو onComplete () بنفسك في سلسلة تنفيذية أخرى أو استخدام المجدولين. عادةً ما يحلل الجميع سلوكهم ، لذلك دعونا نلقي نظرة على هيكلهم.



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



لدى RxJava العديد من أدوات الجدولة القياسية المناسبة لأغراض محددة. إنهم جميعًا يوسعون فئة المجدول المجرد ويطبقون منطقهم الخاص لإدارة العمال. على سبيل المثال ، يشكل ComputationScheduler ، في وقت إنشائه ، مجموعة من العمال ، وعددهم يساوي عدد خيوط المعالج. ثم يستخدم ComputationScheduler العمال لأداء المهام القابلة للتشغيل. يمكنك تمرير Runnable إلى المجدول باستخدام أساليب ScheduleDirect () و SchedulePeriodicallyDirect (). لكلتا الطريقتين ، يأخذ المجدول العامل التالي من التجمع ويمرر Runnable إليه.



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



صورة



وبالتالي ، لدينا عمال مجرّدون يمكنهم تنفيذ أي مخطط توازي. يعطي هذا النهج العديد من المزايا: نمطية ، ومرونة ، وواجهة برمجة تطبيقات واحدة ، وتطبيقات مختلفة. لقد رأينا نهجًا مشابهًا في ExecutorService. بالإضافة إلى ذلك ، يمكننا استخدام جدولة منفصلة عن Observable.



خاتمة



RxJava هي مكتبة قوية جدًا يمكن استخدامها بطرق متنوعة عبر العديد من البنى. لا تقتصر طرق استخدامه على تلك الموجودة ، لذا حاول دائمًا تكييفها بنفسك. ومع ذلك ، تذكر معلومات حول SOLID و DRY ومبادئ التصميم الأخرى ، ولا تنس مشاركة تجربتك مع الزملاء. أتمنى أن تكون قادرًا على تعلم شيء جديد وممتع من المقال ، أراك!



  1. أسباب بدء Netflix باستخدام ReactiveX
  2. عرض RxJava لمجتمع الإنترنت
  3. VIPER
  4. Conal Elliot



All Articles