Sunday, October 4, 2009

Developing Android With Multiple Eclipse Projects

The Android application I'm developing at home has non-Android-specific schema, database access, and business logic, so it makes sense to partition the application into multiple Eclipse projects, some Android-specific and others not.

One benefit of using separate projects is that I can write a second version of the application that runs on a website instead of on a handheld without having to rewrite the bulk of the code.

But there's a catch: when the Android-specific unit tests are moved into a separate Eclipse project, the loader reports "Class resolved by unexpected DEX error" and the unit tests fail with "java.lang.IllegalAccessError: cross-loader access from pre-verified class". (In 2.1 the error remains, but the message has changed to "Class ref in pre-verified class resolved to unexpected implementation".)

I first ran into this problem in 1.5 Android, and upgraded to 1.6 hoping it would go away, because in 1.6 using separate Eclipse projects for unit tests is recommended:

http://mobilebytes.wordpress.com/2009/09/19/new-eclipse-android-test-features


http://www.danielswisher.com/2009/06/as-new-android-developer-i-have-been.html

There is even a toolbar icon and wizard to create an Android test project that points to a separate Android project to be tested.

Googling around, I found other programmers having the same or similar problems:

http://groups.google.com/group/android-platform/browse_thread/thread/20ff41b925e04dd4

But googling also found programmers with multiple projects but not the problem:

http://www.anddev.org/unit_testing_private_methods-t7847.html

What makes some programmers have no problem using multiple Eclipse projects to separate Android application code from Android unit-test code, while other programmers get the loader errors?

The problem shows up when the loader sees the same class loaded more than once:

http://groups.google.com/group/android-developers/browse_thread/thread/3440dd8e11a1b481

which can happen if the main Android code and the Android unit tests share code from another Eclipse project:

http://groups.google.com/group/android-developers/browse_thread/thread/5537ae10e4143240

which is the setup I have.

To reproduce the problem:
  1. Create an ordinary Java project called LoaderProblemCommon, and create this class in it:
  2. package com.loaderproblem;

    public class LoaderLogger
    {
    public static void log(String message)
    {
    System.out.println(message);
    }
    }
  3. Create an Android project called LoaderProblem, add the LoaderProblemCommon project to its build path, and create this class in it:
  4. package com.loaderproblem;

    import android.app.Activity;
    import android.os.Bundle;

    public class Hello extends Activity
    {
    public void onCreate(Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    logHello();
    }

    public static void logHello()
    {
    LoaderLogger.log("Hello");
    }
    }
  5. Add LoaderProblem to the Android manifest.
  6. Create an ordinary Java project called LoaderProblemTestUtils, and create this class in it:
  7. package com.loaderproblem.testutils;

    public class TestUtil
    {
    public static final void doNothing()
    {
    }
    }
  8. Create an Android test project called LoaderProblemTests, add the other three projects to its build path, and create this class in it:
  9. package com.loaderproblem.tests;

    import android.test.AndroidTestCase;

    import com.loaderproblem.Hello;
    import com.loaderproblem.LoaderLogger;
    import com.loaderproblem.testutils.TestUtil;

    public class LogHelloTest extends AndroidTestCase
    {
    public void testLogHello()
    {
    LoaderLogger.log("Hello");
    Hello.logHello();
    TestUtil.doNothing();
    }
    }
The result should look like this:

Compile all four projects, create a brand-new AVD, and run LoaderProblem as an Android application. It should deploy and display:

Now try to run LoaderProblemTests as an Android JUnit test. It will fail with:


Or, for 2.1, it will fail with:

I first tried to fix this problem by editing the build path for LoaderProblemTests so it only depends on the LoaderProblemTestUtils Eclipse project, and then adding back the necessary classes by adding dependencies on the other projects' class folders:



This "worked", but it introduced several unwanted side effects:
  • Now that the dependent project's source is seen as a class folder, one can no longer smoothly refactor across projects or navigate into classes (via F3, for instance), because these files are now treated as a compiled objects. Attaching the source to the classfolder library sort of works, but then it brings up an uneditable version of the source. [reported by a reader]
  • It screws up EclEmma. For details see https://sourceforge.net/tracker/?func=detail&atid=883351&aid=2934081&group_id=177969.
The proper way to fix this problem was reported by another reader:
  1. In the build path for LoaderProblem, click on "Order and Export".
  2. Check the box for LoaderCommon, to let LoaderProblem export the LoaderCommon classes as well as its own.
  3. In the build path for LoaderProblemTests, remove the dependency on the LoaderCommon project. The dependency is already now covered by the LoaderProblem dependency.
  4. Clean, build, rerun LoaderProblem, then rerun LoaderProblemTests.
This solution also fixes the EclEmma problem.