0

Here is my scenario.

I have an android activity in which I want to abstract my I/O dependencies. The dependencies are represented by this interface (edited for brevity and simplicity):

public interface ITimeDataServer {
    TimeRecord[] get(int userID);
    void save(TimeRecord record);
}

What I want is for my activity to be able to call these interface methods, and leave the implementation to be supplied by the calling code. (Pretty standard, I think).

ITimeDataServer myServer;
int myUserID;

void loadRecords() {
    TimeRecord[] records = myServer.get(myUserID);
    // etc...
}

My difficulty is, how can I ensure that myServer gets set?

This seems like a common problem, but I can't find a clean solution.

My first thought would be that myServer would be passed in through the constructor, but Android activities aren't really instantiated with constructors.

I've come up with several solutions, but they're all icky in some way:

Icky Solution 1

Create a static method to launch the activity class which takes an ITimeDataServer parameter and stores it in a static variable from which the activity can access it:

private static ITimeDataSource theDataSource;
public static void launch(Activity currentActivity, ITimeDataSource dataSource) {
    theDataSource = dataSource;
    Intent intent = new Intent(currentActivity, MainActivity.class);
    currentActivity.startActivity(intent);
}

This is icky because (a) the data source is static and not actually associated with the instance, and (b) a consumer could initiate the activity by the standard activity API rather than this static method, which will cause NullPointerException.

Icky Solution 2

I can create a Provider class which provides a singleton instance of ITimeDataSource, which needs to be initialized by the calling library before use:

public class TimeDataSourceProvider {

    private static ITimeDataSource myDataSource = null;

    public void initialize(ITimeDataSource dataSource) {
        myDataSource = dataSource;
    }

    public ITimeDataSource get() {
        if (myDataSource == null)
            throw new NullPointerException("TimeDataSourceProvider.initialize() must be called before .get() can be used.");
        else
            return myDataSource;
    }
}

This seems a little less icky, but it's still a little icky because the activity's dependency is not obvious, and since there may be many paths to launch it, it's highly possible that some of them would forget to call TimeDataSourceProvider.initialize().

Icky solution 3

As a variation on #2, create a static IODependencyProvider class which must be initialized with ALL dependencies on app startup.

public class IODependencyProvider {

    static ITimeDataSource myTimeData;
    static IScheduleDataSource myScheduleData; // etc

    public static void initialize(ITimeDataSource timeData, IScheduleDataSource scheduleData /* etc */) {
        myTimeData = timeData;
        myScheduleData = scheduleData;
        //etc
    }

    public static ITimeDataSource getTimeData() {
        if (myTimeData == null)
            throw new NullPointerException("IODependencyProvider.initialize() must be called before the getX() methods can be used.");
        else
            return myTimeData;
    }

    // getScheduleData(), etc
}

This seems superior to #1 and #2 since a failure to initialize would be much harder to sneak by, but it also creates interdependencies among the data types that otherwise need not exist.

...and other icky variations on that theme.

The common themes that make these solutions crappy:

  • the need to use static fields to pass non-serializable information to an activity
  • the lack of ability to enforce initialization of those static fields (and subsequent haphazardness)
  • inability to clearly identify an activity's dependencies (due to reliance on statics)

What's a nooby Android developer to do?

Overlord Zurg
  • 3,430
  • 2
  • 22
  • 27
  • If there were an annotation for the static initialize() method to say "This method MUST be called during app startup", that would alleviate at least the second point. – Overlord Zurg Mar 30 '16 at 15:15

2 Answers2

1

As long as these dependencies implement Parcelable correctly, you should be able to add them to your intent, then unparcel them as ITimeDataServer and get the correct class.

Steve Sanbeg
  • 887
  • 4
  • 7
  • Even if the interface did implement Parcelable (not trivial), there's no way to REQUIRE that it be included them when the activity is launched, right? (This is one of the things I dislike about Android activities.) – Overlord Zurg Mar 30 '16 at 15:38
  • Right, a lot of the patterns that we'd like to use if we weren't constrained by the framework don't work on Android, where we don't have control over the framework. By using parcelable you have access to the normal Android methods for passing objects between activities, which sounds like what you're trying to do, and may end up being better & easier than rolling your own solution. – Steve Sanbeg Mar 31 '16 at 02:20
  • You can't require anything, but you should be able to encourage it by implementing a common base class for your activities, storing your I/O objects there, and implementing the code to send & receive it when starting activities so it will be mostly transparent to your concrete activity classes. – Steve Sanbeg Mar 31 '16 at 02:23
0

I found a nice solution here, in the least-loved answer.

I define the library activity as abstract and with no default constructor, but a constructor that takes an interface, like so:

public abstract class TimeActivity extends AppCompatActivity {

    private ITimeDataSource myTimeDataSource;
    public TimeActivity(@NonNull ITimeDataSource dataSource) {
        myTimeDataSource = dataSource;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_time);

        // do stuff with myTimeDataSource!
    }
}

Then, the calling code can create a concrete subclass with its chosen implementation that does have a parameterless constructor. No static members, easy-peasy!

This allows you to abstract and inject all sorts of crazy behaviours! Woooo!

(Note that the concrete subclass activity needs to be manually added to AndroidManifest.xml, like all activities, or the app will crash when it tries to launch.)

Community
  • 1
  • 1
Overlord Zurg
  • 3,430
  • 2
  • 22
  • 27