The frontend implementation of Nimbus shares a lot of code between the platforms. To achieve this we're using Kotlin Multiplatform Mobile (KMM).
This document lists the features that are common to both Nimbus Compose and Nimbus SwiftUI, but are also not part of the specification, i.e. resources that exist in this implementation of Nimbus, but are not a required by the specification.
All examples here will be given in Kotlin because this is the language Nimbus Core has been implemented with.
An observer that is triggered whenever an Action runs. It works similarly to the map of Actions itself, but instead of running for a specific Action, this will run for every action.
An action observer is a function that receives an ActionTriggeredEvent
and returns nothing. An ActionTriggeredEvent
is defined as follows:
class ActionTriggeredEvent(
/**
* The action of this event. Use this object to find the name of the action, its properties and metadata.
*/
val action: ServerDrivenAction,
/**
* The scope of the event that triggered this ActionEvent.
*/
val scope: ServerDrivenEvent,
/**
* Every event can update the current state of the application based on the dependency graph. This set starts empty
* when a ServerDrivenEvent is run. A ServerDrivenEvent is what triggers ActionEvents. Use this to tell the
* ServerDrivenEvent (parent) which dependencies might need to propagate its changes to its dependents after it
* finishes running. "Might" because it will still check if the dependency has really changed since the last time its
* dependents were updated.
*/
val dependencies: MutableSet<CommonDependency>,
): ActionEvent
A simple use for this feature would be to log every action that is executed and has log = true
in its metadata.
A real use case for this feature would be Analytics. We could create Analytics records based in the actions that are triggered. The data for the
analytics event could be passed in the Action's metadata
.
Action observers are a list because more than one observer can be registered.
The logger to call when printing errors, warning and information messages. By default, Nimbus will use its DefaultLogger that just prints the
messages to the console. This logger is also usable by the server driven views through the Action log
.
A Logger must implement the following interface:
interface Logger {
fun enable()
fun disable()
fun log(message: String, level: LogLevel)
fun info(message: String)
fun warn(message: String)
fun error(message: String)
}
Where LogLevel
is:
enum class LogLevel {
Info, Warning, Error,
}
A logic to create full URLs from relative paths. By default, Nimbus checks if the path starts with "/". If it does, the full URL is the baseUrl provided in the configuration concatenated with the path. Otherwise, it's the path itself.
A URL builder must implement the following interface:
interface UrlBuilder {
/**
* @param path the path to generate the URL (should be encoded if it contains special characters)
* @returns the final url
*/
fun build(path: String): String
}
The lowest level API for making requests in Nimbus. By default, Nimbus will use its DefaultHttpClient
, which just calls kotlin's ktor lib with the
provided requests. Replacing the Http client is required for most projects. By implementing your own, you can create authentication logic, additional
headers, retrial polices, etc.
An HTTP client must implement the following interface:
interface HttpClient {
suspend fun sendRequest(request: ServerDrivenRequest): ServerDrivenResponse
}
enum class ServerDrivenHttpMethod {
Post,
Get,
Put,
Patch,
Delete,
}
class ServerDrivenRequest(
val url: String,
val method: ServerDrivenHttpMethod?,
val headers: Map<String, String>?,
val body: String?,
)
class ServerDrivenResponse(
val status: Int,
val body: String,
val headers: Map<String, String>,
val bodyBytes: ByteArray,
)
The View client is one layer above the HttpClient, it is used to fetch JSON screens from the backend. By default, Nimbus use the DefaultViewClient
,
which relies on the Http client to fetch the JSON and on the RenderNode
object factory to deserialize the responses.
A View client is also responsible for implementing the prefetch logic, indicated by the property "prefetch" of a navigation action.
A View client must implement the following interface:
interface ViewClient {
/**
* Fetches a view (UI tree) from the server.
*
* @param request the data for the request to make.
* @return the UI tree fetched.
*/
suspend fun fetch(request: ViewRequest): RenderNode
/**
* Pre-fetches a view (UI tree) from the server and stores it. The next fetch will get the stored value instead of
* performing a network request.
*
* Each implementation of the ViewClient decides how its pre-fetch logic works. See the class `DefaultViewClient` for the
* default logic.
*
* @param request the data for the request to make.
*/
fun preFetch(request: ViewRequest)
}
Where RenderNode
is a node in the UI tree and ViewRequest
is:
data class ViewRequest(
/**
* The URL to send the request to. When it starts with "/", it's relative to the BaseUrl.
*/
val url: String,
/**
* The request method. Default is "Get".
*/
val method: ServerDrivenHttpMethod = ServerDrivenHttpMethod.Get,
/**
* The headers for the request.
*/
val headers: Map<String, String>? = null,
/**
* The request body. Invalid for "Get" requests.
*/
val body: Any? = null,
/**
* UI tree to show if an error occurs and the view can't be fetched.
*/
val fallback: RenderNode? = null,
)
An id generator for creating unique ids for nodes in a UI tree when one is not provided by the JSON. By default, the ids are incremental, starting at 0 and prefixed with "nimbus:".
An ID manager must implement the following interface:
interface IdManager {
fun next(): String
}