etiennelenhart / Eiffel

Redux-inspired Android architecture library leveraging Architecture Components and Kotlin Coroutines
MIT License
211 stars 14 forks source link

Feature Proposal - Plugin Support #68

Closed jordond closed 5 years ago

jordond commented 5 years ago

Proposal

Settle in, this is gonna be a long one.

Note: I will be providing samples in this PR, but there are more detailed comments in the code.

References #60 and replaces #68.

I have added the ground work for a basic "Plugin" functionality for Eiffel. Right now the only plugin I have created is a LoggingPlugin which will log Event's to the logcat (more on that below).

EiffelPlugin

Right now the EiffelPlugin interface is very basic.

interface EiffelPlugin {

    val name: String

    // dispatcher is just a tag for logging purposes
    fun <E : Event> onEvent(dispatcher: String, event: E)
}

The name property is currently unused, but my theory behind it was some way to identify the plugin, maybe for logging purposes.

The onEvent function is where the magic happens. Each plugin will be able to react to every Event that is fired off by the EiffelViewModel. This approach is very basic, and perhaps in the future we could add the ability to block or have some kind of "continuation" so the EiffelPlugin could observe the before/after of an event.

We may even want to pass along the scope, dispatch from the EiffelViewModel so that we can hijack the state/actions. In my head I was thinking like a time-travel debugger of sorts.

The dispatcher: String is just a tag of the class that dispatched the action. Perhaps a better approach would be to have Event contain the tag instead.

Event

Right now there are a few supported events that can be fed to the plugins:

sealed class Event(val tag: String = "") {
  data class ViewModelCreated<S : State>(val name: String, val initialState: S) : Event()
  data class ViewModelCleared(val name: String) : Event()
  data class Message(val message: String) : Event("Message")
  data class Custom<T : Any>(val data: T) : Event()
  data class Action<A : StateAction>(val action: A) : Event("Action")
  data class Update<S : State>(val previous: S, val updated: S) : Event("Update")
  data class Interception<S : State, A : StateAction, I : StateInterception<S, A>>(
      val currentState: S,
      val action: A,
      val interception: I
  ) : Event("Interception")
}

Most of these are pretty self-explanatory, but if you need more info, again there are detailed comments in the code.

I am open to adding/removing/modifying these Event's, this is just what I came up with for this PR.

Dispatcher

The dispatcher controls the way the Event's are dispatched to the plugins. I have a basic implementation, but I'm not exactly in-love with it at its current state. It feels dirty accessing the plugins via the global Eiffel but maybe its fine. Again open to feedback/suggestion for it.

interface Dispatcher {
    fun <E : Event> dispatchEvent(dispatcher: String, event: E)

    object Default : Dispatcher {

        override fun <E : Event> dispatchEvent(dispatcher: String, event: E) {
            Eiffel.debugConfig.plugins.forEach { it.onEvent(dispatcher, event) }
        }
    }
}

Packaged plugins

I have included a LoggerPlugin as a packaged plugin. Right now it is not opt-in, and will always be added to the list of plugins. It's also in the same package as eiffel, so we could even move it to it's own module.

I have created a DefaultLoggerPlugin and a ReleaseLoggerPlugin. The latter is useful for production mode. Say you override EiffelViewModel.debug to be true, and you forget to remove it before pushing the code out to production. The release logger will take care of that by doing nothing when it's invoked.

The ReleaseLoggerPlugin is opt-in and you can see an example of that in the sample app below.

interface LoggerPlugin<T> : EiffelPlugin {

    override val name: String
        get() = "Logger"

    val logger: Logger
    val transform: TransformEventMessage<T>
}

The Logger is the interface from the previous PR, just a way to customize how the event is logged. In the sample app, I have it setup to use Timber.

The transform is where the magic happens. Say you want to create a logger that can log to crashlytics, or some other custom solution. Well with transform you can take the Event and transform it to any object you need. There are of course some sensible defaults provided.

Future plans

The whole reason why I decided to tackle a plugin approach was because of another library I saw called Suas. I noticed they had a companion app for the desktop that would display all the store events. So I looked into their source for both the android side, and the desktop side (they use TypeScript and Electron, both I've used extensively). And it looks doable. At least from the current standpoint of viewing the State, and Action's.

You can see their monitor here

So plugins I'm currently envisioning are:

Sample app

It's not complete, but I have created a very basic sample app that showcases the new EiffelPlugin and LoggerPlugin features. It's on a different branch and you can see it here

Notable points of interest are the release/ReleaseApp.kt, and debug/DebugApp.kt. Then there is a basic Fragment and EiffelViewModel.

I will probably expand on it more later.

Here is an example of the log output:

image

It's marked as an error because in DebugApp I have overridden it to always log to Timber.e (for better visibility in the logcat)`.

Closing

If you've made it this far congrats! I know your busy, but I'd appreciate your feedback/criticism/ideas. If we want to get logging in before we get the plugin architecture in, then I can extract it and create a new PR.

etiennelenhart commented 5 years ago

Wow, that looks quite promising. I guess it's just a bit out of the current scope.

I created some issues (#69, #70 and #71) I consider to be of higher importance considering the 5.0.0 release. It already basically deprecates the whole library and I don't want to overwhelm possible migrating users with too many features.

I still think plugin support would be a great addition, I'd just schedule it for the next major release. And since as much as possible should be moved to a separate module, I'd still like to have a simple debug mode for Eiffel's core that we discussed in #67.

jordond commented 5 years ago

Luckily I came prepared ;) #72. In my spare time, I may continue to explore the idea of creating a desktop client to monitor the state.

71 is also very appealing to me, as currently I'm using LiveTemplates, but they are not a perfect solution.

etiennelenhart commented 5 years ago

Sounds good. 🙂

I was also using live templates but found it hugely annoying that they are removed on every Android Studio update.