Эта библиотека в Альфе. API в основном завершен и не будет меняться, но еще предстоит сделать работу, чтобы подготовить его к производству на всех платформах. Для JVM и Multiplatform Библиотека готова к производству, но для iOS, JS и Native она все еще не полностью настроена. Если вы хотите помочь, не стесняйтесь открывать проблему или PR.
Redukks-это набор простых абстракций, защищенных типами, необходимыми для реализации Uniflow и Redux-подобной архитектуры в мультиплатформе Kotlin. Он упрощает создание магазинов, редукторов и действий и обеспечивает простой способ их проверки.
Несмотря на то, что существует много реализаций Uniflow или Redux для многоплатформы Kotlin/Kotlin, большинство из них либо слишком сложны, либо слишком просты. Либо именование слишком «румяно», они заставляют вас одним способом делать вещи, либо абстракции слишком сложны, чтобы их можно было писать на обычном. И хотя существуют архитектуры, которые соответствуют этому стилю, использование, которое они используют, довольно специфичны для Android и запутаны с не-андроидными разработчиками, что затрудняет рассуждение или обсуждение или обсуждение с другими разработчиками.
Поскольку я обнаружил, что повторно использую один и тот же набор абстракций в течение многих лет, я решил превратить их в библиотеку, которую я могу использовать и поделиться.
Цель состоит в том, чтобы обеспечить простые, безопасные типа и тестируемые абстракции для Redux, подобной архитектуре, при этом все еще не привязывая вас к одному шаблону, предоставляя вам абстракции, которые могут помочь вам создать собственное решение. В то время как большинство библиотек пытаются настаивать на полном рисунке Redux, редукторы могут быть накладными расходами для некоторых простых случаев, и вы сможете избежать их, если хотите - именно поэтому Redukks не обеспечивает соблюдение самого рисунка Redux.
Для получения дополнительной информации о том, почему Uniflow и почему Redux вы можете проверить архитектуру разговора (DE).
Управление государством : Redukks предоставляет простой способ управления вашим состоянием, обновлять его предсказуемым способом и прислушиваться к изменениям. Вы можете использовать его для управления состоянием всего вашего приложения, одной функции, иметь общее состояние между различными функциями (экранами) или иметь несколько состояний для одного экрана.
Обработка действий : Redukks обеспечивает простой способ обработки действий и выполнять их предсказуемым способом. Вы можете использовать его для обработки действий из пользовательского интерфейса, сетевых вызовов или любой другой работы, которая вам может понадобиться. Неопинированный характер библиотеки позволяет вам реализовать свои собственные обработчики действий, специфичные для вашего использования - даже позволяя вам сломать шаблон Uniflow, реализуя обратную передачу сигналов.
Тестирование : с Redukks, тестирование вашего состояния, редукторы и действия невероятно просты - как из -за природы API, так и из -за дополнительной безопасности. Вы можете легко проверить свою логику управления состоянием или логику обработки действий без необходимости сложного тестирования API.
Архитектура Uniflow основана на одной простой идее - ваше приложение представляет собой постоянный цикл данных, которые течет в одном направлении. Государство и его изменения обусловлены пользовательским интерфейсом, а пользовательский интерфейс управляется государством. Это позволяет вам иметь единый источник истины и дает вам простой способ думать и рассуждать о вашем приложении.
Между тем, Redux - это шаблон, который помогает вам управлять вашим состоянием. Он основан на идее, что ваше состояние является единственным источником истины, и что его следует обновлять предсказуемым образом.
Redukks - это комбинация этих двух идей - она предоставляет вам основные абстракции для реализации архитектур на основе Uniflow и Redux, но не заставляют вас в единый способ ведения дел.

Сам поток довольно прост - у вас есть состояние X, и у вас есть способы его изменить. Эти способы изменить его называют «редукторами», и они являются единственным способом изменить это состояние. Таким образом, вместо того, чтобы дико обновлять его откуда, где бы мы ни хотели, мы определяем набор «обновлений», которые можно использовать для обновления состояния.
Но, поскольку большинство приложений более сложны, чем простой счетчик, вам также нужен способ справиться с сложной работой. Вот тут -то и идут действия. Вы «отправляете» действие, и оно выполняет некоторую работу, и во время этого выполнения оно может обновить состояние с помощью редукторов или вызвать другие действия.
Это позволяет легко определить и проверить все возможные перестановки изменений состояния, и позволяет легко рассуждать о состоянии вашего приложения и быть уверенным, что это работает так, как вы намеревались.
Gradle Groovy:
dependencies {
implementation ' com.ianrumac.redukks:redukks:0.1.4 '
}Градл Котлин:
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, и она отлично подходит для Compose JetPack (и структура представления!). Просто собирайте магазин как Stateflow или используйте его с 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 {
.. .
}
}А теперь в вашем пользовательском интерфейсе вы можете просто сделать:
val vm : CountViewModel by viewModels()
.. .
vm.dispatch( Actions . AddToClient ( 1 ))Вы также можете контролировать жизненный цикл вашего штата, поднимая его вверх или вниз в дереве охвата жизненного цикла. Например, если вам нужно поделиться государственным магазином, вы можете просто перенести его из IE фрагментного масштаба в масштаб деятельности и передать его вниз к фрагментам. Таким образом, вы можете поделиться состоянием между фрагментами, но при этом есть только один государственный магазин. Вы даже можете предоставить несколько магазинов для действия или предоставить несколько магазинов для вашего пользовательского интерфейса.
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 Compose, поскольку вы можете легко вывести пользовательский интерфейс из состояния (пожалуйста, сопоставьте его на класс ViewModel. Перед этим не смешивайте свои основные модели и модели пользовательского интерфейса).

Поскольку 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> . Это магазин, который может быть обновлен только с помощью определенного типа редуктора. Реализация по умолчанию предоставляется в качестве BasicedudedStore, но вы можете легко создать свой собственный.
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> , который должен обрабатывать действия.
Основной обработчик действий на основе Coroutine реализуется по умолчанию в соответствии с TypedActionHandler , и пользовательский может быть легко реализован путем предоставления Dispatcher<ActionType> или AsyncDispatcher<ActionType> для большего контроля выполнения (через отложенный). Кроме того, приведен класс AsyncAction , который можно использовать для простого прохождения закрытия через конструктор.
Теперь давайте посмотрим, как определить асинхронные действия.
Во -первых, нам нужен контекст, который эти действия могут быть выполнены.
В то время как для простых случаев (то есть просто обновлять магазин) вы можете просто использовать Store<StateType> , лучше создать пользовательский контекст. Это дает вам простой способ получить доступ к зависимостям, издеваться над ними, их замены и т. Д. Кроме того, это хорошая практика, чтобы держать действия в тесном соединении с ними, чтобы вы могли легко увидеть, какие действия необходимы какие зависимости.
Благодаря контекстным приемникам Котлина, код может быть еще более чище в будущем, полностью устраняя необходимость действий и просто реализуя простые функции на приемниках. Но поскольку у большинства кодовых баз еще нет контекстных приемников, мы пока сосредоточимся только на этой реализации.
Во -первых, мы определяем интерфейс 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/сокет/магазин, поддерживаемые комнатой, или действия, которые сигнализируют, когда они выполняются, или другие потоки в вашем магазине. Не бойтесь настроить шаблон для ваших потребностей - мы не пишем код, чтобы удовлетворить шаблоны, мы пишем его для себя, наших товарищей по команде и наших пользователей.
Не стесняйтесь открывать проблему или PR, если у вас есть какие -либо предложения или улучшения.