รหัส Android ของคุณพิสูจน์อนาคต ส่วนที่ 2: การเขียนโปรแกรมเชิงโต้ตอบที่ใช้งานได้จริง

เผยแพร่แล้ว: 2022-09-08

Functional Reactive Programming (FRP) เป็นกระบวนทัศน์ที่รวมการเกิดปฏิกิริยาจากการเขียนโปรแกรมเชิงโต้ตอบกับองค์ประกอบฟังก์ชันการประกาศจากการเขียนโปรแกรมเชิงฟังก์ชัน ช่วยลดความซับซ้อนของงานที่ซับซ้อน สร้างอินเทอร์เฟซผู้ใช้ที่สวยงาม และจัดการสถานะได้อย่างราบรื่น เนื่องจากสิ่งเหล่านี้และประโยชน์ที่ชัดเจนอื่นๆ อีกมากมาย การใช้ FRP จึงเป็นกระแสหลักในการพัฒนามือถือและเว็บ

ไม่ได้หมายความว่าการเข้าใจกระบวนทัศน์การเขียนโปรแกรมนี้เป็นเรื่องง่าย แม้แต่นักพัฒนาที่ช่ำชองอาจสงสัยว่า: "FRP คือ อะไรกันแน่" ในส่วนที่ 1 ของบทช่วยสอนนี้ เราได้กำหนดแนวคิดพื้นฐานของ FRP: การเขียนโปรแกรมเชิงฟังก์ชันและการเขียนโปรแกรมเชิงโต้ตอบ งวดนี้จะเตรียมคุณให้พร้อมสำหรับการใช้งาน โดยมีภาพรวมของไลบรารีที่มีประโยชน์และตัวอย่างการใช้งานโดยละเอียด

บทความนี้เขียนขึ้นโดยคำนึงถึงนักพัฒนา Android แต่แนวคิดมีความเกี่ยวข้องและเป็นประโยชน์ต่อนักพัฒนาที่มีประสบการณ์ในภาษาโปรแกรมทั่วไป

เริ่มต้นใช้งาน FRP: การออกแบบระบบ

กระบวนทัศน์ FRP เป็นวัฏจักรของรัฐและเหตุการณ์ที่ไม่มีที่สิ้นสุด: State -> Event -> State' -> Event' -> State'' -> … (เพื่อเป็นการเตือนความจำ ' ที่ออกเสียงว่า “ไพรม์” หมายถึงเวอร์ชันใหม่ของตัวแปรเดียวกัน) ทุกโปรแกรม FRP เริ่มต้นด้วยสถานะเริ่มต้นที่จะอัปเดตตามแต่ละเหตุการณ์ที่ได้รับ โปรแกรมนี้รวมองค์ประกอบเดียวกันกับที่อยู่ในโปรแกรมปฏิกิริยา:

  • สถานะ
  • เหตุการณ์
  • ไปป์ไลน์ประกาศ (ระบุเป็น FRPViewModel function )
  • สังเกตได้ (ระบุเป็น StateFlow )

ที่นี่ เราได้แทนที่องค์ประกอบปฏิกิริยาทั่วไปด้วยส่วนประกอบและไลบรารีของ Android จริง:

กล่องสีน้ำเงินหลักสองกล่อง "StateFlow" และ "State" มีสองเส้นทางหลักระหว่างกัน อย่างแรกคือผ่าน "สังเกต (ฟังการเปลี่ยนแปลง)" ประการที่สองคือผ่าน "แจ้งเตือน (สถานะล่าสุด)" ไปยังกล่องสีน้ำเงิน "@Composable (JetpackCompose)" ซึ่งจะผ่าน "แปลงการป้อนข้อมูลของผู้ใช้เป็น" เป็นกล่องสีน้ำเงิน "เหตุการณ์" ซึ่งผ่าน "ทริกเกอร์" เป็นกล่องสีน้ำเงิน " ฟังก์ชัน FRPViewModel" และสุดท้ายผ่าน "Produces (สถานะใหม่)" จากนั้น "สถานะ" จะเชื่อมต่อกลับไปที่ "ฟังก์ชัน FRPViewModel" ผ่าน "ทำหน้าที่เป็นอินพุตสำหรับ"
วัฏจักรการเขียนโปรแกรมเชิงโต้ตอบในการทำงานของ 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 ของแอปและทำให้ตัวแปลงของเรามีชีวิต

สี่เหลี่ยมสีเทาขนาดใหญ่ที่มีลูกศรสี่ลูกศรชี้ไปทางขวา จากบนลงล่าง ลูกศรแรกที่มีป้ายกำกับว่า "ปุ่ม" ชี้ไปที่สี่เหลี่ยมผืนผ้าเล็ก ๆ สองรูป: สี่เหลี่ยมผืนผ้าสีน้ำเงินเข้มด้านซ้ายที่มีข้อความตัวพิมพ์ใหญ่ "เมตรเป็นฟุต" และสี่เหลี่ยมผืนผ้าสีฟ้าอ่อนที่มีข้อความ "ฟุต เป็นเมตร" ลูกศรที่สองที่ชื่อ "TextField" ชี้ไปที่สี่เหลี่ยมผืนผ้าสีขาวที่มีข้อความชิดซ้าย "100.0" ลูกศรที่สามที่ระบุว่า "ปุ่ม" ชี้ไปที่สี่เหลี่ยมผืนผ้าสีเขียวที่จัดชิดซ้ายโดยมีข้อความ "แปลง" ลูกศรสุดท้ายที่มีป้ายกำกับว่า "ข้อความ" ชี้ไปที่การอ่านข้อความสีน้ำเงินที่จัดชิดซ้าย: "ผลลัพธ์: 328.08 ฟุต"
ต้นแบบของ 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 สำหรับการตรวจสอบตัวอย่างโค้ดที่นำเสนอในบทความนี้