如何在安卓应用中实现付费和免费版本的最佳方法

67

我已经在安卓市场上发布了一款免费应用,但我想添加一个付费版,提供更好的功能。由于市场告诉我已经有一个使用相同包名的应用程序,我不能上传相同的应用程序并更改一些常量来解锁这些功能。

最清晰的做法是什么?


你能否在付费版本的名称中添加“Pro”吗? - FrustratedWithFormsDesigner
UI会随着新功能而改变吗?这两个版本有什么不同之处? - James Black
你尝试过更改包名吗? - sahil shekhawat
9个回答

52

Android SDK正式解决了共享或公共代码库的问题,称之为Library Project。

http://developer.android.com/tools/projects/index.html

基本上,共享的代码被定义为Library Project,然后付费版本和免费版本只是您Eclipse工作台中的两个不同项目,都引用了上述Library Project。

在构建时,Library Project将与您的任一发行版合并,这正是您想要的。

Android SDK示例源代码包含一个名为TicTacToe的项目,可以帮助您开始使用Library Projects。

祝你好运。

Daniel


4
我曾经在我的应用程序的付费版和免费版中使用过这个解决方案,问题是我已经失去了像以前一样调试源代码的能力。我已经将库链接到源代码,并且在调试模式下可以看到源文件,但我无法查看变量的值,有什么解决方法吗? - rfsbraz
如果免费版保存了一些数据,专业版可以使用这种技术读取这些数据吗?(我的意思是不需要任何特殊文件权限或SD卡权限) - ycomp
2
一个库项目允许你分享代码,但不是数据。在两个应用程序之间共享数据是另一个完全不同的话题。实现内容提供者可能是您需要的解决方案之一(还有其他方法)。 - Daniel Szmulewicz
链接不再可用。 - onaclov2000
谢谢。应该已经修复了。 - Daniel Szmulewicz

35

有几种方法可以实现,但通常只有尝试之后才会看到缺点。这是我的经验:

  1. 解锁应用程序。这很容易实现,创建一个作为许可证的新应用程序:您的原始应用程序应检查解锁器应用程序的签名是否与您的应用程序匹配(如果是,则设备上可用解锁器,否则不可用);如果未安装,则您的解锁器应提示下载您的免费应用程序,否则启动它并删除其启动器图标。

    优点:易于实施,并且只需要维护一个代码库。
    缺点:据说用户有时会对此模型感到困惑,他们根本不明白为什么他们有两个启动器图标或者他们刚刚下载了什么。删除启动器图标很麻烦,更改只有在设备重新启动后才能看到。此外,似乎您将无法使用Google的许可API(LVL),因为您的免费应用无法代表您的付费解锁器应用程序发出许可请求。任何这后者的解决方法都会导致糟糕的用户体验。

  2. 应用内购买。如果您已经在代码中使用了IAP,则很容易实现,否则需要花费相当长的时间才能做到这一点。

    优点:只需要维护一个代码库,用户购买流程相当方便。
    缺点:据说用户担心他们的购买是否“持久”,也就是说,他们不确定如果他们以后将应用程序安装到另一个设备或重新安装它是否仍然可以使用专业版功能。

  3. 免费和付费版本的共享库项目。编码应该不难,看起来像是一个有逻辑的决定,包含大部分应用程序逻辑的单个代码库,并仅维护差异。

    优点:用户对此模型的理解较少混淆,但他们可能担心如果开始使用付费版本,他们的首选项是否会丢失。

  4. 缺点:就我的经验来说,Java/Eclipse对于库不太友好。设置项目正确需要一些时间,但仍然会让人困惑整个流程如何运作,应该放置在哪个清单中,资源会发生什么情况等等。您将面临构建问题,因为项目配置错误,资源无法找到(库项目无法“看到”已引用的资源,因此您必须逐个编辑引用)。此外,此模型不支持资产文件,这意味着您需要使用符号链接、复制或其他魔法来解决问题,这只会让您“单一代码库”项目的混乱变得更糟。当您的项目最终构建成功后,您只需要祈求一切按预期工作,准备好寻找遗漏的图像和隐藏的错误。当然,您还需要为用户提供方便的方法,在首次启动时迁移其偏好设置到付费版本中。

  5. 具有单独的代码库和源控件的免费版和付费版。乍一看,这也似乎是一个不错的主意,因为一个体面的源控制系统可以减轻你的负担,但......

    优点:同第3点。
    缺点:您将维护两个不同的代码库,而除了合并/分支地狱之外什么都没有。应用程序包名称应该不同,因此您可能需要区分每个单独的文件,以保持事情清晰。当然,您还需要为用户提供方便的方法,在首次启动时迁移其偏好设置到付费版本中。

  6. 具有脚本从其中一个派生另一个的免费版和付费版。听起来像是通过一个肮脏的小技巧来实现的,但你可以肯定它能够工作。

    优点:与第3点相同,而且只需要维护单个代码库。
    缺点:创建脚本需要一些时间(确保重命名文件夹,替换包、应用程序和项目名称),如果必要的话,您必须定期更新脚本(如果免费版和付费版之间没有太多差异,这种情况不会发生)。当然,您需要为用户提供方便的方式在首次启动时将其偏好设置迁移到付费版本。

没有完美的解决方案,但以上内容有望指引您正确的方向,如果您正准备开始实施其中一种可能性。


你能给我一些关于方法1的信息吗?我只需要检查解锁应用程序的包名吗,还是有更好的方法?谢谢 - Shmuel
不,仅检查软件包名称是不够的,因为攻击者可以轻松创建并安装一个伪造的解锁应用程序来欺骗您的应用程序。您需要使用相同的密钥签署您的应用程序和解锁器应用程序,然后在您的应用程序中检查签名是否匹配。类似于这样:String mainAppPkg = context.getPackageName(); String keyPkg = "com.example.your.unlocker.key.package.name"; int sigMatch = context.getPackageManager().checkSignatures(mainAppPkg, keyPkg); boolean isInPaidMode = sigMatch == PackageManager.SIGNATURE_MATCH; - Levente Dobson
啊,太酷了。我已经实现了这个功能,准备发布我的应用程序的解锁版本到Play商店。该密钥的唯一目的是让免费版本找到它并移除广告。这是否违反了Google的条款?付费应用程序必须做些什么,还是只能作为捐赠/移除免费应用程序的广告呢?谢谢。 - Shmuel
很多应用程序都使用这个模型,所以我非常怀疑这会对条款造成任何问题。在我看来,这不应该是一个问题。我所知道的这个模型带来的唯一问题就是我在帖子中描述的:用户感到困惑,没有简单的LVL支持,隐藏图标是麻烦和不好的。 - Levente Dobson
对于选项3、4和5,您如何让用户购买付费版本?您是否在应用商店中直接链接到付费版本的“购买”按钮?我看到一个SO答案建议这样做,但它得到了2个踩,却没有解释。 - user1725145
我不是这方面的专家,但有几个选项可供选择。1:在您的免费应用程序的应用商店描述中包含此信息,2:在用户启动应用程序15次后显示弹出窗口,3:显示“购买”按钮,但可能不在应用程序的主屏幕上,而是在“帮助”或“关于”部分中,4:在仅限高级用户访问的功能中显示此提示,5:如果免费应用程序显示广告,请使您的应用程序变得更好,您的用户将在应用商店搜索付费版本。 - Levente Dobson

34

使用Gradle构建系统,现在您可以拥有不同的产品风味,每个风味都有自己的包名。以下是一个示例gradle脚本,其中包含相同应用程序的免费和专业版本。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 19
    buildToolsVersion "19.1"

    defaultConfig {
        applicationId "com.example.my.app"
    }

    productFlavors {
        free {
            applicationId "com.example.my.app"
            minSdkVersion 15
            targetSdkVersion 23
            versionCode 12
            versionName '12'
        }
        pro {
            applicationId "com.example.my.app.pro"
            minSdkVersion 15
            targetSdkVersion 23
            versionCode 4
            versionName '4'
        }
    }

R类仍会在AndroidManifest.xml中指定的包名中生成,所以当切换口味时,您不需要更改任何一行代码。

您可以从Android Studio左下角的Build Variants面板中切换口味。此外,在您想要生成已签名的APK时,Android Studio会询问您要构建的APK的口味。

另外,您可以为每个口味使用不同的资源。例如,您可以在src目录中创建一个pro目录。目录结构应与main目录类似。(例如:如果您想要为专业版本创建不同的启动器图标,则可以将其放置在src\pro\res\drawable中。当您切换到pro口味时,它将替换位于src\main\res\drawable中的免费图标)。

如果您在上述pro资源目录中创建了strings.xml,则主要strings.xmlpro strings.xml将合并以获得最终的strings.xml。如果某个字符串键存在于免费和pro xml中,则将从pro xml中取字符串值。

如果您需要在代码中检查当前版本是专业版还是免费版,则可以使用以下方法。

public boolean isPro() {
    return context.getPackageName().equals("com.example.my.app.pro");
}

获取更多信息,请参阅此处


3
如果使用gradle构建,这绝对是最佳解决方案。 - hgoebl
@JimmyKane 我没有开发过任何穿戴式模块,但似乎你可以在手持和穿戴式模块中都有不同的版本。因此,如果您的穿戴式模块也有免费和付费版本,那么您也应该在穿戴式模块中使用不同版本。请查看这个答案。https://dev59.com/GV4c5IYBdhLWcg3w7t7E#27405731 - Lahiru Chandima
@LahiruChandima 我弄明白了。实际上,你需要两者才能更改包名。如果代码库相同(版本等),则可以省略其余部分。 - Jimmy Kane
最后还需要额外的工作。该死。至少我解决了它。请检查我的答案,再次感谢提供信息!干杯 - Jimmy Kane
这个很好用,谢谢。但是如果你遇到了像“错误:所有风味现在必须属于一个命名的风味维度。风味'flavor_name'未分配到风味维度。”这样的错误,你需要在“productFlavors {”之前添加“flavorDimensions“version””。请参考https://developer.android.com/studio/build/build-variants。 - mili
显示剩余2条评论

15

同一份源代码生成两个应用程序(Eclipse)

市场上常常有只有一位差别的两种应用形式需要管理,也许只是一个变量不同(paidFor = true;),甚至图标可能也不同。

这种方法使用了Mihai在http://blog.javia.org/android-package-name/中解释的包名描述,强调了用于管理源代码和发布apk到Android市场所使用的包名之间的区别。在此示例中,两个发布的包名分别为com.acme.superprogram和com.acme.superprogrampaid,Java源代码包名为com.acme.superprogram。

在清单文件中,Activity被列出并命名为.ActivityName。Mike Wallace在他最近的演讲中指出,前面的点很重要,可以被一个完全限定的包替换。例如,“com.acme.superprogram.DialogManager”可以在manifest.xml文本中替换“.DialogManager”。

步骤1是使用Java源代码管理包名(com.acme.superprogram)将所有activity android:name条目替换为这些完全限定的包名。

然后可以更改清单包名称...

在Eclipse中,这会强制重新编译并创建一个新的R.java文件在gen文件夹中。这里有点棘手;有两个文件夹com.acme.superprogram和com.acme.superprogrampaid,只有一个文件夹有R.java。只需将R.java复制到另一个文件夹中,以便程序可以解析R.layout.xyz项。

当你在...

我已经在几个应用程序上尝试过了。我在模拟器上同时运行这两个应用程序,它们都在我的2.1手机上,并且它们都在Android市场上。


2
两个软件包之间只有一个位的差异(即付费标志),这不是很危险吗?基本上你在免费版本中泄露了全部代码,只是在代码开头加了一个非常容易识别和更改的标志,即使代码被混淆也无济于事。 - CraPo
很好的答案,但不幸的是当我尝试导出时gen目录被清除了 - 建议另外提供一种解决方法作为备选答案。 - Neil Townsend

2

如果你也有一个穿戴模块,那么需要额外的工作。

以下是一些使用口味和构建类型打包穿戴模块的Gradle文件示例。

模块移动 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "com.example.app"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 85
        versionName "2.5.2"
    }
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            embedMicroApp = true
            minifyEnabled false
        }
        release {
            embedMicroApp = true
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true
        }
    }
    productFlavors {
        free{
            applicationId "com.example.app"
        }
        pro{
            applicationId "com.example.app.pro"
        }
    }
}

configurations {
    freeDebugWearApp
    proDebugWearApp
    freeReleaseWearApp
    proReleaseWearApp
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'

    freeDebugWearApp project(path: ':wear', configuration: 'freeDebug')
    proDebugWearApp project(path: ':wear', configuration: 'proDebug')

    freeReleaseWearApp project(path: ':wear', configuration: 'freeRelease')
    proReleaseWearApp project(path: ':wear', configuration: 'proRelease')
}

模块穿戴 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"
    publishNonDefault true

    defaultConfig {
        applicationId "com.example.app"
        minSdkVersion 20
        targetSdkVersion 23
        versionCode 85
        versionName "2.5.2"
    }
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            minifyEnabled false
        }
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true
        }
    }
    productFlavors {
        free {
            applicationId "com.example.app"
        }
        pro {
            applicationId "com.example.app.pro"
        }
    }
}

dependencies {

    ...

}

1
我曾经遇到过同样的问题,我最终决定制作两个Android应用程序,一个名为my_epic_app_free,另一个名为my_epic_app_paid。我的做法是只对付费版进行更改,然后使用单独的Java控制台程序,它只是直接访问所有.java文件,并将它们复制到内存中,调整包名和清单行,然后将它们直接粘贴到免费版中。我在桌面上放置了一个按钮来触发此操作,这样当我在付费版上开发完成后,我只需要按下按钮,然后编译免费版本即可。我让付费应用程序和免费应用程序相互通信,用户甚至可以安装两者,免费版本可以看到付费版本,并解锁到付费版本。
另一个想法是将付费应用程序设置为仅包含一个密钥文件的基本程序。如果运行了付费版,则会自动运行免费版本,而免费版本将表现得像付费版本一样。
免费应用程序检查是否存在付费应用程序的密钥文件,如果存在,则会解锁免费应用程序。这样可以避免代码重复。虽然包名仍然必须不同,但理想情况下,付费应用程序不需要经常更改。

好处:升级到付费版本时,不会丢失用户数据/设置。

缺点:如果用户先安装付费版本,则必须指导他们安装免费版本,这很麻烦,为什么他们必须安装两个应用程序才能使用付费版本?


1

更改包名将有所帮助。
只需使用eclipse更改您的付费应用程序的包名,然后将其放入市场。

那将解决您的问题。


你将如何根据付费/免费来检测/更改应用程序行为? - Jimmy Kane

0

如果您不想使用库项目,并且对一些人在评论中提到的风险感到满意,那么@user426990在理论上提供了一个很好的答案。然而,当进行新的导出构建时,Eclipse似乎会清除gen目录的所有内容(我很难反驳这个作为一般原则的观点)。

基于同样原则的另一种解决方案如下,假设您编写了com.acme.superprogram并希望创建com.acme.superprogrampaid:

确保您的清单文件使用全名指向活动、服务等。根据@user426990的回答,".CoolActivity" 必须列为 com.acme.superprogram.CoolActivity。
在代码中创建一个名为 MyR 的新类(放在 com.acme.superprogram 目录下),具体如下:
``` package com.acme.superprogram; import com.acme.superprogram.R; public final class MyR { public final static R.attr attr = new R.attr(); public final static R.color color = new R.color(); public final static R.dimen dimen = new R.dimen(); public final static R.layout layout = new R.layout(); public final static R.id id = new R.id(); public final static R.string string = new R.string(); public final static R.drawable drawable = new R.drawable(); public final static R.raw raw = new R.raw(); public final static R.style style = new R.style(); public final static R.xml xml = new R.xml(); } ```
您需要根据您使用的资源进行相应调整!例如,您可能不需要 xml 行,但可能需要其他行。查看 gen/com/acme/superprogram 中的真实 R.java 文件,您将需要每个类一个行。您可能需要创建子类。
现在,(a) 从代码中删除所有的 "import com.acme.superprogram.R" 行,(b) 将所有的 "R." 引用替换为 "MyR."。这样,您所有对 R 的引用都会通过一个间接的地方。缺点是它们都会收到不够静态的警告。您有三个选项处理这些警告:可以抑制它们,可以忽略它们,或者可以创建一个更完整的 MyR 版本,其中每个 R 条目都有一行:
``` package com.acme.superprogram; import com.acme.superprogram.R; public final class MyR { final static class attr { final static int XXXX = R.attr.XXXX // And so on ... ```
根据@user426990的建议,现在可以将清单文件中的包从 "com.acme.superprogram" 更改为 "com.acme.superprogrampaid";此时您可能还需要更改名称、启动图标和关键变量。
在 MyR 中更改导入行以导入 com.acme.superprogrampaid.R。

然后你就可以开始了。

附注:记得在最终导出对话框中更改文件名...


2
如果您在编辑时不注意,这很容易变成一场灾难。谢谢,我不需要。 - Jimmy Kane

-6
为什么不创建两个不同的Eclipse项目呢?手动修复与包名称相关的一些问题并不难。注意更改导入语句和清单文件。无论如何,您都需要编写两次代码。当然,在两个地方修复代码是很麻烦的。
正确的解决方案应该在市场上。它应该允许具有相同包名称的两个应用程序,并在安装另一个应用程序时要求替换其中一个。上传时,它应该检查包名称是否真的来自同一软件供应商。
Rene

12
这并不是一个好的解决方案。维护两个代码库远非理想,有更加优雅的解决方案。 - Jason Robinson
+1 这并不是理想的,但对于任何使用适当源代码控制管理的人来说都是完全合理的。 - rds
4
任何“使用适当的源代码管理工具的人”都不会选择这种解决方案。 - Rodrigo Dias
我不明白为什么需要两个代码库。一个代码库可以有两个主文件,而专业的主文件只需扩展主文件即可。因此将必须有两种不同的构建方式,但是两种方式都将使用相同的代码库。 - MiguelMunoz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接