Appearance
Reactive Plugin - Android SampleApp
The UI module
PlatformHub itself is completely decoupled from any plugin implementation outside the use of the few public interfaces available in the SDK, and can be used with any app and any programming architecture.
However, the Redux implementation used by the sample app and its plugins is the recommended approach for interacting with PlatformHub in your app and plugins as it provides a powerful reactive system to keep your data and UI up-to-date.
The Plugin
A Journey plugin might receive a query
kotlin
override fun onReceive(message: BaseMessage.NavigationQuery) = eventHandler[message.messageName]
.onEvent(
payload = JSONObject("{}"),
context = context,
store = if (store.initialised) store else null
)A Journey plugin might subscribe to events
kotlin
override fun subscribe(subscription: SharedFlow<JSONObject>, event: BaseMessage.Event) {
subscription.onEach { result ->
withContext(Dispatchers.Main) {
val handler = eventHandler[Events.valueOf(event.messageName)]
handler?.onEvent(
payload = result,
context = context,
store = if (store.initialised) store else null
)
}
}.launchIn(coroutineScope)
}The Actions
A Journey module handles logic through Actions
kotlin
internal sealed class AccountDetailsAction : Action {
data class Get(val number: String, val refresh: Boolean = false) : Action
data class Loading(val refresh: Boolean) : Action
data class Loaded(val data: AccountDetails) : Action
data class Error(val data: AccountDetails, val message: String) : Action
}The SideEffects
Actions may have a SideEffect which will return a new Action based on the side effect
kotlin
internal class AccountDetailSideEffect @Inject constructor(
private val plugin: ReactivePlugin
) : SideEffect<AccountsPluginStateGroup> {
override fun invoke(
store: Store<AccountsPluginStateGroup>,
action: Action,
next: Next<AccountsPluginStateGroup>
): Action = when (action) {
is AccountDetailsAction.Get -> {
val payloadJson = "{ \"accountNumber\": ${action.number} }".trimIndent()
val message = ReactivePlugin.Queries.GetAccountDetails
plugin.send(
BaseMessage.Query(
messageName = message.name,
payload = JSONObject(payloadJson),
pluginName = DOMAIN_MODEL_PLUGIN_NAME
),
)
AccountDetailsAction.Loading(action.refresh)
}
is PrimaryAccountAction.Set -> {
val payloadJson = """
{
"accountNumber": ${action.number},
"isPrimary": ${action.isPrimary}
}
""".trimIndent()
val message = ReactivePlugin.Queries.SetPrimaryAccount
plugin.send(
BaseMessage.Query(
messageName = message.name,
payload = JSONObject(payloadJson),
pluginName = DOMAIN_MODEL_PLUGIN_NAME
),
)
PrimaryAccountAction.Loading
}
// [...]
else -> next(store, action)
}
}The Reducer
Actions are reduced and update States
kotlin
fun reduceAccountList(state: AccountsPluginStateGroup, action: Action): AccountsPluginStateGroup =
when (action) {
is AccountListAction.Loading -> state.update(AccountListState(isLoading = true))
is AccountListAction.Loaded -> state.update(AccountListState(data = action.data.sortByPrimary()))
is ConnectivityStatusAction.Changed -> state.update(ConnectivityState(action.data))
else -> state
}The States
States contain the information the UI module needs
kotlin
data class AccountListState(
override val isLoading: Boolean = false,
override val isError: Boolean = false,
override val errorMessage: String = "",
val data: List<Account> = emptyList(),
) : State()The UI Binding
The UI module comprises a Delegate class that hosts the Store
kotlin
@HiltViewModel
class AccountListDelegate @Inject constructor(
val plugin: ReactivePlugin
) : ViewModel() {
val store = plugin.store
val state = mutableStateOf(AccountListState())
init {
store.subscribe(state)
store.dispatch(AccountListAction.Fetch)
}
override fun onCleared() {
super.onCleared()
store.unsubscribe(state)
}
}This Delegate class is bound to a Composable View.
When a state in the Store changes, it is propagated to the Delegate class which performs a re-composition on the View.
kotlin
@Composable
fun AccountListScreen(
navController: NavHostController,
delegate: AccountListDelegate = hiltViewModel()
) {
val state = delegate.state.value
when {
state.isLoading -> ShimmerListView()
state.isError -> Text(text = state.errorMessage)
else -> ListView(navController, state.accounts)
}
}Generally, a User interaction will perform operations against the Store
kotlin
PrimaryAccountCard(state) { account, checked ->
store.dispatch(PrimaryAccountAction.Set(account, checked))
}