app 应用内更新

This commit is contained in:
toly
2022-03-19 14:06:18 +08:00
parent dc4002fdb8
commit 3dedd8587a
18 changed files with 401 additions and 204 deletions

View File

@@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.toly1994.flutter_unit">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="flutter_unit"
android:icon="@mipmap/logo">

View File

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

View File

@@ -36,14 +36,13 @@ class Convert {
};
static String convertFileSize(int size){
if(size==null) return '0 kb';
double result = size / 1024.0;
if(result<1024){
return "${result.toStringAsFixed(2)}Kb";
return "${result.toStringAsFixed(2)} Kb";
}else if(result>1024&&result<1024*1024){
return "${(result/1024).toStringAsFixed(2)}Mb";
return "${(result/1024).toStringAsFixed(2)} Mb";
}else{
return "${(result/1024/1024).toStringAsFixed(2)}Gb";
return "${(result/1024/1024).toStringAsFixed(2)} Gb";
}
}

View File

@@ -1,169 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.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:flutter_unit/point_system/api/app_info.dart';
// import 'package:install_plugin/install_plugin.dart';
import 'package:package_info_plus/package_info_plus.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();
// if(dir ==null) return;
//
// 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&&result.data!=null) {
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

@@ -6,9 +6,10 @@ import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/components/permanent/circle_image.dart';
import 'package:flutter_unit/components/permanent/feedback_widget.dart';
import 'package:flutter_unit/update_part/views/app_update_panel.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
@@ -79,7 +80,7 @@ class VersionInfo extends StatelessWidget {
onTap: () => Navigator.of(context).pushNamed(UnitRouter.about_app),
),
Divider(height: 1,indent: 10),
const AppVersionChecker(),
const AppUpdatePanel(),
Divider(height: 1,indent: 10),
ListTile(
title: Text('检查数据库新版本',style: labelStyle),

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/app/blocs/global/global_bloc.dart';
import 'package:flutter_unit/app/blocs/global/global_event.dart';
import 'package:flutter_unit/bloc_exp.dart';
import 'package:flutter_unit/point_system/blocs/point_system_bloc.dart';
import 'package:flutter_unit/widget_system/repositories/repositories.dart';
import 'package:flutter_unit/user_system/bloc/authentic/bloc.dart';
@@ -65,6 +66,7 @@ class _BlocWrapperState extends State<BlocWrapper> {
create: (_) => SearchBloc(repository: repository)),
BlocProvider<PointBloc>(create: (_) => PointBloc()),
BlocProvider<UpdateBloc>(create: (_) => UpdateBloc()),
BlocProvider<PointCommentBloc>(create: (_) => PointCommentBloc()),
], child: widget.child);

View File

@@ -1,24 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/bloc_exp.dart';
import 'package:flutter_unit/app/res/cons.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/painter_system/gallery_unit.dart';
import 'package:flutter_unit/user_system/pages/user/user_page.dart';
import 'package:flutter_unit/components/project/nav/unit_bottom_bar.dart';
import 'package:flutter_unit/components/project/overlay_tool_wrapper.dart';
import 'package:flutter_unit/painter_system/gallery_unit.dart';
import 'package:flutter_unit/user_system/pages/user/user_page.dart';
import 'package:flutter_unit/widget_system/blocs/widget_system_bloc.dart';
import 'package:flutter_unit/widget_system/views/widget_system_view.dart';
import '../../blocs/color_change_bloc.dart';
/// create by 张风捷特烈 on 2020-04-11
/// contact me by email 1981462002@qq.com
/// 说明: 主题结构 左右滑页 + 底部导航栏
class UnitNavigation extends StatefulWidget {
const UnitNavigation();
@@ -33,6 +31,12 @@ class _UnitNavigationState extends State<UnitNavigation> {
// 禁止 PageView 滑动
final ScrollPhysics _neverScroll = const NeverScrollableScrollPhysics();
@override
void initState() {
super.initState();
BlocProvider.of<UpdateBloc>(context).add(CheckUpdate(appName: 'FlutterUnit'));
}
@override
void dispose() {
_controller.dispose(); //释放控制器
@@ -95,12 +99,14 @@ class _UnitNavigationState extends State<UnitNavigation> {
);
// 点击底部按钮事件,切换页面
_onTapBottomNav(int index) {
_controller.animateToPage(index, duration: const Duration(milliseconds: 200), curve: Curves.linear);
if(index!=0){
void _onTapBottomNav(int index) {
_controller.animateToPage(index,
duration: const Duration(milliseconds: 200), curve: Curves.linear);
if (index != 0) {
context.read<ColorChangeCubit>().change(Theme.of(context).primaryColor);
}else{
Color color = Cons.tabColors[context.read<ColorChangeCubit>().state.family.index];
} else {
Color color =
Cons.tabColors[context.read<ColorChangeCubit>().state.family.index];
context.read<ColorChangeCubit>().change(color);
}

5
lib/bloc_exp.dart Normal file
View File

@@ -0,0 +1,5 @@
library bloc_exp;
export 'update_part/bloc/bloc.dart';
export 'update_part/bloc/event.dart';
export 'update_part/bloc/state.dart';

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_unit/app/res/size_unit.dart';
import 'package:flutter_unit/app/res/toly_icon.dart';
import 'package:flutter_unit/update_part/views/update_red_point.dart';
import 'package:flutter_unit/components/permanent/feedback_widget.dart';
/// create by 张风捷特烈 on 2020-04-11
@@ -138,14 +139,22 @@ class _UnitBottomBarState extends State<UnitBottomBar> {
child: FeedbackWidget(
onPressed: () => _updateIndex(3),
onLongPressed: () => _onLongPress(context, 3),
child: Container(
padding: paddingR,
height: SizeUnit.bottom_nav_height,
child: Icon(
TolyIcon.yonghu,
size: getIconSizeByPosition(3),
color: getIconColorByPosition(3),
)),
child: Stack(
children: [
Container(
padding: paddingR,
height: SizeUnit.bottom_nav_height,
child: Icon(
TolyIcon.yonghu,
size: getIconSizeByPosition(3),
color: getIconColorByPosition(3),
)),
Positioned(
left: 20,
top: 5,
child: const UpdateRedPoint())
],
),
),
),
],
@@ -169,6 +178,7 @@ class _UnitBottomBarState extends State<UnitBottomBar> {
void _onLongPress(BuildContext context, int index) {
widget.onItemLongTap?.call(context, index);
}
}

View File

@@ -1,14 +1,15 @@
import 'package:equatable/equatable.dart';
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 {
static Future<ResultBean<AppInfo>> getAppVersion({required String appName}) async {
String errorMsg = "";
var result = await HttpUtil.getInstance()
.client
.get(PathUnit.appInfo)
.get(PathUnit.appInfo+"/$appName")
.catchError((err) {
errorMsg = err.toString();
});
@@ -22,6 +23,7 @@ class AppInfoApi {
appName: result.data['data']['appName'],
appVersion: result.data['data']['appVersion'],
appUrl: result.data['data']['appUrl'],
appSize: result.data['data']['appSize'],
));
} else {
return ResultBean.ok<AppInfo>(null);
@@ -31,14 +33,24 @@ class AppInfoApi {
}
}
class AppInfo{
class AppInfo extends Equatable{
final String appName;
final String appVersion;
final String appUrl;
final int appSize;
AppInfo({
required this.appName,
required this.appVersion,
required this.appUrl,
required this.appSize,
});
@override
List<Object?> get props => [appName,appVersion,appUrl,appSize];
@override
String toString() {
return 'AppInfo{appName: $appName, appVersion: $appVersion, appUrl: $appUrl, appSize: $appSize}';
}
}

View File

@@ -0,0 +1,72 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/app/utils/http_utils/result_bean.dart';
import 'package:flutter_unit/point_system/api/app_info.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:r_upgrade/r_upgrade.dart';
import 'event.dart';
import 'state.dart';
class UpdateBloc extends Bloc<UpdateEvent, UpdateState> {
UpdateBloc() : super(const NoUpdateState()) {
on<CheckUpdate>(_onCheckUpdate);
on<ResetNoUpdate>(_onResetNoUpdate);
on<DownloadEvent>(_onDownloadEvent);
on<DownloadingEvent>(_onDownloadingEvent);
}
void _onCheckUpdate(CheckUpdate event, Emitter<UpdateState> emit) async {
emit(CheckLoadingState());
// await Future.delayed(Duration(seconds: 1));
// 检测更新逻辑
ResultBean<AppInfo> result =
await AppInfoApi.getAppVersion(appName: event.appName);
PackageInfo packageInfo = await PackageInfo.fromPlatform();
if (result.status && result.data != null) {
if (packageInfo.version == result.data!.appVersion) {
emit(NoUpdateState(
isChecked: true,
checkTime: DateTime.now().millisecondsSinceEpoch,
));
} else {
if (result.data != null) {
emit(ShouldUpdateState(
oldVersion: packageInfo.version, info: result.data!));
}
}
} else {
emit(CheckErrorState(error: result.msg));
}
}
void _onResetNoUpdate(ResetNoUpdate event, Emitter<UpdateState> emit) {
emit(const NoUpdateState());
}
late int? id;
late StreamSubscription<DownloadInfo>? subscription;
void _onDownloadEvent(DownloadEvent event, Emitter<UpdateState> emit) async{
id = await RUpgrade.upgrade(event.appInfo.appUrl,
fileName: '${event.appInfo.appName}.apk', isAutoRequestInstall: true);
subscription = RUpgrade.stream.listen((DownloadInfo info) {
double progress = (info.percent ?? 0) / 100;
if (info.status! == DownloadStatus.STATUS_SUCCESSFUL) {
progress = 1;
subscription?.cancel();
add(ResetNoUpdate());
}
add(DownloadingEvent(state: DownloadingState(
appSize: event.appInfo.appSize,
progress: progress
)));
});
}
void _onDownloadingEvent(DownloadingEvent event, Emitter<UpdateState> emit) {
emit(event.state);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_unit/bloc_exp.dart';
import 'package:flutter_unit/point_system/api/app_info.dart';
abstract class UpdateEvent extends Equatable {
const UpdateEvent();
}
// 检查更新 ---> 校验,转换状态
class CheckUpdate extends UpdateEvent {
final String appName;
const CheckUpdate({required this.appName});
@override
List<Object?> get props => [appName];
}
class DownloadEvent extends UpdateEvent {
final AppInfo appInfo;
const DownloadEvent({required this.appInfo});
@override
List<Object?> get props => [appInfo];
}
class DownloadingEvent extends UpdateEvent {
final DownloadingState state;
const DownloadingEvent({required this.state});
@override
List<Object?> get props => [state];
}
// 将状态重置为 NoUpdateState
class ResetNoUpdate extends UpdateEvent {
const ResetNoUpdate();
@override
List<Object?> get props => [];
}

View File

@@ -0,0 +1,61 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_unit/point_system/api/app_info.dart';
abstract class UpdateState extends Equatable {
const UpdateState();
}
class NoUpdateState extends UpdateState {
final bool isChecked;
final int checkTime;
const NoUpdateState({this.isChecked = false, this.checkTime = 0});
@override
List<Object?> get props => [isChecked, checkTime];
}
class CheckLoadingState extends UpdateState {
const CheckLoadingState();
@override
List<Object?> get props => [];
}
class DownloadingState extends UpdateState {
final double progress;
final int appSize;
const DownloadingState({required this.progress, required this.appSize});
@override
List<Object?> get props => [progress, appSize];
}
class CheckErrorState extends UpdateState {
final String error;
const CheckErrorState({required this.error});
@override
List<Object?> get props => [error];
@override
String toString() {
return 'CheckErrorState{error: $error}';
}
}
class ShouldUpdateState extends UpdateState {
final String oldVersion;
final AppInfo info;
const ShouldUpdateState({required this.oldVersion, required this.info});
@override
List<Object?> get props => [oldVersion, info];
@override
String toString() {
return 'ShouldUpdateState{oldVersion: $oldVersion, info: $info}';
}
}

View File

@@ -0,0 +1,108 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/app/utils/Toast.dart';
import 'package:flutter_unit/app/utils/convert.dart';
import 'package:flutter_unit/bloc_exp.dart';
class AppUpdatePanel extends StatelessWidget {
const AppUpdatePanel();
@override
Widget build(BuildContext context) {
return BlocConsumer<UpdateBloc, UpdateState>(
builder: _buildByUpdateState,
listener: _listenerByUpdateState,
);
}
Widget _buildProgress(BuildContext context, double progress, int appSize) {
return Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Column(
children: [
Text(
'${(progress * 100).toStringAsFixed(2)} %',
style: TextStyle(height: 1, fontSize: 12, color: Colors.grey),
),
const SizedBox(
height: 5,
),
Text(
'${Convert.convertFileSize((appSize * progress).floor())}/${Convert.convertFileSize(appSize)}',
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: progress,
),
)
]);
}
Widget _buildByUpdateState(BuildContext context, UpdateState state) {
String info = '检查新版本';
Widget trail = const SizedBox.shrink();
if (state is ShouldUpdateState) {
info = "下载新版本";
trail = Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(
'${state.oldVersion} --> ${state.info.appVersion} ',
style: TextStyle(height: 1, fontSize: 12, color: Colors.grey),
),
const SizedBox(width: 5),
const Icon(Icons.update, color: Colors.green)
]);
}
if (state is CheckLoadingState) {
trail = CupertinoActivityIndicator();
}
if (state is DownloadingState) {
info = "新版本下载中...";
trail = _buildProgress(context, state.progress, state.appSize);
}
return ListTile(
title: Text(
info,
style: const TextStyle(fontSize: 13),
),
trailing: trail,
onTap: () => _tapByState(state, context),
);
}
void _tapByState(UpdateState state, BuildContext context) {
if (state is NoUpdateState) {
BlocProvider.of<UpdateBloc>(context)
.add(CheckUpdate(appName: 'FlutterUnit'));
}
if (state is ShouldUpdateState) {
// 处理下载的事件
BlocProvider.of<UpdateBloc>(context)
.add(DownloadEvent(appInfo: state.info));
}
}
void _listenerByUpdateState(BuildContext context, UpdateState state) {
if (state is NoUpdateState) {
if (state.isChecked) {
Toast.success(context, '当前应用已是最新版本!');
}
}
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_unit/bloc_exp.dart';
class UpdateRedPoint extends StatelessWidget {
const UpdateRedPoint({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget radPoint = Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
);
return BlocBuilder<UpdateBloc, UpdateState>(
builder: (BuildContext context, UpdateState state) {
if (state is ShouldUpdateState) {
return radPoint;
} else {
return const SizedBox.shrink();
}
},
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_unit/app/res/style/behavior/no_scroll_behavior.dart';
import 'package:flutter_unit/app/res/toly_icon.dart';
import 'package:flutter_unit/app/router/unit_router.dart';
import 'package:flutter_unit/update_part/views/update_red_point.dart';
/// create by 张风捷特烈 on 2020-03-26
/// contact me by email 1981462002@qq.com
@@ -27,12 +28,19 @@ class MePageItem extends StatelessWidget {
height: 10,
),
_buildItem(context, TolyIcon.icon_them, '应用设置', UnitRouter.setting),
_buildItem(context, TolyIcon.icon_layout, '数据管理', UnitRouter.data_manage),
_buildItem(context, TolyIcon.icon_collect, '我的收藏', UnitRouter.collect),
_buildItem(
context, TolyIcon.icon_layout, '数据管理', UnitRouter.data_manage),
_buildItem(
context, TolyIcon.icon_collect, '我的收藏', UnitRouter.collect),
Divider(
height: 1,
),
_buildItem(context, Icons.update, '版本信息', UnitRouter.version_info),
Stack(
children: [
_buildItem(context, Icons.update, '版本信息', UnitRouter.version_info,),
Positioned(left: 40, top: 10, child: UpdateRedPoint())
],
),
_buildItem(context, Icons.info, '关于应用', UnitRouter.about_app),
Divider(
height: 1,
@@ -51,7 +59,11 @@ class MePageItem extends StatelessWidget {
icon,
color: Theme.of(context).primaryColor,
),
title: Text(title),
title: Stack(
children: [
Text(title),
],
),
trailing:
Icon(Icons.chevron_right, color: Theme.of(context).primaryColor),
onTap: () {

View File

@@ -345,6 +345,13 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.1"
r_upgrade:
dependency: "direct main"
description:
name: r_upgrade
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.7+3"
share_plus:
dependency: "direct main"
description:

View File

@@ -1,7 +1,7 @@
name: flutter_unit
description: A new Flutter application.
version: 1.6.0
version: 1.6.1
author: 张风捷特烈 <1981462002@qq.com>
homepage: https://juejin.cn/user/149189281194766/posts
@@ -16,6 +16,7 @@ dependencies:
stream_transform: ^2.0.0
equatable: ^2.0.3 # 相等辅助
package_info_plus: ^1.3.0 # 应用包信息
r_upgrade: ^0.3.7+3
sqflite: ^2.0.2 # 数据库
shared_preferences: ^2.0.13 # xml 固化
jwt_decoder: ^2.0.1 # jwt 解析