Skip to content

image.png

接口

https://cloud.tencent.com/developer/article/2019869https://www.wanandroid.com/blog/show/2268https://www.wanandroid.com/blog/show/2

优化网络请求

dio框架 进一步优化网络请求,我们可以设置一个统一的API,来定义所有的请求 同样定义一个单例类,由他来实现对网络请求来的数据进行整理,提供给对应的viewmodel层调用

dart
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层的逻辑进一步简化层如下所示:

dart
///
/// @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/installimage.png 使用我们的刷新组件SmartRefresher包裹着我们需要刷新的内容即可。并且定义一个refreshController控制器可以控制刷新和加载的结束

dart
@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 **返回出去,供调用方区分是加载还是刷新

dart
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

dart
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 封装在一起,并且将组件所需要的值以参数的形式传递进来

dart
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添加一个动画

dart
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组件包裹我们的组件来设置主题,去除波纹渐变

dart
//底部导航栏
//通过设置主题去除波纹效果
bottomNavigationBar: Theme(
    data: Theme.of(context).copyWith(
        splashColor: Colors.transparent,
        highlightColor: Colors.transparent),
    child: buildBottomNavigation()),

并且自定义一个NavigationBarItem替换我们的ActiveIcon来实现选中时的动画效果

dart
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 所以需要使用如下代码禁止网格布局的滑动

dart
// 优化
shrinkWrap: true,
// 禁止滑动
physics: const NeverScrollableScrollPhysics(),
dart
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,
          ),
        ));
  }

我的页面

实现类似线性布局的效果

dart
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,
            ),
          ],
        ),
      ),
    );

注册登录

dart
@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);
              },
            ),
          )
        ],
      ),
    );
  }

简单封装输入框

dart

//通用输入框
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类来保存用户信息,以及用户的登录状态

dart
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保存用户信息

dart
  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;
  }

在需要使用用户信息的地方,全局获取即可

dart
Text(
  (UserHelper.getInstance.userIsLogin == false)
      ? "未登录"
      : UserHelper.getInstance.userBean?.username
              .toString() ??
          "默认用户",
  style: const TextStyle(
      fontSize: 18, color: Colors.black))

保存cookie信息

使用拦截器来实现,在登录接口请求成功的方法中保存返回值中的cookie,保存在本地。 然后在每次调用需要cookie的请求时,我们把cookie从本地获取,设置在请求头信息中。

dart
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

dart
@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,
                      ),
                    )
                  ],
                ),
              );
            },
          );
        },
      ),
    );
  }

image.png

收藏文章

收藏与取消收藏的请求

dart
  //收藏站内文章
  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

dart
//收藏站内文章
  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;
  }

界面层调用

dart
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),
),

退出登录

退出登录的请求

dart
  //登出操作
//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层

dart
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对象

dart
tabController =TabController(length: widget.tabList?.length ?? 0, vsync: this);
dart
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的子页面其实都是一个单独的页面

dart
  @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

dart
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函数

dart
  List<Widget> tabChildren(){
    return widget.tabList?.map((e) => KnowledgeTabChildPage(treeId : e.id)).toList() ?? [];
  }

搜索页面

搜索页面包括一个输入框和一个数据列表 这里的数据列表请求得到的标题字段是带有html样式的,我们可是使用flutter_html这个库实现在手机上加载html样式 并且我们还可以在手机中动态的修改html的样式,使用style属性,它接收一个map对象,可以指定对应标签的各种样式,例如下面的例子就是修改html标签下的字体大小为18。

dart
  #显示html文本
  flutter_html: ^3.0.0-alpha.6
dart
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))},
                  ),
                );
              }));
    });
  }

业务逻辑上,我们可以

  1. 输入要搜索的内容,回车然后就能查询到对应的内容。
  2. 点击取消把输入框中的内容清空,并且清空数据列表

image.png

自定义Loading

实现如下效果使用的是oktoast库中的自定义toast,使用showToastWidget自定义一个圆形加载指示器的吐司,在网络请求开始时展示,请求结束后关闭 image.png

dart
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会创建和销毁多次。

dart
  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/installimage.png 因为flutter_inappwebview是基于分别在android和ios下基于原生实现的。所以需要分别对两个平台做一个区别配置 image.png 封装webview,做一些基础的配置

dart
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进行封装。指定标题的展示样式

dart
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();
  }
}

具体使用:

dart
NavigatorUtils.pushPage(
  context,
  WebViewPage(
    webViewType: WebViewType.URL,
    loadResource: vm.bannerList?[index]?.url ?? "",
    title: vm.bannerList?[index]?.title,
    showTitle: true,
  ));

我的收藏页面

页面布局主要就是一个ListView 没有特别的逻辑与设计

关于我们

image.png

dart
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,)
        ]),
      )
    );
  }
}

检查更新

把应用打包上传大蒲公英 image.png 打包成功后输出再如下目录 image.png 使用蒲公英提供的平台实现检测更新。 image.png

dart

  //检查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来判断是否当前应用是否是最新的。如果有新版本那就返回下载链接

dart
  //检查更新
  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;
  }

最后在界面中使用,通弹窗供用户选中是否更新。

dart
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 image.png