이 라이브러리는 알파에 있습니다. API는 대부분 완전하고 변화하지는 않지만 모든 플랫폼에서 생산을 준비하기 위해 여전히 수행해야 할 작업이 있습니다. JVM 및 Multiplatform의 경우 라이브러리는 생산을 사용할 준비가되었지만 iOS, JS 및 Native의 경우 여전히 완전히 설정되지 않았습니다. 도움을주고 싶다면 문제 나 PR을 열어주십시오.
Redukks는 Kotlin Multiplatform에서 Uniflow 및 Redux와 같은 아키텍처를 구현하는 데 필요한 간단한 유형-안전 추상화 세트입니다. 상점, 감속기 및 동작의 생성을 단순화하고 간단한 테스트 방법을 제공합니다.
Kotlin/Kotlin Multiplatform에는 많은 Uniflow 또는 Redux 구현이 있지만 대부분은 너무 복잡하거나 너무 간단합니다. 이름 지정이 너무 "레 럭시"이기 때문에 일을하는 한 가지 방법으로 강요하거나 추상화가 너무 복잡하여 규칙적으로 작성됩니다. 또한이 스타일을 준수하는 아키텍처가 있지만, 사용하는 이름은 Android에 매우 적합하며 비 Android 개발자에게 혼란스러워서 다른 개발자에 대해 추론하거나 토론하기가 더 어려워집니다.
몇 년 동안 동일한 추상화 세트를 재사용하는 것을 발견 한 이후로, 나는 그것들을 재사용하고 공유 할 수있는 라이브러리로 만들기로 결정했습니다.
목표는 Redux와 같은 아키텍처에 대한 간단하고 유형-안전 및 테스트 가능한 추상화를 제공하는 동시에 단일 패턴에 완전히 묶지 않아 자신의 솔루션을 구축하는 데 도움이되는 추상화를 제공하는 것입니다. 대부분의 라이브러리는 완전한 Redux 패턴을 푸시하려고 시도하지만, 일부 간단한 경우에도 리더스가 오버 헤드가 될 수 있으며, 원하는 경우 피할 수 있어야합니다. 그래서 Redukks는 Redux 패턴 자체를 시행 하지 않습니다.
Uniflow와 Redux가 이유에 대한 더 많은 추론을 보려면 건축 구조 구성 (DE)을 확인할 수 있습니다.
주 관리 : Redukks는 주를 관리하고 예측 가능한 방식으로 업데이트하고 변경 사항을 듣는 간단한 방법을 제공합니다. 이를 사용하여 전체 앱의 상태를 관리 할 수 있으며, 단일 기능, 다른 기능 (화면)간에 공유 상태가 있거나 한 화면의 여러 상태가 있습니다.
액션 처리 : Redukks는 작업을 처리하고 예측 가능한 방식으로 실행하는 간단한 방법을 제공합니다. UI, 네트워크 통화 또는 필요한 기타 작업의 작업을 처리하는 데 사용할 수 있습니다. 라이브러리의 비회화되지 않은 특성을 사용하면 Usecase와 관련된 고유 한 액션 핸들러를 구현할 수 있습니다. 심지어 신호 전달을 구현하여 Uniflow 패턴을 깨뜨릴 수 있습니다.
테스트 : Redukks, 상태 테스트, Redukks, Reducer 및 동작은 API의 특성과 추가 컴파일 안전으로 인해 매우 간단합니다. 복잡한 테스트 API의 필요없이 상태 관리 논리 또는 작업 처리 로직을 쉽게 테스트 할 수 있습니다.
Uniflow 아키텍처는 하나의 간단한 아이디어를 기반으로합니다. 앱은 한 방향으로 흐르는 일정한 데이터주기입니다. 상태와 변화는 UI에 의해 주도되며 UI는 국가에 의해 주도됩니다. 이를 통해 진실의 단일 소스를 가질 수 있으며 앱에 대해 간단한 생각과 이유를 제공합니다.
한편 Redux는 주를 관리하는 데 도움이되는 패턴입니다. 그것은 당신의 상태가 단일 진실의 원천이며 예측 가능한 방식으로 업데이트되어야한다는 생각에 기초합니다.
Redukks는이 두 가지 아이디어의 조합입니다. Uniflow 및 Redux 기반 아키텍처를 구현하기위한 기본 추상화를 제공하지만 한 가지 방법으로 강요하지는 않습니다.

흐름 자체는 매우 간단합니다. 상태 X가 있고 변경할 수있는 방법이 있습니다. 변경하는 방법을 "감속기"라고하며 해당 상태를 바꿀 수있는 유일한 방법입니다. 따라서 우리가 원하는 곳에서 크게 업데이트하는 대신 상태를 업데이트하는 데 사용할 수있는 일련의 "업데이트"를 정의합니다.
그러나 대부분의 응용 프로그램은 간단한 카운터보다 더 복잡하기 때문에 복잡한 작업도 처리 할 수있는 방법이 필요합니다. 그곳에서 행동이 들어오는 곳입니다. 당신은 행동을 "파견"하고 일부 작업을 실행하며, 그 실행 중에는 감속기를 통해 상태를 업데이트하거나 다른 작업을 호출 할 수 있습니다.
이를 통해 상태 변경의 가능한 모든 순열을 쉽게 정의하고 테스트 할 수 있으며, 앱 상태에 대해 쉽게 추론 할 수 있으며 의도하는 방식에 대해 확신 할 수 있습니다.
Gradle Groovy :
dependencies {
implementation ' com.ianrumac.redukks:redukks:0.1.4 '
}Gradle Kotlin :
implementation( " com.ianrumac.redukks:redukks:0.1.4 " ) data class CountState ( val total : Int )
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
}FeatureContext 클래스를 만듭니다. interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions 정의하십시오 sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class AddToClient ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . Add (result))
})
class SubtractFromClient ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . Subtract (result))
})
} data class CountWithAnAPI ( val scope : CoroutineScope ) : CountContext {
override val client = CountingAPI ()
override val store = reducedStore( CountState ( 0 ))
override val handler =
actionHandlerFor(scope, this ) // this is a coroutine scope that will be used to execute the actions
}
val context = CountWithAnAPI (scope)
// In a class that implements Dispatcher<Actions>
// This is actually a call to context.handler.dispatch(Actions.Add(1))
dispatch( Actions . Add ( 1 )) interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : ActionDispatcher < Actions >
data class CountState ( val total : Int )
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
}
sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class AddToClient ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . Add (result))
})
class SubtractFromClient ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . Subtract (result))
})
}
} val context = CountWithAnAPI (scope)
val handler = context.handler
handler.dispatch( Actions . AddToClient ( 1 ))
context.store.listen().collectLatest { state ->
// do something with the state
} Android의 뷰 모델과 함께 Redukks를 쉽게 사용할 수 있으며 Jetpack Compose (및 View Framework도 적합합니다). 스토어를 주 흐름으로 수집하거나 LIVEDATA와 함께 사용하기 만하면됩니다. ViewModel은 Dispatcher<Actions> 구현하여 업스트림 작업을 파견 할 수 있습니다 (동작 핸들러에 위임). 또한 이러한 것들을 처리하는 ReduxViewModel 과 같은 추상화를 만들 수도 있습니다.
ViewModel과 함께 사용하는 것은 간단합니다.
@HiltViewModel
class CountViewModel @Inject constructor(
val actionHandler : Dispatcher < CountContext . Actions >,
val store : Store < CountContext . State >
) : ViewModel(), Dispatcher<Actions> by actionHandler {
// Note: Not a real implementation, be careful where you collect and map your state
val state = store.listen().map( this ::mapToViewModel)
.stateIn(viewModelScope, SharingStarted . WhileSubscribed (), initialValue = CountVM ( 0 ))
fun mapToViewModel ( it : CountContext . State ): CountVM {
.. .
}
}그리고 이제 UI에서는 간단하게 할 수 있습니다.
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))수명주기 스코프 트리에서 위 또는 아래로 들어가서 상태의 수명주기를 제어 할 수도 있습니다. 예를 들어, State Store를 공유 해야하는 경우 IE 단편 범위에서 활동 범위로 이동하여 조각을 아래쪽으로 전달할 수 있습니다. 이런 식으로, 당신은 조각들 사이에 상태를 공유 할 수 있지만 여전히 단일 상태 저장소 만 있습니다. 행동을 위해 여러 매장을 제공하거나 UI에 여러 상점을 제공하여 결합 할 수도 있습니다.
typealias CountStore = Store < CountState >
typealias UserStore = Store < UserState >
interface CountContext {
val countStore : CountStore
val userStore : UserStore
}
class AuthenticatedCounterContext ( userStore : UserStore ) {
override val userStore = userStore
override val countStore = CountStore ( CountState ( 0 ))
}
// ...
class AddForUser : Actions ({
val count = countStore.state.total
countStore.update( Updates . Add (1))
userStore.update( UpdateStateForUser (countStore.total))
})
이것은 주에서 UI를 쉽게 도출 할 수 있기 때문에 JetPack Compose와 아름답게 작동합니다 (이를 수행하기 전에 ViewModel 클래스에 매핑하고 핵심 모델과 UI 모델을 혼합하지 마십시오).

Redukks는 모듈 식 추상화 세트이므로 원하는 복잡성 수준에 필요한 부품 만 사용할 수 있습니다. Redukks는 추상화의 몇 가지 기본 구현을 제공하지만 쉽게 직접 만들 수 있습니다.
가장 간단한 케이스, 간단한 상점부터 시작하겠습니다. 우리는 단순한 카운터 구현으로 시작하며, 감속기 나 동작이없는 상점만으로 시작합니다. 일반적으로 Reducer를 쓰지 않으려는 경우 BasicStore 클래스를 사용할 수 있습니다. Basicstore 클래스는 현재 상태를 취하고 새로운 상태를 반환하는 폐쇄로 update 기능을 노출시키는 간단한 매장입니다.
data class CountState ( val total : Int )
val store = BasicStore ( CountState ( 0 ))
store.update {
it.copy(it.total + 1 )
}
println (store.state.total) // prints 1
store.listen().collectLatest { state ->
// do something with the state
} 이제 우리는 RE학적 업체를 사용하여 State 업데이트를 TypeSAFE 방식으로 정의 할 수 있습니다. 그래서 감속기를 만들어 봅시다. 감속기는 Reducer<StateType> 인터페이스를 구현하는 객체 일뿐입니다. 즉, 현재 상태를 취하고 새로운 상태를 반환하는 폐쇄가 reduce 듭니다. 글쓰기와 같습니다
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}그러나 약간 더 유형의 안전성을 사용하면 가능한 모든 상태 변경이 사전 정의되고 테스트 될 수 있습니다.
sealed class Updates ( override val reduce : ( CountState ) -> CountState ) : Reducer<CountState> {
class Add ( val number : Int ) : Updates({
state.copy(state.total + number)
})
class Subtract ( val number : Int ) : Updates({
state.copy(state.total - number)
})
} 이제이 감속기를 사용하려면 ReducedStore<StateType, ReducerType> 만들어야합니다. 정의 된 감속기 유형을 통해서만 업데이트 할 수있는 상점입니다. 기본 구현은 BASICEDUCEDSTORE로 제공되지만 직접 쉽게 만들 수 있습니다.
val basicStore = BasicStore ( CountState ( 0 ))
// you can either provide it with a store or the initial state to use a BasicStore
val store = BasicReducedStore < CountState , Updates >(basicStore)
// or use the helper function
val store = createReducedStore< CountState , Updates >( CountState ( 0 ))
store.update( Updates . Add ( 1 ))
store.listen().collectLatest { state ->
// do something with the state
}동작은 실행할 수있는 비동기 코드입니다. 일반적으로 네트워크 통화와 같은 부작용을 수행하거나 다른 작업 및 업데이트를 발송하는 데 사용됩니다. 예를 들어, 서버에서 항목 목록을 가져온 다음 결과와 함께 상태를 업데이트하기 위해 조치를 발송합니다.
행동은 일반적으로 종속성에 액세스 할 수 있어야하기 때문에 조금 더 복잡합니다. 이를 위해 모든 종속성이 포함 된 <Context> 제공 한 다음 수신자로 컨텍스트로 실행되는 작업을 작성하므로 this 액세스 할 수 있습니다. 이를 통해 우리는 그것들을 Dispatcher<ActionType> 로 파견하여 행동을 처리해야합니다.
기본 Corootine 기반 액션 핸들러는 TypedActionHandler 에서 기본적으로 구현되며 더 많은 실행 컨트롤을 위해 Dispatcher<ActionType> 또는 AsyncDispatcher<ActionType> 을 제공하여 사용자 정의를 쉽게 구현할 수 있습니다 (Deferred를 통해). 또한, 생성자를 통해 단순히 폐쇄를 전달하는 데 사용할 수있는 AsyncAction 클래스가 제공됩니다.
이제 비동기 동작을 정의하는 방법을 살펴 보겠습니다.
먼저, 우리는 이러한 조치를 실행할 수 있다는 맥락이 필요합니다.
간단한 경우 (예 : 단순히 상점을 업데이트하는 경우) Store<StateType> 만 사용할 수 있지만 사용자 정의 컨텍스트를 만드는 것이 좋습니다. 이를 통해 종속성에 쉽게 접근하고 조롱하고 교체하는 등의 쉬운 방법을 제공합니다. 또한 작업을 단단히 결합하여 조치를 유지하는 것이 좋습니다.
Kotlin의 컨텍스트 수신기가 진행되면서 코드는 향후 더 깨끗해질 수 있으며, 동작이 완전히 제거되고 수신자에게 간단한 기능을 구현하는 것입니다. 그러나 대부분의 코드베이스에는 아직 컨텍스트 수신기가 없으므로 지금은이 구현에만 집중할 것입니다.
먼저 필요한 종속성을 포함하는 CountContext 인터페이스를 정의합니다.
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} 그런 다음 TypedAction<CountContext> 구현하는 밀봉 된 클래스를 통해 작업을 정의 할 수 있습니다.
sealed class Actions ( override val execute : suspend CountContext .() -> Unit ) : TypedAction<CountContext> {
class Add ( val number : Int ) : Actions({
// here, we have access to all the CountContext properties that are captured during execution
// For example, we can access the store and update it with the result of the network call.
val result = client.add(number)
state.update( Updates . UpdateState (result))
})
class Subtract ( val number : Int ) : Actions({
val result = client.subtract(number)
state.update( Updates . UpdateState (result))
})
}이제 컨텍스트를 구현하고 디스패처를 사용하여 작업을 실행할 수 있습니다.
data class CountWithAnAPI ( val scope : CoroutineScope ) : CountContext {
override val client = CountingAPI ()
override val store = BasicReducedStore < CountState , Updates >( CountState ( 0 ))
override val handler = createActionHandler(scope, this )
}
// In a class that implements Dispatcher<Actions>, like a viewmodel that delegates it to the handler
// This is actually a call to context.handler.dispatch(Actions.Add(1))
dispatch( Actions . Add ( 1 ))
dispatch( Actions . Subtract ( 1 ))
감속기와 동작 모두에 대한 테스트는 매우 쉽습니다. 감속기의 경우 단순히 상태와 리소셔를 만들 수 있습니다. 즉, val state = CountState(0) 및 val reducer = Updates.Add(1) reducer.reduce(state) 만들 수 있습니다. 이제 결과가 예상되는 결과라고 주장 할 수 있습니다.
@Test
fun `update the total count by adding 1` () {
val state = CountState ( 0 )
val reducer = Updates . Add ( 1 )
val result = reducer.reduce(state)
assertEquals( 1 , result.total)
} 조치의 경우 컨텍스트와 디스패처 (즉, val context = CountWithAnAPI(TestCoroutineScope()) 및 val action = Action.Add(1) 만들 수있는 다음 action.execute(context) 호출 할 수 있습니다. 이렇게하면 컨텍스트를 쉽게 조롱하고 동작이 예상대로 실행되거나 Mockk와 같은 조롱 라이브러리를 사용하여 Mocks가 올바르게 실행되는 모의를 확인할 수 있기 때문에 작업 테스트 방법을 제공합니다.
@Test
fun `update the total count and add from client` () = runTest {
val context = CountWithAnAPI ( this )
val action = Actions . Add ( 1 )
action.execute(context)
assertEquals( 1 , context.store.state.total)
}각 구성 요소는 쉽게 사용자 정의 할 수 있도록하여 자신의 직접 쉽게 만들 수있는 방식으로 작성됩니다. 상점이나 디스패처를 구현함으로써 API/소켓/객실 뒷면 상점과 같은 것을 쉽게 가질 수 있습니다. 패턴을 필요에 맞게 사용자 정의하는 것을 두려워하지 마십시오. 우리는 패턴을 만족시키기 위해 코드를 작성하지 않고, 우리 자신, 팀원 및 사용자를 위해 작성합니다.
제안이나 개선이 있으면 문제 나 PR을 자유롭게 열어주십시오.