Pocket Knife

"Binding" utility for Android


Introduction

Intent and Bundle utility library for Android; which uses annotation processing to generate the boilerplate code for you. This library is based heavily on ButterKnife and Dagger.

The generated code is debuggable and looks similar to this:

// Bundle Keys
private static final String BUNDLE_COUNTER = "BUNDLE_COUNTER";

public void saveInstanceState(Fragment fragment, Bundle bundle) {
    bundle.putInt(BUNDLE_COUNTER, fragment.counter);
}

The main use cases for bundles are saving state in ui pieces and passing arguments to a fragment. Pocket Knife makes both of these easy with the following annotations respectively:

  • @SaveState
  • @BindArgument

Saving and Restoring State

To save a field to the save state bundle in activities and fragments first annotate the field with @SaveState

...
@SaveState
int anInt;
...

Then in onSaveInstanceState call PocketKnife.saveInstanceState(this, outState); to save annotated fields to the bundle.

public void onSaveInstanceState(Bundle outState) {
  ...
  PocketKnife.saveInstanceState(this, outState);
  ...
}

To restore from the bundle call PocketKnife.restoreInstanceState(this, savedInstanceState); in onCreate.

public void onCreate(Bundle savedInstanceState) {
  ...
  PocketKnife.restoreInstanceState(this, savedInstanceState);
  ...
}

Binding Arguments

To bind arguments into a field for a Fragment first annotate the field with @BindArgument. Pocket Knife will find and automatically cast the corresponding value.

...
@BindArgument
int anInt;
@BindArgument(ARG_LONG)
long aLong;
...

Then in onActivityCreated or another appropriate method call in a fragment PocketKnife.bindArguments(this);

public void onActivityCreated(Bundle savedInstanceState) {
  ...
  PocketKnife.bindArguments(this);
  ...
}

Binding Extras

To bind extras into a field for an activity first annotate the field with @BindExtra. Pocket Knife will find and automatically cast the corresponding value.

...
@BindExtra
int anInt;
@BindExtra(EXTRA_LONG)
long aLong;
...

Then in onCreate or another appropriate method call in an activity PocketKnife.bindExtras(this);

public void onActivityCreated(Bundle savedInstanceState) {
  ...
  PocketKnife.bindExtras(this);
        ...
}

To create a bundle, create an interface and annotate the methods with @BundleBuilder.

public interface Bundles {
  ...
  @BundleBuilder
  Bundle getBundle(long individualId);
  ...
}

Pocket Knife will generate the class PocketKnife<interface name> (PocketKnifeBundles for the example above).

To create an intent create an interface and annotate the methods with @IntentBuilder and at least an action or a class. Data values for the intent are passed as a String or Uri argument annotated with @Data.

public interface Intents {
  ...
  @IntentBuilder(cls=SimpleActivity.class)
  Intent getIntent(long individualId);

  @IntentBuilder(action=Intent.ACTION_VIEW)
  Intent getIntent(@Data String data);
  ...
}

Pocket Knife will generate the class PocketKnife<interface name> (PocketKnifeIntents for the example above).

To serialize nonstandard types to bundles and intents annotate variables and parameters with @BundleBuilder and @IntentBuilder respectively. These annotations take a PocketKnifeBundleSerializer and PocketKnifeIntentSerializer as parameters as follows.

Example saving class Foo to bundles and intents

public class Foo {
    private String bar;
    private int baz;
    ...
}

Example serializer

public class FooSerializer implements PocketKnifeBundleSerializer<Foo>,
                                      PocketKnifeIntentSerializer<Foo> {
  private static final String BAR = ".BAR";
  private static final String BAZ = ".BAZ";

  @Override
  public void put(Bundle bundle, Foo foo, String keyPrefix) {
    bundle.putString(keyPrefix + BAR, foo.getBar());
    bundle.putInt(keyPrefix + BAZ, foo.getBaz());
  }

  @Override
  public Foo get(Bundle bundle, Foo foo, String keyPrefix) {
    foo.setBar(bundle.getString(keyPrefix + BAR));
    foo.setBaz(bundle.getInt(keyPrefix + BAZ));
    return foo;
  }

  @Override
  public void put(Intent intent, Foo foo, String keyPrefix) {
    intent.putExtra(keyPrefix + BAZ, foo.getBaz());
    intent.putExtra(keyPrefix + BAR, foo.getBar());
  }

  @Override
  public Foo get(Intent intent, Foo foo, String keyPrefix) {
    foo.setBar(intent.getStringExtra(keyPrefix + BAR));
    foo.setBaz(intent.getIntExtra(keyPrefix + BAZ, 0));
    return foo;
  }
}

Example usage

  ...
  @SaveState
  @BundleSerializer(FooSerializer.class)
  Foo saveFoo;

  @BindArgument
  @BundleSerializer(FooSerializer.class)
  Foo argFoo;

  @BindExtra
  @IntentSerializer(FooSerializer.class)
  Foo extraFoo;
  ...
  public interface Builder {
    @BundleBuilder
    Bundle buildBundle(@BundleSerializer(FooSerializer.class) Foo argFoo);

    @IntentBuilder(action = "test")
    Intent buildIntent(@IntentSerializer(FooSerializer.class)Foo extraFoo);

    @FragmentBuilder
    Fragment buildFragment(@BundleSerializer(FooSerializer.class) Foo argFoo);
  }

Latest Core JAR Latest Compiler JAR

You will need to include the core JAR in your application's runtime. In order to activate code generation you will need to include the compiler JAR in your build at compile time.

The source code to Pocket Knife, its sample, and this website is available on GitHub.

Maven

<dependency>
  <groupId>com.vikingsen</groupId>
  <artifactId>pocketknife-core</artifactId>
  <version>(insert latest version)</version>
</dependency>
<dependency>
  <groupId>com.vikingsen</groupId>
  <artifactId>pocketknife-compiler</artifactId>
  <version>(insert latest version)</version>
  <scope>provided</scope>
</dependency>

Gradle

compile 'com.vikingsen:pocketknife-core:(insert
                                                                                                                                                       latest
                                                                                                                                                       version)'
provided 'com.vikingsen:pocketknife-compiler:(insert latest version)'

ProGuard

Pocket Knife generates and uses classes dynamically which means that static analysis tools like ProGuard may think they are unused. In order to prevent them from being removed, explicitly mark them to be kept. To prevent ProGuard renaming classes that use @Bind* on a member field the keepclasseswithmembernames option is used.

-keep class pocketknife.** { *; }
-dontwarn pocketknife.internal.**
-keep class **$$BundleAdapter { *; }
-keep class **$$IntentAdapter { *; }

-keepclasseswithmembernames class * {
    @pocketknife.* <fields>;
}

-keepclasseswithmembernames class * {
    @pocketknife.* <methods>;
}
Copyright 2014-2015 Jordan Hansen

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.