安卓软件开发工具包快速入门
安卓软件开发工具包快速入门
欢迎阅读 云眼灰度发布(特性标帜) 的 Android SDK 快速入门指南。
按照本指南中的步骤可以完成创建灰度发布(特性标帜)标帜、推出标帜,以及运行 A/B 测试等操作。
第一部分:创建和配置灰度发布(特性标帜)标帜
1. 创建“全栈灰度发布(特性标帜)”项目
需要一个 云眼帐户才能完成本指南的操作。如果还没有帐户,可以在云眼平台(https://app.eyeofcloud.com/register)注册一个免费帐户。如果已有帐户,请打开或创建一个新的灰度发布(特性标帜)项目。
对于新注册的帐号,在欢迎页面里点击“创建灰度发布(特性标帜)项目”。
对于已经建立项目的帐号,点击切换项目->新建项目->创建灰度发布(特性标帜)项目,即可创建新项目。
2. 在项目中创建buy标帜
灰度发布(特性标帜)项目创建完毕后,转到主页面菜单栏上“灰度发布(特性标帜)”页面,点击创建标帜,将新标帜命名为“buy”。
3. 在buy标帜中添加变量
buy标帜建立后,点击此标帜,在标帜设置-缺省变量处依次添加变量,本案例为了简化操作和节省时间,变量名称和缺省值都采用云眼平台提供的默认值。
4. 在事件管理中增加事件buy
转到菜单栏上“事件管理”页面,点击新建事件按钮。将事件标识命名为buy,点击创建事件,创建完成。
第二部分:编码实现灰度发布(特性标帜)和AB测试
1. 安装 云眼灰度发布(特性标帜) Android SDK。
在项目的build.gradle中,将MavenCentral添加到存储库。
build.gradle
repositories {
mavenCentral()
google()
jcenter()
}
在模块的build.gradle中,添加云眼 Android SDK依赖项,并通过单击立即同步来同步gradle。
build.gradle
dependencies {
implementation "com.eyeofcloud.ab:android-sdk:3.13.4"
implementation("com.eyeofcloud.ab:core-api:3.1.0"){
//如果出现重复类加载问题,可以类似如下进行排除
exclude group: 'javax.annotation'
exclude module: 'annotations'
}
implementation "org.slf4j:slf4j-api:1.7.30"
implementation "androidx.work:work-runtime:2.7.1"
implementation "commons-codec:commons-codec:1.15"
}
云眼 Android SDK通过MavenCentral分发。完整的源代码可在gitee上找到。
2. 调用SDK方法,创建eyeofcloudClient实例
在代码合适的位置(在 MyApplication.java 文件里)创建eyeofcloudClient实例,并把实例放在全局变量中,以便后续使用它。创建eyeofcloudClient实例需要传入sdkKey。本演示程序为了演示方便从页面输入sdkKey,但是实际应用时,需要直接写在代码里。sdkKey可以在云眼平台上拿到。
在代码能够获得用户id和属性信息的位置,通过调用eyeofcloudClient实例的方法createUserContext,得到user对象,user对象非常重要,它包含AB测试需要的2个主要方法decide和trackEvent。调用createUserContext需要传入两个参数:userId 和 attributes。 可以用设备加随机数作为userId,只要能够唯一识别用户即可。attributes是key、value对组成的数组,一般基于业务系统中的用户属性信息来构建。 user对象产生后,可以把它作为属性变量添加到app对象上,以便在其他地方可以使用。
MyApplication.java
package com.eyeofcloud.demo.flag;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import com.eyeofcloud.ab.EyeofcloudUserContext;
import com.eyeofcloud.ab.android.sdk.EyeofcloudClient;
import com.eyeofcloud.ab.android.sdk.EyeofcloudManager;
import org.json.JSONObject;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class MyApplication extends Application {
public static MyApplication app;
final public static String DEFUAULT_SDK_KEY = "1000122_2e2d7fbdea1afc51";
private EyeofcloudManager eyeofcloudManager;
private EyeofcloudUserContext user;
private String sdkKey;
private Map<String, Object> attributes;
final public static String USER_ID = "userId";
final public static String SDK_KEY = "sdkKey";
final public static String ATTRIBUTES = "attributes";
public static MyApplication getInstance() {
return app;
}
@Override
public void onCreate() {
super.onCreate();
app = this;
}
public void initEyeofcloud() {
//List<EyeofcloudDecideOption> defaultDecideOptions = Arrays.asList(EyeofcloudDecideOption.DISABLE_DECISION_EVENT);
EyeofcloudManager eyeofcloudManager = EyeofcloudManager.builder()
.withEventDispatchInterval(1L, TimeUnit.SECONDS)
.withDatafileDownloadInterval(15, TimeUnit.MINUTES)
.withSDKKey(app.getSdkKey())
//.withDefaultDecideOptions(defaultDecideOptions)
.build(app.getApplicationContext());
//eyeofcloudManager.initialize(app.getApplicationContext(), R.raw.datafile, eyeofcloudClient -> {
// On AVD, it's reported that setRequiredNetworkType(NetworkType.CONNECTED) not work, if so we can use below code
// which can skip calling of setRequiredNetworkType(NetworkType.CONNECTED). On physical mobile, below code is not required and should be commented out.
// WorkerScheduler.requestOnlyWhenConnected = false;
EyeofcloudClient eyeofcloudClient = eyeofcloudManager.initialize(app.getApplicationContext(), R.raw.datafile); //, eyeofcloudClient -> {
if (eyeofcloudClient.isValid()) {
// createUserContext
String userId = app.getAnonUserId();
Map<String, Object> attributes = app.getAttributes();
user = eyeofcloudClient.createUserContext(userId, attributes);
// attributes can be set in this way too
user.setAttribute("Country", "中国");
user.trackEvent("buy");
}
}
public EyeofcloudUserContext getUser() {
return user;
}
public String getSdkKey() {
if (sdkKey == null || sdkKey.isEmpty()) {
SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(SDK_KEY, Context.MODE_PRIVATE);
sdkKey = sharedPreferences.getString(SDK_KEY, null);
}
return sdkKey;
}
public void setSdkKey(String sdkKey) {
this.sdkKey = sdkKey;
if (sdkKey != null) {
SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(SDK_KEY, Context.MODE_PRIVATE);
sharedPreferences.edit().putString(SDK_KEY, sdkKey).apply();
}
}
public Map<String, Object> getAttributes() {
if (attributes == null || attributes.isEmpty()) {
attributes = new HashMap<>();
SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(ATTRIBUTES, Context.MODE_PRIVATE);
try {
JSONObject attributesJson = new JSONObject(sharedPreferences.getString(ATTRIBUTES, null));
Iterator<String> keysItr = attributesJson.keys();
while (keysItr.hasNext()) {
String key = keysItr.next();
attributes.put(key, attributesJson.get(key));
}
} catch (Exception e) {
e.printStackTrace();
}
}
return attributes;
}
public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes;
JSONObject jsonAttributes = new JSONObject(attributes);
SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(ATTRIBUTES, Context.MODE_PRIVATE);
sharedPreferences.edit().putString(ATTRIBUTES, jsonAttributes.toString()).apply();
}
public String getAnonUserId() {
// this is a convenience method that creates and persists an anonymous user id,
// which we need to pass into the activate and track calls
SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(USER_ID, Context.MODE_PRIVATE);
String id = sharedPreferences.getString(USER_ID, null);
if (id == null) {
id = Build.MODEL + new Date().getTime();
// comment this out to get a brand new user id every time this function is called.
// useful for incrementing results page count for QA purposes
sharedPreferences.edit().putString(USER_ID, id).apply();
}
return id;
}
public static String getConnectivityStatusString(Context context) {
String status = null;
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork != null) {
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
status = "Wifi enabled";
return status;
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
status = "Mobile data enabled";
return status;
}
} else {
status = "No internet is available";
return status;
}
return status;
}
}
3. 调用decide(),获取并使用变量。
在buy页面加载时,调用user的一个重要方法:decide,获得当前用户在灰度发布(特性标帜)buy的抽签分桶结果decision对象,decision里包含抽中的各个变量的值。我们可以用这些变量值控制页面显示,业务逻辑,AI参数,推荐算法等。
BuyViewModel.java
package com.eyeofcloud.demo.flag.ui.buy;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.eyeofcloud.demo.flag.MyApplication;
import com.eyeofcloud.demo.flag.R;
import com.eyeofcloud.demo.flag.databinding.FragmentBuyBinding;
public class BuyFragment extends Fragment {
private FragmentBuyBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
MyApplication app = (MyApplication) getActivity().getApplication();
BuyViewModel buyViewModel =
new ViewModelProvider(this).get(BuyViewModel.class);
binding = FragmentBuyBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView buttonTextView = binding.button;
buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), buttonTextView::setText);
final TextView ruleKeyTextView = binding.ruleKey;
buyViewModel.getRuleKey().observe(getViewLifecycleOwner(), ruleKeyTextView::setText);
final TextView variationKeyTextView = binding.variationKey;
buyViewModel.getVariationKey().observe(getViewLifecycleOwner(), variationKeyTextView::setText);
final TextView stringTextView = binding.stringValue;
buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), stringTextView::setText);
Button button = binding.button;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// trackEvent
app.getUser().trackEvent("buy");
}
});
if (app.getSdkKey() != null && !app.getSdkKey().isEmpty()) {
buyViewModel.init(app);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
String connection = MyApplication.getConnectivityStatusString(getContext());
// Set the message show for the Alert time
builder.setMessage("Connection status: " + connection +",SDK Key尚未设置,是否使用缺省SDK Key:" + MyApplication.DEFUAULT_SDK_KEY + "?");
// Set Alert Title
builder.setTitle("注意!");
// Set Cancelable false for when the user clicks on the outside the Dialog Box then it will remain show
builder.setCancelable(false);
// Set the positive button with yes name Lambda OnClickListener method is use of DialogInterface interface.
builder.setPositiveButton("是,用缺省", (dialog, which) -> {
// When the user click yes button then app will close
//finish();
app.setSdkKey(MyApplication.DEFUAULT_SDK_KEY);
buyViewModel.init(app);
NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
navController.navigate(R.id.navigation_buy);
});
// Set the Negative button with No name Lambda OnClickListener method is use of DialogInterface interface.
builder.setNegativeButton("否,去设置", (dialog, which) -> {
// If user click no then dialog box is canceled.
dialog.cancel();
NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
navController.navigate(R.id.navigation_config);
});
// Create the Alert dialog
AlertDialog alertDialog = builder.create();
// Show the Alert Dialog box
alertDialog.show();
}
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
5. 调用trackEvent方法,将事件发出
BuyFragment.java
package com.eyeofcloud.demo.flag.ui.buy;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import com.eyeofcloud.demo.flag.MyApplication;
import com.eyeofcloud.demo.flag.R;
import com.eyeofcloud.demo.flag.databinding.FragmentBuyBinding;
public class BuyFragment extends Fragment {
private FragmentBuyBinding binding;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
MyApplication app = (MyApplication) getActivity().getApplication();
BuyViewModel buyViewModel =
new ViewModelProvider(this).get(BuyViewModel.class);
binding = FragmentBuyBinding.inflate(inflater, container, false);
View root = binding.getRoot();
final TextView buttonTextView = binding.button;
buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), buttonTextView::setText);
final TextView ruleKeyTextView = binding.ruleKey;
buyViewModel.getRuleKey().observe(getViewLifecycleOwner(), ruleKeyTextView::setText);
final TextView variationKeyTextView = binding.variationKey;
buyViewModel.getVariationKey().observe(getViewLifecycleOwner(), variationKeyTextView::setText);
final TextView stringTextView = binding.stringValue;
buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), stringTextView::setText);
Button button = binding.button;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// trackEvent
app.getUser().trackEvent("buy");
}
});
if (app.getSdkKey() != null && !app.getSdkKey().isEmpty()) {
buyViewModel.init(app);
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
String connection = MyApplication.getConnectivityStatusString(getContext());
// Set the message show for the Alert time
builder.setMessage("Connection status: " + connection +",SDK Key尚未设置,是否使用缺省SDK Key:" + MyApplication.DEFUAULT_SDK_KEY + "?");
// Set Alert Title
builder.setTitle("注意!");
// Set Cancelable false for when the user clicks on the outside the Dialog Box then it will remain show
builder.setCancelable(false);
// Set the positive button with yes name Lambda OnClickListener method is use of DialogInterface interface.
builder.setPositiveButton("是,用缺省", (dialog, which) -> {
// When the user click yes button then app will close
//finish();
app.setSdkKey(MyApplication.DEFUAULT_SDK_KEY);
buyViewModel.init(app);
NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
navController.navigate(R.id.navigation_buy);
});
// Set the Negative button with No name Lambda OnClickListener method is use of DialogInterface interface.
builder.setNegativeButton("否,去设置", (dialog, which) -> {
// If user click no then dialog box is canceled.
dialog.cancel();
NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
navController.navigate(R.id.navigation_config);
});
// Create the Alert dialog
AlertDialog alertDialog = builder.create();
// Show the Alert Dialog box
alertDialog.show();
}
return root;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
6. 运行一下小程序
至此,编码工作完成,但是灰度发布(特性标帜)标志还没有启动。我们先看一下在不启动灰度发布(特性标帜),用缺省值来运行小程序时界面的样子。此时,按钮显示的是缺省的“购买”两个字。
第三部分:运行灰度发布(特性标帜)实验
小程序产品或运营人员,在云眼灰度发布(特性标帜)平台上操作来改变按钮的文字,并跟踪和评估各个变体的转化效果。
1. 修改buy标帜变量形成不同变体
点击buy标帜,将缺省变量中string variable的默认值设置为“购买”。添加两个变体:变体1和变体2,变体1中设置string variable值为“立即购买”,变体2中设置string variable值为“我想要”。
2. 为灰度发布(特性标帜)buy配置AB测试规则
配置完buy标帜的变量和变体后,开始配置buy标帜的AB测试规则,在development环境下点击“增加规则”->“AB测试”, 实验受众选择缺省的所有访客,百分比为100%,指标设置为事件buy的唯一转化,变体1和变体2的流量权重设置为50%对50% 点击保存。
3. 启动并运行灰度发布(特性标帜)。
buy标帜规则设置完成后,我们打开buy标帜。
然后模拟用户访问buy页面。可以看到,此次访问结果为变体1,购买按钮的文字信息为“立即购买”,说明将用户分桶到了变体1。我们可以卸载并重新访问小程序,来模拟另一个新用户,这次购买按钮的文字信息是“我想要”,说明用户分桶到了变体2。实际运营时,可以做很多种实验,比如定向到不同受众,配置各种类型的指标,以及人工智能流量调整等,我们会有专门视频来讲解。
|||
第四部分、分析结果,推出获胜变体
分析结果,推出获胜变体。小程序灰度发布(特性标帜)标帜推出一段时间后,我们分析实验结果,根据实验结果推出获胜变体。
1. 查看实验报告,分析结果。
点击buy标帜上AB测试中的“查看结果”按钮。
根据变体1和变体2主指标数值和统计显著性来判定获胜者,统计显著性一般要大于95%。本案例中可以看出变体1胜出,指标提升55.69%,统计显著性达到98%。
2. 根据实验结果推出获胜变体。
确定胜出变体后,我们可以将将少部分流量(比如5%)继续用于AB测试,其余流量(95%)推出胜出的变体1。
本教程只是指导您完成最简单的灰度发布(特性标帜),在灰度发布(特性标帜)之前进行 A/B 测试。
下表显示灰度发布(特性标帜)和 A/B 测试之间的差异:
灰度发布(特性标帜) | A/B 测试规则 |
---|---|
可以将标帜推广到一定比例的一般用户群(或特定受众),或者在遇到错误时回滚。 | 在投资交付之前,通过 A/B 测试标帜进行实验,这样您就知道要构建什么。跟踪用户在标帜变体中的行为,然后使用 云眼平台 统计引擎解释实验结果。 |
回顾和结论
为了实施本案例的AB测试,我们总共做了4部分的工作,可以简单概括为:实验的设计、实现、实施和行动,与PDCA过程类似。
每部分工作都需要处理AB测试相关的信息,并由相关人员来负责完成,我们把参与AB测试的人员分为业务人员和技术人员两大类。
部分 | 工作内容 | 概括 | 处理信息 | 参与者 |
---|---|---|---|---|
一 | 创建灰度发布(特性标帜)标帜 | 设计 | 变量、事件 | 业务人员:产品、运营、数据; 技术人员:开发、QA |
二 | 灰度发布(特性标帜)编码实现 | 实现 | 变量、事件 | 技术人员:开发、QA |
三 | 配置并运行灰度发布(特性标帜) | 实施 | 实验、变体、指标、流量 | 业务人员:产品、运营、数据 |
四 | 分析结果,推出获胜变体 | 行动 | 实验、变体、指标、流量、结果、推出 | 业务人员:产品、运营、数据 |
从表格中,我们可以看到,只有第一部分工作是由业务人员和技术人员共同完成的,而他们共同处理的信息只有变量和事件,在后面的第三和第四部分中更多的信息,技术人员不需要知道。这样的好处是,可以尽最大可能减少业务人员和技术人员之间的相互依赖。业务人员在实施AB测试过程中,不需要依赖技术人员;技术人员在实现AB测试时也不依赖业务人员。大家可以相对独立、高效的进行工作。这是云眼灰度发布(特性标帜)/AB测试方案的一大优势。 另外,云眼AB测试结束后可以无缝的实现对特定人群的定向发布,做到AB测试和灰度发布(特性标帜)的有机结合,这是云眼产品的另一大特点。此外,云眼产品在AB测试/灰度发布(特性标帜)各个环节里还有很多独特的功能,我们将在后续的视频中详细介绍。
祝贺!您已成功设置并启动了第一个灰度发布(特性标帜)实验。虽然此示例侧重于优化销售过程,但 云眼 实验平台可以支持任何场景的实验用例。
培训视频可以从这里观看
本案例的 Android App demo代码可以从这里获取