Cette bibliothèque est en alpha. L'API est principalement complète et ne changera pas, mais il y a encore du travail à faire pour préparer la production sur toutes les plateformes. Pour JVM et MultipLatform, la bibliothèque est prête pour une utilisation en production, mais pour iOS, JS et natif, il n'est toujours pas complètement configuré. Si vous voulez aider, n'hésitez pas à ouvrir un problème ou un RP.
Redukks est un ensemble d'abstractions simples de type type nécessaire pour implémenter l'architecture Uniflow et Redux sur le multiplaform Kotlin. Il simplifie la création de magasins, de réducteurs et d'actions, et fournit un moyen simple de les tester.
Bien qu'il existe de nombreuses implémentations Uniflow ou Redux pour le multiplatifform Kotlin / Kotlin, la plupart d'entre elles sont soit trop complexes, soit trop simples. Soit la dénomination est trop "Reduxy", ils vous forcent à faire des choses, soit les abstractions sont trop complexes pour être écrites sur le régulier. Et bien qu'il existe des architectures conformes à ce style, la dénomination qu'ils utilisent est assez spécifique pour Android et déroutant pour les développeurs non Android, ce qui rend plus difficile de raisonner ou de discuter avec d'autres développeurs.
Depuis que je me suis retrouvé à réutiliser le même ensemble d'abstractions au fil des ans, j'ai décidé de faire d'eux une bibliothèque que je peux à la fois réutiliser et partager.
L'objectif est de fournir des abstractions simples, sécurisées et testables pour l'architecture de type Redux, tout en ne vous attachant pas complètement à un seul modèle, vous fournissant des abstractions qui peuvent vous aider à créer votre propre solution. Alors que la plupart des bibliothèques essaient de faire pression pour un motif redux complet, les réducteurs peuvent être des frais généraux pour certains cas simples, et vous devriez pouvoir les éviter si vous le souhaitez - c'est pourquoi Redukks n'applique pas le motif Redux lui-même.
Pour plus de raisonnement sur les raisons pour lesquelles Uniflow et pourquoi Redux, vous pouvez consulter la conduite de la conversation (DE).
Gestion de l'État : Redukks fournit un moyen simple de gérer votre état, de le mettre à jour de manière prévisible et d'écouter les modifications. Vous pouvez l'utiliser pour gérer l'état de toute votre application, une seule fonctionnalité, avoir un état partagé entre différentes fonctionnalités (écrans) ou avoir plusieurs états pour un seul écran.
Gestion des actions : Redukks fournit un moyen simple de gérer les actions et de les exécuter de manière prévisible. Vous pouvez l'utiliser pour gérer les actions de l'interface utilisateur, des appels de réseau ou tout autre travail dont vous pourriez avoir besoin. La nature non opinée de la bibliothèque vous permet d'implémenter vos propres gestionnaires d'action spécifiques à votre usecase - vous permettant même de casser le modèle Uniflow en implémentant la signalisation vers l'arrière.
Tests : avec Redukks, tester votre état, les réducteurs et les actions est incroyablement simple - à la fois en raison de la nature de l'API et de la sécurité supplémentaire. Vous pouvez facilement tester votre logique de gestion de l'État ou votre logique de traitement d'action sans avoir besoin d'API de test complexes.
L'architecture Uniflow est basée sur une idée simple - votre application est un cycle constant de données qui circule dans une direction. L'état et ses changements sont motivés par l'interface utilisateur, et l'interface utilisateur est motivée par l'État. Cela vous permet d'avoir une seule source de vérité et vous offre un moyen simple de penser et de raisonner sur votre application.
Pendant ce temps, Redux est un modèle qui vous aide à gérer votre état. Il est basé sur l'idée que votre état est une seule source de vérité et qu'il devrait être mis à jour de manière prévisible.
Redukks est une combinaison de ces deux idées - il vous fournit les abstractions de base pour mettre en œuvre des architectures Uniflow et Redux, mais ne vous force pas à faire une seule façon de faire les choses.

Le flux lui-même est assez simple - vous avez un état X et vous avez n moyens de le changer. Ces façons de le changer sont appelées «réducteurs» et ils sont le seul moyen de changer cet état. Ainsi, au lieu de le mettre à jour fougueux de partout où nous voulons, nous définissons un ensemble de "mises à jour" qui peuvent être utilisées pour mettre à jour l'état.
Mais, comme la plupart des applications sont plus complexes qu'un simple compteur, vous avez également besoin d'un moyen de gérer un travail complexe. C'est là que les actions entrent en jeu.
Cela facilite la définition et le test de toutes les permutations possibles des changements d'état, et facilite la raison de l'état de votre application et d'être confiant que cela fonctionne comme vous l'avez prévu.
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 qui contient toutes les dépendances pour votre fonctionnalité (ou pour vos actions). interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions qui peuvent être exécutées dans le contexte 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
} Vous pouvez facilement utiliser Redukks avec ViewModel d'Android et il s'adapte très bien à Jetpack Compose (et le cadre de vue aussi!). Collectez simplement le magasin en tant que flux d'État ou utilisez-le avec LiveData. Votre vue ViewModel peut également implémenter Dispatcher<Actions> afin que vous puissiez expédier les actions en amont (déléguer au gestionnaire d'action). Vous pouvez également créer des abstractions pour cela, comme un ReduxViewModel qui gèrent ces choses pour vous.
L'utiliser avec un ViewModel est simple:
@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 {
.. .
}
}Et maintenant dans votre interface utilisateur, vous pouvez simplement faire:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))Vous pouvez également contrôler le cycle de vie de votre état en le hissant vers le haut ou vers le bas dans l'arbre de la portée du cycle de vie. Par exemple, si vous avez besoin de partager un magasin d'État, vous pouvez simplement le déplacer de IE une portée de fragment à une portée d'activité et la transmettre aux fragments. De cette façon, vous pouvez partager l'état entre les fragments, mais n'a toujours qu'un seul magasin d'État. Vous pouvez même fournir plusieurs magasins pour une action ou fournir plusieurs magasins à votre interface utilisateur pour combiner.
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))
})
Cela fonctionne à merveille avec Jetpack Compose, car vous pouvez facilement dériver l'interface utilisateur de l'état (s'il vous plaît, mappez-le à une classe ViewModel avant de le faire, ne mélangez pas vos modèles principaux et vos modèles d'interface utilisateur).

Étant donné que Redukks est un ensemble modulaire d'abstractions, vous ne pouvez utiliser que les parties dont vous avez besoin pour le niveau de complexité que vous souhaitez. Redukks fournit quelques implémentations par défaut des abstractions, mais vous pouvez facilement créer la vôtre.
Commençons par le cas le plus simple, un magasin simple. Nous commencerons par une simple contre-mise en œuvre, avec seulement un magasin sans réducteurs ni actions. En général, si vous préférez ne pas écrire de réducteurs, vous pouvez utiliser la classe BasicStore , qui est un magasin simple exposant une fonction de update avec une fermeture qui prend l'état actuel et renvoie un nouvel état.
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
} Maintenant, nous pouvons également utiliser un réducteur pour définir les mises à jour d'état d'une manière type. Créons donc un réducteur. Les réducteurs ne sont que des objets qui implémentent l'interface Reducer<StateType> , ce qui signifie qu'ils ont une fermeture reduce qui prend l'état actuel et renvoie un nouvel état. C'est comme écrire
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}Mais avec un peu plus de sécurité, afin que vous puissiez être certains que toutes les modifications d'état possibles sont prédéfinies et testées.
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)
})
} Maintenant, pour utiliser ce réducteur, nous devons créer un ReducedStore<StateType, ReducerType> . Il s'agit d'un magasin qui ne peut être mis à jour que via le type de réducteur défini. Une implémentation par défaut est fournie comme BasicredStStore, mais vous pouvez facilement créer le vôtre.
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
}Les actions sont des pièces de code asynchrones qui peuvent être exécutées. Habituellement, ils sont utilisés pour effectuer des effets secondaires, comme les appels de réseau ou pour expédier d'autres actions et mises à jour. Par exemple, récupérer une liste d'éléments à partir d'un serveur, puis répartir une action pour mettre à jour l'état avec le résultat.
Les actions sont un peu plus compliquées, car elles doivent généralement pouvoir accéder aux dépendances. Pour ce faire, nous fournissons un <Context> qui contient toutes les dépendances, puis créons des actions qui sont exécutées avec le contexte en tant que récepteur, ce qui signifie qu'ils y ont accès comme this . Avec cela, nous pouvons les expédier à un Dispatcher<ActionType> , qui devrait gérer les actions.
Un gestionnaire d'action basé sur Coroutine de base est implémenté par défaut sous TypedActionHandler et un personnalisé peut être facilement implémenté en fournissant un Dispatcher<ActionType> ou AsyncDispatcher<ActionType> pour plus de contrôle d'exécution (via Deferred). En outre, est fourni une classe AsyncAction qui peut être utilisée pour simplement passer la fermeture via le constructeur.
Maintenant, voyons comment définir les actions asynchrones.
Tout d'abord, nous avons besoin d'un contexte sur lequel ces actions peuvent être exécutées.
Bien que pour des cas simples (c'est-à-dire à la mise à jour simplement du magasin), vous pouvez simplement utiliser un Store<StateType> , il est préférable de créer un contexte personnalisé. Cela vous donne un moyen facile d'accéder aux dépendances, de les moquer, de les remplacer, etc. En outre, c'est une bonne pratique de garder les actions étroitement couplées à eux, afin que vous puissiez facilement voir quelles actions ont besoin de quelles dépendances.
Avec les récepteurs de contexte de Kotlin sur le chemin, le code peut être encore plus propre à l'avenir, supprimant entièrement le besoin d'actions et implémentant simplement des fonctions simples sur les récepteurs. Mais comme la plupart des bases de code n'ont pas encore de récepteurs de contexte, nous nous concentrerons uniquement sur cette implémentation pour l'instant.
Tout d'abord, nous définissons une interface CountContext qui contient les dépendances dont nous avons besoin:
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} Ensuite, nous pouvons définir des actions via des classes scellées qui implémentent 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))
})
}Maintenant, nous pouvons implémenter le contexte et utiliser le répartiteur pour exécuter des actions:
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 ))
Les tests sont assez faciles, pour les réducteurs et les actions. Pour les réducteurs, vous pouvez simplement créer un état et un réducteur, c'est-à-dire val state = CountState(0) et val reducer = Updates.Add(1) , puis Call reducer.reduce(state) . Vous pouvez maintenant affirmer que le résultat est celui attendu.
@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)
} Pour les actions, vous pouvez créer un contexte et un répartiteur, c'est-à-dire val context = CountWithAnAPI(TestCoroutineScope()) et val action = Action.Add(1) , puis appelez action.execute(context) . Cela vous fournit un moyen facile de tester les actions, car vous pouvez facilement se moquer du contexte et affirmer que les actions sont exécutées comme prévu ou utiliser des bibliothèques moqueuses telles que MockK pour vérifier correctement les simulations exécutées.
@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)
}Chaque composant est écrit d'une manière qui permet une personnalisation facile, afin que vous puissiez facilement créer le vôtre. Ce qui signifie qu'en mettant en œuvre un magasin ou un répartiteur, vous pouvez facilement avoir des choses comme un magasin API / socket / salle soutenu, ou des actions qui signalent lorsqu'ils sont terminés, ou d'autres flux de votre magasin. N'ayez pas peur de personnaliser le modèle à vos besoins - nous n'écrivons pas de code pour satisfaire les modèles, nous l'écrivons pour nous-mêmes, nos coéquipiers et nos utilisateurs.
N'hésitez pas à ouvrir un problème ou un RP si vous avez des suggestions ou des améliorations.