应用内升级检测逻辑

This commit is contained in:
toly
2021-07-20 08:14:15 +08:00
parent d45d1e4ead
commit 1f0ff2353c
15 changed files with 669 additions and 14 deletions

View File

@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name="io.flutter.app.FlutterApplication"

40
lib/app/api/app_info.dart Normal file
View File

@@ -0,0 +1,40 @@
import 'package:flutter_unit/app/res/path_unit.dart';
import 'package:flutter_unit/app/utils/http_utils/http_util.dart';
import 'package:flutter_unit/app/utils/http_utils/result_bean.dart';
class AppInfoApi {
static Future<ResultBean<AppInfo>> getAppVersion() async {
String errorMsg = "";
var result = await HttpUtil.getInstance()
.client
.get(PathUnit.appInfo)
.catchError((err) {
errorMsg = err.toString();
});
// 获取的数据非空且 status = true
if (result.data != null && result.data['status']) {
// 说明有数据
if (result.data['data'] != null) {
return ResultBean.ok<AppInfo>(
AppInfo(
appName: result.data['data']['appName'],
appVersion: result.data['data']['appVersion'],
appUrl: result.data['data']['appUrl'],
));
} else {
return ResultBean.ok<AppInfo>(null);
}
}
return ResultBean.error('请求错误: $errorMsg');
}
}
class AppInfo{
final String appName;
final String appVersion;
final String appUrl;
AppInfo({this.appName, this.appVersion, this.appUrl});
}

View File

@@ -12,6 +12,7 @@ class PathUnit{
static const categoryDataSync = '/categoryData/sync';
static const categoryData = '/categoryData';
static const appInfo = '/appInfo/name/FlutterUnit';
static const login = '/login';

View File

@@ -14,4 +14,20 @@ class Toast {
backgroundColor: color??Theme.of(context).primaryColor,
));
}
static void error(BuildContext context,String msg){
toast(context,msg, color:Colors.red, );
}
static void warning(BuildContext context,String msg){
toast(context,msg, color:Colors.orange, );
}
static void success(BuildContext context,String msg){
toast(context,msg, color:Theme.of(context).primaryColor, );
}
static void green(BuildContext context,String msg){
toast(context,msg, color:Colors.green, );
}
}

View File

@@ -34,4 +34,18 @@ class Convert {
GalleryType.anim: "动画手势",
GalleryType.art: "艺术画廊",
};
static String convertFileSize(int size){
if(size==null) return '0 kb';
double result = size / 1024.0;
if(result<1024){
return "${result.toStringAsFixed(2)}Kb";
}else if(result>1024&&result<1024*1024){
return "${(result/1024).toStringAsFixed(2)}Mb";
}else{
return "${(result/1024/1024).toStringAsFixed(2)}Gb";
}
}
}

View File

@@ -0,0 +1,197 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'rnd.dart';
import 'particle.dart';
final easingDelayDuration = Duration(seconds: 10);
/// Probabilities of Hour, Minute, Noise.
// final particleDistributions = [2, 4, 100];
/// Number of "arms" to emit noise particles from center.
final int noiseAngles = 2000;
/// Threshold for particles to go rouge. Lower = more particles.
final rougeDistributionLmt = 85;
/// Threshold for particles to go jelly. Lower = more particles.
final jellyDistributionLmt = 97;
class ClockFx with ChangeNotifier {
double width; //宽
double height;//高
double sizeMin; // 宽高最小值
Offset center; //画布中心
Rect spawnArea; // 粒子活动区域
List<Particle> particles; // 所有粒子
int numParticles;// 最大粒子数
DateTime time; //时间
ClockFx({
@required Size size,
@required DateTime time,
this.numParticles = 5000,
}) {
this.time = time;
particles = List<Particle>.filled(numParticles, null);
setSize(size);
}
void init() {
for (int i = 0; i < numParticles; i++) {
particles[i] = Particle(color:Colors.black );
resetParticle(i);
}
}
void setTime(DateTime time) {
this.time = time;
}
void setSize(Size size) {
width = size.width;
height = size.height;
sizeMin = min(width, height);
center = Offset(width / 2, height / 2);
spawnArea = Rect.fromLTRB(
center.dx - sizeMin / 100,
center.dy - sizeMin / 100,
center.dx + sizeMin / 100,
center.dy + sizeMin / 100,
);
init();
}
/// Resets a particle's values.
Particle resetParticle(int i) {
Particle p = particles[i];
p.size = p.a = p.vx = p.vy = p.life = p.lifeLeft = 0;
p.x = center.dx;
p.y = center.dy;
return p;
}
void tick(Duration duration) {
updateParticles(duration); // 更新粒子
notifyListeners();// 通知监听者(画板)更新
}
void updateParticles(Duration duration){
var secFrac = DateTime.now().millisecond / 1000;
var vecSpeed = duration.compareTo(easingDelayDuration) > 0
? max(.2, Curves.easeInOutSine.transform(1 - secFrac))
: 1;
var vecSpeedInv = Curves.easeInSine.transform(secFrac);
var maxSpawnPerTick = 10;
particles.asMap().forEach((i, p) {
p.x -= p.vx * vecSpeed;
p.y -= p.vy * vecSpeed;
p.dist = _getDistanceFromCenter(p);
p.distFrac = p.dist / (sizeMin / 2);
p.lifeLeft = p.life - p.distFrac;
p.vx -= p.lifeLeft * p.vx * .001;
p.vy -= p.lifeLeft * p.vy * .001;
if (p.lifeLeft < .3) {
p.size -= p.size * .0015;
}
if (p.distribution > rougeDistributionLmt && p.distribution < jellyDistributionLmt) {
var r = Rnd.getDouble(.2, 2.5) * vecSpeedInv * p.distFrac;
p.x -= p.vx * r + (p.distFrac * Rnd.getDouble(-.4, .4));
p.y -= p.vy * r + (p.distFrac * Rnd.getDouble(-.4, .4));
}
if (p.distribution >= jellyDistributionLmt) {
var r = Rnd.getDouble(.1, .9) * vecSpeedInv * (1 - p.lifeLeft);
p.x += p.vx * r;
p.y += p.vy * r;
}
if (p.lifeLeft <= 0 || p.size <= .5) {
resetParticle(i);
if (maxSpawnPerTick > 0) {
_activateParticle(p);
maxSpawnPerTick--;
}
}
});
}
void _activateParticle(Particle p) {
p.x = Rnd.getDouble(spawnArea.left, spawnArea.right);
p.y = Rnd.getDouble(spawnArea.top, spawnArea.bottom);
p.isFilled = Rnd.getBool();
p.size = Rnd.getDouble(3, 8);
p.distFrac = 0;
p.distribution = Rnd.getInt(1, 2);
double angle = Rnd.ratio * pi * 2;
var am = _getMinuteRadians();
var ah = _getHourRadians() % (pi * 2);
var d = pi / 18;
//
// Probably not the most efficient solution right here.
do {
angle = Rnd.ratio * pi * 2;
} while (_isBetween(angle, am - d, am + d) ||
_isBetween(angle, ah - d, ah + d) );
p.life = Rnd.getDouble(0.75, .8);
p.size = sizeMin *
(Rnd.ratio > .8
? Rnd.getDouble(.0015, .003)
: Rnd.getDouble(.002, .006));
p.vx = sin(-angle);
p.vy = cos(-angle);
p.a = atan2(p.vy, p.vx) + pi;
double v = Rnd.getDouble(.5, 1);
p.vx *= v;
p.vy *= v;
}
double _getDistanceFromCenter(Particle p) {
var a = pow(center.dx - p.x, 2);
var b = pow(center.dy - p.y, 2);
return sqrt(a + b);
}
/// Gets the radians of the hour hand.
double _getHourRadians() =>
(time.hour * pi / 6) +
(time.minute * pi / (6 * 60)) +
(time.second * pi / (360 * 60));
/// Gets the radians of the minute hand.
double _getMinuteRadians() =>
(time.minute * (2 * pi) / 60) + (time.second * pi / (30 * 60));
/// Checks if a value is between two other values.
bool _isBetween(double value, double min, double max) {
return value >= min && value <= max;
}
}

View File

@@ -0,0 +1,88 @@
import 'dart:math';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'clock_fx.dart';
/// create by 张风捷特烈 on 2021/2/7
/// contact me by email 1981462002@qq.com
/// 说明:
class ClockWidget extends StatefulWidget {
final double radius;
const ClockWidget({Key key, this.radius = 100}) : super(key: key);
@override
_ClockWidgetState createState() => _ClockWidgetState();
}
class _ClockWidgetState extends State<ClockWidget>
with SingleTickerProviderStateMixin {
Ticker _ticker;
ClockFx _fx;
@override
void initState() {
super.initState();
_ticker = createTicker(_tick)..start();
_fx = ClockFx(
size: Size(widget.radius * 2, widget.radius * 2),
time: DateTime.now(),
);
}
@override
void dispose() {
_ticker.dispose();
_fx.dispose();
super.dispose();
}
void _tick(Duration duration) {
_fx.tick(duration);
if (_fx.time.second != DateTime.now().second) {
_fx.setTime(DateTime.now());
}
}
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(widget.radius * 2, widget.radius * 2),
painter: ClockFxPainter(fx: _fx),
);
}
}
/// Alpha value for noise particles.
const double noiseAlpha = 160;
class ClockFxPainter extends CustomPainter {
final ClockFx fx;
ClockFxPainter({@required this.fx}) : super(repaint: fx);
@override
void paint(Canvas canvas, Size size) {
fx.particles.forEach((p) {
double a;
a = max(0.0, (p.distFrac - .13) / p.distFrac) * 255;
a = min(a, min(noiseAlpha, p.lifeLeft * 3 * 255));
int alpha = a.floor();
Paint circlePaint = Paint()
..style = PaintingStyle.fill
..color = p.color.withAlpha(alpha);
canvas.drawCircle(Offset(p.x, p.y), p.size, circlePaint);
});
}
@override
bool shouldRepaint(covariant ClockFxPainter oldDelegate) =>
oldDelegate.fx != fx;
}

View File

@@ -0,0 +1,40 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class Particle {
double x; // x 坐标
double y; // y 坐标
double vx; // x 速度
double vy; // y 速度
double a; // 发射弧度
double dist; // 距离画布中心的长度
double distFrac;// 距离画布中心的百分比
double size;// 粒子大小
double life; // 粒子寿命
double lifeLeft; // 粒子剩余寿命
bool isFilled; // 是否填充
Color color; // 颜色
int distribution; // 分配情况
Particle({
this.x = 0,
this.y = 0,
this.a = 0,
this.vx = 0,
this.vy = 0,
this.dist = 0,
this.distFrac = 0,
this.size = 0,
this.life = 0,
this.lifeLeft = 0,
this.isFilled = false,
this.color = Colors.blueAccent,
this.distribution = 0,
});
}

View File

@@ -0,0 +1,48 @@
import 'dart:math';
class Rnd {
static int _seed = DateTime.now().millisecondsSinceEpoch;
static Random random = Random(_seed);
static set seed(int val) => random = Random(_seed = val);
static int get seed => _seed;
/// Gets the next double.
static get ratio => random.nextDouble();
/// Gets a random int between [min] and [max].
static int getInt(int min, int max) {
return min + random.nextInt(max - min);
}
/// Gets a random double between [min] and [max].
static double getDouble(double min, double max) {
return min + random.nextDouble() * (max - min);
}
/// Gets a random boolean with chance [chance].
static bool getBool([double chance = 0.5]) {
return random.nextDouble() < chance;
}
/// Randomize the positions of items in a list.
static List shuffle(List list) {
for (int i = 0, l = list.length; i < l; i++) {
int j = random.nextInt(l);
if (j == i) {
continue;
}
dynamic item = list[j];
list[j] = list[i];
list[i] = item;
}
return list;
}
/// Randomly selects an item from a list.
static dynamic getItem(List list) {
return list[random.nextInt(list.length)];
}
}

View File

@@ -0,0 +1,166 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_unit/app/api/app_info.dart';
import 'package:flutter_unit/app/utils/Toast.dart';
import 'package:flutter_unit/app/utils/convert.dart';
import 'package:flutter_unit/app/utils/http_utils/http_util.dart';
import 'package:flutter_unit/app/utils/http_utils/result_bean.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:package_info/package_info.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
class AppVersionChecker extends StatefulWidget {
const AppVersionChecker({Key key}) : super(key: key);
@override
_AppVersionCheckerState createState() => _AppVersionCheckerState();
}
enum VersionState { none, loading, shouldUpdate, downloading }
class _AppVersionCheckerState extends State<AppVersionChecker> {
final TextStyle labelStyle = TextStyle(fontSize: 13);
String oldVersion = '';
String newVersion = '';
int totalSize =0;
String url = 'http://toly1994.com/file/FlutterUnit.apk';
ValueNotifier<VersionState> versionState =
ValueNotifier<VersionState>(VersionState.none);
ValueNotifier<double> progress = ValueNotifier<double>(0);
_doDownload() async {
Directory dir = await getExternalStorageDirectory();
String dstPath = path.join(dir.path, 'FlutterUnit.apk');
if(File(dstPath).existsSync()){
InstallPlugin.installApk(dstPath, 'com.toly1994.flutter_unit');
return;
}
versionState.value = VersionState.downloading;
await HttpUtil.getInstance().client.download(url, dstPath,
onReceiveProgress: _onReceiveProgress,
options: Options(receiveTimeout: 24 * 60 * 60 * 1000));
versionState.value = VersionState.none;
InstallPlugin.installApk(dstPath, 'com.toly1994.flutter_unit');
}
void _onReceiveProgress(int count, int total) {
totalSize = total;
progress.value = count / total;
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text('检查新版本', style: labelStyle),
trailing: ValueListenableBuilder(
valueListenable: versionState,
builder: _buildTrailByState,
),
onTap: () async {
if (versionState.value == VersionState.shouldUpdate &&
Platform.isAndroid) {
_doDownload();
return;
}
if (versionState.value == VersionState.downloading) {
return;
}
versionState.value = VersionState.loading;
ResultBean<AppInfo> result = await AppInfoApi.getAppVersion();
PackageInfo packageInfo = await PackageInfo.fromPlatform();
if (result.status) {
print('${result.data.appName}:${result.data.appVersion}');
if (packageInfo.version == result.data.appVersion) {
Toast.success(context, '当前应用已是最新版本!');
versionState.value = VersionState.none;
} else {
oldVersion = packageInfo.version;
newVersion = result.data.appVersion;
Toast.green(context, '检测到新版本【${result.data.appVersion}】,可点击更新!');
versionState.value = VersionState.shouldUpdate;
}
} else {
print('${result.msg}');
versionState.value = VersionState.none;
}
},
);
}
Widget _buildTrailByState(
BuildContext context, VersionState value, Widget child) {
switch (value) {
case VersionState.none:
return const SizedBox();
case VersionState.loading:
return const CupertinoActivityIndicator();
case VersionState.shouldUpdate:
return Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
'$oldVersion --> $newVersion ',
style: TextStyle(height: 1, fontSize: 12, color: Colors.grey),
),
const SizedBox(
width: 5,
),
const Icon(
Icons.update,
color: Colors.green,
)
]);
case VersionState.downloading:
return ValueListenableBuilder(
valueListenable: progress, builder: _buildProgress);
}
return const SizedBox();
}
Widget _buildProgress(BuildContext context, double value, Widget child) {
return Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Column(
children: [
Text(
'${(value * 100).toStringAsFixed(2)} %',
style: TextStyle(height: 1, fontSize: 12, color: Colors.grey),
),
const SizedBox(
height: 5,
),
Text(
'${Convert.convertFileSize((totalSize * value).floor())}/${Convert.convertFileSize(totalSize)}',
style: TextStyle(height: 1, fontSize: 10, color: Colors.grey),
),
],
),
const SizedBox(
width: 15,
),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
backgroundColor: Colors.grey,
value: value,
),
)
]);
}
}

View File

@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:package_info/package_info.dart';
class VersionShower extends StatefulWidget {
const VersionShower({Key key}) : super(key: key);
@override
_VersionShowerState createState() => _VersionShowerState();
}
class _VersionShowerState extends State<VersionShower> {
String version = '1.0.0';
@override
void initState() {
super.initState();
_fetchVersion();
}
@override
Widget build(BuildContext context) {
return Text('Version $version');
}
void _fetchVersion() async{
PackageInfo packageInfo = await PackageInfo.fromPlatform();
if(mounted)
setState(() {
version= packageInfo.version;
});
}
}

View File

@@ -1,12 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_unit/app/api/app_info.dart';
import 'package:flutter_unit/app/res/str_unit.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/app/res/style/behavior/no_scroll_behavior.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/app/utils/http_utils/result_bean.dart';
import 'package:flutter_unit/views/components/permanent/circle_image.dart';
import 'package:flutter_unit/views/components/permanent/feedback_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'version/app_version_checker.dart';
import 'version/version_shower.dart';
/// create by 张风捷特烈 on 2020/6/16
/// contact me by email 1981462002@qq.com
/// 说明:
@@ -54,13 +59,11 @@ class VersionInfo extends StatelessWidget {
children: <Widget>[
CircleImage(image: AssetImage("assets/images/icon_head.webp"),size: 80,),
Text('Flutter Unit',style: TextStyle(fontSize: 20,fontWeight: FontWeight.bold),),
Text('Version ${StrUnit.version}'),
const VersionShower(),
],
);
}
Widget _buildCenter(BuildContext context) {
final labelStyle= TextStyle(fontSize: 13);
return Padding(
@@ -77,13 +80,7 @@ class VersionInfo extends StatelessWidget {
onTap: () => Navigator.of(context).pushNamed(UnitRouter.about_app),
),
Divider(height: 1,indent: 10),
ListTile(
title: Text('检查新版本',style: labelStyle),
trailing: _nextIcon(context),
onTap: () {
},
),
const AppVersionChecker(),
Divider(height: 1,indent: 10),
ListTile(
title: Text('检查数据库新版本',style: labelStyle),

View File

@@ -19,8 +19,8 @@ const BorderRadius _kBorderRadius = BorderRadius.only(
bottomRight: Radius.circular(15),
);
const _kTabTextStyle = TextStyle(color: Colors.white, shadows: [
const Shadow(color: Colors.black, offset: Offset(0.5, 0.5), blurRadius: 0.5)
const TextStyle _kTabTextStyle = TextStyle(color: Colors.white, shadows: [
Shadow(color: Colors.black, offset: Offset(0.5, 0.5), blurRadius: 0.5)
]);
class _TolyAppBarState extends State<TolyAppBar>

View File

@@ -149,6 +149,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
install_plugin:
dependency: "direct main"
description:
name: install_plugin
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.1"
intl:
dependency: "direct main"
description:
@@ -205,6 +212,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
path:
dependency: transitive
description:

View File

@@ -15,11 +15,12 @@ dependencies:
flutter_bloc: ^7.0.0 # 状态管理
equatable: ^2.0.0 # 相等辅助
package_info: ^2.0.2 # 应用包信息
sqflite: ^2.0.0+3 # 数据库
shared_preferences: ^2.0.5 # xml 固化
jwt_decoder: ^2.0.1 # jwt 解析
toggle_rotate: ^0.0.5
install_plugin: ^2.0.1
flutter_star: ^0.1.2 # 星星组件
url_launcher: ^6.0.3 # url
share: ^2.0.1 # 文字分享