Android Studio project that builds for both Wear OS and normal app, but shares source files

China☆狼群 提交于 2020-07-31 05:03:32

问题


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?


回答1:


namespaces

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.)

directory structure

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. >

manifest

  • 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.

  • Resources referred to within the manifest ... are in a kind of flat namespace merged from the resource names of the library resources and the dependent module resources. It is very easy for a conflict to arise.

Gradle build file

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.

shortcomings of Android Studio

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.

Java source

classes

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.

resources

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

the R package in shared libraries

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...

resource XML

TODO -- how to access resources of a library module from within resources of a dependent module?

minimum SDK versions

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.)

external library dependencies

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.



来源:https://stackoverflow.com/questions/62694206/android-studio-project-that-builds-for-both-wear-os-and-normal-app-but-shares-s

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!