Esta biblioteca está em Alpha. A API está principalmente completa e não estará mudando, mas ainda há trabalho a ser feito para torná -lo pronto para a produção em todas as plataformas. Para JVM e multiplataforma, a biblioteca está pronta para uso da produção, mas para iOS, JS e nativo ainda não está completamente configurado. Se você quiser ajudar, sinta -se à vontade para abrir um problema ou um PR.
Redukks é um conjunto de abstrações simples seguras de tipo necessárias para implementar a arquitetura Uniflow e Redux na multiplataforma de Kotlin. Ele simplifica a criação de lojas, redutores e ações e fornece uma maneira simples de testá -las.
Embora existam muitas implementações uniflow ou redux para multiplataforma Kotlin/Kotlin, a maioria delas é muito complexa ou muito simples. A nomeação é "Reduxy" demais, eles o forçam a uma maneira de fazer as coisas ou as abstrações são complexas demais para serem escritas regularmente. E, embora existam arquiteturas que estão em conformidade com esse estilo, a nomeação que eles usam é bastante específica para o Android e confusos para desenvolvedores não-e-eróides, dificultando a razão sobre ou discutir com outros desenvolvedores.
Desde que me vi reutilizando o mesmo conjunto de abstrações ao longo dos anos, decidi transformá-los em uma biblioteca que posso reutilizar e compartilhar.
O objetivo é fornecer abstrações simples, seguras de tipo e testável para a arquitetura do tipo Redux, embora ainda não o vincule completamente a um único padrão, fornecendo abstrações que podem ajudá-lo a criar sua própria solução. Enquanto a maioria das bibliotecas tenta pressionar por um padrão redux completo, os redutores podem ser uma sobrecarga para alguns casos simples, e você poderá evitá -los, se quiser - é por isso que Redukks não aplica o próprio padrão de redux.
Para obter mais raciocínio sobre por que o Uniflow e por que Redux, você pode conferir a arquitetura (DE) da conversa.
Gerenciamento de Estado : Redukks fornece uma maneira simples de gerenciar seu estado, atualizá -lo de uma maneira previsível e ouvir alterações. Você pode usá -lo para gerenciar o estado de todo o seu aplicativo, um único recurso, ter um estado compartilhado entre diferentes recursos (telas) ou ter vários estados para uma tela.
Manuseio de ação : Redukks fornece uma maneira simples de lidar com ações e executá -las de maneira previsível. Você pode usá -lo para lidar com ações da interface do usuário, chamadas de rede ou qualquer outro trabalho necessário. A natureza não opinionada da biblioteca permite que você implemente seus próprios manipuladores de ação específicos para a sua USECASE - mesmo permitindo que você quebre o padrão Uniflow, implementando a sinalização atrasada.
Teste : com redukks, testar seu estado, redutores e ações é incrivelmente simples - tanto devido à natureza da API quanto à segurança extra compilada. Você pode testar facilmente sua lógica de gerenciamento de estado ou sua lógica de manuseio de ação sem a necessidade de APIs de teste complexos.
A arquitetura Uniflow é baseada em uma idéia simples - seu aplicativo é um ciclo constante de dados que flui em uma direção. O estado e suas mudanças são conduzidos pela interface do usuário, e a interface do usuário é motivada pelo estado. Isso permite que você tenha uma única fonte de verdade e fornece uma maneira simples de pensar e raciocinar sobre o seu aplicativo.
Enquanto isso, o Redux é um padrão que ajuda a gerenciar seu estado. É baseado na ideia de que seu estado é uma única fonte de verdade e que deve ser atualizada de maneira previsível.
Redukks é uma combinação dessas duas idéias - fornece as abstrações básicas para implementar arquiteturas Uniflow e Redux, mas não o force a uma única maneira de fazer as coisas.

O fluxo em si é bastante simples - você tem um estado X e tem n maneiras de alterá -lo. Essas maneiras de mudá -lo são chamadas de "redutores" e são a única maneira de mudar esse estado. Portanto, em vez de atualizá -lo descontraído de onde quisermos, definimos um conjunto de "atualizações" que podem ser usadas para atualizar o estado.
Mas, como a maioria dos aplicativos é mais complexa que um contador simples, você também precisa de uma maneira de lidar com um trabalho complexo. É aí que entra as ações. Você "envia" uma ação e ela executa algum trabalho e, durante essa execução, pode atualizar o estado por meio de redutores ou invocar outras ações.
Isso facilita a definição e o teste de todas as permutações possíveis das mudanças de estado e facilita a razão sobre o estado do seu aplicativo e confiante de que funciona como você pretendia.
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 que contenha todas as dependências para o seu recurso (ou para suas ações). interface CountContext {
val client : CountingAPI
val store : Store < CountState >
}Actions que podem ser executadas no contexto 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
} Você pode usar facilmente Redukks com o ViewModel do Android e ele se encaixa muito bem com a composição do JetPack (e a estrutura de exibição também!). Basta coletar a loja como um fluxo de Estado ou usá -lo com Livivedata. Seu ViewModel também pode implementar Dispatcher<Actions> para que você possa despachar as ações a montante (delegue -as para o manipulador de ação). Você também pode criar abstrações para isso, como um ReduxViewModel que lida com essas coisas para você.
Usá -lo com um viewModel é simples:
@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 {
.. .
}
}E agora na sua interface do usuário, você pode simplesmente fazer:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))Você também pode controlar o ciclo de vida do seu estado, elevando -o para cima ou para baixo na árvore do escopo do ciclo de vida. Por exemplo, se você precisar compartilhar uma loja estadual, poderá movê -la de um escopo de fragmento para um escopo de atividade e passá -lo para baixo para os fragmentos. Dessa forma, você pode compartilhar o estado entre fragmentos, mas ainda tem apenas uma única loja de estado. Você pode até fornecer várias lojas para uma ação ou fornecer várias lojas à sua interface do usuário para combinar.
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))
})
Isso funciona lindamente com a JetPack Compõe, pois você pode derivar facilmente a interface do usuário do estado (por favor, mapeie -o para uma classe ViewModel antes de fazê -lo, não misture seus modelos principais e seus modelos de interface do usuário).

Como o Redukks é um conjunto modular de abstrações, você pode usar apenas as peças necessárias para o nível de complexidade que deseja. O Redukks fornece algumas implementações padrão das abstrações, mas você pode criar facilmente o seu.
Vamos começar com o caso mais simples, uma loja simples. Começaremos com uma simples implementação de contador, com apenas uma loja sem redutores ou ações. Em geral, se você preferir não escrever redutores, pode usar a classe BasicStore , que é uma loja simples que exponha uma função de update com um fechamento que leva o estado atual e retorna um novo estado.
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
} Agora, também podemos usar um redutor para definir atualizações de estado de maneira a favor. Então, vamos criar um redutor. Redutores são apenas objetos que implementam a interface Reducer<StateType> , o que significa que eles têm um fechamento reduce que leva o estado atual e retorna um novo estado. É como escrever
fun addNumber ( state : CountState , number : Int ) : CountState {
return state.copy(state.total + number)
}Mas com um pouco mais de segurança do tipo, para que você possa ter certeza de que todas as mudanças de estado possíveis são predefinidas e testadas.
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)
})
} Agora, para usar esse redutor, precisamos criar um ReducedStore<StateType, ReducerType> . Esta é uma loja que só pode ser atualizada através do tipo de redutor definido. Uma implementação padrão é fornecida como base básica, mas você pode criar facilmente o seu.
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
}As ações são peças de código assíncronas que podem ser executadas. Geralmente, eles são usados para executar efeitos colaterais, como chamadas de rede ou despachar outras ações e atualizações. Por exemplo, buscando uma lista de itens de um servidor e, em seguida, enviando uma ação para atualizar o estado com o resultado.
As ações são um pouco mais complicadas, pois geralmente precisam acessar dependências. Para fazer isso, fornecemos um <Context> que contém todas as dependências e, em seguida, criamos ações executadas com o contexto como receptor, o que significa que eles têm acesso a ele como this . Com isso, podemos enviá -los para um Dispatcher<ActionType> , que deve lidar com ações.
Um manipulador básico de ação baseado em coroutina é implementado por padrão em TypedActionHandler e um personalizado pode ser facilmente implementado, fornecendo um Dispatcher<ActionType> ou AsyncDispatcher<ActionType> para mais controle de execução (via adiado). Além disso, fornecido é uma classe AsyncAction que pode ser usada para simplesmente passar no fechamento via construtor.
Agora, vamos ver como definir ações assíncronas.
Primeiro, precisamos de um contexto em que essas ações possam ser executadas.
Enquanto para casos simples (ou seja, simplesmente atualizando a loja), você pode usar uma Store<StateType> , é melhor criar um contexto personalizado. Isso oferece uma maneira fácil de acessar dependências, zombar delas, substituí -las etc. Além disso, é uma boa prática manter as ações firmemente acopladas a elas, para que você possa ver facilmente quais ações precisam de quais dependências.
Com os receptores de contexto de Kotlin a caminho, o código pode ser ainda mais limpo no futuro, removendo completamente a necessidade de ações e apenas implementando funções simples nos receptores. Mas como a maioria das bases de código ainda não possui receptores de contexto, focaremos apenas essa implementação por enquanto.
Primeiro, definimos uma interface CountContext que contém as dependências de que precisamos:
interface CountContext {
val client : CountingAPI
val store : Store < CountState >
val handler : Dispatcher < Actions >
} Em seguida, podemos definir ações por meio de classes seladas que implementam 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))
})
}Agora, podemos implementar o contexto e usar o despachante para executar ações:
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 ))
Os testes são bastante fáceis, para redutores e ações. Para redutores, você pode simplesmente criar um estado e um redutor, ou seja, val state = CountState(0) e val reducer = Updates.Add(1) , e depois Chame reducer.reduce(state) . Agora você pode afirmar que o resultado é o esperado.
@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)
} Para ações, você pode criar um contexto e um despachante, ou seja, val context = CountWithAnAPI(TestCoroutineScope()) e val action = Action.Add(1) e, em seguida, ligue para action.execute(context) . Isso fornece uma maneira fácil de testar ações, pois você pode zombar facilmente do contexto e afirmar que as ações são executadas conforme o esperado ou usar bibliotecas de zombaria, como o Mockk, para verificar zombares executadas corretamente.
@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)
}Cada componente é escrito de uma maneira que permita fácil personalização, para que você possa criar facilmente o seu. Significado ao implementar uma loja ou um despachante, você pode facilmente ter coisas como uma loja de apoio API/Socket/Room, ou ações que sinalizam quando estão concluídas ou outros fluxos em sua loja. Não tenha medo de personalizar o padrão de suas necessidades - não estamos escrevendo código para satisfazer os padrões, estamos escrevendo para nós mesmos, nossos colegas de equipe e nossos usuários.
Sinta -se à vontade para abrir um problema ou um PR se tiver alguma sugestão ou melhorias.