Flutter -- 项目实践

导航

import 'package:flutter/material.dart';
import 'home/Home.dart';
import 'study/Study.dart';
import 'mine/Mine.dart';

class Index extends StatefulWidget {
  Index({Key key}) : super(key: key);

  @override
  _IndexState createState() => _IndexState();
}

class _IndexState extends State<Index> {
  final List<BottomNavigationBarItem> bottomNavItems = [
    BottomNavigationBarItem(
      backgroundColor: Colors.blue,
      icon: Icon(Icons.home),
      label: '首页',
    ),
    BottomNavigationBarItem(
      backgroundColor: Colors.green,
      icon: Icon(Icons.message),
      label: '学习',
    ),
    BottomNavigationBarItem(
      backgroundColor: Colors.red,
      icon: Icon(Icons.person),
      label: '我',
    ),
  ];

  final List pages = [
    {
      "appBar": AppBar(
        title: Text("拉勾教育"),
        centerTitle: true,
        elevation: 0,
      ),
      "page": Home(),
    },
    {
      "appBar": AppBar(
        title: Text("学习中心"),
        centerTitle: true,
        elevation: 0,
      ),
      "page": Study(),
    },
    {
      "appBar": AppBar(
        title: Text("个人中心"),
        centerTitle: true,
        elevation: 0,
      ),
      "page": Mine(),
    },
  ];
  
  int currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: pages[currentIndex]['appBar'],
      bottomNavigationBar: BottomNavigationBar(
        items: bottomNavItems,
        currentIndex: currentIndex,
        selectedItemColor: Colors.green,
        type: BottomNavigationBarType.fixed,
        onTap: (index) async {
          setState(() {
            currentIndex = index
          });
        },
      ),
      body: pages[currentIndex ]['page']
    );
  }
}

Fluro 路由

  • 企业级的路由框架 - Fluro

  • 创建 lib/routes/RoutesHandler.dart

    import 'package:flutter/material.dart';
    import 'package:fluro/fluro.dart';
    import '../pages/Index.dart';
    import '../pages/notfound/NotFound.dart';
    import '../pages/user/Login.dart';
    import '../pages/mine/ProviderTest.dart';
    import '../pages/course/CourseDetail.dart';
    import '../pages/mine/Profile.dart';
    import '../pages/pay/Pay.dart';
    import '../pages/splash/Splash.dart';
    
    /// 空页面
    var notfoundHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return NotFound();
      }
    );
    
    /// 首页
    var indexHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return Index();
      }
    );
    
    /// 登陆页
    var loginHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return Login();
      }
    );
    
    /// Provider 功能测试页
    var providerTestHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return ProviderTest();
      }
    );
    
    /// 课程详情
    var courseDetailHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        print('详情参数');
        print(params);
        return CourseDetail(id: int.parse(params['id'].first), title: params['title'].first);
      }
    );
    
    /// 编辑个人信息
    var profileHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return Profile();
      }
    );
    
    /// 支付页面
    var payHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        print(params);
        return Pay(id: int.parse(params['id'].first), title: params['title'].first);
      }
    );
    
    /// Splash 页
    var splashHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, List<String>> params) {
        return Splash();
      }
    );
    
  • 声明路由
    创建 lib/routes/Routes.dart

    import 'package:fluro/fluro.dart';
    import 'RoutesHandler.dart';
    
    class Routes {
      static void configureRoutes(FluroRouter router) {
        router.define('/', handler: indexHandler);
        router.define('/login', handler: loginHandler);
        router.define('/provider_test', handler: providerTestHandler);
        router.define('/course_detail', handler: courseDetailHandler);
        router.define('/profile', handler: profileHandler);
        router.define('/pay', handler: payHandler);
        router.define('/splash', handler: splashHandler);
        router.notFoundHandler = notfoundHandler;
      }
    }
    
  • 然后把路由相关的内容,也放到 lib/utils/Global.dart 中

    import 'package:fluro/fluro.dart';
    
    class G {
      /// 路由
      static FluroRouter router;
    }
    
  • 在入口文件(lib/main.dart)中初始化 router

    import 'package:fluro/fluro.dart';
    import 'routes/Routes.dart';
    import 'utils/Global.dart';
    
    void main() {
      // 初始化路由
      FluroRouter router = new FluroRouter();
      Routes.configureRoutes(router);
      // 初始化后的路由,放到全局组件中
      G.router = router;
    }
    

状态管理
Flutter -- 项目实践_第1张图片
Flutter -- 项目实践_第2张图片
接口调用 - Dio

  • 初始化 Dio
    创建 lib/api/initDio.dart

    import 'package:dio/dio.dart';
    import '../utils/Global.dart';
    import '../providers/UserProvider.dart';
    import 'package:provider/provider.dart';
    
    Dio initDio() {
      // 声明 Dio 的配置项
      BaseOptions _baseOptions = BaseOptions(
        baseUrl: "http://eduboss.lagou.com", // 接口请求地址
        connectTimeout: 5000, // 请求服务器的超时时间
      );
    
      Dio dio = Dio(_baseOptions); // 初始化
    
      // 添加拦截器
      dio.interceptors.add(
        InterceptorsWrapper(
          // 请求拦截
          onRequest: (RequestOptions options) {
            print('请求之前进行拦截');
            /// 将 access_token 封装到 header 中
            var user = G.getCurrentContext().read<UserProvider>().user;
            if (user.isNotEmpty) {
              // print(user['access_token']);
              options.headers['Authorization'] = user['access_token'];
            }
          },
          // 响应拦截
          onResponse: (Response response) {
            print('响应之前进行拦截');
            if (response.data == null || response.data['state'] != 1) {
              print('响应失败:'+response.data['message']);
              response.data = null;
            }
            return response;
          },
          // 报错拦截
          onError: (DioError e) {
            return e;
          }
        )
      );
    
      // 返回 dio
      return dio;
    }
    
  • 使用 Dio

    • 创建 lib/api/API.dart

      import 'package:dio/dio.dart';
      import 'InitDio.dart';
      import 'AdAPI.dart';
      
      class API {
        Dio _dio;
      
        API() {
          _dio = initDio();
        }
      
        /// 广告接口
        AdAPI get ad => AdAPI(_dio);
      }
      
    • 创建 lib/api/API.dart

      import 'package:dio/dio.dart';
      
      class AdAPI {
        final Dio _dio;
      
        AdAPI(this._dio);
      
        /// 广告列表 = 首页
        Future<dynamic> adList({ String spaceKeys = '999'}) async {
          Response res = await _dio.get('/front/ad/getAllAds',
            queryParameters: {
              "spaceKeys": spaceKeys
            }
          );
      
          List adList = res.data['content'][0]['adDTOList'];
          return adList;
        }
      }
      
    • 为了操作方便,我们可以把常用内容统一放到一个全局文件中

      import 'package:flutter/material.dart';
      import '../api/API.dart';
      
      class G {
        /// 初始化 API
        static final API api = API();
      }
      

屏幕适配 – flutter_screenutil

ScreenUtilInit(
  designSize: Size(750, 1334), // 初始化设计尺寸
  allowFontScaling: false,  // 字体大小是否跟随终端
  builder: () => MaterialApp(
    navigatorKey: G.navigatorKey,
    title: 'Flutter Demo',
    // home: Index(),
    onGenerateRoute: G.router.generator,
    initialRoute: '/splash',
  ),
);

Flutter -- 项目实践_第3张图片
flutter_html – 富文本内容展示

  • 安装 (https://pub.dev/packages/flutter_html)

  • 配置 gradle-plugin 的中文镜像

    • 修改两个文件
      • Flutter_SDK/packages/flutter_tools/gradle/flutter.gradle
      • Flutter 项目/android/build.gradle
    • 添加一行内容
      • maven{url’https://maven.aliyun.com/repository/gradle-plugin/’
  • 使用

    import 'package:flutter_html/flutter_html.dart';
    
    //......
    
    Html(data: "

    标题1

    "
    ); /// 在 Flutter 中展示 HTML 代码

Flutter -- 项目实践_第4张图片
image_picker (调用终端的摄像头)

详情查看:https://pub.dev/packages/image_picker

showModalBottomSheet (弹出底部列表)

showModalBottomSheet(
  context: context, 
  builder: (BuildContext context) {
    return renderBottomSheet(context);
  }
);

Widget renderBottomSheet(BuildContext context) {
    return Container(
      height: 160,
      child: Column(
        children: [
          InkWell(
            onTap: () {
              _takePhoto();
              G.router.pop(context);
            },
            child: Container(
              child: Text('拍照'),
              height: 50,
              alignment: Alignment.center,
            )
          ),
          InkWell(
            onTap: () {
              _openGallery();
              G.router.pop(context);
            },
            child: Container(
              child: Text('从相册中选取'),
              height: 50,
              alignment: Alignment.center,
            )
          ),
          Container(
            color: Colors.grey[200],
            height: 10,
          ),
          InkWell(
            onTap: () {
              G.router.pop(context);
            },
            child: Container(
              child: Text('取消'),
              height: 50,
              alignment: Alignment.center,
            )
          )
        ],
      )
    );
  }

跳转到指定 URL 地址

  • 在 APP 中,跳转到指定 URL 地址,需要使用第三方插件 url_launcher。
    详情查看:https://pub.dev/packages/url_launcher

    import 'package:url_launcher/url_launcher.dart';
    /// ...
    
    doPay() {
      // 发起支付
      G.api.order.createPay(orderNo: orderNo, channel: payment).then((value) {
        if (value != false) {
          print('支付成功');
          print(value);
          /// 跳转到支付链接
          _launchURL(value['payUrl']);
        } else {
          print('支付失败');
        }
      });
    }
    
    void _launchURL(_url) async => 
      await canLaunch(_url) 
      ? 
      await launch(_url) 
      : 
      throw '不能跳转到 $_url';
    

Splash 页面

Splash 页面就是打开 APP 时,看到的第一个广告页

import 'package:flutter/material.dart';
import 'dart:async';
import '../../utils/Global.dart';

class Splash extends StatefulWidget {
  Splash({Key key}) : super(key: key);

  @override
  _SplashState createState() => _SplashState();
}

class _SplashState extends State<Splash> {
  Timer _timer;
  int counter = 3;

  /// 倒计时
  countDown() async {
    var _duration = Duration(seconds: 1);
    Timer(_duration, () {
      /// 等待1秒之后,再计时
      _timer = Timer.periodic(_duration, (timer) {
        counter--;
        if (counter == 0) {
          // 执行跳转
          goHome();
        } else {
          setState(() {
            // 标记当前组件为 dirty,然后触发 rebuild
          });
        }
      });
      return _timer;
    });
  }

  void goHome() {
    _timer.cancel();
    G.router.navigateTo(context, '/');
  }

  @override
  void initState() {
    super.initState();

    countDown(); // 指定倒计时
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment(1.0, -1.0),
      children: [
        ConstrainedBox(
          constraints: BoxConstraints.expand(),
          child: Image.asset(
            "lib/assets/images/splash.jpeg",
            fit: BoxFit.fill
          )
        ),
        Container(
          color: Colors.grey,
          margin: EdgeInsets.fromLTRB(0, 50, 10, 0),
          padding: EdgeInsets.all(5),
          child: TextButton(
            onPressed: () {
              goHome();
            }, 
            child: Text(
              "$counter 跳过广告",
              style: TextStyle(
                color: Colors.white,
                fontSize: 14
              )
            ),
          )
        ),
      ]
    );
  }

  @override
  void dispose() {
    super.dispose();
  }
}

项目优化

异步 UI 更新
试想这样一种场景:异步请求接口,在数据还未请求回来的时候,UI 就已经更新了。此时,UI 会因为拿不到数据而报错。
而异步 UI 更新,就是为了解决这一问题的

FutureBuilder

  • future 接收Future类型的值,实际上就是我们的异步函数,通常是接口请求函数

  • initialData初始数据,在异步请求完成之前使用

  • builder:是一个回调函数,接收两个参数一个AsyncWidgetBuilder类型的值

    builder: (
      BuildContext context,
      AsyncSnapshot<dynamic>snapshot
    ) {
      ///...
    }
    

    AsyncSnapshot(即 snapshot)中封装了三个内容:

    • connectionState(连接状态 - 一共有四个)
      • none:当前未连接到任何异步计算。
      • waiting:连接成功等待交互
      • active:正在交互中,可以理解为正在返回数据
      • done:交互完成,可以理解为数据返回完成。通过 snapshot.data 获取数据
    • data(实际上就是 future 执行后返回的数据)
    • error(实际上就是 future 错误时返回的错误信息)
    FutureBuilder(
            // future: Future.delayed(Duration(seconds: 3), () async {
            //   return await G.api.course.courseDetail(id: widget.id);
            // }),
            future: G.api.course.courseDetail(id: widget.id),
            builder: (context, snapshot) {
              switch (snapshot.connectionState) {
                // case ConnectionState.none: // 初始态
                //   return ElevatedButton(
                //     onPressed: () {}, 
                //     child: Text('点击发送请求i')
                //   );
                //   break;
                case ConnectionState.waiting: // 正在等待
                  return Center(
                    child: CircularProgressIndicator()
                  );
                case ConnectionState.done:
                  if (snapshot.hasError) {
                    return Center(
                      child: Text(
                        '${snapshot.error}',
                        style: TextStyle(color: Colors.red)
                      )
                    );
                  } else if (snapshot.hasData) {
                    print('异步请求成功');
                    var course = snapshot.data;
                    return SingleChildScrollView(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          // ...
                        ]
                      )
                    );
                  } else {
                    return Center(
                      child: CircularProgressIndicator()
                    );
                  }
                  break;
                default:
                  return Container();
                  break;
              }
            }
          )
        );
    

保持页面状态

  • IndexedStack
    一次加载多有的 Tab 页面,但同时,只展示其中一个。

    body: IndexedStack(
      index: curIndex,
      children: pages.map<Widget>((e) => e['page']).toList(),
    ),
    
  • AutomaticKeepAliveClientMixin
    保持某些页面的状态

    // Home page
    class Home extends StatefulWidget {
      Home({Key key}) : super(key: key);
    
      @override
      _HomeState createState() => _HomeState();
    }
    
    // 1. 使用 AutomaticKeepAliveClientMixin
    class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin {
      @override
      bool get wantKeepAlive => true; // 2. 声明 wantKeepAlive
    
      // 避免 initState 重复调用
      @override
      void initState() {
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        super.build(context); // 3. 在构造方法中调用父类的 build 方法
      }
    
      @override
      void dispose() {
        super.dispose();
      }
    }
    
  • Tab 中,只保持某些页面的状态(需要修改 Tab 实现)

    • 声明 PageController

      PageController _pageController;
      
    • 初始化 PageController

      @override
        void initState() {
          // 2. 初始化 PageController
          _pageController = PageController(
            initialPage: G.getCurrentContext().watch<CurrentIndexProvider>().currentIndex
          );
          super.initState();
        }
      
    • 修改 Tab 的 body

       body: PageView(
         controller: _pageController,
         children: pages.map<Widget>((e) => e['page']).toList(),
       )
      
    • 跳转到指定页面

      onTap: (index) async {
        // 4. 跳转到指定页面
        setState(() {
          _pageController.jumpToPage(index);
        });
      },
      

DevTools
DevTools 是一套Dart和Flutter性能调试工具。

安装 DevTools

  • 编辑器中
    在 Android Studio 或 VS Code 中,只要你安装了 Flutter 插件,则 DevTools 也已经默认安装了。

  • 在命令行中

    • 如果在你的环境变量PATH中有pub, 可以运行:

      pub global activate devtools
      
    • 如果环境变量PATH中有flutter , 可以运行:

      flutter pub global activate devtools
      

启动 DevTools

  • VS Code 中
    Flutter -- 项目实践_第5张图片

  • 命令行中

    • 如果在你的环境变量PATH中有pub, 可以运行:

      pub global run devtools
      
    • 如果环境变量PATH中有flutter , 可以运行:

      flutter pub global run devtools
      

Flutter -- 项目实践_第6张图片
启动应用

  • debug 模式启动

    flutter run
    
  • profile 模式启动

    flutterrun--profile
    

Flutter -- 项目实践_第7张图片
使用 DevTools

  • Flutter Inspector
    这是一款用于可视化和浏览 Flutter Widget 树的工具。

  • Performance
    性能分析

  • CPU Profiler
    CPU 分析器,可以通过此视图记录应用的运行会话,并查看 CPU 在哪些方法中耗费了大量时间,然后就可以决定应该在哪里进行优化。

  • Memory
    查看应用在特定时刻的内存使用情况

  • Debugger
    调试器

  • Network
    网络请求调试,例如:接口调试,HTTP 分析等

  • Logging
    查看日志,支持关键词搜索。日志内容包括:

    • Dart 运行时的垃圾回收事件。
    • Flutter 框架事件,比如创建帧的事件。
    • 应用的 stdout 和 stderr 输出。
    • 应用的自定义日志事件。
  • App Size
    App 打包后,可以对 APP 的大小进行分析

Flutter -- 项目实践_第8张图片

以 profile 方式启动 flutter (flutter run --profile)
报错: Flutter Profile mode is not supported by sdk gphone x86.
原因:模拟器是 x86 的,不支持 profile方式运行,将模拟器换成 x64 的即可。

你可能感兴趣的:(笔记,泛客户端开发,flutter)