Skip to content

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))
}