mirror of
https://github.com/MinimalBible/MinimalBible.github.io
synced 2024-12-21 06:08:19 -05:00
Post on OGHolder and Retrospective
This commit is contained in:
parent
54cc057b9b
commit
f0b4841efd
210
_posts/2014-07-22-the-ogholder-pattern.md
Normal file
210
_posts/2014-07-22-the-ogholder-pattern.md
Normal file
@ -0,0 +1,210 @@
|
||||
---
|
||||
layout: post
|
||||
title: "The OGHolder Pattern"
|
||||
modified: 2014-08-02 22:09:10 -0400
|
||||
tags: [fragment, dagger, mortar, flow]
|
||||
image:
|
||||
feature:
|
||||
credit:
|
||||
creditlink:
|
||||
comments:
|
||||
share:
|
||||
---
|
||||
Scoped ObjectGraphs to the max
|
||||
--------------------------------
|
||||
|
||||
*Technical Disclaimer: The technique detailed below is inspired by the [Mortar](http://corner.squareup.com/2014/01/mortar-and-flow.html) library developed by Square. One of the things Mortar accomplishes for you is allowing scoped ObjectGraphs to be tied to your activity, meaning that when the Activity dies, the things in the ObjectGraph can be garbage collected. And when the screen is rotated, you don't have to re-create the ObjectGraph. I didn't want to use Mortar for my app, but re-created the technique. Check it out.*
|
||||
|
||||
It's totally possible I'm not the first person to implement everything this way, but I still hope to claim naming rights anyway. I think I came up with a great name. Just so you know, "OGHolder" is the ObjectGraph Holder. But programmers need all the OG status they can get.
|
||||
|
||||
By any means, let me set out the problem:
|
||||
|
||||
Scoped `ObjectGraph`s are a feature of [Dagger](http://square.github.io/dagger/) that allow you to do some nifty things. Essentially, the `ObjectGraph` is built in parts, rather than all at once. This means you have a root graph for your application, and each `Activity` has a graph that builds on top of this. There are a couple benefits:
|
||||
|
||||
1. You don't have to build the entire `ObjectGraph` in one shot. Depending on the modules you inject, this can turn into a significant speed increase.
|
||||
2. When scoped graphs go out of scope, they are garbage collected - meaning you don't have to store all dependencies in memory for the entirety of an `Application`'s lifecycle.
|
||||
|
||||
So, given that scoped graphs are pretty hot stuff, how do you go about doing it?
|
||||
|
||||
**RootModules.java**
|
||||
|
||||
{% highlight java %}
|
||||
@Module(
|
||||
library=true
|
||||
)
|
||||
class RootModules {
|
||||
MyApp application;
|
||||
public RootModules(MyApp application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
MyApp provideApplication() {
|
||||
return application;
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**ActivityModules.java**
|
||||
{% highlight java %}
|
||||
@Module(
|
||||
injects=MyActivity.class,
|
||||
addsTo=RootModules.class // 1
|
||||
)
|
||||
class ActivityModules {
|
||||
|
||||
@Provides @Singleton
|
||||
SomeService provideService(MyApp application) {
|
||||
return new SomeService(application);
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**MyActivity.java**
|
||||
{% highlight java %}
|
||||
public class MyActivity extends Activity {
|
||||
@Inject
|
||||
SomeService someService;
|
||||
|
||||
ObjectGraph mObjectGraph;
|
||||
|
||||
public void inject(Object o) {
|
||||
if (mObjectGraph == null) {
|
||||
ObjectGraph root = ((MyApp)getApplication())
|
||||
.getObjectGraph();
|
||||
mObjectGraph = root.plus(new ActivityModules()); // 2
|
||||
}
|
||||
mObjectGraph.inject(o); // 3
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
inject(this); // 4
|
||||
|
||||
// Do other Activity things here...
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
Alright, time for some explanation.
|
||||
|
||||
1. Our `ActivityModules` class declares itself as adding to the `RootModules` class. **This means Dagger is expecting us to call .plus() to add ActivityModules to RootModules.**
|
||||
|
||||
1a. For the technically inclined, this is semantically different from `@Module(includes=...)`, as the `includes` option tells Dagger to build the included graph at the same time as the root.
|
||||
|
||||
2. Given that we get the root `ObjectGraph` from our `Application`, we now need to add the `ActivityModules` to it. The way this is done is by calling `.plus()` on the original graph, and storing the new graph.
|
||||
|
||||
3. We're now ready to inject anybody that needs it, so inject people using the new scoped graph.
|
||||
|
||||
4. Inject the `SomeService` object into the `Activity` so we can use it.
|
||||
|
||||
Given all this, we've now arrived at a working activity that can make use of SomeService. However, we do have an issue - whenever the screen is rotated, we'll create a new `SomeService`. If the service is marked as a `@Singleton`, why is it re-created when you rotate the screen?
|
||||
|
||||
Enforcing @Singleton
|
||||
--------------------
|
||||
|
||||
To understand why your singletons aren't actually singletons, you need a little picture of the `Activity` lifecycle. Try checking the documentation [here](http://developer.android.com/training/basics/activity-lifecycle/recreating.html). The important part is this:
|
||||
|
||||
> Your activity will be destroyed and recreated each time the user rotates the screen.
|
||||
|
||||
So, let's break down what happens:
|
||||
|
||||
1. Your initial `Activity` grabs the root `ObjectGraph`, **creates a new graph using `plus()`**, sets up `SomeService`, and then injects itself.
|
||||
|
||||
2. You rotate the screen, and your `Activity` is destroyed, then re-created.
|
||||
|
||||
3. The `onCreate()` method is called again now that the `Activity` is created - and when you go to inject again, you find that `mObjectGraph` is null again.
|
||||
|
||||
4. So, we create another graph, with another `SomeService`, to inject with.
|
||||
|
||||
So, each `ObjectGraph` enforces the `@Singleton` annotation. However, if you create a new `ObjectGraph`, all bets are off. How do we combine scoped graphs and singletons then?
|
||||
|
||||
Introducing: The new OG
|
||||
-----------------------
|
||||
|
||||
This section of the post is largely based on another [fantastic tutorial](http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html). The basic premise is this: `Fragment`s can be persisted across configuration changes. They can also hold on to Objects without a need to be serialized. **Thus, store our ObjectGraph in the Fragment when it's created, and check there before we build another.**
|
||||
|
||||
To demonstrate the principle, we need to add only a single class:
|
||||
|
||||
**OGHolder.java**
|
||||
{% highlight java %}
|
||||
public class OGHolder extends Fragment {
|
||||
private final static String TAG = "OGHolder";
|
||||
private ObjectGraph mObjectGraph;
|
||||
|
||||
// Use FragmentActivity for the support library
|
||||
public static OGHolder get(Activity activity) { // 1
|
||||
// Use getSupportFragmentManager for support library
|
||||
FragmentManager manager = activity.getFragmentManager();
|
||||
OGHolder holder = (OGHolder) manager.findFragmentByTag(TAG); // 2
|
||||
if (holder == null) {
|
||||
holder = new OGHolder();
|
||||
manager.beginTransaction().add(holder, TAG).commit();
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true); // 3
|
||||
}
|
||||
|
||||
public void persistGraph(ObjectGraph graph) {
|
||||
mObjectGraph = graph;
|
||||
}
|
||||
|
||||
public ObjectGraph fetchGraph() {
|
||||
return mObjectGraph;
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
And edit the Activity a little bit:
|
||||
|
||||
**MyActivity.java**
|
||||
{% highlight java %}
|
||||
// ...
|
||||
public void inject(Object o) {
|
||||
if (mObjectGraph == null) { // 4
|
||||
// Check the holder to see if this is a restart
|
||||
OGHolder holder = OGHolder.get(this);
|
||||
mObjectGraph = holder.fetchGraph();
|
||||
|
||||
// If the holder doesn't have a graph...
|
||||
if (mObjectGraph == null) {
|
||||
ObjectGraph root = ((MyApp)getApplication())
|
||||
.getObjectGraph();
|
||||
mObjectGraph = root.plus(new ActivityModules());
|
||||
holder.persistGraph(mObjectGraph); // 5
|
||||
}
|
||||
}
|
||||
mObjectGraph.inject(this);
|
||||
}
|
||||
// ...
|
||||
{% endhighlight %}
|
||||
|
||||
So let's get into the Secret Sauce:
|
||||
|
||||
1. We have a static method that is responsible for looking up the `OGHolder` tied to this `Activity`. This method works because the `FragmentManager` is scoped to each `Activity` - we can have multiple `OGHolder` fragments with the same tag without worrying about collision.
|
||||
|
||||
2. Get the `OGHolder` instance. If it doesn't exist, create a new one. When creating a new instance though, make sure to `.commit()`!
|
||||
|
||||
3. This is the super-special magic piece of code. Long story short, this notifies Android to persist the fragment across restart.
|
||||
|
||||
4. Now it's time to get our `ObjectGraph`. Get the OGHolder for this `Activity`, and then see if the holder's graph is null.
|
||||
|
||||
5. If the holder's graph was null, we need to create a new one, and then tie it to the holder to persist it! Finally, inject, and then we're done.
|
||||
|
||||
And if you run the above code, you'll find that `SomeService` is only ever created once, since we don't lose the graph we created.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
First things first, as detailed in the tutorial above, please don't use a different method for persisting objects across configuration change/screen rotate. This is the official way of doing things.
|
||||
|
||||
Second, while you can extend the technique to persist objects other than the `ObjectGraph`, strongly consider whether those objects shouldn't already be in the `ObjectGraph` anyway.
|
||||
|
||||
Finally, use this technique liberally. It allows you to be efficient both in terms of speed and memory usage. You only need to add a small Fragment and a couple lines to the injection. And in my case, I shaved off seconds of time processing because the expensive singletons didn't get re-created on screen rotate.
|
||||
|
||||
Hope this is helpful, feel free to contact me if you have questions!
|
155
_posts/2014-08-01-replatform-retrospective.md
Normal file
155
_posts/2014-08-01-replatform-retrospective.md
Normal file
@ -0,0 +1,155 @@
|
||||
---
|
||||
layout: post
|
||||
title: "Replatform Retrospective"
|
||||
modified: 2014-08-02 22:11:40 -0400
|
||||
tags: [replatform, android studio, eclipse, hindsight, testing]
|
||||
image:
|
||||
feature:
|
||||
credit:
|
||||
creditlink:
|
||||
comments:
|
||||
share:
|
||||
---
|
||||
|
||||
So you don't make the same mistakes.
|
||||
------------------------------------
|
||||
|
||||
Over the past couple of weeks I re-built MinimalBible fully on top of Android Studio. Previously when starting this project, I used Eclipse for a couple reasons. First being Eclipse was what was currently used in production. Second, the foundational library I rely on ([jSword](https://github.com/crosswire/jsword)) was an existing Java library, so I would have to engineer the Gradle build myself to make it compatible with Android Studio.
|
||||
|
||||
Now all this was well and good, but I eventually wanted to migrate to Android Studio. It was new and robust, could do cool things, didn't have to mess with plugins, etc. After having used Android Studio for a while, it seems like most reputable libraries are moving there. Plus, IntelliJ is simply a better IDE than is Eclipse.
|
||||
|
||||
However, the migration process ended up being fairly challenging. I spent days migrating the jSword build to Ant, plus figuring out the random paths in the build.gradle file, plus the [apt](https://bitbucket.org/hvisser/android-apt) library for annotation processing... But the straw that broke the camel's back was testing. I had so many problems figuring out how to make Unit Tests work, I eventually gave up. It was time for a re-design, and I'm glad I did.
|
||||
|
||||
So, MinimalBible was built from the ground-up all over again. It ended up not being nearly as time-consuming or complicated as I expected. But, here's some of the lessons I've learned in the process. The idea is that nobody should have to make these errors again.
|
||||
|
||||
Lesson 1: Keep testing in mind
|
||||
------------------------------
|
||||
|
||||
One of the things I was looking forward to in switching to Android Studio was getting testing working. The way I had originally structured the application, it was borderline impossible to use Mock objects. I have more of the specifics in another post **link here**, but here's the problem in a nutshell:
|
||||
|
||||
**Problem:**
|
||||
|
||||
* Everything being injected needs to get access to the objects it is being injected with
|
||||
* Using the `Application` object seems like a good idea - it's available globally, so we can store all the dependencies there.
|
||||
* There's no way to swap out the `Application` instance during testing to provide mock objects. And any modifications done to the instance (like forcibly over-riding objects) are global - it becomes impossible to guarantee a consistent environment.
|
||||
|
||||
**Solution:**
|
||||
|
||||
**Build in testing from the start.** It's been really helpful for me to be able to change code and still have a sanity check to make sure I didn't break anything. My code coverage right now is still terrible, but I have a platform to make sure I can actually do this going forward.
|
||||
|
||||
Lesson 2: Static objects are awful
|
||||
----------------------------------
|
||||
|
||||
Every enterprise application I've worked on, and most Android applications, all have a dependency injection system of some form. I've previously outlined a number of them **link here**, but I settled on Dagger.
|
||||
|
||||
**Problem:**
|
||||
|
||||
* Many times you need to ensure that only one instance of an object exists during execution
|
||||
* Add a static method to retrieve an instance of the object - everyone goes through the static method
|
||||
* This guarantees testing is impossible - you can't ever change the static method everyone else relies on. Thus, you can test that method, but never change it.
|
||||
|
||||
**Solution:**
|
||||
|
||||
**Use your dependency injector the way it was meant to be used.** I'm talking as few static references to anything as possible. Two examples of how I do this:
|
||||
|
||||
### Configuration as code:
|
||||
Both modules provide a singleton list that is used as configuration. It will not change during the application lifecycle, and I probably won't touch it again during development. But if I want to test how the objects inside the list are used, I can. This isn't possible if the "valid categories" are a static list.
|
||||
|
||||
**MainConfig.java**:
|
||||
{% highlight java %}
|
||||
@Module()
|
||||
class MainConfig {
|
||||
@Provides @Singleton
|
||||
List<BookCategory> provideValidCategories() {
|
||||
return new ArrayList<BookCategory>() {{
|
||||
put(Category1);
|
||||
put(Category2);
|
||||
}};
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**TestConfig.java**:
|
||||
{% highlight java %}
|
||||
@Module()
|
||||
class TestConfig {
|
||||
@Provides @Singleton
|
||||
List<BookCategory> provideValidCategories() {
|
||||
return new ArrayList<BookCategory>() {{
|
||||
put(mockCategory1);
|
||||
}};
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
### Dynamic Injection references:
|
||||
I've been talking a lot about testing, and here's some more. Bear with me.
|
||||
|
||||
One of the problems I ran into during testing was how objects being injected got access to the graph of their dependencies. Previously, I would call something like `MyApp.inject(this)`, relying on a static reference to `MyApp`. Instead, each object should be given a interface that they can inject from, and you can worry about the interface implementation elsewhere. Consider the below:
|
||||
|
||||
**MyFragment.java**
|
||||
{% highlight java %}
|
||||
class MyFragment extends Fragment() {
|
||||
public static MyFragment newInstance(Injector i) {
|
||||
MyFragment f = new MyFragment();
|
||||
i.inject(f);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**MyActivity.java**
|
||||
{% highlight java %}
|
||||
class MyActivity extends Activity implements Injector {
|
||||
// We're going to assume that the ObjectGraph is created
|
||||
// elsewhere
|
||||
private ObjectGraph mObjectGraph;
|
||||
private void inject(Object o) {
|
||||
mObjectGraph.inject(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
MyFragment f = MyFragment.newInstance(this);
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**MyActivityTest.java**
|
||||
{% highlight java %}
|
||||
class MyActivityTest extends TestCase implements Injector {
|
||||
// Again, created elsewhere
|
||||
private ObjectGraph mObjectGraph;
|
||||
private void inject(Object o) {
|
||||
mObjectGraph.inject(o);
|
||||
}
|
||||
|
||||
public void testMyFragment() {
|
||||
MyFragment f = MyFragment.newInstance(this);
|
||||
// Actually do the testing stuff...
|
||||
}
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
**Injector.java**
|
||||
{% highlight java %}
|
||||
interface Injector {
|
||||
public void inject(Object o);
|
||||
}
|
||||
{% endhighlight %}
|
||||
|
||||
So now after all this code we have a `Fragment` that can be injected by both the actual activity, or run in isolation with a TestCase. Nice.
|
||||
|
||||
Lesson 3: Design choices matter
|
||||
-------------------------------
|
||||
|
||||
Probably the most important lesson I can convey: **If you see a bad pattern, now is the time to fix it.** There were a number of different things that this re-platform allowed me to fix, and apply lessons that I've learned. And working in enterprise, I've seen how challenging it is to refactor code that's stuck in a broken design pattern. Every bit of time put in to make sure you start with good design pays incredible dividends.
|
||||
|
||||
So if you think that you have a better design idea, now's the time to make it happen. Don't let technological [cruft](http://en.wikipedia.org/wiki/Cruft) build up, be ruthless about cutting that junk out. If you see a problem early, fix it. There are so many ways to get the point across, but it really is that important.
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
I learned a lot from re-implementing MinimalBible, and it's been great being able to re-think and start fresh. Plus, catching these things early makes life so much easier later on.
|
||||
|
||||
Hope this is helpful for making sure you can avoid issues in the future!
|
Loading…
Reference in New Issue
Block a user