By tagging (annotating) functions of interest, DevFun provides direct access to these functions at run-time through a number of means - such as an automatically generated "Developer Menu".
- Quick Start
- Key Features
- Showcase
- Annotations Overview and Custom Annotations/Properties
- Dependency Injection
- Troubleshooting
- Build
- Contributing
- License
Reasoning
While developing some feature Z
, there's nothing more annoying than having to go through X
, to get to Y
, to test
your changes on Z
. So it's not uncommon for developers to sometimes try and shortcut that process... Which inevitably
leads to your humiliation when your colleagues notice you committed said shortcut.
Example
Simply adding the @DeveloperFunction
annotation to a function/method is all that is needed.
class MyClass {
@DeveloperFunction
fun someFunction() {
// ...
}
}
See the documentation for advanced usage, including custom names, custom arguments, groups, function processing, etc.
- REQUIRED Android Gradle 3.0.0+ (due to #37140464 and KT-16589)
- Recommended to use Kotlin 1.3.31, though should work on previous versions (somewhat untested)
- Recommended to use KAPT3 (
plugins { id("kotlin-kapt") }
), though KAPT1 also works - Compiled with
minSdkVersion
>= 15 - Built against AndroidX 1.0.0 (this is equivalent to Android Support libraries 28.0.0)
See Migrating to AndroidX for more information (it's actually quite easy).
Also, despite the migration docs above mentioning-rc01
for some things - most things have final releases Google's Maven Repo
Add the DevFun Gradle plugin to your build script.
If you can use the Gradle plugins
block (which locates and downloads it for you):
plugins {
id("com.nextfaze.devfun") version "2.1.0"
}
Or the legacy method using apply
;
Add the plugin to your classpath (found in the jcenter()
repository):
buildscript {
dependencies {
classpath("com.nextfaze.devfun:devfun-gradle-plugin:2.1.0")
}
}
And in your build.gradle
:
apply {
plugin("com.nextfaze.devfun")
}
Can be categorized into 5 types:
- Main: minimum required libraries (annotations and compiler).
- Core: extend the functionality or accessibility of DevFun (e.g. add menu/http server).
- Inject: facilitate dependency injection for function invocation.
- Util: frequently used or just handy functions (e.g. show Glide memory use).
- Invoke UI: provide additional views for the invocation UI (e.g. a color picker for
int
args)
Add jcenter()
repository or via bintray directly: (will be put on MavenCentral eventually)
repositories {
maven { setUrl("https://dl.bintray.com/nextfaze/dev-fun") }
}
Add dependencies to build.gradle:
dependencies {
// Annotations, Compiler, and Developer Menu
kaptDebug("com.nextfaze.devfun:devfun-compiler:2.1.0")
implementation("com.nextfaze.devfun:devfun-annotations:2.1.0")
debugImplementation("com.nextfaze.devfun:devfun-menu:2.1.0")
// Dagger 2.x component inspector - only if using Dagger 2.x!
debugImplementation("com.nextfaze.devfun:devfun-inject-dagger2:2.1.0")
// Chrome Dev Tools JavaScript console integration
// NOTE: Stetho 1.4.x has some issues with latest (alpha) AGP & SDK 28
debugImplementation("com.nextfaze.devfun:devfun-stetho:2.1.0")
// HTTP server and simple index page
debugImplementation("com.nextfaze.devfun:devfun-httpd:2.1.0")
debugImplementation("com.nextfaze.devfun:devfun-httpd-frontend:2.1.0")
// Glide util functions
debugImplementation("com.nextfaze.devfun:devfun-util-glide:2.1.0")
// Leak Canary util functions
debugImplementation("com.nextfaze.devfun:devfun-util-leakcanary:2.1.0")
/*
* Transitively included libs - in general you don"t need to add these explicitly (except maybe for custom module libs).
*/
// Adds view factory handler for @ColorPicker for invoke UI - transitively included via devfun-menu
// debugImplementation("com.nextfaze.devfun:devfun-invoke-view-colorpicker:2.1.0")
// DevFun core - transitive included from menu et al.
// debugImplementation("com.nextfaze.devfun:devfun:2.1.0")
}
That's it!
Start adding @DeveloperFunction
annotations to methods (can be private, and arguments will be injected or rendered as needed), and these will be added to the menu.
For advanced uses and configuration such as custom categories, item groups, argument providers, etc.
- See the wiki, or
- Extensive (Dokka generated) documentation can be accessed at GitHub Pages.
A Content Provider
is used to automatically initialize DevFun, and most simple Dagger 2.x object graphs should work.
See Initialization for more information or for manual initialization details.
Attempts are made to be aware of the current app state, in that the "Context" category should contain only the items that are relevant to the current screen (i.e. methods tagged in the current Activity and any attached Fragments).
If an annotated function has argument types that cannot be injected then DevFun will attempt to render a UI for you to manually set them (right). The arguments can be injected or otherwise. Simple types and a color picker UI is provided by default.
Optional annotations can be added to configure/tweak their state and custom types can be added:
@From
for an initial value.@Ranged
on anumber
type for a constrained value.@ColorPicker
on anint
types renders a color picker.
DevFun is designed to be modular, in terms of both its dependencies (limiting impact to main source tree) and its plugin-like architecture. See Components for more information.
- Keep your methods private, but be able to easily invoke them when needed.
- Avoid those one-time development conditions.
- Provide future developers (undoubtedly yourself) access to these quick dev switches "oh, there's already a way to toggle xyz".
An easy to use developer menu accessible at any time via a floating cog button added by the devfun-menu
module (right):
Can be accessed via a floating cog, the tilde/grave key `
, or the volume button sequence down, down, up, down
.
When DevFun can't inject a type it will attempt to render a UI for manual entry. By default all simple type are supported. View factories can be provided to allow DevFun to render a custom view for other types.
An example of this is the @ColorPicker
annotation used
by the menu cog overlay (second right) the renders a color picker view for an int parameter.
Adding @DeveloperLogger
to a property, type, or
function will generate a floating text overlay that (by default) updates once a second with the .toString()
of that reference.
The loggers are context-aware with additional configuration options available with a long-press.
This feature is somewhat experimental in that it's quite new and has not been used extensively beyond the demo app and a few in-house apps so please report any issues you have with it.
Adding @DeveloperProperty
as well will allow you
to tap on the overlay to edit its value using the invocation UI system.
Using the HTTPD modules devfun-httpd
and devfun-httpd-frontend
, a local server can be exported using ADB adb forward tcp:23075 tcp:23075
and accessed via http://localhost:23075 where you can invoke functions from your browser:
With the devfun-stetho
module functions are exported and available to be invoked directly from Chrome's Developer Tools console:
DevFun has three types of annotations. "Functions", "References", and "Categories". These are defined using the meta
annotation @DeveloperAnnotation
.
- Function: intended to be invoked
- References: allows "tagging" elements
- Categories: purely for naming/grouping/ordering
The majority of DevFun's annotations are SOURCE
retained. If for some reason you want a RUNTIME
retained or whatever then copy-paste
an existing annotation and change the retention.
@DeveloperFunction
Annotating a function allows you to invoke it at run-time using best-effort DI and/or a UI for value entry.@DeveloperArguments
The same as@DeveloperFunction
but allows you to supply a list of default argument values. Each entry is like adding another@DeveloperFunction
with default arguments.@DeveloperProperty
Allows you to set a property value (only simple types supported at the moment)
@DeveloperReference
Tags the element - retrieve later at run-time viadevFun.developerReferences<DeveloperReference>()
- in general you'll want to create your own annotation "tag".@Dagger2Component
Used in conjunction with dagger2 inject lib. Use it to tag functions/properties/extensions to tell DevFun how/where to get your components.@DeveloperLogger
Results in a floating overlay they calls the function every 1 second (by default - long-press to change) showing the.toString()
of the return value. When used in conjunction with@DeveloperProperty
tapping the overlay shows the edit dialog.
As mentioned above, they are for naming/grouping/ordering. There is just the standard one by default, however you could create one that has different default values or whatever.
@DeveloperCategory
Used to override default category names/groups/ordering@ContextCategory
Used to manually flag an item into the "Context" category (usually reserved for Android "scoped" items such as Activity/Fragment/etc)
As mentioned, DevFun annotations are defined using @DeveloperAnnotation
. Properties named the same as the DevFun supplied defaults have
the same meaning/behaviour.
Other properties will be "serialized" and available during the transformation phase (or from the definition object) - the definition will
implement WithProperties
DevFun uses a simple InstanceProvider
interface to
source object instances.
Providers are checked most recently added first (i.e. user-supplied providers will be checked first). A number of default providers are added, including;
- Android
Application
,Activity
,Context
(currentActivity
orApplication
),Fragment
, andView
types. - All DevFun related objects
- Support for Kotlin
object
instances - Object instantiation (new instance + DI). (opt-in only)
For more details see wiki entry Dependency Injection.
Easiest method is to use devfun-inject-dagger2
module - by default it should work just by adding the dependency depending on your setup
(if the components are located in the Application and/or Activity classes). However if you use extension functions to retrieve your
components (or you put them in weird places for whatever reason), then you can annotate the functions/properties/getters with
@Dagger2Component
.
If all else fails you can define your own instance provider with utility functions from devfun-inject-dagger2
(see the demo for an example).
Dagger has been tested on the demo app from versions 2.4 to 2.17, and various in-house apps on more recent versions, and should function correctly for most simple scopes/graphs.
For reference the demo app uses three scopes; Singleton, Retained (fragments), and an Activity scope. It uses both type-annotated scoping and provides scoping. It keeps component instances in the activity and obtains the singleton scope via an extension function. In general this should cover most use cases - if you encounter any problems please create an issue.
DevFun uses a number of methods iteratively to introspect the generated components/modules, however depending on scoping, visibility, and instantiation of a type it can be difficult to determine the source/scope in initial (but faster) introspection methods.
When all else fails DevFun will use a form of heavy reflection to introspect the generated code. For older versions of dagger, types with a
custom scope and no constructor arguments are not necessarily obtainable from Dagger by any other means. To help with this ensure your
scope is @Retention(RUNTIME)
so that DevFun wont unintentionally create a new instance when it can't find it right away.
Due to the way Dagger generates/injects it is not possible to obtain the instance of non-scoped types from the generated component/module as its instance is created/injected once (effectively inlined) at the inject site. It is intended to allow finding instances based on the context of the dev. function in the future (i.e. if the dev. function is in a fragment then check for the injected instance in the fragment etc.) - if this is desirable sooner make a comment in the issue #26.
Though the inject module uses Dagger, it does not export it as a dependency (declared compileOnly
). This is to stop DevFun from
affecting your project's version. The inject module only uses references to Dagger's annotations @Component
, @Module
, and @Provides
.
It also references annotations from javax.inject
(which is pulled in from Dagger). It does not use/generate any other aspect of the
Dagger APIs explicitly (only via reflection during introspection) and thus is compatible with all Dagger versions from 2.4.
DevFun was design with Kotlin in mind. Having said that, Kotlin is implicitly compatible with Java and thus DevFun should work as expected when used in/with Java code (you will still need to use KAPT however as the generated code is in Kotlin).
Submit an issue if you encounter any problems as it is desirable that there be 100% compatibility.
See the wiki for advanced configuration details.
Documentation (Dokka) can be accessed at GitHub Pages.
SLF4J is used for all logging.
By default trace
is disable unless library is a snapshot build (i.e. BuildConfig.VERSION.endsWith("-SNAPSHOT")
).
This can also be toggled at any time via devFunVerbose
DevFun was compiled against Kotlin 1.3.31.
Earlier versions of Kotlin are generally untested and unsupported (if you need support for an older version make an issue).
Thus if you receive a dependency conflict error such as:
Error: Conflict with dependency 'org.jetbrains.kotlin:kotlin-stdlib' in project ':app'. Resolved versions for app (1.1.2-3) and test app (1.1.1) differ. See http://g.co/androidstudio/app-test-app-conflict for details.
The simplest resolution is updating your Kotlin version to match.
If this is not possible, you can force Gradle to use a specific version of Kotlin:
// Force specific Kotlin version
configurations.all {
resolutionStrategy.force(
"org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion",
"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion",
"org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
)
}
The gradle plugin com.nextfaze.devfun
should automatically handle/provide the annotation processor with your project build configuration,
but can also be configured within your build file (DevFunExtension):
devFun {
packageSuffix = "..."
packageRoot = "..."
packageOverride = "..."
}
However due to limitations of the build system this can/is somewhat derived with string splitting and/or by using known relative paths to
processor outputs as needed.
e.g. To determine the package/buildType/flavor, BuildConfig.java
is located as:
(?<buildDir>.*)(/intermediates/classes/|/tmp/kotlin-classes/|/tmp/kapt3/classes/)(?<variant>.*)/META-INF/services/.*
If your build system has been customised or for whatever reason the processor cannot identify your build information then
you can manually specify the required information using annotation processor options (values via APT args will override devFun {}
gradle
plugin values).
Apply using Android DSL:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
argument("devfun.argument", "value")
}
}
}
}
See com.nextfaze.devfun.compiler for list of options.
The DevFun Gradle plugin is used to tell DevFun your build variant information. It does this by setting APT values via
DevFunKotlinGradlePlugin
which implements KotlinGradleSubplugin
- this is from Kotlin's kotlin-gradle-plugin-api
package.
Unfortunately Kotlin have been changing their interface definition for recent minor versions (<1.2.5x, 1.2.6x, and 1.2.7x), so if another
plugin is using a different version of KotlinGradleSubplugin
one of them will fail.
However as of 2.0.0-RC3 the appropriate Subplugin version is dynamically injected at run-time, and DevFun's Gradle Plugin does not declare any dependencies so as not to pollute/alter the plugin classpath (instead relying on the Kotlin plugin to pull them in) - so DevFun itself should not be causing this error. If removing the DevFun Gradle Plugin fixes this error please report this!
Having said that, most of the time you don't not need the Gradle plugin - without it DevFun will attempt to "guess" your variant
information using various file naming/path regex magics during the APT phase directly (and generally it's pretty good at it).
TLDR; If you can remove the plugin and it still works than that's fine.
Worst case scenario you can explicitly tell DevFun where (the package) you want the generated files via APT args:
android.defaultConfig.javaCompileOptions.annotationProcessorOptions {
argument("devfun.package.override", "my.app.devfun.generated")
}
Very basic support for proguard exists (libraries keep their generated sources), though it has not been tested extensively - DevFun is intended for Debug builds and thus support for Proguard has never been much of a concern.
- Rules that are supplied by DevFun libraries can be found in proguard-rules-common.pro (the Glide util module also adds a couple).
- See the demo proguard-rules.pro for the minimum you'll need in your own app.
As one of the goals of DevFun is to keep your production code clean, most of DevFun's annotations are SOURCE
retained and cannot be
referenced in your proguard rules. However if an annotated element is public
or internal
DevFun will reference the element directly in
its generated code (rather than via reflection). Exceptions to this are (at the moment); @Constructable
, @ColorPicker
, @Ranged
,
and @From
, which are RUNTIME
retained (it is desirable in the future that these too become SOURCE
retained).
When DevFun is unable to find the instance of a type it throws ClassInstanceNotFoundException
- this happens when
there is no instance provider capable of providing the type to DevFun. If there is a view factory defined for the type the
invocation UI will be shown for manual entry, otherwise and error dialog will be shown with the exception details.
See the wiki entry on Dependency Injection for details on how to set up custom instance providers.
For a quick and dirty fix utility functions captureInstance
and singletonInstance
can create an instance
provider that you can give to DevFun devFun.instanceProviders += singletonInstance { ... }
.
e.g.
class SomeType : BaseType
val provider = captureInstance { someObject.someType } // triggers for SomeType or BaseType
val someOtherThing = singletonInstance { MyClass() } // lazy initialization
val anotherThing = singletonInstance { devFun.get<MyType>() } // lazy initialization using DevFun to inject/create type
If you want to reduce the type range then specify its base type manually:
val provider = captureInstance<BaseType> { someObject.someType } // triggers only for BaseType
Be aware of leaks! The lambda could implicitly hold a local this
reference.
If you are using Android Gradle versions prior to 3.0.0, then this is likely due to tooling issues where APT generated
resources of Application projects were not packaged into the APK (see #37140464
and KT-16589).
The extreme hacks/support for this were removed in DevFun 0.1.2 and are unlikely to be re-implemented.
Android Gradle 3.0.0 and 3.1.0 are quite stable and its unlikely newer versions of Android Studio will support earlier than that anyway.
Open project using Android Studio (usually latest preview). Opening in IntelliJ is untested, though it should work.
git clone [email protected]:NextFaze/dev-fun.git
cd dev-fun
Artifacts
Build using standard gradle.
./gradlew build
Demo
See the included demo project for a simple app.
./gradlew :demo:installDebug
adb shell monkey -p com.nextfaze.devfun.demo.debug -c android.intent.category.LAUNCHER 1
- See RELEASING.md for building artifacts and documentation.
DevFun is intended for developers (though it can be handy for testers). Because of this the API is intended to be quite open and flexible. So if there is anything you want or think should be added then create an issue or PR and more than likely it will be accepted.
- Any bugs please report (desirably with reproduction steps) and/or PR/fix them.
- Any crashes please report with the stack trace (doesn't need to be reproducible) and it'll be fixed (if DevFun's fault) and/or wrapped with proper error handling functionality.
Feel free also to submit your own handy util functions or whatever for addition.
Copyright 2019 NextFaze
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.