اندروید موثر، قسمت 2: برنامه نویسی واکنشی تابعی


برنامه نویسی واکنشی تابعی (FRP) الگویی است که واکنش پذیری حاصل از برنامه ریزی واکنشی را با ترکیب تابع اعلامی از برنامه ریزی تابعی ترکیب می کند. کارهای پیچیده را ساده می کند، رابط های کاربری ظریفی ایجاد می کند و حالت را به راحتی مدیریت می کند. با توجه به این موارد و بسیاری سایر مزایای واضح، استفاده از FRP در توسعه موبایل و وب در حال تبدیل شدن به جریان اصلی است.

این بدان معنا نیست که درک این پارادایم برنامه نویسی آسان است – حتی توسعه دهندگان باتجربه ممکن است تعجب کنند: “دقیقاً چیست؟ است FRP؟» در قسمت 1 این آموزش، مفاهیم اساسی FRP را تعریف کردیم: برنامه نویسی تابعی و برنامه نویسی واکنشی. این قسمت با مروری بر کتابخانه های مفید و اجرای نمونه دقیق، شما را برای اعمال آن آماده می کند.

این مقاله با در نظر گرفتن توسعه دهندگان اندروید نوشته شده است، اما این مفاهیم برای هر توسعه دهنده ای که در زبان های برنامه نویسی عمومی تجربه دارد، مرتبط و مفید است.

شروع کار با FRP: طراحی سیستم

پارادایم FRP یک چرخه بی پایان از حالت ها و رویدادها است: State -> Event -> State' -> Event' -> State'' -> …. (بعنوان یادآوری، 'هر برنامه FRP با یک حالت اولیه شروع می شود که با هر رویدادی که دریافت می کند به روز می شود. این برنامه شامل همان عناصری است که در یک برنامه واکنشی وجود دارد:

  • دولت
  • رویداد
  • خط لوله اعلامی (نشان داده شده به عنوان FRPViewModel function)
  • قابل مشاهده (نشان داده شده به عنوان StateFlow)

در اینجا، عناصر واکنشی عمومی را با مؤلفه‌ها و کتابخانه‌های واقعی Android جایگزین کرده‌ایم:

دو جعبه اصلی آبی،
چرخه برنامه نویسی واکنشی عملکردی در اندروید.

مجموعه‌ای از کتابخانه‌ها و ابزارهای اندرویدی وجود دارد که می‌توانند به شما در شروع کار با FRP کمک کنند، و همچنین به برنامه‌نویسی عملکردی مرتبط هستند:

  • پیچک FRP: این کتابخانه ای است که من نوشتم که برای اهداف آموزشی در این آموزش استفاده خواهد شد. این به عنوان نقطه شروعی برای رویکرد شما به FRP در نظر گرفته شده است، اما برای استفاده در تولید در نظر گرفته نشده است زیرا فاقد پشتیبانی مناسب است. (در حال حاضر من تنها مهندسي هستم كه آن را نگهداري مي كند.)
  • فلش: این یکی از بهترین و محبوب ترین است کاتلین کتابخانه‌هایی برای FP، یکی که ما نیز در برنامه نمونه خود استفاده خواهیم کرد. این تقریباً همه چیزهایی را که برای عملکردی در کاتلین نیاز دارید و در عین حال نسبتاً سبک وزن است، فراهم می کند.
  • Jetpack Compose: این جعبه ابزار توسعه فعلی اندروید برای ایجاد رابط کاربری بومی است و سومین کتابخانه ای است که امروز از آن استفاده خواهیم کرد. برای توسعه دهندگان اندرویدی مدرن ضروری است—من توصیه می کنم آن را یاد بگیرید و حتی اگر قبلاً این کار را نکرده اید، رابط کاربری خود را مهاجرت کنید.
  • جریان: این API جریان داده واکنشی ناهمزمان Kotlin است. اگرچه در این آموزش با آن کار نمی کنیم، اما با بسیاری از کتابخانه های رایج اندروید مانند RoomDB، Retrofit و Jetpack سازگار است. Flow به طور یکپارچه با کوروتین ها کار می کند و واکنش پذیری را فراهم می کند. به عنوان مثال، هنگامی که با RoomDB استفاده می شود، Flow تضمین می کند که برنامه شما همیشه با آخرین داده ها کار می کند. اگر تغییری در جدول رخ دهد، جریان های وابسته به این جدول بلافاصله مقدار جدید را دریافت خواهند کرد.
  • از جعبه: این پلتفرم آزمایشی، پشتیبانی آزمایشی مبتنی بر ویژگی مربوط به کد دامنه FP خالص را ارائه می دهد.

اجرای یک برنامه تبدیل فوت/متر نمونه

بیایید نمونه ای از FRP در محل کار را در یک برنامه اندروید ببینیم. ما یک برنامه ساده ایجاد خواهیم کرد که مقادیر را بین متر (m) و فوت (ft) تبدیل می کند.

برای اهداف این آموزش، من فقط بخش‌هایی از کد حیاتی برای درک FRP را پوشش می‌دهم که برای سادگی از برنامه نمونه مبدل کامل خود اصلاح شده‌اند. اگر می خواهید در Android Studio دنبال کنید، پروژه خود را با یک فعالیت Jetpack Compose ایجاد کنید و پیکان را نصب کنید و پیچک 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: تبدیل مقدار ورودی را با استفاده از نوع تبدیل انجام می دهد.

اکنون به مدل view خود ادامه می دهیم.

خط لوله اعلامی: کنترل کننده رویداد و ترکیب عملکرد

مدل view شامل کد کنترل کننده رویداد و ترکیب تابع (خط لوله اعلامی) ما است:

// 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 از Flow به عنوان یک جریان داده واکنشی استفاده می کند).
  • handleEvent(Event): suspend () -> S: حالت بعدی را به صورت ناهمزمان با an تولید می کند Event. پیاده‌سازی زیربنایی یک برنامه جدید برای هر رویداد راه‌اندازی می‌کند.
  • stateVal(): S: وضعیت فعلی را برمی گرداند.
  • updateState((S) -> S): S را به روز می کند ViewModelایالت

حال، بیایید به چند روش مربوط به ترکیب تابع نگاه کنیم:

  • then: دو تابع را با هم می سازد.
  • asParamTo: یک تابع تولید می کند g() = f

  • thenInvokeAfter: دو تابع را می سازد و سپس آنها را فراخوانی می کند.

updateState و thenInvokeAfter روش های کمکی هستند که در قطعه کد بعدی نشان داده شده اند. آنها در کد مدل نمای باقیمانده ما استفاده خواهند شد.

خط لوله اعلامی: پیاده سازی عملکرد اضافی

مدل view ما همچنین شامل اجرای تابع برای تنظیم نوع و مقدار تبدیل، انجام تبدیل‌های واقعی و قالب‌بندی نتیجه نهایی است:

// 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 به عنوان ورودی برای ترکیبات دیگر عمل می کند. ممکن است متوجه شده باشید که کد نشان داده شده در بالا خالص نیست: ما در حال جهش هستیم protected abstract val _state: MutableStateFlow<S> که در FRPViewModel، هر زمان که استفاده می کنیم عوارض جانبی ایجاد می کند updateState {}. کد FP کاملا خالص برای اندروید در Kotlin امکان پذیر نیست.

از آنجایی که ترکیب توابع غیر خالص می تواند منجر به نتایج غیرقابل پیش بینی شود، رویکرد ترکیبی عملی ترین است: در بیشتر موارد از توابع خالص استفاده کنید و مطمئن شوید که هر عملکرد ناخالص دارای عوارض جانبی کنترل شده است. این دقیقاً همان کاری است که ما در بالا انجام دادیم.

قابل مشاهده و رابط کاربری

مرحله آخر ما این است که رابط کاربری برنامه خود را تعریف کنیم و مبدل خود را زنده کنیم.

یک مستطیل خاکستری بزرگ با چهار فلش که از سمت راست به آن اشاره دارد.  از بالا به پایین، اولین فلش، با برچسب
ماکتی از رابط کاربری برنامه.

رابط کاربری برنامه ما کمی "زشت" خواهد بود، اما هدف این مثال نشان دادن FRP است، نه ساختن یک طراحی زیبا با استفاده از Jetpack Compose.

// ConverterScreen.kt
@Composable
fun BoxWithConstraintsScope.ConverterScreen(screen: ConverterScreen) {
    FRP<ConvState, ConvEvent, ConverterViewModel> { state, onEvent ->
        UI(state, onEvent)
    }
}

کد UI ما از اصول اولیه Jetpack Compose در کمترین خط کد ممکن استفاده می کند. با این حال، یک عملکرد جالب وجود دارد که قابل ذکر است: FRP<ConvState, ConvEvent, ConverterViewModel>. FRP یک تابع قابل ترکیب از چارچوب Ivy FRP است که چندین کار را انجام می دهد:

  • مدل view را با استفاده از آن نمونه سازی می کند @HiltViewModel.
  • مدل view را رعایت می کند State با استفاده از Flow
  • وقایع را به 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 را می توان در آن یافت UI عملکرد از ConverterScreen.kt فایل. اگر می‌خواهید برنامه یا کد را آزمایش کنید، می‌توانید مخزن Ivy FRP را شبیه‌سازی کنید و آن را اجرا کنید. sample برنامه در اندروید استودیو شبیه ساز شما ممکن است نیاز داشته باشد افزایش ذخیره سازی قبل از اجرای برنامه

معماری تمیزتر اندروید با FRP

با درک پایه ای قوی از برنامه نویسی عملکردی، برنامه نویسی واکنشی، و در نهایت، برنامه نویسی واکنشی تابعی، شما آماده بهره مندی از مزایای FRP و ساخت معماری تمیزتر و قابل نگهداری اندروید هستید.

وبلاگ مهندسی تاپتال از Tarun Goyal بابت بررسی نمونه کدهای ارائه شده در این مقاله تشکر می کند.



منبع

Matthew Newman

Matthew Newman Matthew has over 15 years of experience in database management and software development, with a strong focus on full-stack web applications. He specializes in Django and Vue.js with expertise deploying to both server and serverless environments on AWS. He also works with relational databases and large datasets