รหัส Android ของคุณพิสูจน์อนาคต ส่วนที่ 2: การเขียนโปรแกรมเชิงโต้ตอบที่ใช้งานได้จริง
เผยแพร่แล้ว: 2022-09-08Functional Reactive Programming (FRP) เป็นกระบวนทัศน์ที่รวมการเกิดปฏิกิริยาจากการเขียนโปรแกรมเชิงโต้ตอบกับองค์ประกอบฟังก์ชันการประกาศจากการเขียนโปรแกรมเชิงฟังก์ชัน ช่วยลดความซับซ้อนของงานที่ซับซ้อน สร้างอินเทอร์เฟซผู้ใช้ที่สวยงาม และจัดการสถานะได้อย่างราบรื่น เนื่องจากสิ่งเหล่านี้และประโยชน์ที่ชัดเจนอื่นๆ อีกมากมาย การใช้ FRP จึงเป็นกระแสหลักในการพัฒนามือถือและเว็บ
ไม่ได้หมายความว่าการเข้าใจกระบวนทัศน์การเขียนโปรแกรมนี้เป็นเรื่องง่าย แม้แต่นักพัฒนาที่ช่ำชองอาจสงสัยว่า: "FRP คือ อะไรกันแน่" ในส่วนที่ 1 ของบทช่วยสอนนี้ เราได้กำหนดแนวคิดพื้นฐานของ 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 สำหรับการสร้าง UI ดั้งเดิมและเป็นไลบรารีที่สามที่เราจะใช้ในวันนี้ เป็นสิ่งสำคัญสำหรับนักพัฒนา Android ยุคใหม่ ฉันขอแนะนำให้เรียนรู้และย้าย UI ของคุณหากคุณยังไม่ได้ดำเนินการ
- โฟ ลว์ : นี่คือ API สตรีมข้อมูลปฏิกิริยาแบบอะซิงโครนัสของ Kotlin; แม้ว่าเราจะไม่ได้ใช้งานมันในบทช่วยสอนนี้ แต่ก็เข้ากันได้กับไลบรารี Android ทั่วไปมากมาย เช่น RoomDB, Retrofit และ Jetpack Flow ทำงานได้อย่างราบรื่นกับ coroutines และให้ปฏิกิริยา ตัวอย่างเช่น เมื่อใช้กับ RoomDB Flow ช่วยให้มั่นใจได้ว่าแอปของคุณจะทำงานกับข้อมูลล่าสุดเสมอ หากมีการเปลี่ยนแปลงในตาราง โฟลว์ที่ขึ้นอยู่กับตารางนี้จะได้รับค่าใหม่ทันที
- Kotest : แพลตฟอร์มการทดสอบนี้ให้การสนับสนุนการทดสอบตามคุณสมบัติที่เกี่ยวข้องกับรหัสโดเมน FP ล้วนๆ
การใช้แอพแปลงฟุต/เมตรตัวอย่าง
มาดูตัวอย่าง FRP ในที่ทำงานในแอป Android กัน เราจะสร้างแอปง่ายๆ ที่แปลงค่าระหว่างเมตร (m) และฟุต (ft)
สำหรับวัตถุประสงค์ของบทช่วยสอนนี้ ฉันครอบคลุมเฉพาะส่วนของโค้ดที่สำคัญต่อการทำความเข้าใจ 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
: float ที่ผู้ใช้ป้อน ซึ่งเราจะแปลงในภายหลัง -
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 กำลังใช้ Flow เป็นสตรีมข้อมูลปฏิกิริยา) -
handleEvent(Event): suspend () -> S
: สร้างสถานะถัดไปแบบอะซิงโครนัสที่กำหนดEvent
การใช้งานพื้นฐานเปิดตัว coroutine ใหม่สำหรับแต่ละเหตุการณ์ -
stateVal(): S
: ส่งกลับสถานะปัจจุบัน -
updateState((S) -> S): S
อัพเดตสถานะของViewModel
ตอนนี้ มาดูวิธีการสองสามวิธีที่เกี่ยวข้องกับองค์ประกอบของฟังก์ชัน:
-
then
: ประกอบด้วยสองหน้าที่ด้วยกัน -
asParamTo
: สร้างฟังก์ชันg() = f(t)
จากf(T)
และค่าt
(ของประเภทT
) -
thenInvokeAfter
: ประกอบด้วยสองฟังก์ชันแล้วเรียกใช้งานเหล่านั้น
updateState
และ thenInvokeAfter
เป็นวิธีการช่วยเหลือที่แสดงในข้อมูลโค้ดถัดไป พวกเขาจะใช้ในโค้ดโมเดลมุมมองที่เหลืออยู่ของเรา
The Declarative Pipeline: การใช้งานฟังก์ชันเพิ่มเติม
โมเดลมุมมองของเรายังมีการใช้งานฟังก์ชันสำหรับการตั้งค่าประเภทและมูลค่าการแปลงของเรา ดำเนินการแปลงจริง และจัดรูปแบบผลลัพธ์สุดท้ายของเรา:
// 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
) เป็นอินพุตและสร้างฟังก์ชันที่ส่งออกสถานะใหม่ที่มีผลลัพธ์ของอินพุตที่แปลงแล้ว ใน pseudocode เราสามารถสรุปได้ดังนี้: 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 ที่บริสุทธิ์โดยสมบูรณ์สำหรับ Android ใน Kotlin ไม่สามารถทำได้
เนื่องจากการเขียนฟังก์ชันที่ไม่บริสุทธิ์อาจนำไปสู่ผลลัพธ์ที่คาดเดาไม่ได้ วิธีไฮบริดจึงเป็นวิธีที่ใช้ได้จริงที่สุด: ใช้ฟังก์ชันบริสุทธิ์เป็นส่วนใหญ่ และตรวจสอบให้แน่ใจว่าฟังก์ชันที่ไม่บริสุทธิ์มีการควบคุมผลข้างเคียง นี่คือสิ่งที่เราทำข้างต้น
สังเกตได้และ UI
ขั้นตอนสุดท้ายของเราคือการกำหนด UI ของแอปและทำให้ตัวแปลงของเรามีชีวิต
UI ของแอปของเราจะค่อนข้าง "น่าเกลียด" แต่เป้าหมายของตัวอย่างนี้คือสาธิต 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 ซึ่งทำหน้าที่หลายอย่าง:
- สร้างอินสแตนซ์โมเดลมุมมองโดยใช้
@HiltViewModel
- สังเกต
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
ใน Android Studio อีมูเลเตอร์ของคุณอาจต้องการพื้นที่เก็บข้อมูลเพิ่มขึ้นก่อนที่แอปจะทำงานได้
สถาปัตยกรรม Android ที่สะอาดขึ้นด้วย FRP
ด้วยความเข้าใจพื้นฐานที่แข็งแกร่งของการเขียนโปรแกรมเชิงฟังก์ชัน การเขียนโปรแกรมเชิงโต้ตอบ และสุดท้ายคือการเขียนโปรแกรมเชิงโต้ตอบเชิงหน้าที่ คุณพร้อมที่จะเก็บเกี่ยวผลประโยชน์จาก FRP และสร้างสถาปัตยกรรม Android ที่สะอาดขึ้นและบำรุงรักษาได้มากขึ้น
บล็อก Toptal Engineering ขอขอบคุณ Tarun Goyal สำหรับการตรวจสอบตัวอย่างโค้ดที่นำเสนอในบทความนี้