Reputation: 450
I have a very small Android app that I have ported to Wear OS. It works OK. But now I have two separate projects, whose source files are 99.5% identical. How can I put both versions in one project, so only one copy of each common source file is needed?
(For instance, the Manifest file needs to be tailored -- at least for the uses-feature android.hardware.type.watch
, and one source file needs to be different -- menus in the Android app have to be handled differently on the Wear app. One resource was tailored for the small screen size. Everything else is identical.)
I tried making two modules in the one project, one "app" the other "wear". But since modules seem to correspond to directories, this doesn't directly address the problem of shared source files.
I played with "Build Configurations" -- but I see nothing about paths there. I spent some time with "Build Types", which deal with "dependencies", but I couldn't sort out how to make one module look into the other module's directory tree for, say a res/ directory.
What is the right way to resolve this?
Upvotes: 1
Views: 1587
Reputation: 450
It is important to keep the namespaces of source and resources of different modules distinct. Exactly how this is done is arbitrary.
In a simple case of an Android app and a Wear OS app that share code and resources, I adopted the following structure:
org.domain.theproject
<shared code and resources under this>
org.domain.theproject.app
<Android app code and resources>
org.domain.theproject.wear
<Wear OS app code and resources>
The namespace of each module is defined in the Gradle build file.
If everything is set up properly, resources for dependent modules are merged with those of the library module, so that a reference such as
android:icon="@mipmap/ic_launcher"
could refer to a resource of that name defined in the library module, or one defined in the
(I don't know what happens when there is a resource name conflict.)
I have been using this structure in projects where an Android app and a Wear OS app share a common library:
TheProject/
AppModule/
src/main/
java/org/domain/theproject/
app/
<Android app source files>
res/
layout/
< etc. >
SharedModule/
src/main/
java/org/domain/theproject/
<shared library source files>
res/
layout/
<etc. >
WearModule/
src/main/
java/org/domain/theproject/
wear/
<Wear OS app source files>
res/
layout/
< etc. >
Each module must have its own manifest file, AndroidManifest.xml
, under
src/main/
Only those for apps are copied into the final APK file, though.
Library modules have a very simple manifest: two lines suffice, something like:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.domain.theproject"/>
This serves to indicate the package under which compiled resources of that library can be accessed.
Manifests for modules for Wear OS or Android apps will be more elaborate -- the usual structure with elements
<application>
and <activity>
Names of packages within the manifest prefixed with a dot '.' are relative to the package
attribute of the <manifest>
tag. Other packages, especially those within a shared library module, may be accessed by a full explicit package path.
The above package structure can be neatly realized by a manifest structured like this:
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.domain.theproject.app">
<!-- ... -->
<application >
<activity android:name=".MainActivity">
<!-- ... -->
The activity name will then be understood to be org.domain.theproject.app.MainActivity
.
Also, the package
attribute provides a package for the R
resource in Java code: in the above case, the default R
is really org.domain.theproject.R
.
Library build.gradle
files must begin with
apply plugin: 'com.android.library'
App build.gradle
files must begin with
apply plugin: 'com.android.application'
Besides the usual module dependencies, the build.gradle
of app modules must list dependencies on the library modules they use. For instance, if an app module is dependent on library module "shared", then its build.gradle
file must indicate that dependency in its dependencies section, like:
dependencies {
implementation project(':shared')
//...
}
Of course, build.gradle
files for Wear OS apps will also have dependencies for Wear OS, and Android apps will have dependencies for, say, AndroidX. But if a library they depend on has the same external dependencies, linking conflicts may occur. See "external library dependencies" below.
Note that the applicationId
property, as in
android {
defaultConfig {
applicationId "org.domain.theproject"
}}
does not necessarily reflect the Java class structure (although that is a good convention). It is rather the identifier for the app (or package?) that the OS uses to identify the app.
Android Studio squirrels information away willy-nilly, and regularly gets confused as to what it put where. All sorts of perplexing behavior results and much time is wasted. (This being just one general area of bug infestations in the program.)
It happens horribly often that things stop making sense, and
Build -> "Clean Project"
does not suffice, and nor does that, plus
File -> "Sync Project with Gradle Files".
Sometimes one must
File -> "Invalidate Caches/Restart"
And the fact is, sometimes one must turn AS off, go in with a console, and manually delete the hidden directories etc. I will not go into that here.
In doing the sorts of things described here, you will surely run into such issues, especially where it comes to the R interface between resources and Java code. I don't have any clear way to identify when these problems are happening, or which measure to take. You just have to develop a feeling for it.
Obvious to those experienced with Java: the package names in Java files must reflect the directory structure; within AS, that means relative to the module's directory
src/main/java/
Library module classes are merged with those of the dependent modules at that level -- class names can be imported across modules in the usual Java way. So to effect the structure above, a Java file
If the Gradle config and manifest are configured properly, classes from the library module can be accessed from dependent modules as though the directories beneath the modules' src/main/java
directory had been merged.
The package that holds the automatically-generated resource package R
is
defined by the <manifest>
tag's package
attribute in the module's manifest file.
But it is possible to refer explicitly to resources defined within other packages.
For instance, within the code of the dependent app package
org.domain.theproject.app,
the unqualified symbol R
refers to the generated resource package
org.domain.theproject.app.R
The resources of the library package
org.domain.theproject
may be referenced explicitly in the same code via
org.domain.theproject.R
An annoyance of converting conventional app code to a shared library: the elements in R
are not static -- so they can't be used in switch
statements. It's not a big deal to convert them to if...else
, but if you have a lot of them...
TODO -- how to access resources of a library module from within resources of a dependent module?
It is possible for the minimum SDK version of the library modules to be lower than that of the dependent app modules, but not the other way around.
Thus, the minimum SDK version for a Wear OS module could be the lowest possible for Wear apps (23), while that of the Android app module in the same project could be the lowest possible for AndroidX (14), provided the shared library module has its minimum SDK version set to 14.
The target SDK versions of all modules should match. (I guess -- haven't checked.)
I have managed in simple examples to avoid using Jetifier, by manually resolving dependency conflicts between dependent modules and library modules. This way, APK file can turn out much smaller. It requires some thought, and experimentation.
This is all accomplished int the Gradle build file's dependencies
block. I think the principle is: starting with the independent library modules,
load required dependencies with the property
implementation
but in dependent modules that require the same dependencies, load them with the property
compileOnly
So if the library module and the app module need the external library
'androidx.recyclerview:recyclerview:1.1.0'
the library will have listed in its dependencies
implementation 'androidx.recyclerview:recyclerview:1.1.0'
while the app module will have
compileOnly 'androidx.recyclerview:recyclerview:1.1.0'
(If both are listed as implementation
, then "Duplicate class" errors occur;
if the independent class is missing the compileOnly
, errors saying a package
that "does not exist" will occur.)
Further, it is also possible to tailor just what is linked in the implementation
directive by using exclude
properties.
Upvotes: 3