Skip to main content

Calculator

Calculator

The two calculator sample apps (adapted from Orbit) demonstrate:

  • how to use Blocs in iOS
  • how to use Blocs with or without Android ViewModel
  • how to use data binding with Blocs on Android
  • how to use sealed classes or enums classes as Action

Calculator 1

The more conventional implementation uses a sealed class as Action:

sealed class CalculatorAction {
object Clear : CalculatorAction()
object Add : CalculatorAction()
data class Digit(val digit: Int) : CalculatorAction()
// more actions ...
}

which are processed using single-action reducers:

fun bloc(context: BlocContext) = bloc<CalculatorState, CalculatorAction>(context, CalculatorState()) {
reduce<Clear> { CalculatorState() }
reduce<Add> { state.resetErrors().apply(Operator.Add) }
reduce<Digit> { state.resetErrors().digit(action.digit) }
// more reducers...

Android

On Android toLiveData() converts the StateStream to LiveData so it can be bound to the view using data binding:

class CalculatorViewModel : ViewModel() {

private val bloc = bloc(blocContext())

val state = toLiveData(bloc)
<data>
<variable
name="viewmodel"
type="com.onegravity.bloc.calculator.CalculatorViewModel" />
</data>

<!-- display State -->
<TextView
android:text="@{viewmodel.state.toString()}"/>

<!-- send an Action -->
<Button
android:onClick="@{(view) -> viewmodel.button(view.id)}"/>

iOS

On iOS the standard BlocHolder/ BlocObserver pattern is used:

struct CalculatorView: View {
private let holder = BlocHolder { CalculatorKt.bloc(context: $0) }

@ObservedObject
private var model: BlocObserver<CalculatorState, CalculatorAction, KotlinUnit>

init() {
self.model = BlocObserver(self.holder.value)
}

var body: some View {

// ...

// display State
Text("\(model.value)")

// ...

Button(action: {
// send an Action
holder.value.send(value: CalculatorAction.Clear())
}) {
Text("Clear")
}

Calculator 2

The second calculator uses enum classes as Action:

enum class ActionEnum(val digit: Int? = null) {
Clear,
Add,
Digit0(0),
Digit1(1),
// more actions...
}

which are also processed using single-action reducers but with a slightly different syntax:

fun blocEnum(context: BlocContext) = bloc<CalculatorState, ActionEnum>(context, CalculatorState()) {
reduce(ActionEnum.Clear) { CalculatorState() }
reduce(ActionEnum.Add) { state.resetErrors().apply(Operator.Add) }
reduce(ActionEnum.Subtract) { state.resetErrors().apply(Operator.Subtract) }
// more reducers...

Android

On Android the Bloc is created in the Activity itself, eliminating the ViewModel (at least on the surface):

class CalculatorActivity : AppCompatActivity() {
val bloc by getOrCreate { calculatorBloc(it) }

val state by lazy { toLiveData(bloc) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

bind<ActivityCalculatorNoVmBinding>(R.layout.activity_calculator_no_vm) { it.activity = this }
}

fun button(action: ActionEnum) {
bloc.send(action)
}
}
<data>
<variable
name="activity"
type="com.onegravity.bloc.calculator.CalculatorActivity" />
<import type="com.onegravity.bloc.sample.calculator.ActionEnum" />
</data>

<!-- display State -->
<TextView
android:text="@{activity.state.toString()}"/>

<!-- send an Action -->
<Button
android:onClick="@{() -> activity.button(ActionEnum.Clear)}"/>

iOS

There's no implementation on iOS side but if there were, it would look very similar to the first implementation:

struct CalculatorView: View {
private let holder = BlocHolder { CalculatorEnumsKt.blocEnum(context: $0) }

@ObservedObject
private var model: BlocObserver<CalculatorState, ActionEnum, KotlinUnit>

init() {
self.model = BlocObserver(self.holder.value)
}

var body: some View {
// ...

// display State
Text("\(model.value)")

// ...

Button(action: {
// send an Action
holder.value.send(value: ActionEnum.clear)
}) {
Text("Clear")
}