該庫在Alpha中。 API大部分是完整的,不會改變,但是仍有工作要做以使其在所有平台上準備就緒。對於JVM和MultipLatform,該庫可以供生產使用,但是對於iOS,JS和本機,它仍然不是完全設置的。如果您想提供幫助,請隨時打開問題或公關。
Redukks是在Kotlin MultiPlatform上實現Uniflow和類似Redux的架構所需的一組簡單類型的抽象。它簡化了商店,還原器和動作的創建,並提供了一種簡單的測試方法。
儘管Kotlin/Kotlin Multiplatform有許多Uniflow或Redux實現,但其中大多數要么太複雜,要么太簡單了。要么命名過於“重新”,它們會迫使您進入一種做事的方式,或者抽象太複雜,無法寫在常規上。儘管有一些符合這種風格的體系結構,但他們使用的命名對於Android和非Android開發人員感到困惑,因此很難與其他開發人員進行推理或討論。
由於我發現自己多年來重複使用了相同的抽象集,因此我決定將它們置於圖書館,我可以重複使用並分享。
目的是為類似Redux的體系結構提供簡單,類型和可測試的抽象,同時仍然沒有將您完全綁定到單個模式,從而為您提供可以幫助您構建自己的解決方案的抽象。儘管大多數庫都試圖推動全面的redux模式,但還原器可能是某些簡單情況下的開銷,如果您願意,您應該能夠避免使用它們 - 這就是為什麼Redukks不會強制執行Redux模式本身的原因。
有關為什麼uniflow以及為什麼Redux的更多推理,您可以查看構造體系結構的談話(de)。
國家管理:Redukks提供了一種簡單的方法來管理您的狀態,以可預測的方式對其進行更新並傾聽更改。您可以使用它來管理整個應用程序的狀態,一個功能,在不同功能(屏幕)之間具有共享狀態或一個屏幕的多個狀態。
動作處理:Redukks提供了一種處理操作的簡單方法,並以可預測的方式執行它們。您可以使用它來處理UI,網絡調用或您可能需要的任何其他工作中的操作。圖書館的未開放性質使您可以實現特定於用戶酶的操作處理程序 - 甚至可以通過實現向後信號來打破單纖維模式。
測試:通過Redukks,測試您的狀態,還原和動作非常簡單 - 既是由於API的性質和額外的編譯安全性。您可以輕鬆地測試您的狀態管理邏輯或操作處理邏輯,而無需進行複雜的測試API。
Uniflow架構基於一個簡單的想法 - 您的應用程序是一個恆定的數據周期,該數據沿一個方向流動。狀態及其更改是由UI驅動的,UI是由國家驅動的。這使您可以擁有一個真實的來源,並為您提供有關應用程序思考和理由的簡單方法。
同時,Redux是一種幫助您管理狀態的模式。它基於這樣的想法,即您的狀態是一個真實的來源,應該以可預測的方式對其進行更新。
Redukks是這兩個想法的組合 - 它為您提供了實現Uniflow和基於Redux的架構的基本抽象,但並不迫使您進入一種做事的方式。

流本身非常簡單 - 您有一個狀態X,並且您有N方法可以更改它。改變它的方式被稱為“還原器”,它們是改變該狀態的唯一方法。因此,我們沒有在任何地方從任何地方進行瘋狂的更新,而是定義了一組可用於更新狀態的“更新”。
但是,由於大多數應用程序比簡單的計數器更為複雜,因此您也需要一種處理複雜工作的方法。這就是動作的進來。您“派遣”了一項操作並執行一些工作,在執行期間,它可以通過還原器更新狀態或調用其他操作。
這使得可以輕鬆定義和測試狀態更改的所有可能排列,並使其易於推理您的應用程序狀態,並確信它的工作方式。
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
}您可以輕鬆地將Redukks與Android的ViewModel一起使用,並且非常適合JetPack組成(以及視圖框架!)。只需將商店作為狀態流收集或與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 ))您還可以通過在生命週期範圍樹中向上或向下吊起狀態來控制狀態的生命週期。例如,如果您需要共享一個州商店,則可以將其從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))
})
這與JetPack組合非常合作,因為您可以輕鬆地從狀態得出UI(請在這樣做之前將其映射到ViewModel類中,不要混合您的核心模型和UI型號)。

由於Redukks是一組模塊化的抽象集,因此您只能在所需的複雜度級別上使用所需的零件。 Redukks提供了一些抽象的默認實現,但是您可以輕鬆地創建自己的默認實現。
讓我們從最簡單的情況開始,一個簡單的商店。我們將從簡單的反實施開始,只有一個沒有還原或操作的商店。通常,如果您不想編寫還原器,則可以使用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
}現在,我們還可以使用還原器以類型的方式定義狀態更新。因此,讓我們創建一個還原器。還原器只是實現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> 。這是只能通過定義的還原類型更新的商店。默認實現作為BasicReductsstore提供,但是您可以輕鬆地創建自己的實現。
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> ,該>應處理操作。
默認情況下,根據TypedActionHandler實現了基本的基於Coroutine的操作處理程序,並且可以通過提供Dispatcher<ActionType>或AsyncDispatcher<ActionType>來輕鬆實現自定義一個,以進行更多執行控制(通過遞延)。另外,提供的是一個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) ,然後call action.execute(context) 。這為您提供了一種簡單的測試操作方法,因為您可以輕鬆模擬上下文並斷言這些操作是按預期執行的,或使用模擬庫(例如Mockk)來驗證正確執行的模擬。
@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/插座/房間後備商店之類的東西,或者在完成後發出信號的操作或商店中的其他流量。不要害怕根據您的需求自定義模式 - 我們不是編寫代碼來滿足模式,而是為自己,我們的隊友和用戶編寫。
如果您有任何建議或改進,請隨時開放問題或公關。