接口
https://cloud.tencent.com/developer/article/2019869https://www.wanandroid.com/blog/show/2268https://www.wanandroid.com/blog/show/2
优化网络请求
dio框架 进一步优化网络请求,我们可以设置一个统一的API,来定义所有的请求 同样定义一个单例类,由他来实现对网络请求来的数据进行整理,提供给对应的viewmodel层调用
import '../http/dio_instance.dart';
import '../net/http_helper.dart';
import 'home/home_article_list_entity.dart';
import 'home/home_banner_entity.dart';
///
/// @DIR_PATH:lib/bean
/// @TIME:2024/6/7 18:43
/// @AUTHOR:starr
///
///
class Api {
static Api instance = Api._();
Api._();
//获取首页banner数据
Future<List<HomeBannerEntity?>?> getBannerData() async {
var rsp = await DioInstance.getInstance().get(path: HttpHelper.BANNER);
HomeBannerListEntity bannerData = HomeBannerListEntity.fromJson(rsp);
return bannerData.bannerList;
}
//获取首页文章列表
Future<List<HomeArticleListDatas>?> getHomeListData() async {
var json = await DioInstance.getInstance().get(path: HttpHelper.HOME_LIST);
HomeArticleListEntity articleData = HomeArticleListEntity.fromJson(json);
return articleData.datas;
}
//获取首页置顶数据
Future<List<HomeArticleListDatas>?> getHomeTopListData() async {
var json =
await DioInstance.getInstance().get(path: HttpHelper.HOME_TOP_LIST);
HomeTopHomeArticleListEntity topListData =
HomeTopHomeArticleListEntity.fromJson(json);
return topListData.topList;
}
}
通过加入一个API层,我们的vm层的逻辑进一步简化层如下所示:
///
/// @DIR_PATH:lib/viewmodel/home
/// @TIME:2024/5/4 14:37
/// @AUTHOR:starr
///
class HomeViewModel with ChangeNotifier {
List<HomeBannerEntity?>? bannerList;
List<HomeArticleListDatas>? homeList = [];
//首页banner图片
Future getBannerData() async {
List<HomeBannerEntity?>? list = await Api.instance.getBannerData();
bannerList = list ?? [];
notifyListeners();
}
//首页文章
Future getHomeListData() async {
List<HomeArticleListDatas>? list = await Api.instance.getHomeListData();
homeList?.addAll(list ?? []);
notifyListeners();
}
//首页置顶文章
Future getHomeTopListData() async {
List<HomeArticleListDatas>? list = await Api.instance.getHomeTopListData();
homeList?.clear();
homeList?.addAll(list ?? []);
}
//初始化首页文章列表
Future initHomeListData() async{
await getHomeTopListData();
await getHomeListData();
}
}
实现上拉刷新下拉加载
官网地址:https://github.com/peng8350/flutter_pulltorefresh/blob/master/README_CN.md pub地址:https://pub.dev/packages/pull_to_refresh/install 使用我们的刷新组件SmartRefresher包裹着我们需要刷新的内容即可。并且定义一个refreshController控制器可以控制刷新和加载的结束
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<HomeViewModel>(
create: (context) => homeViewModel,
child: SafeArea(
child: SmartRefresher(
controller: refreshController,
enablePullDown: true,
enablePullUp: true,
header: ClassicHeader(),
footer: ClassicFooter(),
onLoading: () {
//下拉加载
refreshOrLoadMore(true);
},
onRefresh: () {
//下拉刷新
homeViewModel.getBannerData().then((value){
refreshOrLoadMore(false);
});
},
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
//第一部分:banner
_banner(),
//第二部分:ListView
_listView(),
]),
),
)));
}
在这里我们也调整了home_vm的逻辑,添加一个入参loadMore来区分当前用户是刷新操作还是加载操作 并且使用了ValueChanged<bool>? callback
来对外提供带值回调,将我们的**loadMore **返回出去,供调用方区分是加载还是刷新
import 'package:flutter/cupertino.dart';
import 'package:flutter_base/bean/home/home_banner_entity.dart';
import '../../bean/api.dart';
import '../../bean/home/home_article_list_entity.dart';
///
/// @DIR_PATH:lib/viewmodel/home
/// @TIME:2024/5/4 14:37
/// @AUTHOR:starr
///
class HomeViewModel with ChangeNotifier {
int pageCount = 0;
List<HomeBannerEntity?>? bannerList;
List<HomeArticleListDatas>? homeList = [];
//首页banner图片
Future getBannerData() async {
List<HomeBannerEntity?>? list = await Api.instance.getBannerData();
bannerList = list ?? [];
notifyListeners();
}
//首页文章
Future<List<HomeArticleListDatas>?> _getHomeListData(bool loadMore) async {
List<HomeArticleListDatas>? list =
await Api.instance.getHomeListData("$pageCount");
if (list != null && list.isNotEmpty) {
return list;
} else {
//已经加载再最后一页或者加载的数据为空,
if (loadMore && pageCount > 0) {
pageCount--;
}
return [];
}
}
//首页置顶文章
Future<List<HomeArticleListDatas>?> _getHomeTopListData(bool loadMore) async {
if (loadMore) {
return [];
}
List<HomeArticleListDatas>? list = await Api.instance.getHomeTopListData();
return list;
}
//初始化首页文章列表
Future initHomeListData(bool loadMore, {ValueChanged<bool>? callback}) async {
//加载更多
if (loadMore) {
pageCount++;
} else {
pageCount = 0;
homeList?.clear();
}
//先获取置顶数据
_getHomeTopListData(loadMore).then((topList) {
//第一次加载数据
if (!loadMore) {
homeList?.addAll(topList ?? []);
}
//再获取首页列表
_getHomeListData(loadMore).then((allList) {
homeList?.addAll(allList ?? []);
notifyListeners();
callback?.call(loadMore);
});
});
}
}
简单封装一下,smartRefresh
import 'package:flutter/cupertino.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
///
/// @DIR_PATH:lib/page/common
/// @TIME:2024/6/11 11:14
/// @AUTHOR:starr
///
///
class SmartRefreshWidget extends StatelessWidget {
final bool? enablePullDown;
final bool? enablePullUp;
final Widget? header;
final Widget? footer;
final VoidCallback? onRefresh;
final VoidCallback? onLoading;
final RefreshController controller;
final Widget child;
const SmartRefreshWidget(
{super.key,
this.enablePullDown,
this.enablePullUp,
this.header,
this.footer,
this.onRefresh,
this.onLoading,
required this.controller,
required this.child});
@override
Widget build(BuildContext context) {
return SmartRefresher(
controller: controller,
enablePullUp: enablePullUp ?? true,
enablePullDown: enablePullDown ?? false,
header: header ?? const ClassicHeader(),
footer: footer ?? const ClassicFooter(),
onRefresh: onRefresh,
onLoading: onLoading,
child: child);
}
}
底部导航栏
BottomNavigationBar + PageView实现 亦可BottomNavigationBar + IndexedStack实现
封装导航栏
将PageView和BottomNavigationBar 封装在一起,并且将组件所需要的值以参数的形式传递进来
import 'package:flutter/material.dart';
///
/// @DIR_PATH:lib/page/common
/// @TIME:2024/6/11 11:52
/// @AUTHOR:starr
/// 通用的底部导航栏组件
class NavigationBarWidget extends StatefulWidget {
NavigationBarWidget(
{super.key,
required this.pages,
required this.labels,
required this.icons,
required this.onPageChange,
required this.controller,
this.activeIcons,
this.currentIndex}) {
if (pages.length != labels.length &&
pages.length != icons.length &&
pages.length != activeIcons?.length) {
throw Exception("数组长度不一致");
}
}
//页面数组
final List<Widget> pages;
//底部标题
final List<String> labels;
//导航栏icon数组:未选中
final List<Widget> icons;
//导航栏icon数组:选中
final List<Widget>? activeIcons;
//page的控制器
final PageController controller;
//当前页面
int? currentIndex;
//切换事件
ValueChanged<int>? onPageChange;
@override
State createState() {
return _NavigationBarWidgetState();
}
}
class _NavigationBarWidgetState extends State<NavigationBarWidget> {
@override
Widget build(BuildContext context) {
//Scaffold 用来搭建页面的主体结构
return Scaffold(
//页面的主内容区
//可以是单独的StatefulWidget 也可以是当前页面构建的如Text文本组件
body: SafeArea(
child: PageView(
//设置PageView不可滑动切换
// physics: const NeverScrollableScrollPhysics(),
//PageView的控制器
controller: widget.controller,
//PageView中的四个子页面
children: widget.pages,
onPageChanged: (index) {
setState(() {
widget.currentIndex = index;
});
},
),
),
//底部导航栏
bottomNavigationBar: buildBottomNavigation(),
);
}
BottomNavigationBar buildBottomNavigation() {
return BottomNavigationBar(
items: _barItemList(),
type: BottomNavigationBarType.fixed,
currentIndex: widget.currentIndex ?? 0,
onTap: (index) {
widget.controller.animateToPage(index,
duration: const Duration(microseconds: 400),
curve: Curves.easeInOutQuart);
widget.onPageChange?.call(index);
setState(() {
widget.currentIndex = index;
});
},
);
}
List<BottomNavigationBarItem> _barItemList() {
final List<BottomNavigationBarItem> items = [];
for (int index = 0; index < widget.pages.length; index++) {
items.add(BottomNavigationBarItem(
icon: widget.icons[index],
label: widget.labels[index],
activeIcon: widget.activeIcons?[index] ?? widget.icons[index]));
}
return items;
}
}
进一步我们可以去去除点击时的波纹效果,以及我们在选中时给icon添加一个动画
import 'package:flutter/material.dart';
import 'package:flutter_base/page/common/navigation_bar_item.dart';
///
/// @DIR_PATH:lib/page/common
/// @TIME:2024/6/11 11:52
/// @AUTHOR:starr
/// 通用的底部导航栏组件
class NavigationBarWidget extends StatefulWidget {
NavigationBarWidget(
{super.key,
required this.pages,
required this.labels,
required this.icons,
required this.onPageChange,
required this.controller,
this.activeIcons,
this.currentIndex}) {
if (pages.length != labels.length &&
pages.length != icons.length &&
pages.length != activeIcons?.length) {
throw Exception("数组长度不一致");
}
}
//页面数组
final List<Widget> pages;
//底部标题
final List<String> labels;
//导航栏icon数组:未选中
final List<Widget> icons;
//导航栏icon数组:选中
final List<Widget>? activeIcons;
//page的控制器
final PageController controller;
//当前页面
int? currentIndex;
//切换事件
ValueChanged<int>? onPageChange;
@override
State createState() {
return _NavigationBarWidgetState();
}
}
class _NavigationBarWidgetState extends State<NavigationBarWidget> {
@override
Widget build(BuildContext context) {
//Scaffold 用来搭建页面的主体结构
return Scaffold(
//页面的主内容区
//可以是单独的StatefulWidget 也可以是当前页面构建的如Text文本组件
body: SafeArea(
child: PageView(
//设置PageView不可滑动切换
// physics: const NeverScrollableScrollPhysics(),
//PageView的控制器
controller: widget.controller,
//PageView中的四个子页面
children: widget.pages,
onPageChanged: (index) {
setState(() {
widget.currentIndex = index;
});
},
),
),
//底部导航栏
//通过设置主题去除波纹效果
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent),
child: buildBottomNavigation()),
);
}
BottomNavigationBar buildBottomNavigation() {
return BottomNavigationBar(
items: _barItemList(),
type: BottomNavigationBarType.fixed,
currentIndex: widget.currentIndex ?? 0,
onTap: (index) {
widget.controller.animateToPage(index,
duration: const Duration(microseconds: 400),
curve: Curves.easeInOutQuart);
widget.onPageChange?.call(index);
setState(() {
widget.currentIndex = index;
});
},
);
}
List<BottomNavigationBarItem> _barItemList() {
final List<BottomNavigationBarItem> items = [];
for (int index = 0; index < widget.pages.length; index++) {
items.add(BottomNavigationBarItem(
icon: widget.icons[index],
label: widget.labels[index],
activeIcon: NavigationBarItem(builder: (context) {
return widget.activeIcons?[index] ?? widget.icons[index];
})));
}
return items;
}
}
通过如下,使用Theme组件包裹我们的组件来设置主题,去除波纹渐变
//底部导航栏
//通过设置主题去除波纹效果
bottomNavigationBar: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent),
child: buildBottomNavigation()),
并且自定义一个NavigationBarItem替换我们的ActiveIcon来实现选中时的动画效果
class NavigationBarItem extends StatefulWidget {
const NavigationBarItem({super.key, required this.builder});
final WidgetBuilder builder;
@override
State<StatefulWidget> createState() => _NavigationBarItemState();
}
class _NavigationBarItemState extends State<NavigationBarItem>
with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> animation;
@override
void initState() {
// TODO: implement initState
super.initState();
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
controller.forward();
animation = Tween<double>(begin: 0.7, end: 1).animate(controller);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(scale: animation, child: widget.builder(context));
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
热词界面
网格布局的使用,使用GridView.builder来构建网格,gridDelegate属性来设置网格之间的间距,以及宽高比 因为界面整体使用了SingleChildScrollView
所以需要使用如下代码禁止网格布局的滑动
// 优化
shrinkWrap: true,
// 禁止滑动
physics: const NeverScrollableScrollPhysics(),
Widget _gridView(bool isWebsite,
{List<CommonWebsiteEntity>? websiteList,
List<SearchHotKeyEntity>? hotKeyList,
ValueChanged<String>? itemTap}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: GridView.builder(
// 优化
shrinkWrap: true,
// 禁止滑动
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
//主轴间距
mainAxisSpacing: 5,
//主轴最大宽度
maxCrossAxisExtent: 120,
//宽高比
crossAxisSpacing: 5,
//横轴间距
childAspectRatio: 2.5),
itemBuilder: (context, index) {
if (isWebsite) {
return _item(
name: websiteList?[index].name,
itemTap: itemTap,
link: websiteList?[index].link);
} else {
return _item(name: hotKeyList?[index].name, itemTap: itemTap);
}
},
itemCount: isWebsite == true
? websiteList?.length ?? 0
: hotKeyList?.length ?? 0),
);
}
Widget _item({String? name, ValueChanged<String>? itemTap, String? link}) {
return GestureDetector(
onTap: () {
if (link != null) {
itemTap?.call(link ?? "");
} else {
itemTap?.call(name ?? "");
}
},
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 0.5),
borderRadius: const BorderRadius.all(Radius.circular(10))),
child: Text(
name ?? "",
textAlign: TextAlign.center,
),
));
}
我的页面
实现类似线性布局的效果
Widget _itemWidgetDefault(BuildContext context, GestureTapCallback onTap,
IconData leftIcon, String title, IconData rightIcon) {
return InkWell(
onTap: onTap,
child: Container(
height: 50,
margin: EdgeInsets.symmetric(horizontal: 10, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10)),
border: Border.all(color: Colors.grey, width: 0.5)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(leftIcon, size: 20),
const SizedBox(width: 20),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall,
)),
Icon(
rightIcon,
size: 18,
),
],
),
),
);
注册登录
@override
Widget build(BuildContext context) {
return Scaffold(
///填充布局
body: Stack(
children: [
//第一部分 第一层 渐变背景
buildBackground(),
//第二部分 第二层 气泡
// BubbleWidget(),
//第三部分 高斯模糊
buildBlureWidget(),
//第四部分 顶部的文字 logo 的Hero动画
buildHeroLogo(context),
//第五部分 输入框与按钮
FadeTransition(
opacity: _fadeAnimationController,
child: buildColumn(context),
),
//第六部分 左上角的关闭按钮
Positioned(
top: 44,
left: 10,
child: CloseButton(
onPressed: () {
NavigatorUtils.pop(context);
},
),
)
],
),
);
}
简单封装输入框
//通用输入框
Widget commonInput(
{String? hintText,
Icon? icon,
TextEditingController? controller,
FocusNode? focusNode,
bool? isObscure,
ValueChanged<String>? onSubmitted}) {
return TextField(
decoration: InputDecoration(
hintText: hintText ?? "",
prefixIcon: icon ?? const Icon(Icons.email_outlined)),
controller: controller,
focusNode: focusNode,
obscureText: isObscure ?? false,
onSubmitted: onSubmitted,
);
}
//通用密码输入框
Widget passwordInput(
{String? hintText,
Icon? icon,
TextEditingController? controller,
FocusNode? focusNode,
required bool isObscure,
ValueChanged<String>? onSubmitted,
VoidCallback? onObscurePress}) {
return TextField(
decoration: InputDecoration(
hintText: hintText ?? "",
prefixIcon: icon ?? const Icon(Icons.email_outlined),
suffixIcon: IconButton(
icon:
Icon(isObscure == true ? Icons.visibility_off : Icons.visibility),
onPressed: onObscurePress,
)),
controller: controller,
focusNode: focusNode,
obscureText: isObscure,
onSubmitted: onSubmitted,
);
}
完善登录
登录功能中,引入一个UserHelper类来保存用户信息,以及用户的登录状态
import 'package:flutter_base/bean/login/user_info_entity.dart';
import 'package:flutter_base/common/sp_key.dart';
import '../utils/sp_utils.dart';
///
/// @DIR_PATH:lib/common
/// @TIME:2023/11/10 16:21
/// @AUTHOR:starr
/// 用户信息操作类
class UserHelper {
// 私有构造函数
UserHelper._() {
// 具体初始化代码
}
///获取单例对象
static UserHelper getInstance = UserHelper._();
//用户基本信息模型
UserInfoEntity? _userBean;
//获取 UserBean
UserInfoEntity? get userBean => _userBean;
//userBean的设置方法
set userBean(UserInfoEntity? bean) {
_userBean = bean;
//缓存用户信息
SPUtils.saveObject(spUserBeanKey, bean);
}
///判断用户是否登录的便捷方法
bool get userIsLogin => userBean == null ? false : true;
///是否同同意隐私与用户协议
bool? _userProtocol = false;
bool? get userProtocol => _userProtocol;
set userProtocol(bool? flag) {
_userProtocol = flag;
//保存同意的标识
SPUtils.save(spUserProtocolKey, flag);
}
//判断用户是否同意用户协议便捷方法
bool? get isUserProtocol => _userProtocol ?? false;
///用来初始化用户信息的缓存数据
Future<bool> init() async {
///加载缓存数据
Map<String, dynamic> map = await SPUtils.getObject(spUserBeanKey);
///解析缓存数据
_userBean = UserInfoEntity.fromJson(map);
return Future.value(true);
}
//退出登录 清除数据
void exitLogin() {
_userBean = null;
SPUtils.remove(spUserBeanKey);
}
}
我们在LoginViewModel中,当请求登录接口成功返回用户数据时,使用UserHelper保存用户信息
Future<bool?> login() async {
UserInfoEntity? userInfo = await Api.instance
.login(name: registerInfo.name, password: registerInfo.password);
if (userInfo.username != null && userInfo.username?.isNotEmpty == true) {
//保存用户名
UserHelper.getInstance.userBean = userInfo;
return true;
}
showToast("登录失败");
return false;
}
在需要使用用户信息的地方,全局获取即可
Text(
(UserHelper.getInstance.userIsLogin == false)
? "未登录"
: UserHelper.getInstance.userBean?.username
.toString() ??
"默认用户",
style: const TextStyle(
fontSize: 18, color: Colors.black))
保存cookie信息
使用拦截器来实现,在登录接口请求成功的方法中保存返回值中的cookie,保存在本地。 然后在每次调用需要cookie的请求时,我们把cookie从本地获取,设置在请求头信息中。
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_base/common/user_helper.dart';
///
/// @DIR_PATH:lib/http
/// @TIME:2024/6/12 15:54
/// @AUTHOR:starr
/// 保存cookie拦截器
class CookieInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
//添加请求信息
options.headers[HttpHeaders.cookieHeader] =
UserHelper.getInstance.cookieList;
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.path.contains("user/login")) {
dynamic list = response.headers[HttpHeaders.setCookieHeader];
List<String> cookieList = [];
if (list is List) {
for (String? cookie in list) {
cookieList.add(cookie ?? "");
}
}
UserHelper.getInstance.cookieList = cookieList;
}
super.onResponse(response, handler);
}
}
体系页面
体系页面就是一个简单的ListView
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) {
return viewModel;
},
child: Consumer<KnowledgeViewModel>(
builder: (context, vm, child) {
return ListView.builder(
itemCount: vm.entity?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5),
),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
vm.entity?[index].name ?? "",
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(
height: 5,
),
Text(
vm.getSubTitle(vm.entity?[index].children) ?? "",
style: TextStyle(
fontSize: 14, color: Colors.grey[600]),
),
],
),
),
Container(
child: const Icon(
Icons.arrow_forward_ios,
size: 18,
),
)
],
),
);
},
);
},
),
);
}
收藏文章
收藏与取消收藏的请求
//收藏站内文章
Future<bool?> collect(String articleId) async{
var json = await DioInstance.getInstance().post(path: "lg/collect/${articleId}/json");
if(json == true && json is bool){
print("收藏 $json");
return true;
}
return false;
}
//取消收藏
//lg/uncollect_originId/2333/json
Future<bool?> unCollect(String articleId) async{
var json = await DioInstance.getInstance().post(path: "lg/uncollect_originId/$articleId/json");
if(json == true && json is bool){
print("取消收藏 $json");
return true;
}
return false;
}
对应的ViewModel
//收藏站内文章
Future<bool?> collect(String articleId,int index) async{
bool? success = await Api.instance.collect(articleId);
if(success == true){
homeList?[index].collect = true;
notifyListeners();
}
return success;
}
//取消收藏
Future<bool?> unCollect(String articleId,int index) async{
bool? success = await Api.instance.unCollect(articleId);
if(success == true){
homeList?[index].collect = false;
notifyListeners();
}
return success;
}
界面层调用
GestureDetector(
onTap: (){
if(itemData?.collect == true){
homeViewModel.unCollect("${itemData?.id}",index).then((value){
if(value == true){
showToast("取消收藏");
}
});
}else{
homeViewModel.collect("${itemData?.id}",index).then((value) {
if(value == true){
showToast("收藏成功");
}
});
}
},
child: Image.asset(
itemData?.collect == true ? "assets/images/a/ic_collect.png" : "assets/images/a/ic_uncollect.png" ,
width: 35, height: 35),
),
退出登录
退出登录的请求
//登出操作
//user/logout/json
Future<bool?> exitLogin() async{
var json = await DioInstance.getInstance().get(path: "user/logout/json");
if(json == true && json is bool){
print("退出登录 $json");
return true;
}
return false;
}
ViewModel层
import 'package:flutter/cupertino.dart';
import 'package:flutter_base/common/user_helper.dart';
import 'package:flutter_base/http/api.dart';
import 'package:flutter_base/utils/sp_utils.dart';
///
/// @DIR_PATH:lib/viewmodel/mine
/// @TIME:2024/6/13 12:29
/// @AUTHOR:starr
class MineViewModel with ChangeNotifier{
Future exitLogin(ValueChanged<bool> callback)async{
bool? isSuccess = await Api.instance.exitLogin();
if(isSuccess == true){
UserHelper.getInstance.exitLogin();
SPUtils.removeAll();
}
callback.call(isSuccess ?? false);
}
}
体系子页面
Tab的实现 tab属性需要一个TabController控制器,vsync参数传入一个SingleTickerProviderStateMixin对象
tabController =TabController(length: widget.tabList?.length ?? 0, vsync: this);
import 'package:flutter/material.dart';
import 'package:flutter_base/bean/knowledge/knowlege_entity.dart';
import 'package:flutter_base/viewmodel/knowledge/knowledge_detail_vm.dart';
import 'package:provider/provider.dart';
///
/// @DIR_PATH:lib/bean/knowledge/detail
/// @TIME:2024/6/13 14:47
/// @AUTHOR:starr
class KnowledgeDetailPage extends StatefulWidget{
const KnowledgeDetailPage({super.key, required this.tabList});
final List<KnowlegeDataChildren>? tabList;
@override
State<StatefulWidget> createState() => _KnowledgeDetailState();
}
class _KnowledgeDetailState extends State<KnowledgeDetailPage> with SingleTickerProviderStateMixin{
late TabController tabController;
KnowledgeDetailViewModel viewModel =KnowledgeDetailViewModel();
@override
void initState() {
super.initState();
tabController =TabController(length: widget.tabList?.length ?? 0, vsync: this);
viewModel.initTabs(widget.tabList);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(create: (context){
return viewModel;
},child: Scaffold(
appBar: AppBar(
title: TabBar(
tabs: viewModel.tabs,
controller: tabController,
labelColor: Colors.blue,
indicatorColor: Colors.blue,
isScrollable: true,
),
),
body: SafeArea(
child: TabBarView(
controller: tabController,
children: [],
),
),
),);
}
}
TabBarView的实现 每一个TabBarView的子页面其实都是一个单独的页面
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(create: (context){
return viewModel;
},child: Scaffold(
appBar: AppBar(
title: TabBar(
tabs: viewModel.tabs,
controller: tabController,
labelColor: Colors.blue,
indicatorColor: Colors.blue,
isScrollable: true,
),
),
body: SafeArea(
child: TabBarView(
controller: tabController,
children: tabChildren(),
),
),
),);
}
因此单独实现一个KnowledgeTabChildPage
import 'package:flutter/material.dart';
import 'package:flutter_base/page/common/smart_refresh_widget.dart';
import 'package:flutter_base/viewmodel/knowledge/knowledge_detail_vm.dart';
import 'package:provider/provider.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
///
/// @DIR_PATH:lib/viewmodel/knowledge
/// @TIME:2024/6/13 15:13
/// @AUTHOR:starr
///
class KnowledgeTabChildPage extends StatefulWidget{
const KnowledgeTabChildPage({super.key,this.treeId});
final int? treeId;
@override
State<StatefulWidget> createState() => KnowledgeTabChildState();
}
class KnowledgeTabChildState extends State<KnowledgeTabChildPage>{
KnowledgeDetailViewModel viewModel = KnowledgeDetailViewModel();
RefreshController controller = RefreshController();
@override
void initState() {
super.initState();
viewModel.getKnowledgeDetail(false, widget.treeId);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(create: (context){
return viewModel;
},child: Scaffold(body: Consumer<KnowledgeDetailViewModel>(builder: (context,vm,child){
return SmartRefreshWidget(
onLoading: (){
refreshOrLoad(true);
},
onRefresh: (){
refreshOrLoad(false);
},
controller: controller,
child: ListView.builder(
itemCount: vm.itemList.length,
itemBuilder: (context,index){
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(5),
),
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Text(
vm.itemList[index].title ?? "",
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
);
}
)
);
})));
}
void refreshOrLoad(loadMore){
viewModel.getKnowledgeDetail(loadMore, widget.treeId).then((value){
if(loadMore){
controller.loadComplete();
}else{
controller.refreshCompleted();
}
});
}
}
而tabbarView的数量就是我们的tab的数量,所以可以使用map函数
List<Widget> tabChildren(){
return widget.tabList?.map((e) => KnowledgeTabChildPage(treeId : e.id)).toList() ?? [];
}
搜索页面
搜索页面包括一个输入框和一个数据列表 这里的数据列表请求得到的标题字段是带有html样式的,我们可是使用flutter_html这个库实现在手机上加载html样式 并且我们还可以在手机中动态的修改html的样式,使用style
属性,它接收一个map对象,可以指定对应标签的各种样式,例如下面的例子就是修改html标签下的字体大小为18。
#显示html文本
flutter_html: ^3.0.0-alpha.6
Widget _listView() {
return Consumer<SearchViewModel>(builder: (context, vm, child) {
return Expanded(
child: ListView.builder(
itemCount: vm.searchList?.length ?? 0,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(width: 1,color: Colors.grey))),
child: Html(
data: vm.searchList?[index].title ?? "",
style: {"html": Style(fontSize: FontSize(18))},
),
);
}));
});
}
业务逻辑上,我们可以
- 输入要搜索的内容,回车然后就能查询到对应的内容。
- 点击取消把输入框中的内容清空,并且清空数据列表
自定义Loading
实现如下效果使用的是oktoast库中的自定义toast,使用showToastWidget自定义一个圆形加载指示器的吐司,在网络请求开始时展示,请求结束后关闭
import 'package:flutter/material.dart';
import 'package:oktoast/oktoast.dart';
///
/// @DIR_PATH:lib/page/common
/// @TIME:2024/6/22 16:41
/// @AUTHOR:starr
///
class Loading {
Loading._(); //私有化构造方法
static bool showing = false;
static void dismissAll() {
dismissAllToast();
showing = false;
}
static Future show() async {
if(!showing){
showing = true;
showToastWidget(
Container(
constraints: const BoxConstraints.expand(),//并且撑满整个屏幕
color: Colors.transparent,
child: Align(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20),color: Colors.grey),
child: const CircularProgressIndicator(//圆形进度指示器
//圆形进度指示器
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
),
),
duration: Duration(days: 1),
handleTouch: true //无法触摸屏幕
);
}
}
}
如何使用,可以在直接在dio_instace中调用,每一次网络请求前展示,结束后自动关闭 这样写的好处是,不用在每一个界面中都去写show和dismiss。但是这样写也存在问题,那就是如果加载一个页面执行了多个网络请求,那么Loading会创建和销毁多次。
get({
required String path,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async{
Response? response;
try {
Loading.show();
response = await _dio.get(path,
queryParameters: params,
options: options ?? Options(
method: HttpMethod.GET,
receiveTimeout: _defaultTime,
sendTimeout: _defaultTime
),
cancelToken: cancelToken);
Loading.dismissAll();
}on DioException catch(e){
showToast(e.toString());
Loading.dismissAll();
}
return response?.data;
}
webview页面的实现
使用的是一个插件 https://pub.dev/packages/flutter_inappwebview/install 因为flutter_inappwebview是基于分别在android和ios下基于原生实现的。所以需要分别对两个平台做一个区别配置 封装webview,做一些基础的配置
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter_base/page/common/loading_toast.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
///
/// @DIR_PATH:lib/page/common
/// @TIME:2024/6/22 18:34
/// @AUTHOR:starr
///
///需要加载的内容类型
enum WebViewType {
HTML,
URL,
}
//定义js通信回调
typedef dynamic JsChannelCallback(List<dynamic> argument);
class WebViewWidget extends StatefulWidget {
const WebViewWidget(
{super.key,
required this.webViewType,
required this.loadResource,
this.jsChannelMap,
this.onWebViewCreated,
this.clearCache});
//需要加载的内容类型
final WebViewType webViewType;
//需要加载的内容
final String loadResource;
//是否清除缓存再加载
final bool? clearCache;
//与js通信的channel集合
final Map<String, JsChannelCallback>? jsChannelMap;
//提供接口,供外部自定义实现webview的加载逻辑
final Function(InAppWebViewController controller)? onWebViewCreated;
@override
State<StatefulWidget> createState() {
return _WebViewWidgetState();
}
}
class _WebViewWidgetState extends State<WebViewWidget> {
late InAppWebViewController webViewController;
final GlobalKey webviewKey = GlobalKey();
//因为flutter_inappwebview是基于分别在android和ios下基于原生实现的
//所以需要分别对两个平台做一个区别配置
InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
//跨平台配置,两个平台下都会生效
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
mediaPlaybackRequiresUserGesture: false,
),
//安卓平台下的配置
android: AndroidInAppWebViewOptions(
builtInZoomControls: false, //不允许缩放
useHybridComposition: true //支持Hybrid
),
//IOS平台下的配置
ios: IOSInAppWebViewOptions(allowsInlineMediaPlayback: true));
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return InAppWebView(
key: webviewKey,
initialOptions: options,
onWebViewCreated: (controller) {
webViewController = controller;
//是否清除缓存再加载
if (widget.clearCache == true) {
controller.clearCache();
}
if (widget.onWebViewCreated == null) {
if (widget.webViewType == WebViewType.HTML) {
webViewController.loadData(data: widget.loadResource);
} else if (widget.webViewType == WebViewType.URL) {
webViewController.loadUrl(
urlRequest: URLRequest(url: Uri.parse(widget.loadResource)));
}
} else {
widget.onWebViewCreated?.call(controller);
}
//注册与js通信回调
widget.jsChannelMap?.forEach((handlerName, callback) {
webViewController.addJavaScriptHandler(
handlerName: handlerName, callback: callback);
});
},
onConsoleMessage: (controller, consoleMessage) {
log("consoleMessage ====来自与js的打印==== \n $consoleMessage");
},
onProgressChanged: (InAppWebViewController controller, int progress) {},
onLoadStart: (InAppWebViewController controller, Uri? url) {
Loading.show();
},
onLoadStop: (InAppWebViewController controller, Uri? url) {
Loading.dismissAll();
},
onLoadError: (InAppWebViewController controller, Uri? url, int code,
String message) {
Loading.dismissAll();
},
onPageCommitVisible: (InAppWebViewController controller, Uri? url) {},
);
}
}
对webview_page进行封装。指定标题的展示样式
import 'package:flutter/material.dart';
import 'package:flutter_base/page/common/webview_widget.dart';
import 'package:flutter_html/flutter_html.dart';
///
/// @DIR_PATH:lib/page/home
/// @TIME:2024/5/3 8:52
/// @AUTHOR:starr
///
class WebViewPage extends StatefulWidget {
final String? link;
const WebViewPage(
{required this.webViewType,
required this.loadResource,
super.key,
this.title,
this.link,
this.showTitle,
this.jsChannelMap});
//是否显示标题
final bool? showTitle;
//标题内容
final String? title;
//需要加载的内容类型
final WebViewType webViewType;
//需要加载的数据
final String loadResource;
//与js通信的channel集合
final Map<String, JsChannelCallback>? jsChannelMap;
@override
_WebViewPageState createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: (widget.showTitle ?? false)
? AppBar(
title: _buildAppBarTitle(widget.showTitle, widget.title),
)
: null,
),
body: SafeArea(
child: WebViewWidget(
webViewType: widget.webViewType,
loadResource: widget.loadResource,
jsChannelMap: widget.jsChannelMap,
),
));
}
Widget _buildAppBarTitle(bool? showTitle, String? title) {
var show = showTitle ?? false;
return show
? Html(
data: title ?? "",
style: {"html": Style(fontSize: FontSize(15))},
)
: const SizedBox.shrink();
}
}
具体使用:
NavigatorUtils.pushPage(
context,
WebViewPage(
webViewType: WebViewType.URL,
loadResource: vm.bannerList?[index]?.url ?? "",
title: vm.bannerList?[index]?.title,
showTitle: true,
));
我的收藏页面
页面布局主要就是一个ListView 没有特别的逻辑与设计
关于我们
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
///
/// @DIR_PATH:lib/page/mine
/// @TIME:2024/6/23 13:39
/// @AUTHOR:starr
/// 关于我的页面
///
class AboutUsPage extends StatefulWidget {
const AboutUsPage({super.key});
@override
State<StatefulWidget> createState() => AboutUsPageState();
}
class AboutUsPageState extends State<AboutUsPage> {
String _versionName = "v";
Future getVersion() async {
PackageInfo info = await PackageInfo.fromPlatform();
var version = info.version;
_versionName = "v$version";
print(_versionName);
setState(() {});
}
@override
void initState() {
super.initState();
//等待页面初始化完成再去设置信息
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
getVersion();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("关于我们"),
),
body: SafeArea(
child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
Image.asset("assets/images/a/ic_app.png",
width: 50, height: 50),
Text(_versionName),
const SizedBox(width: double.infinity,)
]),
)
);
}
}
检查更新
把应用打包上传大蒲公英 打包成功后输出再如下目录 使用蒲公英提供的平台实现检测更新。
//检查app版本是否有更新
//https://www.pgyer.com/apiv2/app/check
Future<CheckAppUpdateData> checkAppUpdate() async{
var json = await DioInstance.getInstance().changeBaseUrl("https://www.pgyer.com/")
.post(
path: "apiv2/app/check",
params: {"_api_key":"e37969d742fb5c1f1eee813481f409d8","appKey":"e531ad80fcbef36794a7f93ffc8ee672"});
DioInstance.getInstance().changeBaseUrl("https://www.wanandroid.com/");
CheckAppUpdateEntity update = CheckAppUpdateEntity.fromJson(json);
return update.data;
}
通过比较当前应用的versionCode与线上版本的buildBuildVersion来判断是否当前应用是否是最新的。如果有新版本那就返回下载链接
//检查更新
Future<String?> checkAppUpdate() async{
var packInfo=await PackageInfo.fromPlatform();
String versionCode = packInfo.buildNumber;
CheckAppUpdateData data = await Api.instance.checkAppUpdate();
String onlineAppVersionCode = data.buildBuildVersion ?? "0";
print("lixu ${int.tryParse(versionCode)} ${int.tryParse(onlineAppVersionCode)}");
try{
if((int.tryParse(versionCode) ?? 0) < ((int.tryParse(onlineAppVersionCode) ?? 0))){
SPUtils.save(SP_APP_VERSION, onlineAppVersionCode);
return data.downloadURL;
}else{
SPUtils.save(SP_APP_VERSION, versionCode);
return null;
}
}catch(e){
SPUtils.save(SP_APP_VERSION, versionCode);
return null;
}
}
//跳转外部应用
Future jumpToOutLink(String url)async{
final uri = Uri.parse(url ?? "");
if(await canLaunchUrl(uri)){
return launchUrl(uri);
}
return null;
}
最后在界面中使用,通弹窗供用户选中是否更新。
viewModel.checkAppUpdate().then((url){
if(url!=null && url.isNotEmpty==true){
showCommonAlertDialog(context: context,
selectText: "更新",
cancleText: "取消", contentMessag:"检查到有新版本!",
selectCallBack: (){
viewModel.jumpToOutLink(url);
});
}else{
showToast("当前已经是最新版本");
}
});
这里使用到了url_launcher库,来实现跳转到外部应用,这里是跳转到手机默认浏览器去下载apk