Diese Bibliothek ist in Alpha. Die API ist größtenteils vollständig und wird sich nicht ändern, aber es muss noch gearbeitet werden, um die Produktion auf allen Plattformen vorzubereiten. Für JVM und Multiplattform ist die Bibliothek bereit für den Produktionsgebrauch, aber für iOS, JS und Native ist sie immer noch nicht vollständig eingerichtet. Wenn Sie helfen möchten, können Sie ein Problem oder eine PR eröffnen.
Redukks ist eine Reihe einfacher Abtrktionen vom Typ Typ-sicher, die für die Implementierung von Uniflow und Redux-ähnlichen Architektur auf Kotlin-Multiplattform erforderlich sind. Es vereinfacht die Schaffung von Geschäften, Reduzierern und Aktionen und bietet eine einfache Möglichkeit, sie zu testen.
Während es viele Uniflow- oder Redux -Implementierungen für Kotlin/Kotlin -Multiplattform gibt, sind die meisten von ihnen entweder zu komplex oder zu einfach. Entweder ist die Benennung zu "reduxy", sie zwingen Sie in eine Art, Dinge zu tun, oder die Abstraktionen sind zu komplex, um regelmäßig geschrieben zu werden. Und obwohl es Architekturen gibt, die diesem Stil entsprechen, ist die Benennung, die sie verwenden, für Android und verwirrend für Nicht-und-Roid-Entwickler, was es schwieriger macht, andere Entwickler zu argumentieren oder mit ihnen zu diskutieren.
Seit ich mich im Laufe der Jahre im Laufe der Jahre die gleiche Reihe von Abstraktionen wieder verwendete, habe ich beschlossen, sie in eine Bibliothek zu machen, die ich sowohl wiederverwenden als auch teilen kann.
Ziel ist es, einfache, type sichere und überprüfbare Abstraktionen für reduxähnliche Architektur bereitzustellen, während Sie Sie dennoch nicht vollständig an ein einzelnes Muster anschließen und Ihnen Abstraktionen zur Verfügung stellen, mit denen Sie Ihre eigene Lösung aufbauen können. Während die meisten Bibliotheken versuchen, ein volles Redux -Muster zu veranlassen, können Reduzierer für einige einfache Fälle ein Overhead sein, und Sie sollten in der Lage sein, sie zu vermeiden, wenn Sie möchten. Deshalb erzwingt Redukks das Redux -Muster selbst nicht.
Weitere Begründungen, warum Uniflow und warum Redux, können Sie sich die Talk (DE) -Reconing -Architektur ansehen.
State Management : Redukks bietet eine einfache Möglichkeit, Ihren Zustand zu verwalten, ihn auf vorhersehbare Weise zu aktualisieren und Änderungen anzuhören. Sie können es verwenden, um den Status Ihrer gesamten App zu verwalten, eine einzelne Funktion, einen gemeinsamen Zustand zwischen verschiedenen Funktionen (Bildschirmen) oder mehrere Zustände für einen Bildschirm.
Aktionshandhabung : Redukks bietet eine einfache Möglichkeit, Aktionen zu handhaben und sie auf vorhersehbare Weise auszuführen. Sie können es verwenden, um Aktionen aus der Benutzeroberfläche, den Netzwerkanrufen oder anderen von Ihnen benötigten Arbeiten zu erledigen. Mit der nicht optimierten Natur der Bibliothek können Sie Ihre eigenen Aktionen implementieren, die für Ihre Usecase spezifisch sind. Auch wenn Sie das Uniflow -Muster brechen, indem Sie die Signalübertragung rückwärts implementieren.
Testen : Bei Redukks ist das Testen Ihres Staates, Reduzierer und Handlungen unglaublich einfach - sowohl aufgrund der Art der API als auch der zusätzlichen Kompilierung. Sie können Ihre staatliche Verwaltungslogik oder Ihre Aktionshandhabungslogik problemlos testen, ohne dass komplexe Test -APIs erforderlich sind.
Die Uniflow -Architektur basiert auf einer einfachen Idee - Ihre App ist ein konstanter Datenzyklus, der in eine Richtung fließt. Der Staat und seine Änderungen werden von der Benutzeroberfläche angetrieben, und die Benutzeroberfläche wird vom Staat angetrieben. Auf diese Weise haben Sie eine einzige Quelle der Wahrheit und bieten Ihnen eine einfache Möglichkeit, über Ihre App nachzudenken und zu begründen.
In der Zwischenzeit ist Redux ein Muster, das Ihnen hilft, Ihren Zustand zu verwalten. Es basiert auf der Idee, dass Ihr Zustand eine einzige Quelle der Wahrheit ist und dass er auf vorhersehbare Weise aktualisiert werden sollte.
Redukks ist eine Kombination dieser beiden Ideen - sie bietet Ihnen die grundlegenden Abstraktionen, um Uniflow- und Redux -basierte Architekturen zu implementieren, zwingt Sie jedoch nicht in eine einzige Art und Weise, Dinge zu tun.

Der Fluss selbst ist recht einfach - Sie haben einen Zustand X und Sie haben keine Möglichkeiten, ihn zu ändern. Diese Möglichkeiten, es zu ändern, werden als "Reduzierer" bezeichnet und sie sind die einzige Möglichkeit, diesen Zustand zu ändern. Anstatt es wild von wo immer wir will, definieren wir eine Reihe von "Updates", mit denen der Status aktualisiert werden kann.
Da die meisten Anwendungen komplexer sind als ein einfacher Zähler, benötigen Sie eine Möglichkeit, auch komplexe Arbeiten zu bewältigen. Hier kommen Aktionen ins Spiel. Sie "schicken" eine Aktion und sie führen einige Arbeiten aus, und während dieser Ausführung kann der Status über Reduzierer aktualisiert oder andere Aktionen aufgerufen werden.
Dies erleichtert einfach, alle möglichen Permutationen staatlicher Änderungen zu definieren und zu testen, und erleichtert den Zustand Ihrer App und ist zuversichtlich, dass es so funktioniert, wie Sie es beabsichtigt haben.
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 -Klasse, die alle Abhängigkeiten für Ihre Funktion (oder Ihre Aktionen) enthält. interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions , die im Kontext ausgeführt werden können 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
} Sie können redukks problemlos mit Androids ViewModel verwenden und es passt hervorragend zu Jetpack Compose (und dem View -Framework!). Sammeln Sie einfach den Laden als Stateflow oder verwenden Sie ihn mit Livedata. Ihr ViewModel kann auch Dispatcher<Actions> implementieren, sodass Sie die Aktionen stromaufwärts versenden können (delegieren Sie sie an den Aktionshandler). Sie können auch Abstraktionen dafür erstellen, z. B. ein ReduxViewModel , das diese Dinge für Sie verarbeitet.
Die Verwendung mit einem ViewModel ist einfach:
@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 {
.. .
}
}Und jetzt können Sie in Ihrer Benutzeroberfläche einfach:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))Sie können auch den Lebenszyklus Ihres Zustands steuern, indem Sie ihn im Lebenszyklus -Bereich hoch oder runter hochziehen. Wenn Sie beispielsweise einen State Store teilen müssen, können Sie ihn einfach von IE ein Fragmentbereich in einen Aktivitätsbereich übertragen und an die Fragmente nach unten übergeben. Auf diese Weise können Sie den Staat zwischen Fragmenten teilen, haben aber immer noch nur einen einzelnen Staatsprotokoll. Sie können sogar mehrere Geschäfte für eine Aktion bereitstellen oder Ihrer Benutzeroberfläche mehrere Geschäfte zur Verfügung stellen.
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))
})
Dies funktioniert wunderbar mit Jetpack Compose, da Sie die Benutzeroberfläche problemlos aus dem Staat ableiten können (bitte mischen Sie ihn einer ViewModel -Klasse, bevor Sie dies tun, Ihre Kernmodelle und Ihre UI -Modelle nicht mischen).

Da Redukks ein modularer Satz von Abstraktionen ist, können Sie nur die Teile verwenden, die Sie für die gewünschte Komplexitätsebene benötigen. Redukks bietet einige Standardimplementierungen der Abstraktionen, aber Sie können problemlos Ihre eigenen erstellen.
Beginnen wir mit dem einfachsten Fall, einem einfachen Geschäft. Wir beginnen mit einer einfachen Gegenimplementierung mit nur einem Geschäft ohne Reduzierer oder Aktionen. Wenn Sie es vorziehen, keine Reduzierer zu schreiben, können Sie die BasicStore -Klasse verwenden. Dies ist ein einfacher Speicher, in dem eine update -Funktion mit einem Schließung im aktuellen Zustand enthält und einen neuen Staat zurückgibt.
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
} Jetzt können wir auch einen Reduzierer verwenden, um staatliche Updates auf typesafe Weise zu definieren. Lassen Sie uns also einen Reduzierer erstellen. Reduzierer sind nur Objekte, die die Schnittstelle Reducer<StateType> implementieren, was bedeutet, dass sie einen reduce haben, der den aktuellen Zustand aufnimmt und einen neuen Zustand zurückgibt. Es ist wie beim Schreiben
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}Aber mit ein bisschen mehr Typensicherheit, sodass Sie sicher sein können, dass alle möglichen Zustandsänderungen vordefiniert und getestet werden.
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)
})
} Um diesen Reduzierer zu verwenden, müssen wir nun einen ReducedStore<StateType, ReducerType> erstellen. Dies ist ein Geschäft, das nur über den definierten Reduzierertyp aktualisiert werden kann. Eine Standardimplementierung wird als grundlegender reduzierter Store bereitgestellt, Sie können jedoch problemlos Ihre eigenen erstellen.
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
}Aktionen sind asynchrone Codestücke, die ausgeführt werden können. Normalerweise werden sie verwendet, um Nebenwirkungen wie Netzwerkaufrufe auszuführen oder andere Aktionen und Aktualisierungen zu versenden. Wenn Sie beispielsweise eine Liste von Elementen von einem Server abrufen und dann eine Aktion zum Aktualisieren des Status mit dem Ergebnis senden.
Aktionen sind etwas komplizierter, da sie normalerweise in der Lage sein müssen, auf Abhängigkeiten zuzugreifen. Dazu bieten wir einen <Context> , das alle Abhängigkeiten enthält, und erstellen dann Aktionen, die mit dem Kontext als Empfänger ausgeführt werden, was bedeutet, dass sie this zugreifen können. Damit können wir sie an einen Dispatcher<ActionType> senden, der Aktionen behandeln sollte.
Ein grundlegender Coroutine -basierter Aktionshandler wird standardmäßig unter TypedActionHandler implementiert, und ein benutzerdefinierter kann einfach implementiert werden, indem ein Dispatcher<ActionType> oder AsyncDispatcher<ActionType> für eine weitere Ausführungssteuerung (über aufgeschobene) bereitgestellt wird. Darüber hinaus ist eine AsyncAction -Klasse vorgesehen, mit der der Verschluss einfach über den Konstruktor bestanden werden kann.
Lassen Sie uns nun sehen, wie wir asynchronen Aktionen definieren können.
Erstens brauchen wir einen Kontext, in dem diese Aktionen ausgeführt werden können.
Während Sie für einfache Fälle (dh einfach die Aktualisierung des Geschäfts) nur einen Store<StateType> verwenden können, ist es besser, einen benutzerdefinierten Kontext zu erstellen. Dies gibt Ihnen eine einfache Möglichkeit, auf Abhängigkeiten zuzugreifen, sie zu verspotten, sie zu ersetzen usw. Außerdem ist es eine gute Praxis, die Aktionen eng mit ihnen verbunden zu halten, damit Sie leicht sehen können, welche Aktionen welche Abhängigkeiten erforderlich sind.
Mit Kotlins Kontextempfängern auf dem Weg kann der Code in Zukunft noch sauberer sein, die Notwendigkeit von Aktionen vollständig beseitigt und einfach einfache Funktionen für die Empfänger implementieren. Da die meisten Codebasen noch keine Kontextempfänger haben, konzentrieren wir uns vorerst nur auf diese Implementierung.
Zunächst definieren wir eine GrafContext -Schnittstelle, die die Abhängigkeiten enthält, die wir benötigen:
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} Anschließend können wir Aktionen über versiegelte Klassen definieren, die TypedAction<CountContext> implementieren:
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))
})
}Jetzt können wir den Kontext implementieren und den Dispatcher verwenden, um Aktionen auszuführen:
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 ))
Das Testen ist für Reduzierer und Aktionen ganz einfach. Bei Reduzierern können Sie einfach einen Zustand und einen Reduzierer erstellen, dh val state = CountState(0) und val reducer = Updates.Add(1) , und rufen Sie dann reducer.reduce(state) . Jetzt können Sie behaupten, dass das Ergebnis das erwartete ist.
@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)
} Für Aktionen können Sie einen Kontext und einen Dispatcher erstellen, dh val context = CountWithAnAPI(TestCoroutineScope()) und val action = Action.Add(1) und dann action.execute(context) . Dies bietet Ihnen eine einfache Möglichkeit, Aktionen zu testen, da Sie den Kontext problemlos verspotten und behaupten können, dass die Aktionen wie erwartet ausgeführt werden, oder verspottete Bibliotheken wie MockK, um die ordnungsgemäße Mocks zu überprüfen.
@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)
}Jede Komponente ist so geschrieben, dass Sie eine einfache Anpassung ermöglichen, sodass Sie problemlos Ihre eigenen erstellen können. Das heißt, indem Sie einen Geschäft oder einen Dispatcher implementieren, können Sie leicht Dinge wie API/Socket/Room -Backed -Store oder Aktionen haben, die signalisieren, wenn sie fertig sind, oder in anderen Strömen in Ihrem Geschäft. Haben Sie keine Angst, das Muster an Ihre Bedürfnisse anzupassen - wir schreiben keinen Code, um die Muster zu befriedigen. Wir schreiben es für uns selbst, unsere Teamkollegen und unsere Benutzer.
Fühlen Sie sich frei, ein Problem oder eine PR zu öffnen, wenn Sie Vorschläge oder Verbesserungen haben.