Tools for working with redux-kotlin in an Android environment.
Note: experimental & changing frequently! The latest changes can be found on master-SNAPSHOT
through JitPack.
This library provides a ready-made framework for working with Redux in a Kotlin Android environment. Getting off the ground is relatively easy, given that you are familiar with Redux or transactional state management. It provides two main entry points in ReduxActivity
and ReduxFragment
:
class MainActivity : ReduxActivity<AppState, CounterState>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ExampleApp.injector.inject(this)
}
override fun onCreateViewModel(): ReduxViewModel<AppState> = ExampleApp.injector.appViewModel()
override fun onCreateViewComponent(): ViewComponent<CounterState> =
CounterViewComponent(inflater = layoutInflater, dispatch = reduxViewModel.dispatch)
override fun onSelectState(state: AppState): CounterState = state.counterState
override fun performSideEffect(
state: AppState,
action: Any
) {
Toast.makeText(this, "Side effect triggered for $action!", Toast.LENGTH_SHORT).show()
}
}
State is exactly what it sounds like - just values. It should be an immutable data class that implements the State
interface represents the current state of your application or screen. A simple State
hierarchy for an application might look something like this:
data class CounterState(val count: Int = 0) : State
data class ToDoState(val toDoItems: Set<String> = emptySet()) : State
data class AppState(
val counterState: CounterState = CounterState(),
val toDoState: ToDoState = ToDoState()
) : State
This is a simple class, for extension, that handles binding state values to the UI in a render(...)
function. Whenever a new state is computed and emitted, the render function will be invoked. In order to prevent excessive or unnecessary calls to the render function, subscriptions to state changes are distinct by default. This can be changed by overriding the distinct(): Boolean
function in a given activity or fragment. This is also where you bind your UI to emit actions. A simple ViewComponent
implementation for a Counter UI might look like this:
class CounterViewComponent(
container: ViewGroup? = null,
inflater: LayoutInflater,
dispatch: Dispatcher
) : ViewComponent<CounterState>() {
override val binding: CounterLayoutBinding =
CounterLayoutBinding.inflate(inflater, container, false).apply {
incrementButton.setOnClickListener {
dispatch(Increment())
}
decrementButton.setOnClickListener {
dispatch(Decrement())
}
}
override fun render(state: CounterState) {
binding.counterTextView.text = state.count.toString()
}
}
A ReduxViewModel
is for use with instances of ReduxActivity
and ReduxFragment
. It's responsible for managing subscriptions to the redux store across configuration changes. There will typically be a single instance per application.
When subscribing via ReduxViewModel
or ReduxStoreManager
, you're given the option to provide a function for mapping application state to sub-states. A simple selection method might look something like:
override fun onSelectState(state: AppState): CounterState = state.counterState
Traditionally, Redux uses different varieties of Middleware (thunk, saga, promise, etc.) to handle async operations. While you could resort to using middleware for your async operations, the problem with Android is that we generally need to scope our async operations to the lifecycle of a particular component, like a Fragment or Activity. This leads to hacky solutions for exporting LifecycleOwner
s to middleware, or having singletons hold a long-lived reference to the current LifecycleOwner
. If you've got many fragments, this gets complicated. These solutions are often times complex and prone to leaks. We also get some nice tools to handle operations inside these lifecycle-sensitive components, like Kotlin Coroutines and scopes like lifecycleScope { ... }
, which encourage using Fragments & Activities as Controllers rather than Views.
For those reasons, the default Store
instance provided by this library is enhanced via Redux Store Enhancer to allow post-dispatch operations. You can subscribe to the current state + the action returned by the dispatch()
function. ReduxFragment
and ReduxActivity
provide performSideEffect(state, action)
for you to override. You're free to abstract away your async operations in whatever manner you please. Subscriptions to state & side effects are also lifecycle sensitive, and are auto-paused / resumed / canceled.
- It's entirely possible and allowable to forego the use of
ReduxViewModel
,ReduxActivity
, andReduxFragment
and simply use the enhanced store offered here. - It's also entirely possible, though not encouraged, to write your own Async Middleware.
- If you find yourself applying the above two points, you may not need this framework :)
An example application is provided here.
Copyright 2020 Carter Hudson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.