前言:
-
开发前请通读一下此文档,尤其是6,7两部分,对Google开发者账号的相关要求有一个基本了解。
-
由于Google对新注册的个人开发者账号上架App有强制要求(20个测试账号连续测试14天,同时并不是一轮就能顺利通过),因此需尽早准备Googl开发者账号账号,避免因账号原因导致进度延迟。
-
封闭测试版本需要将网站相关信息填写完整才能正常提审,因此生产站域名的购买与配置需提前准备,同时后台同学配置服务器与网站也至少需要2天左右的时间。
基础知识
Flutter文档
Dart语言教程:
除了继承,实现,需要了解扩展(extension)及混入(mixin)的使用.
开发规范文档:
API接口文档:
Note: 由于后端使用的PHP,部分接口返回的数据格式不一致,在处理返回数据时需要做好数据格式的保护处理.
日常开发时除了查看接口文档,也可以基于mobile测试站来查看请求接口及返回数据.
请求接口需要在请求的header中进行加密处理,规则如下:
-
获取token: 接口 v1/access_token, PSOT请求类型
-
请求token的Header需包括以下内容:
tokenHeader.putIfAbsent("AppId", () => appId); //后端分配的Appid
//currentTimeStamp 当前时间戳(秒)
//appId 后端分配
//appSecret 后端分配
//nonce 本地生成的4位随机数
signKey = MD5(Utf8.encode("$currentTimeStamp$appId$appSecret$nonce"))
tokenHeader.putIfAbsent("Signature", () => signKey); //加密后的签名数据,具体加密规则如上
tokenHeader.putIfAbsent("Nonce", () => nonce); // 4位随机数
tokenHeader.putIfAbsent("Timestamp", () => currentTimeStamp); //当前时间戳(秒)
-
其他接口请求的Header需包括以下内容:
headerMap.putIfAbsent("clientversion", () =>clientVersion); //当前App的版本
//currentTimeStamp 当前时间戳(秒)
//appId 后端分配
//nonce 本地生成的4位随机数
//注意此处不需要secret
signature = MD5(Utf8.encode("$currentTimeStamp$appId$nonce"))
headerMap.putIfAbsent("Signature", () =>signature);//加密后的签名数据,具体加密规则如上
headerMap.putIfAbsent("Timestamp", () =>currentTimeStamp); //当前时间戳(秒)
headerMap.putIfAbsent("Nonce", () =>nonce); // 4位随机数
headerMap.putIfAbsent("UUID", () =>uuid); //设备的UUID
headerMap.putIfAbsent("devicetype", () => "2"); //设备类型“1”:iOS,“2”:Android
-
Api接口替换加密规则:
项目中会用到的三方插件库:
官方插件库:
The official repository for Dart and Flutter packa
其他插件库:
状态管理,路由处理可使用GetX(主要是简单):
参考官方介绍了解其状态管理,路由管理及依赖管理的具体使用方式
网络交互:
网络框架Dio:
需要了解如何对其封装以及拦截器的使用
对于Dio的封装可参考该项目:
Json转dart处理:
JasoToDart 客户端工具
其他JasoToDart在线工具
DartJInstantly parse JSON in any language | quicktype
屏幕适配:
ScreenUtils:
flutter_screenutil | Flutter package
了解之后直接用即可,需要注意需要根据设计图的大小进行相应的初始化,同时在使用时注意根据实际情况正确的使用扩展名,避免UI上出现问题。
UI相关:
列表组件:
SmartRefresh
pull_to_refresh | Flutter package
EasyRfresh
easy_refresh | Flutter package
也可以使用其他熟悉的列表组件。
需要注意最好对列表组件进行一个基础的封装,方便使用。
网格组件:
flutter_staggered_grid_view | Flutter package
图片选择器:
image_picker | Flutter package
官方图片选择库,调用系统相册/相机
其他的三方图片选择库
wechat_assets_picker | Flutter package
基于photo_manager实现的微信风格的图片选择框架,可给予它实现一些自定义的UI
图片展示相关组件:
extended_image | Flutter package
强大的官方 Image 扩展组件, 支持加载以及失败显示,缓存网络图片,缩放拖拽图片,图片浏览(微信掘金效果),滑动退出页面(微信掘金效果),编辑图片(裁剪旋转翻转),保存,绘制自定义效果等功能
cached_network_image:
cached_network_image | Flutter package
IM
Web Socket 插件
web_socket_channel | Dart package
关于WebSocket协议介绍的文章很多,可自行上网搜素了解
cpp_backend_awsome_blog/【NO.23】一篇文章彻底搞懂websocket协议
以下将以测试服务器为例介绍内部IM连接的基本使用:
后端相关文档
客户端:
1. 消息类型枚举
首先,我们来看一下消息类型的枚举。这些是我们在处理消息时会用到的常见类型。
enum MessageTypeEnum {
CONNECTED("connected"),
PING("ping"),
PONG("pong"),
SAY("say"),
SAY_TO_GROUP("say_to_group"),
RECALL("recall"),
ERROR("error"),
REPORT("report"),
/// 连接IM时返回
GROUP_UID("group_uids"),
JOIN_GROUP("join_group"),
LEAVE_GROUP("leave_group"),
GROUP_CONNECTED("group_connected"),
GROUP_ERROR("group_error"),
//需要设置的状态. //Chatroom online status, 1:Online, 2:Offline, 3:Invisible, 4:Busy, 5:Away
SET_GROUP_STATUS("set_group_status"),
//{type: "check_online", check_uid: 121625785}
//{type: "check_online", check_uid: 121625785, is_online: 0, time: 1654658574}
CHECK_ONLINE("check_online"),
UN_KNOW("un_know");
final String value;
const MessageTypeEnum(this.value);
@override
String toString() => 'The MessageTypeEnum $name value is $value';
}
这些类型用于区分不同的消息。例如,CONNECTED
表示已连接,PING
和 PONG
用于心跳检测,SAY
用于发送消息等。
2. “say” 消息类型
在 SAY
这个消息类型下有60多种子类型,具体请根据自己项目需求选择合适的类型,同时兼容处理自己不支持的消息类型的UI。
可以查看
了解这些子类型的详情。在“字典管理”这个tab中有所有消息类型的记录。
3. IM初始化基础处理
初始化连接
在进行IM初始化时,需要注意我们发送和接收的消息体都是JSON格式的。同时,还需处理网络状态变化和重连策略。
以下是代码片段,仅供参考:
///url组装 host url + token
chatHostUrl = "wss://chat.masonvips.com/ws?token=";
String url ="${HttpApi.chatHostUrl}${AppCacheService.instance.tokenEntity?.accessToken}"
channel = IOWebSocketChannel.connect(url);
channel?.stream.listen(_data, onError: _onError, onDone: _onDone);
void _onError(error) {
// 错误处理
}
void _onDone() {
// 连接关闭处理
}
///在_data 处理所有收到的消息逻辑
_data(event) {
Map<String, dynamic> resultData = json.decode(event.toString());
MessageEntity messageEntity = MessageEntity.fromJson(resultData);
var type = MessageTypeEnum.values.firstWhere(
(element) => element.value == messageEntity.type,
orElse: () => MessageTypeEnum.UN_KNOW);
switch (type) {
case MessageTypeEnum.PING:
///收到服务器的ping消息,需要给服务器回一条pong
pingInterval = DateTime.now().millisecondsSinceEpoch;
channel?.sink.add("{\"type\":\"pong\"}");
break;
case MessageTypeEnum.PONG:
channel?.sink.add("{\"type\":\"ping\"}");
break;
case MessageTypeEnum.CONNECTED:
connectionStatus.value = ConnectionStatusEnum.connected;
needReConnect = true;
break;
case MessageTypeEnum.RECALL:
break;
case MessageTypeEnum.REPORT:
///接收对方已读的消息时间
break;
case MessageTypeEnum.ERROR:
//{"type":"error","time":1606703469,"code":30001029,"message":"The user is unavailable now."}
//{"type":"error","time":1606703469,"code":30003002,"message":"Input parameters error."}
//{"type":"error","time":1606703469,"code":10001001,"message":"Please login first"}
if (resultData.containsKey("message")) {
showSnackBar(resultData['message']);
}
break;
case MessageTypeEnum.CHECK_ONLINE:
break;
default:
break;
}
for (var listener in _messageCallBack) {
///把消息分发到自己需要处理的地方
listener.call(messageEntity);
}
}
发送消息
只简单列举了发送文档和图片消息,除开系统消息,其他消息类型发送逻辑跟这两个整体上是一致的
发送文字消息
以下是发送文字消息的示例:
客户端发送的消息体
channel?.sink.add(json.encode(messageData).toString());
{
"type": "say",
"to": 2281027,
"message": "send text msg",
"local_id": "mobile-tmp-a71b5eb4-f41e-a511-37c2-ea502fdc24b3",
"complete": 0,
"showHandleBox": {
"show": false,
"type": 1
},
"created": 1717640053,
"isPlay": false,
"type_id": 1
}
服务端回执的消息体
{
"type": "say",
"type_id": 1,
"duration": 0,
"url": "",
"width": 0,
"height": 0,
"from_uid": "2038426",
"from_name": "daltonS",
"to_uid": 2281027,
"content": "send text msg",
"time": 1717640054,
"message_id": "11960191",
"room_id": "1dc4edceceacff2644b83ed8a463181c",
"show_recall": 1,
"local_id": "mobile-tmp-a71b5eb4-f41e-a511-37c2-ea502fdc24b3",
"x_info": "",
"blurred": 0,
"daysInfo": {
"sender": 1717640054,
"receiver": 0,
"f_s_t": 0
}
}
发送图片消息
首先调用 /v1/upload_file
上传图片,获取图片的 attachId
,然后使用该 attachId
发送图片消息。
上传图片得到的响应示例:
{
"code": 200,
"data": [
{
"attachId": 1113843,
"url": "https://pic.masonvips.com/b/f7d94d6437e6096fb188f4b1ca6c0304.jpg",
"width": 1080,
"height": 2340,
"hasFace": 0
}
],
"message": ""
}
客户端发送图片消息的示例:
客户端再发送图片信息体
{
"type": "say",
"to": 2281027,
"message": "[photo]",
"local_id": "mobile-tmp-448a84a0-4f80-2284-6f7c-ec2f94446b65",
"complete": 0,
"showHandleBox": {
"show": false,
"type": 1
},
"created": 1717640375,
"isPlay": false,
"type_id": 2,
"attach_id": 1113843,
"url": "https://pic.masonvips.com/b/f7d94d6437e6096fb188f4b1ca6c0304.jpg",
"width": 1080,
"height": 2340
}
服务端回执
{
"type": "say",
"type_id": 2,
"duration": 0,
"url": "https://pic.masonvips.com/b/f7d94d6437e6096fb188f4b1ca6c0304.jpg",
"width": "1080",
"height": "2340",
"from_uid": "2038426",
"from_name": "daltonS",
"to_uid": 2281027,
"content": "[photo]",
"time": 1717640380,
"message_id": "11960192",
"room_id": "1dc4edceceacff2644b83ed8a463181c",
"show_recall": 1,
"local_id": "mobile-tmp-448a84a0-4f80-2284-6f7c-ec2f94446b65",
"x_info": "",
"blurred": 0,
"daysInfo": {
"sender": 1717640380,
"receiver": 0,
"f_s_t": 0
}
}
在项目中会用到的其他插件:
权限相关插件
permission_handler:
https://pub.flutter-io.cn/packages/permission_handle
权限检查,权限请求,注意iOS端需要在podfile及plist里面进行相关配置。
设备信息
device_info_plus:
device_info_plus | Flutter package
android_id:
包信息
package_info_plus:
package_info_plus | Flutter package
UUID:
加解密:
crypto:
主要用于请求头的加密以及返回数据的解密
网页加载:
webview_flutter:
webview_flutter | Flutter package
flutter_inappwebview:
flutter_inappwebview | Flutter package
应用内支付:
in_app_purchase
in_app_purchase | Flutter package
支付的代码可以直接参考其example
存储:
shared_preferences:
shared_preferences | Flutter package
更多学习资料可参考:
IDE AI插件等
阿里通义灵码:
codeium:
Gemini:
最新版本的AndroidStudio自带Gemini AI工具,注意需要登录Google账号才能使用
本地资源索引生成:
其他资源:
查看线上App的元数据,上下架情况等
七麦数据 -专业移动产品商业分析平台-关键词优化-ASA优化
一些推荐的微信公众号:
白鲸出海
独立开发者猫哥
Enjoy出海开发者服务平台
风海铜锣
GSYTech
GDG
谷歌开发者
GO海联盟
iOS开发拾遗
开发者账号注意事项
注意新账号注册\登录等操作都不能在办公室的网络下进行,同时需要使用固定的设备以及VN。
关于google账号关联的相关信息可参考一下文档
20个账号+14天封闭测试规则:对于新注册的google开发者账号,必须要满足20个账号连续封闭测试14天,才能提交正式版本,因此大家需要提前进行准备,先提交一个能够正常运行的基础版本App先完成封闭测试的要求,待功能开发完成后即可快速的提审上线。
-
封测版本需要注意App的完整性及一致性,不要出现阻断性bug,也不要有与商店截图描述不相符的功能或页面。
以下是关于如何寻找封闭测试人员的一些资源方案:
-
在各大社交论坛上发布相关测试需求,相互协助
-
互助App
-
三方平台
(经jeremy测试,20apptester此三方服务商反馈无法下载我们的App,同时客服的回信速度很慢,大家优先使用其他的商家)
-
国内QQ,微信平台等互助群
由于Google封测的政策没有明确说明对账号的要求,对于是否能使用同一账号测试多个App,测试账号是否会导致关联这类问题具有不确定性,以上方案可能会存在风险,但目前暂无其他更可靠的方案,只能先尝试再根据具体情况进行调整。
注意事项:
目前不少开发者都遇到了封测失败,要求进行更多测试的情况,以下为网络上的一些经验总结及注意事项:
Here a few tips to pass the questions step after your app has been tested for 14 days.
Fill in the inputs with answers between 250 to 300 symbols
Be honest and upload updated version of your app
Explain how your app is providing value to users and the google play store.
https://www.reddit.com/r/AndroidClosedTesting/
提审上架流程
-
关于创建提审App的流程请查看以下文档:
如何在Google后台创建一个App并提交审核
-
域名购买:
域名通常是在Godaddy上进行购买,大家可以根据产品的名字在该网站上自行查询价格合适的域名,然后联系Sally进行购买,购买完成后联系后端工程师Bear进行生产站环境的配置并建立对应的YT指派予Bear,同时最好自己准备一个静态网站的模版,让后端工程师协助建站。
需要注意App的名字不要侵权,可以在以下网站上查询相关词汇是否已被注册:
-
PPSA(使用协议及隐私政策):
PPSA可以找Lulu协助提供一个模版,同时需要注意自行检查一遍版本的内容替换产品的名字及网站等信息,并移除自己App中没有的功能描述。
-
测试账号:
iOS及Android在提交审核时都需要提供测试账号给审核人员使用,因此请自行注册一个生产环境账号,并完善基础信息(基础信息不要乱填,同时邮箱必须使用真实存在的邮箱),账号注册完成后将对应的邮箱、密码、生产站域名发给Carl配置白名单(避免因触发各类验证,而导致被拒审),同时在Censor中将该账号升级为Gold账号,并备注此账号提供给Google Play或Apple的审核的测试账号,每次提审前记得检查一下测试账号是否能正常使用。
Tips:在Google play的封测过程中,如果使用的第三方平台反馈注册后的账号被系统Block了,可以单独注册 一个账号并加入白名单,提供给第三方测试使用,后续在封测完成后必须删除该账号。
-
支付配置:
在Google play后台创建后订阅后,将订阅ID,价格,包名,域名,服务器通知地址这些信息发给Dale进行后台的相关配置。
可参考以下网站教程: