تحقق من كود Android الخاص بك في المستقبل ، الجزء 2: البرمجة التفاعلية الوظيفية قيد التنفيذ
نشرت: 2022-09-08البرمجة التفاعلية الوظيفية (FRP) هي نموذج يجمع بين التفاعل من البرمجة التفاعلية مع تكوين الوظيفة التصريحية من البرمجة الوظيفية. فهو يبسط المهام المعقدة ، ويخلق واجهات مستخدم أنيقة ، ويدير الحالة بسلاسة. نظرًا لهذه الفوائد والعديد من المزايا الأخرى الواضحة ، فإن استخدام FRP أصبح سائدًا في تطوير الأجهزة المحمولة والويب.
هذا لا يعني أن فهم نموذج البرمجة هذا سهل - حتى المطورين المتمرسين قد يتساءلون: "ما هو بالضبط FRP؟" في الجزء الأول من هذا البرنامج التعليمي ، حددنا المفاهيم الأساسية لـ FRP: البرمجة الوظيفية والبرمجة التفاعلية. ستجهزك هذه الدفعة لتطبيقها ، مع نظرة عامة على المكتبات المفيدة ونموذج تنفيذ مفصل.
تمت كتابة هذه المقالة مع وضع مطوري Android في الاعتبار ، ولكن المفاهيم ذات صلة ومفيدة لأي مطور لديه خبرة في لغات البرمجة العامة.
الشروع في العمل مع FRP: تصميم النظام
نموذج FRP عبارة عن دورة لا نهاية لها من الحالات والأحداث: State -> Event -> State' -> Event' -> State'' -> …
. (للتذكير ، '
، وضوحا "رئيس" ، يشير إلى نسخة جديدة من نفس المتغير.) كل برنامج FRP يبدأ بحالة أولية سيتم تحديثها مع كل حدث يستقبله. يتضمن هذا البرنامج نفس العناصر الموجودة في البرنامج التفاعلي:
- حالة
- حدث
- خط الأنابيب التعريفي (يشار إليه على أنه
FRPViewModel function
) - يمكن ملاحظتها (يشار إليها باسم
StateFlow
)
هنا ، قمنا باستبدال العناصر التفاعلية العامة بمكونات ومكتبات Android حقيقية:
استكشاف مكتبات وأدوات FRP
هناك مجموعة متنوعة من مكتبات وأدوات Android التي يمكن أن تساعدك في البدء باستخدام FRP ، وهي ذات صلة أيضًا بالبرمجة الوظيفية:
- Ivy FRP : هذه مكتبة كتبتها وسيتم استخدامها للأغراض التعليمية في هذا البرنامج التعليمي. يُقصد به أن يكون نقطة انطلاق لنهجك في FRP ولكنه ليس مخصصًا للاستخدام الإنتاجي كما هو لأنه يفتقر إلى الدعم المناسب. (أنا حاليًا المهندس الوحيد الذي يقوم بصيانته).
- Arrow : هذه واحدة من أفضل وأشهر مكتبات Kotlin لـ FP ، وسنستخدمها أيضًا في نموذج التطبيق الخاص بنا. إنه يوفر كل ما تحتاجه تقريبًا للعمل في Kotlin مع الحفاظ على وزن خفيف نسبيًا.
- Jetpack Compose : هذه هي مجموعة أدوات التطوير الحالية لنظام Android لبناء واجهة مستخدم أصلية وهي المكتبة الثالثة التي سنستخدمها اليوم. إنه ضروري لمطوري Android الحديثين - أوصي بتعلمه وحتى ترحيل واجهة المستخدم الخاصة بك إذا لم تكن قد قمت بذلك بالفعل.
- التدفق : هذا هو Datastream التفاعلي غير المتزامن لـ Kotlin ؛ على الرغم من أننا لا نعمل معها في هذا البرنامج التعليمي ، إلا أنها متوافقة مع العديد من مكتبات Android الشائعة مثل RoomDB و Retrofit و Jetpack. يعمل Flow بسلاسة مع coroutines ويوفر التفاعل. عند استخدامه مع RoomDB ، على سبيل المثال ، يضمن Flow أن تطبيقك سيعمل دائمًا مع أحدث البيانات. في حالة حدوث تغيير في جدول ، ستتلقى التدفقات المعتمدة على هذا الجدول القيمة الجديدة على الفور.
- Kotest : توفر منصة الاختبار هذه دعمًا للاختبار قائمًا على الملكية فيما يتعلق برمز مجال FP الخالص.
تنفيذ نموذج تطبيق تحويل قدم / أمتار
دعونا نرى مثالاً على FRP في العمل في تطبيق Android. سننشئ تطبيقًا بسيطًا يحول القيم بين الأمتار (م) والقدم (قدم).
لأغراض هذا البرنامج التعليمي ، أقوم فقط بتغطية أجزاء التعليمات البرمجية الحيوية لفهم FRP ، والتي تم تعديلها من أجل البساطة من تطبيق نموذج المحول الكامل الخاص بي. إذا كنت ترغب في المتابعة في Android Studio ، فأنشئ مشروعك باستخدام نشاط Jetpack Compose ، وقم بتثبيت Arrow و Ivy FRP. ستحتاج إلى إصدار minSdk
28 أو أعلى ونسخة لغة من Kotlin 1.6+.
حالة
لنبدأ بتحديد حالة تطبيقنا.
// ConvState.kt enum class ConvType { METERS_TO_FEET, FEET_TO_METERS } data class ConvState( val conversion: ConvType, val value: Float, val result: Option<String> )
فئة دولتنا تشرح نفسها بنفسها إلى حد ما:
-
conversion
: نوع يصف ما نقوم بتحويله بين - قدم إلى متر أو متر إلى قدم. -
value
: العائمة التي يدخلها المستخدم ، والتي سنحولها لاحقًا. -
result
: نتيجة اختيارية تمثل تحويلاً ناجحًا.
بعد ذلك ، نحتاج إلى التعامل مع إدخال المستخدم كحدث.
حدث
لقد حددنا ConvEvent
كفئة مختومة لتمثيل مدخلات المستخدم:
// ConvEvent.kt sealed class ConvEvent { data class SetConversionType(val conversion: ConvType) : ConvEvent() data class SetValue(val value: Float) : ConvEvent() object Convert : ConvEvent() }
دعونا نفحص أغراض أعضائها:
-
SetConversionType
: يختار ما إذا كنا نحول من قدم إلى متر أو من متر إلى قدم. -
SetValue
: يضبط القيم الرقمية التي سيتم استخدامها للتحويل. -
Convert
: يقوم بتحويل القيمة المدخلة باستخدام نوع التحويل.
الآن ، سوف نواصل مع نموذج وجهة نظرنا.
خط الأنابيب التعريفي: معالج الأحداث وتكوين الوظيفة
يحتوي نموذج العرض على كود معالج الأحداث الخاص بنا وتكوين الوظيفة (خط الأنابيب التعريفي):
// ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { companion object { const val METERS_FEET_CONST = 3.28084f } // set initial state override val _state: MutableStateFlow<ConvState> = MutableStateFlow( ConvState( conversion = ConvType.METERS_TO_FEET, value = 1f, result = None ) ) override suspend fun handleEvent(event: ConvEvent): suspend () -> ConvState = when (event) { is ConvEvent.SetConversionType -> event asParamTo ::setConversion then ::convert is ConvEvent.SetValue -> event asParamTo ::setValue is ConvEvent.Convert -> stateVal() asParamTo ::convert } // ... }
قبل تحليل التنفيذ ، دعنا نكسر بعض العناصر الخاصة بمكتبة Ivy FRP.
FRPViewModel<S,E>
هو قاعدة نموذج عرض مجردة تنفذ بنية FRP. في الكود الخاص بنا ، نحتاج إلى تنفيذ الطرق التالية:
-
val _state
: يحدد القيمة الأولية للحالة (يستخدم Ivy FRP التدفق كتدفق بيانات تفاعلي). -
handleEvent(Event): suspend () -> S
: ينتج الحالة التالية بشكل غير متزامن مع إعطاءEvent
. يطلق التطبيق الأساسي كوروتين جديد لكل حدث. -
stateVal(): S
: إرجاع الحالة الحالية. -
updateState((S) -> S): S
حالةViewModel
.
الآن ، دعنا نلقي نظرة على بعض الطرق المتعلقة بتكوين الوظيفة:
-
then
: يؤلف وظيفتين معًا. -
asParamTo
: ينتج دالةg() = f(t)
منf(T)
وقيمةt
(من النوعT
). -
thenInvokeAfter
: يؤلف وظيفتين ثم يستدعيهما.
updateState
و thenInvokeAfter
هما طريقتان مساعدتان معروضتان في مقتطف الكود التالي ؛ سيتم استخدامها في كود نموذج العرض المتبقي.
خط الأنابيب التعريفي: تطبيقات الوظائف الإضافية
يحتوي نموذج العرض الخاص بنا أيضًا على تطبيقات وظيفية لتحديد نوع التحويل وقيمته ، وإجراء التحويلات الفعلية ، وتنسيق النتيجة النهائية:
// ConverterViewModel.kt @HiltViewModel class ConverterViewModel @Inject constructor() : FRPViewModel<ConvState, ConvEvent>() { // ... private suspend fun setConversion(event: ConvEvent.SetConversionType) = updateState { it.copy(conversion = event.conversion) } private suspend fun setValue(event: ConvEvent.SetValue) = updateState { it.copy(value = event.value) } private suspend fun convert( state: ConvState ) = state.value asParamTo when (stateVal().conversion) { ConvType.METERS_TO_FEET -> ::convertMetersToFeet ConvType.FEET_TO_METERS -> ::convertFeetToMeters } then ::formatResult thenInvokeAfter { result -> updateState { it.copy(result = Some(result)) } } private fun convertMetersToFeet(meters: Float): Float = meters * METERS_FEET_CONST private fun convertFeetToMeters(ft: Float): Float = ft / METERS_FEET_CONST private fun formatResult(result: Float): String = DecimalFormat("###,###.##").format(result) }
من خلال فهم وظائف مساعد Ivy FRP الخاصة بنا ، نحن على استعداد لتحليل الكود. لنبدأ بالوظيفة الأساسية: convert
. يقبل convert
الحالة ( ConvState
) كمدخلات وينتج دالة تنتج حالة جديدة تحتوي على نتيجة المدخلات المحولة. في الكود الكاذب ، يمكننا تلخيصه على النحو التالي: State (ConvState) -> Value (Float) -> Converted value (Float) -> Result (Option<String>)
.
معالجة الحدث Event.SetValue
مباشرة ؛ يقوم ببساطة بتحديث الحالة بالقيمة من الحدث (على سبيل المثال ، يقوم المستخدم بإدخال رقم ليتم تحويله). ومع ذلك ، فإن التعامل مع حدث Event.SetConversionType
أكثر إثارة للاهتمام لأنه يقوم بأمرين:
- يحدّث الحالة بنوع التحويل المحدد (
ConvType
). - يستخدم
convert
لتحويل القيمة الحالية بناءً على نوع التحويل المحدد.
باستخدام قوة التكوين ، يمكننا استخدام convert: State -> State
function كمدخل للتركيبات الأخرى. ربما لاحظت أن الشفرة الموضحة أعلاه ليست خالصة: نحن protected abstract val _state: MutableStateFlow<S>
في FRPViewModel
، مما يؤدي إلى آثار جانبية كلما استخدمنا updateState {}
. كود FP النقي تمامًا لنظام Android في Kotlin غير ممكن.
نظرًا لأن إنشاء وظائف غير نقية يمكن أن يؤدي إلى نتائج غير متوقعة ، فإن النهج الهجين هو الأكثر عملية: استخدم وظائف خالصة للجزء الأكبر ، وتأكد من أن أي وظائف غير نقية لها آثار جانبية مسيطر عليها. هذا بالضبط ما فعلناه أعلاه.
يمكن ملاحظته وواجهة المستخدم
خطوتنا الأخيرة هي تحديد واجهة مستخدم تطبيقنا وإضفاء الحيوية على محولنا.
ستكون واجهة مستخدم تطبيقنا "قبيحة" بعض الشيء ، لكن الهدف من هذا المثال هو إظهار FRP ، وليس إنشاء تصميم جميل باستخدام Jetpack Compose.
// ConverterScreen.kt @Composable fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) { FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent -> UI(state, onEvent) } }
يستخدم كود واجهة المستخدم الخاص بنا مبادئ Jetpack Compose الأساسية في أقل عدد ممكن من أسطر التعليمات البرمجية. ومع ذلك ، هناك وظيفة واحدة مثيرة للاهتمام تستحق الذكر: FRP<ConvState, ConvEvent, ConverterViewModel>
. FRP
هي وظيفة قابلة للتكوين من إطار Ivy FRP ، والتي تقوم بعدة أشياء:
- يجسد نموذج العرض باستخدام
@HiltViewModel
. - يلاحظ
State
نموذج العرض باستخدام التدفق. - ينشر الأحداث إلى
ViewModel
برمزonEvent: (Event) -> Unit)
. - يوفر
@Composable
ذات ترتيب أعلى تقوم بنشر الحدث وتتلقى أحدث حالة. - يوفر اختياريًا طريقة لتمرير
initialEvent
، والذي يتم استدعاؤه بمجرد بدء تشغيل التطبيق.
إليك كيفية تنفيذ وظيفة FRP
في مكتبة Ivy FRP:
@Composable inline fun <S, E, reified VM : FRPViewModel<S, E>> BoxWithConstraintsScope.FRP( initialEvent: E? = null, UI: @Composable BoxWithConstraintsScope.( state: S, onEvent: (E) -> Unit ) -> Unit ) { val viewModel: VM = viewModel() val state by viewModel.state().collectAsState() if (initialEvent != null) { onScreenStart { viewModel.onEvent(initialEvent) } } UI(state, viewModel::onEvent) }
يمكنك العثور على الكود الكامل لمثال المحول في GitHub ، ويمكن العثور على رمز واجهة المستخدم بالكامل في وظيفة UI
لملف ConverterScreen.kt
. إذا كنت ترغب في تجربة التطبيق أو الكود ، فيمكنك استنساخ مستودع Ivy FRP وتشغيل التطبيق sample
في Android Studio. قد يحتاج المحاكي إلى مساحة تخزين أكبر قبل تشغيل التطبيق.
نظافة أندرويد معمارية مع FRP
من خلال الفهم التأسيسي القوي للبرمجة الوظيفية والبرمجة التفاعلية وأخيرًا البرمجة التفاعلية الوظيفية ، فأنت على استعداد لجني فوائد FRP وبناء بنية أندرويد أنظف وأكثر قابلية للصيانة.
تعرب مدونة Toptal Engineering عن امتنانها لـ Tarun Goyal لمراجعة عينات التعليمات البرمجية المقدمة في هذه المقالة.