تهدف هذه المقالة إلى إظهار كيفية استخدام Kotlin Coroutines وإزالة Reaxtive eXtensions (Rx) .
فوائد
للبدء ، دعنا نفكر في أربعة فوائد لـ Coroutines عبر Rx:
تعليق على الحجب
لتشغيل التعليمات البرمجية غير المحظورة باستخدام Rx ، يمكنك كتابة شيء مثل هذا:
Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text = "$it seconds have passed" }
مما يخلق فعالية موضوع جديد. المواضيع هي كائنات ثقيلة من حيث الذاكرة والأداء.
كلاهما أمر بالغ الأهمية في عالم تطوير المحمول.
يمكنك تحقيق نفس السلوك باستخدام المقتطف التالي:
launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } }
في الأساس ، Coroutines هي خيوط خفيفة الوزن ولكننا لا ننشئ أي خيط حقيقي.
نحن هنا نستخدم وظيفة التأخير غير المحظورة () ، وهي وظيفة تعليق خاصة لا تمنع سلسلة الرسائل ولكنها تعلق Coroutine.
الضغط الخلفي التعامل مع أكثر من دليل
يكون الضغط الخلفي عندما تنتج المواد القابلة للملاحظة عناصر بسرعة أكبر مما يستهلكها مراقبوها.
أثناء استخدام Rx ، عليك تحديد كيفية تعاملك مع الضغط الخلفي بشكل صريح.
هناك طريقتان أساسيتان:
- استخدام الاختناق ، مخازن أو مشغلي النوافذ
- نموذج السحب التفاعلي
في حين أن Coroutines يمكن تعليق أنها توفر إجابة طبيعية للتعامل مع الضغط.
وبالتالي ، لا توجد إجراءات إضافية مطلوبة.
مزامنة نمط الكود على المزامنة
تتمثل الطبيعة الأساسية لتطبيق الجوال في الرد على تصرفات المستخدم. هذا هو السبب في أن التفاعلات التفاعلية ستكون خيارًا جيدًا.
ومع ذلك ، يجب عليك كتابة رمز بأسلوب وظيفي. إذا كنت تستخدم الكتابة بأسلوب حتمي ، فقد يكون ذلك صعباً بعض الشيء.
بينما يمكّنك Coroutines من كتابة التعليمات البرمجية غير المتزامنة كما لو كانت وظائف مزامنة عادية. على سبيل المثال ،
suspend fun showTextFromRemote() { val text = remote.getText() textView.text = text }
حتى أنا أعمل بأسلوب وظيفي لفترة طويلة ، ما زال من الأسهل قراءة وتصحيح التعليمات البرمجية الضرورية.
مواطن على الطرف الثالث
Coroutines هي ميزة البناء الأصلي في Kotlin.
ليس لديك لإضافة أي تبعيات إضافية. حاليا ، يمكن لجميع المكتبات الرئيسية التعامل مع coroutines.
على سبيل المثال ،
التحديثية
interface Api { @Get("users") suspend fun loadUsers() : List<User> }
غرفة
interface Dao { @Update suspend fun update(user: UserEntity) }
لذلك ، يمكنك إنشاء تطبيق يتم تعليقه بالكامل - بدء طبقة واجهة المستخدم ، من خلال المجال وينتهي في طبقة البيانات.
التطبيق
دعنا نذهب إلى العمل. سنقوم بإنشاء تطبيق سيد التفاصيل الكلاسيكية.
سوف تحتوي الصفحة الأولى على قائمة لا حصر لها من عمليات التسليم.
عند النقر فوق العنصر ، سنفتح صفحة تفصيلية.
أيضًا ، سندعم الوضع غير المتصل بالإنترنت - سيتم تخزين جميع البيانات في ذاكرة التخزين المؤقت.
علاوة على ذلك ، سأستخدم بنية MVVM حيث يتم لعب دور ViewModel بواسطة Fragment بدلاً من ViewModel من AAC. هناك عدة أسباب:
شظايا عادة ما تكون أصلع جدا - فقط ربط viewModel إلى XML.
لا يمكن القيام بميزات مثل إعداد لون شريط الحالة في AAC ViewModel - عليك تشغيل طريقة التجزئة. إن استخدام الجزء كـ ViewModel سيتيح لنا تخزين جميع الوظائف ذات الصلة (إدارة شاشة واحدة معينة) في فصل واحد.
أولاً ، دعنا ننشئ BaseViewModel:
abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO){ protected abstract val layoutId: Int protected abstract val bindings: B protected lateinit var viewBinding: V override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { viewBinding = DataBindingUtil.inflate(inflater, layoutId, container, false) return viewBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.lifecycleOwner = viewLifecycleOwner viewBinding.setVariable(BR.bindings, bindings) } override fun onDestroy() { cancel() super.onDestroy() } }
نحتفل بـ ViewModel على أنه CoroutineScope حتى نتمكن من بدء تشغيل coroutines داخل نماذج العرض وأي مقصورات تم إطلاقها ستقتصر على دورة حياة جزء ما.
يجب أن نحدد بشكل صريح طريقة cancel()
دورة نهاية دورة حياة النطاق لإلغاء جميع طلبات التشغيل لتجنب تسرب الذاكرة.
لقد قمنا بتعيين retainInstance = true
بحيث لا تتم إعادة إنشاء جزء في تغييرات التكوين حتى نتمكن من إكمال جميع الطلبات طويلة الأجل.
أيضًا ، يتعين علينا تعيين lifecycleOwner على الربط لتشغيل ربط البيانات ثنائي الاتجاه .
التعامل مع استثناء
وفقا لوثائق Coroutines:
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). The former treat exceptions as unhandled, similar to Java's Thread.uncaughtExceptionHandler
نظرًا لأننا نستخدم أداة إنشاء الإطلاق في معظم الحالات ، يتعين علينا تحديد CoroutineExceptionHandler
CoroutineExceptionHandler هو CoroutineContext.Element والذي يمكن استخدامه لإنشاء سياق coroutine باستخدام عامل التشغيل plus.
سوف أعلن معالج ثابت على النحو التالي:
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> Timber.e(throwable) }
وتغيير BaseViewModel:
abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler)
من هنا على أي استثناء حدث في coroutine أطلقت داخل نطاق ViewModel سيتم تسليمها إلى معالج معين.
بعد ذلك ، أحتاج إلى إعلان API و DAO الخاصين بي:
interface DeliveriesApi { @GET("deliveries") suspend fun getDeliveries(@Query("offset") offset: Int, @Query("limit") limit: Int): List<DeliveryResponse> } @Dao interface DeliveryDao { @Query("SELECT * FROM ${DeliveryEntity.TABLE_NAME}") fun getAll(): DataSource.Factory<Int, DeliveryEntity> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(delivery: DeliveryEntity) }
كما ترون ، قمت بوضع علامة على الطرق كتعليق ، حتى نتمكن فقط من إعلان كائنات الاستجابة المتوقعة. وعلاوة على ذلك ، فإن إلغاء coroutine الأصل سوف يلغي مكالمة الشبكة كذلك.
الشيء نفسه بالنسبة DAO.
الفرق الوحيد هو أننا نريد توفير القدرة على مراقبة قاعدة البيانات.
أسهل طريقة هي استخدام دعم البيانات المباشرة المضمنة. ولكن إذا وضعنا علامة على getAll () كمعلق ، فسيؤدي ذلك إلى حدوث خطأ في الترجمة
خطأ:
Not sure how to convert a Cursor to this method's return type ...
هنا لا نحتاج إلى التعليق بسبب:
- يتم تنفيذ طلبات Db في الخلفية افتراضيًا
- نتيجة LiveData تدرك دورة الحياة بحيث لا نحتاج إلى إلغائها يدويًا
علينا أن ندمج بطريقة أو بأخرى مصادر البيانات عن بعد والمحلية.
يجدر بنا أن نتذكر - يجب أن يكون هناك نقطة واحدة للحقيقة.
وفقا لتصميم حاليا خارج ، سيكون التخزين المحلي. لذلك ، نلاحظ حالة قاعدة البيانات. عندما لا يكون هناك شيء يمكن استرجاعه ، نطلب البيانات من جهاز التحكم عن بعد وإدخالها في قاعدة البيانات.
سوف نقدم فئة الإدراج
data class Listing<T>( val pagedList: LiveData<PagedList<T>>, val dataState: LiveData<DataState>, val refreshState: LiveData<DataState>, val refresh: () -> Unit, val retry: () -> Unit )
دعنا نذهب val بواسطة فال:
- pagedList - البيانات الرئيسية التي تم إنشاؤها كـ PagedList لتمكين التمرير اللانهائي وملفوفة مع LiveData لتمكين مراقبة البيانات
- dataState - واحدة من ثلاث حالات يمكن أن تكون فيها بياناتنا: النجاح ، التشغيل ، الخطأ. ملفوفة أيضًا على LiveData لمراقبة التغييرات
- refreshState - عندما نقوم بتشغيل تحديث البيانات من خلال التمرير السريع إلى التحديث ، نحتاج إلى بعض الأدوات التي نميزها بين ملاحظات طلب التحديث وتعليقات طلبات الصفحة التالية. بالنسبة للإصدار السابق ، نود أن نظهر خطأ في نهاية القائمة ، لكن بالنسبة لخطأ التحديث ، نود أن نظهر رسالة توست وإخفاء أداة تحميل.
- refresh () - رد الاتصال ليتم تشغيله عند التمرير السريع للتحديث
إعادة المحاولة () - رد الاتصال لتشغيل خطأ تحميل pagedList
التالي ، نموذج عرض القائمة:
class DeliveryListViewModel : BaseViewModel<DeliveryListBindings, DeliveryListBinding>(), DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings { override val layoutId: Int = R.layout.delivery_list override val bindings: DeliveryListBindings = this private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } private val listing = deliveryGateway.getDeliveries() override val dataState = listing.dataState override val isRefreshing = Transformations.switchMap(listing.refreshState) { MutableLiveData(it == DataState.Loading) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupList() setupRefresh() } private fun setupList() { val adapter = DeliveriesAdapter(this, this) viewBinding.deliveries.adapter = adapter viewBinding.deliveries.setHasFixedSize(true) listing.pagedList.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) listing.dataState.observe(viewLifecycleOwner, Observer { adapter.updateDataState(it) }) } private fun setupRefresh() { listing.refreshState.observe(viewLifecycleOwner, Observer { if (it is DataState.Error) { Toast.makeText(context, it.message, LENGTH_SHORT).show() } }) } override fun refresh() { listing.refresh() } override fun onDeliveryClicked(delivery: Delivery) { view?.findNavController()?.navigate(DeliveryListViewModelDirections.toDetails(delivery)) } override fun onRetryClicked() { listing.retry() } }
لنبدأ من إعلان الفصل.
أولاً وقبل كل شيء DeliveryListBindings و DeliveryListBinding. الأول هو واجهة المعلنة لدينا لنموذج عرض الغراء مع عرض XML. والثاني هو فئة autogenerated على أساس XML. نحتاج إلى الثانية لضبط واجهة الربط ودورة الحياة على XML.
علاوة على ذلك ، من الممارسات الجيدة الإشارة إلى وجهات النظر باستخدام هذا الرابط المولّد تلقائيًا بدلاً من استخدام مادة kotlin الاصطناعية.
يمكن أن يكون الأمر كذلك عندما لا تتم الإشارة إليها من خلال طريقة العرض الاصطناعية في العرض الحالي. مع ربط البيانات ، سوف تفشل بسرعة حتى في مرحلة الترجمة.
ثم ، ثلاث واجهات: DeliveryListBindings ، DeliveryListItemBindings ، DeliveryListErrorBindings.
- DeliveryListBindings - روابط للشاشة نفسها. على سبيل المثال ، يحتوي على طريقة تحديث () تسمى على التمرير العمودي.
- DeliveryListItemBindings - روابط عنصر في القائمة. على سبيل المثال ، onClicked ()
- DeliveryListErrorBindings - روابط طريقة عرض الأخطاء التي تعد أيضًا عنصر القائمة المعروض في حالة الخطأ. على سبيل المثال ، يحتوي على طريقة إعادة المحاولة ()
وبالتالي ، فإننا نتعامل مع كل شيء في نموذج العرض الفردي نظرًا لأنه يمثل شاشة واحدة ولكن أيضًا يتبع مبدأ فصل الواجهة
دعنا نلفت الانتباه بشكل خاص إلى هذا الخط:
private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) }
يحتاج DeliveryGateway إلى تنفيذ طلبات خارج السلسلة الرئيسية. لذلك ، فإنه يحتاج إما إلى الإعلان عن طرق مثل تعليق أو CoroutineScope لإطلاق coroutines جديدة في هذا النطاق. سوف نختار الطريقة الثانية لأننا نحتاج إلى LiveData من البداية ومن ثم سننتظر التحديثات منها. إنها تشبه إلى حد بعيد الاشتراك في مثيل liveData عندما نمرر دورة حياة المالك (والتي غالباً ما تشير إلى "هذا"). هنا بنفس الطريقة التي نعبر بها "هذا" باسم CoroutineScope
تتكون واجهة CoroutineScope من حقل وحيد - CoroutineContext. في جوهرها ، النطاق والسياق هما نفس الأشياء. الفرق بين السياق والنطاق هو في الغرض المقصود منه.
لمعرفة المزيد حول هذا أود أن أوصي مقال لرومان إليزاروف. لذلك ، سيؤدي توفير النطاق إلى DeliveryGateway أيضًا إلى استخدام نفس السياق. على وجه التحديد موضوع ، وظيفة والاستثناء معالج.
الآن دعونا نلقي نظرة على DeliveryGateway نفسها:
class DeliveryBoundGateway( private val db: DataBase, private val api: DeliveriesApi, private val deliveryDao: DeliveryDao, private val coroutineScope: CoroutineScope ) : DeliveryGateway { private val boundaryCallback = DeliveriesBoundaryCallback( api = api, coroutineScope = coroutineScope, handleResponse = { insertIntoDatabase(it) } ) @MainThread override fun getDeliveries(): Listing<Delivery> { val refreshTrigger = MutableLiveData<Unit>() val refreshState = Transformations.switchMap(refreshTrigger) { refresh() } val pagingConfig = Config( initialLoadSizeHint = PAGE_SIZE, pageSize = PAGE_SIZE, prefetchDistance = PAGE_SIZE ) val deliveries = deliveryDao.getAll() .toLiveData( config = pagingConfig, boundaryCallback = boundaryCallback ) return Listing( pagedList = deliveries, dataState = boundaryCallback.dataState, retry = { boundaryCallback.helper.retryAllFailed() }, refresh = { refreshTrigger.value = null }, refreshState = refreshState ) } @MainThread private fun refresh(): LiveData<DataState> { boundaryCallback.refresh() val dataState = MutableLiveData<DataState>() dataState.value = DataState.Loading coroutineScope.launch { try { val deliveries = api.getDeliveries(0, PAGE_SIZE) db.withTransaction { deliveryDao.clear() insertIntoDatabase(deliveries) } dataState.postValue(DataState.Loaded) } catch (throwable: Throwable) { Timber.w(throwable) dataState.postValue(DataState.Error(throwable.message)) } } return dataState } private suspend fun insertIntoDatabase(deliveries: List<DeliveryResponse>) { deliveries.forEach { delivery -> val entity = deliveryConverter.fromNetwork(delivery) deliveryDao.insert(entity) } } companion object { const val PAGE_SIZE = 20 } }
نحن هنا نبني هيكل LiveData من البداية ثم نستخدم بيانات تحميل coroutines وننشرها على LiveData. أيضًا ، نحن نستخدم تطبيق PagedList.BoundaryCallback () لغراء قاعدة البيانات المحلية وواجهة برمجة التطبيقات عن بُعد. عندما نصل إلى نهاية قائمة الحدود المقسمة إلى صفحات يتم تشغيل Callback وتحميل الجزء التالي من البيانات.
كما ترون نحن نستخدم coroutineScope لإطلاق coroutines جديدة.
نظرًا لأن هذا النطاق يساوي دورة حياة الجزء - سيتم إلغاء جميع الطلبات المعلقة في رد الاتصال onDestroy()
.
صفحة تفاصيل التسليم واضحة تمامًا - نحن فقط نمرر كائن التسليم كـ Parcelable من الشاشة الرئيسية باستخدام مكون التنقل حفظ المكون الإضافي args. في شاشة التفاصيل ، قم ببساطة بربط إعطاء كائن إلى XML.
class DeliveryViewModel : BaseViewModel<DeliveryBindings, DeliveryBinding>(), DeliveryBindings { override val layoutId: Int = R.layout.delivery override val bindings: DeliveryBindings = this private val args: DeliveryViewModelArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.delivery = args.delivery viewBinding.image.clipToOutline = true } }
هنا هو الرابط لكود github المصدر.
اهلا وسهلا بكم في ترك التعليقات وفتح القضايا.