미래에 대비한 Android 코드, 2부: 함수형 리액티브 프로그래밍 실행
게시 됨: 2022-09-08FRP(Functional Reactive Programming)는 리액티브 프로그래밍의 반응성과 함수형 프로그래밍의 선언적 함수 구성을 결합한 패러다임입니다. 복잡한 작업을 단순화하고 우아한 사용자 인터페이스를 생성하며 상태를 원활하게 관리합니다. 이러한 이점과 기타 여러 가지 분명한 이점으로 인해 FRP 사용은 모바일 및 웹 개발에서 주류가 되고 있습니다.
그렇다고 해서 이 프로그래밍 패러다임을 이해하는 것이 쉬운 것은 아닙니다. 노련한 개발자라도 "FRP 가 정확히 무엇인가요?"라고 궁금해할 수 있습니다. 이 튜토리얼의 1부에서는 FRP의 기본 개념인 함수형 프로그래밍과 반응형 프로그래밍을 정의했습니다. 이 기사에서는 유용한 라이브러리에 대한 개요와 자세한 샘플 구현과 함께 이를 적용할 수 있도록 준비합니다.
이 기사는 Android 개발자를 염두에 두고 작성되었지만 개념은 일반 프로그래밍 언어에 대한 경험이 있는 모든 개발자에게 관련성이 있고 유익합니다.
FRP 시작하기: 시스템 설계
FRP 패러다임은 상태와 이벤트의 끝없는 순환입니다. State -> Event -> State' -> Event' -> State'' -> …
(알림으로 "프라임"으로 발음되는 '
는 동일한 변수의 새 버전을 나타냅니다.) 모든 FRP 프로그램은 수신되는 각 이벤트로 업데이트되는 초기 상태로 시작합니다. 이 프로그램에는 반응 프로그램의 요소와 동일한 요소가 포함되어 있습니다.
- 상태
- 이벤트
- 선언적 파이프라인(
FRPViewModel function
로 표시됨) - 관찰 가능(
StateFlow
로 표시)
여기에서 일반 반응 요소를 실제 Android 구성 요소 및 라이브러리로 대체했습니다.
FRP 라이브러리 및 도구 탐색
FRP를 시작하는 데 도움이 되고 함수형 프로그래밍과도 관련된 다양한 Android 라이브러리 및 도구가 있습니다.
- Ivy FRP : 이 튜토리얼에서 교육용으로 사용할 라이브러리입니다. FRP에 대한 접근 방식의 출발점으로 사용하기 위한 것이지만 적절한 지원이 부족하기 때문에 프로덕션 용도로는 사용할 수 없습니다. (나는 현재 그것을 유지하는 유일한 엔지니어입니다.)
- Arrow : 이것은 FP용으로 가장 훌륭하고 인기 있는 Kotlin 라이브러리 중 하나이며 샘플 앱에서도 사용할 것입니다. 상대적으로 가벼운 상태를 유지하면서 Kotlin에서 기능하는 데 필요한 거의 모든 것을 제공합니다.
- Jetpack Compose : 네이티브 UI를 빌드하기 위한 Android의 현재 개발 툴킷이며 오늘 사용할 세 번째 라이브러리입니다. 이것은 최신 Android 개발자에게 필수적입니다. 학습하고 UI를 아직 마이그레이션하지 않았다면 마이그레이션하는 것이 좋습니다.
- Flow : Kotlin의 비동기 반응 데이터 스트림 API입니다. 이 튜토리얼에서는 작업하지 않지만 RoomDB, Retrofit 및 Jetpack과 같은 많은 일반적인 Android 라이브러리와 호환됩니다. Flow는 코루틴과 원활하게 작동하고 반응성을 제공합니다. 예를 들어 RoomDB와 함께 사용하면 Flow는 앱이 항상 최신 데이터로 작동하도록 합니다. 테이블에 변경 사항이 발생하면 이 테이블에 종속된 흐름은 즉시 새 값을 받습니다.
- Kotest : 이 테스트 플랫폼은 순수 FP 도메인 코드와 관련된 속성 기반 테스트 지원을 제공합니다.
샘플 피트/미터 변환 앱 구현
Android 앱에서 작동하는 FRP의 예를 살펴보겠습니다. 미터(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
가 주어지면 비동기적으로 다음 상태를 생성합니다. 기본 구현은 각 이벤트에 대해 새 코루틴을 시작합니다. -
stateVal(): S
: 현재 상태를 반환합니다. -
updateState((S) -> S): S
ViewModel
의 상태를 업데이트합니다.
이제 함수 구성과 관련된 몇 가지 방법을 살펴보겠습니다.
-
then
: 두 개의 함수를 함께 구성합니다. -
asParamTo
: f(T)에서 함수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
함수를 다른 구성에 대한 입력으로 사용할 수 있습니다. 위에서 설명한 코드가 순수하지 않다는 것을 눈치채셨을 수도 있습니다. FRPViewModel
에서 protected abstract val _state: MutableStateFlow<S>
를 변경하여 updateState {}
를 사용할 때마다 부작용이 발생합니다. Kotlin에서 Android용으로 완전히 순수한 FP 코드는 실현 가능하지 않습니다.
순수하지 않은 함수를 구성하는 것은 예측할 수 없는 결과를 초래할 수 있으므로 하이브리드 접근 방식이 가장 실용적입니다. 대부분 순수 함수를 사용하고 순수하지 않은 함수가 부작용을 제어하는지 확인합니다. 이것이 바로 위에서 수행한 작업입니다.
관찰 가능 및 UI
마지막 단계는 앱의 UI를 정의하고 변환기에 생명을 불어넣는 것입니다.
우리 앱의 UI는 약간 "추한" 것이지만 이 예제의 목표는 Jetpack Compose를 사용하여 아름다운 디자인을 구축하는 것이 아니라 FRP를 시연하는 것입니다.
// 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
을 사용하여 뷰 모델을 인스턴스화합니다. - Flow를 사용하여 뷰 모델의
State
를 관찰합니다. -
onEvent: (Event) -> Unit)
코드를 사용하여ViewModel
에 이벤트를 전파합니다. - 이벤트 전파를 수행하고 최신 상태를 수신하는
@Composable
고차 함수를 제공합니다. - 선택적으로 앱이 시작되면 호출되는
initialEvent
를 전달하는 방법을 제공합니다.
Ivy FRP 라이브러리에서 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
파일의 UI
기능에서 찾을 수 있습니다. 앱이나 코드를 실험해보고 싶다면 Ivy FRP 저장소를 복제하고 Android Studio에서 sample
앱을 실행할 수 있습니다. 앱을 실행하려면 에뮬레이터의 저장용량을 늘려야 할 수 있습니다.
FRP로 더 깔끔한 Android 아키텍처
함수형 프로그래밍, 반응형 프로그래밍, 마지막으로 함수형 반응형 프로그래밍에 대한 강력한 기초 이해를 통해 FRP의 이점을 얻고 더 깨끗하고 유지 관리 가능한 Android 아키텍처를 구축할 준비가 된 것입니다.
Toptal 엔지니어링 블로그는 이 기사에 제공된 코드 샘플을 검토한 Tarun Goyal에게 감사를 표합니다.