更新日期

更新内容

2022年5月13日之前

豆瓣主页界面

2022年5月13日

1、添加了豆瓣电影界面代码地址;2、美食广场App项目创建

2022年5月14日

美食广场【1、初始化项目;2、路由初步配置;3、创建若干界面;4、主页界面至2.26】

2022年5月15日

美食广场【1、内容从2.27开始至2.8.4;2、代码更新至github】

2022年5月16日

美食广场【1、内容至2.9.4;2、代码更新至github】

2022年5月17日

美食广场【1、APP国际化;2、flutter测试】

1、项目一:豆瓣电影界面

代码地址

点击进入源码

1.1 评分组件

flutter android 外界纹理 flutter 界面_android

import 'package:flutter/material.dart';

main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
            primarySwatch: Colors.blue, splashColor: Colors.transparent),
        home: HYStarRating(
          rating: 5,
          count: 5,
        ));
  }
}

class HYStarRating extends StatefulWidget {
  final double rating; //分数
  final double maxRating; //最大分数
  final int count; //星星的棵树
  final double size; //星星的大小
  final Color unselectedColor; //未选中的星星颜色
  final Color selectedColor; //选中的星星颜色
  final Widget selectedImage; //选中后的图片
  final Widget unselectedImage; //未选中时的图片

  //构造函数
  HYStarRating({
    required this.rating,
    this.maxRating = 10,
    this.count = 5,
    this.size = 30,
    this.unselectedColor = const Color(0xffbbbbbb),
    this.selectedColor = const Color(0xffff0000),
    selectedImage,
    unselectedImage,

    //初始化列表
  }): unselectedImage = unselectedImage ?? Icon(Icons.star_border, color: unselectedColor, size: size),
      selectedImage = selectedImage ?? Icon(Icons.star, color: selectedColor, size: size);

  @override
  State<HYStarRating> createState() => _HYStarRatingState();
}

class _HYStarRatingState extends State<HYStarRating> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(""),
        ),
        body: Center(
          //未选中图标叠上一定数量的选中状态的图标,使用stack
          child: Stack(
            children: [
              Row(
                //设置row为最短长度,从而居中
                mainAxisSize: MainAxisSize.min,
                children: buildUnselectedStar(),
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: buildSelectedStar(),
              )
            ],
          ),
        ));
  }

  //生成指定个数的图标(未选中时的星星)
  List<Widget> buildUnselectedStar() {
    return List.generate(widget.count, (index) {
      return widget.unselectedImage;
    });
  }
  
  List<Widget> buildSelectedStar() {
    final star = widget.selectedImage;
    List<Widget> stars = [];
    double oneValue = widget.maxRating / widget.count;    //计算一个星星的分值
    int entireCount = (widget.rating / oneValue).floor();    //floor向下取整;ceil向上取整

    //完整星星
    for (var i = 0; i < entireCount; i++) {
      stars.add(star);
    }

    //剩余部分多大
    double leftWidth = ((widget.rating / oneValue) - entireCount) * widget.size;
    final halStar = ClipRect(
      clipper: HYStarClipper(leftWidth),//裁剪星星
      child: star,
    );
    stars.add(halStar);

    //超过最大判断
    if(stars.length > widget.count) {
      return stars.sublist(0, widget.count);
    }
    return stars;
  }
}

class HYStarClipper extends CustomClipper<Rect> {   //以矩形传入对象进行裁剪
  double width;

  //构造函数
  HYStarClipper(this.width);

  @override
  Rect getClip(Size size) {
    //从左侧0开始到距离20,从顶部0到距离整个大小都保留,剩余部分裁剪
    return Rect.fromLTRB(0, 0, width, size.height);
  }

  //重新剪裁
  @override
  bool shouldReclip(HYStarClipper oldClipper) {
    return oldClipper.width != width;
  }
}

1.2 虚线组件

flutter android 外界纹理 flutter 界面_flutter_02

import 'package:flutter/material.dart';

main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
            primarySwatch: Colors.blue, splashColor: Colors.transparent),
        home: Scaffold(
          appBar: AppBar(
            title: Text("Demo"),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  width: 200,
                  child: HYDashLine(
                    dashWidth: 5,
                    count: 20,
                    axis: Axis.horizontal,
                  ),
                ),
                Container(
                  height: 200,
                  child: HYDashLine(
                    dashHeight: 5,
                    count: 20,
                    axis: Axis.vertical,
                  ),
                ),
              ],
            ),
          ),
        ));
  }
}

class HYDashLine extends StatelessWidget {
  final Axis axis;
  final double dashWidth;
  final double dashHeight;
  final int count;
  final Color color;

  HYDashLine(
      {this.axis = Axis.horizontal,
      this.dashWidth = 1,
      this.dashHeight = 1,
      this.count = 10,
      this.color = Colors.red});

  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: axis,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(count, (index) {
        return SizedBox(
          width: dashWidth,
          height: dashHeight,
          child: DecoratedBox(
            decoration: BoxDecoration(color: color),
          ),
        );
      }),
    );
  }
}

1.3 项目地址

点击进入项目地址github 界面截图(在chrom上运行项目)

flutter android 外界纹理 flutter 界面_android studio_03

1.4 项目目录结构

flutter android 外界纹理 flutter 界面_ide_04


说明:assets文件夹中存放底部图标、“想看”按钮图标等静态图片;

model包放json数据转换来的对象、实体;pages包存放页面,包括group、home、main、mall、profile、subject。以home为例,与home相关的dart文件都存放在此处;main下面存放底部导航栏图标的bottom_bar_item.dart、初始化数据的initialize_items.dart;Widgets包里面是上文提到的虚线组件HYDashLineWidget和评分组件HYStarRatingWidget。最后是service包,存放网络请求数据的封装好的类。程序从main.dart开始加载。

1.5 界面介绍(以主页为例)

flutter android 外界纹理 flutter 界面_android studio_05


从main.dart开始加载

flutter android 外界纹理 flutter 界面_flutter_06


对APP主题颜色进行设置,进入HYMainPage。先设置底部的导航栏,

flutter android 外界纹理 flutter 界面_android studio_07


这里要用到IndexedStack,stack本身可以使得多个widget重叠在一起,而indexStack多了个index来指定显示在最上面的是编号为index的widget。

flutter android 外界纹理 flutter 界面_android studio_08


因为使得代码更加公正好看,就将pages数组放到initialize_item.dart中,而这个initialize_item.dart是用于初始化数据。

主页内容为一个列表,采用ListView.builder构建

flutter android 外界纹理 flutter 界面_ide_09


对于里面的每一项,在home_movie_item.dart中编写。

在此之前,需要用到封装好的HttpRequest类,调用request方法获取电影资源。

import 'package:dio/dio.dart';
import 'package:flutter_learn/service/config.dart';

class HttpRequest {
  static final BaseOptions baseOption = BaseOptions(
      baseUrl: HttpConfig.baseURL, connectTimeout: HttpConfig.timeout);
  static final Dio dio = Dio(baseOption);

  static Future<T> request<T>(String url, {method, params, inter}) async {
    //创建单独配置
    final options = Options(method: method);

    //全局拦截器
    //创建默认的拦截器
    Interceptor dInter = InterceptorsWrapper(onRequest: (options, handler) {
      print("请求拦截");
      return handler.next(options);
    }, onError: (error, handle) {
      print("错误拦截");
      return handle.next(error);
    }, onResponse: (response, handler) {
      print("响应拦截");
      handler.next(response);
    });
    List<Interceptor> inters = [dInter];

    //请求单独拦截器
    if(inter != null){
      inters.add(inter);
    }

    //统一添加
    dio.interceptors.addAll(inters);

    //发送网络请求
    try {
      Response response =
          await dio.request(url, queryParameters: params, options: options);
      //返回数据
      return response.data;
    } on DioError catch (e) {
      //返回错误
      return Future.error(e);
    }
  }
}

可以看到大的布局是Column,分别是排名、每个Item的info,再是原名;对于Item的Info,是row布局,从左至右,但是中间电影信息要嵌套expanded,使得长度可以随着两侧宽度变化,有多大占据多大。再是虚线和“想看”按钮。最后是电影信息,包裹一个Column,分别是电影标题、评分、体裁

flutter android 外界纹理 flutter 界面_android studio_10


由此,结构清晰

flutter android 外界纹理 flutter 界面_android_11

【界面完善】底部图标会有闪烁,gaplessPlayback属性设置为true,表示无缝加载Image

flutter android 外界纹理 flutter 界面_ide_12

【打印日志工具】

import 'package:stack_trace/stack_trace.dart';

class HyCustomTrace {
  final StackTrace _trace;
  final Object message;

  HyCustomTrace(this.message, this._trace){
    _parseTrace();
  }

  void _parseTrace() {
    final chain = Chain.forTrace(_trace);
    // 拿出其中一条信息
    final frames = chain.toTrace().frames;
    final frame = frames[1];
    // 打印
    print("所在文件:${frame.uri} 所在行 ${frame.line} 所在列 ${frame.column};打印信息:$message");
  }
}

flutter android 外界纹理 flutter 界面_android_13

构建一个类来专门打印Log

flutter android 外界纹理 flutter 界面_flutter_14

2、项目二:美食广场

2.1 代码地址

点击进入源码

2.2 项目创建

flutter create XXXXX

2.3 初始化项目

  • Icon
  • flutter android 外界纹理 flutter 界面_flutter_15

  • appId
  • flutter android 外界纹理 flutter 界面_android_16

  • 启动图
    Android
  • flutter android 外界纹理 flutter 界面_ide_17

  • App名称
  • flutter android 外界纹理 flutter 界面_android studio_18

  • ios
  • flutter android 外界纹理 flutter 界面_List_19

2.3.1项目目录

flutter android 外界纹理 flutter 界面_android studio_20


主目录包括UI和Core,子目录

flutter android 外界纹理 flutter 界面_List_21

2.3.2 主题

import 'package:flutter/material.dart';

class HYAppTheme {
  //共有属性
  static const double xSmallFontSize = 14;
  static const double smallFontSize = 16;
  static const double normalFontSize = 22;
  static const double largeFontSize = 24;
  static const double xLargeFontSize = 24;

  //普通模式
  static const Color norTextColors = Colors.red;
  static final ThemeData norTheme = ThemeData(
    primarySwatch: Colors.amber, //包含大部分颜色设置
    canvasColor: Color.fromRGBO(255, 254, 222, 1), //APP背景颜色
    textTheme: const TextTheme(
      bodySmall: TextStyle(fontSize: xSmallFontSize),
      displaySmall: TextStyle(fontSize: smallFontSize),
      displayMedium: TextStyle(fontSize: normalFontSize),
      displayLarge: TextStyle(fontSize: largeFontSize),
    ),
  );

  //暗黑模式
  static const Color darkTextColors = Colors.green;
  static final ThemeData darkTheme = ThemeData(
    primarySwatch: Colors.grey,
    textTheme: const TextTheme(
      bodyText1: TextStyle(
        fontSize: normalFontSize,
        color: darkTextColors,
      ),
    ),
  );
}

包括通常模式下的主题和暗黑模式下的主题,暗黑模式之后设置

2.3.3 屏幕适配

flutter android 外界纹理 flutter 界面_android_22


之前学习笔记有提到屏幕适配,这里就加上工具

import '../../ui/shared/size_fit.dart';

extension DoubleFit on double{
  double get px {
    return HYSizeFit.setPx(this);
  }
  double get rpx {
    return HYSizeFit.setRpx(this);
  }
}
import '../../ui/shared/size_fit.dart';
extension IntFit on int {
  double get px {
    return HYSizeFit.setPx(this.toDouble());
  }
  double get rpx {
    return HYSizeFit.setRpx(this.toDouble());
  }
}

用法就很简单,比如FontSize:20,就在20后面加上".px"

2.4 路由初步设置

flutter android 外界纹理 flutter 界面_android_23

import 'package:favorcate/ui/pages/main/main.dart';
import 'package:flutter/material.dart';

class HYRouter {
  static final String initialRoute = HYMainScreen.routeName; //main路由

  static final Map<String, WidgetBuilder> routes = {
    HYMainScreen.routeName: (ctx) => HYMainScreen(),

  };
  static final RouteFactory generateRoute = (setting) {
    return null;
  };
  static final RouteFactory unKnownRoute = (setting) {
    return null;
  };
}

在main.dart中添加initialRoute、routes、generateRoute、unKnownRoute

import 'package:favorcate/core/router/route.dart';
import 'package:favorcate/ui/shared/app_theme.dart';
import 'package:flutter/material.dart';

main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '美食广场',

      //主题
      theme: HYAppTheme.norTheme,

      //路由
      initialRoute: HYRouter.initialRoute,
      routes: HYRouter.routes,
      onGenerateRoute: HYRouter.generateRoute,
      onUnknownRoute: HYRouter.unKnownRoute,
    );
  }
}

2.5 创建若干界面,并添加路由映射

flutter android 外界纹理 flutter 界面_List_24

2.5.1 main.dart

设置底部导航栏BottomNavigationBar,和点击切换页面的效果

import 'package:favorcate/ui/pages/main/initialize_items.dart';
import 'package:flutter/material.dart';

class HYMainScreen extends StatefulWidget {
  static const String routeName = "/";

  @override
  State<HYMainScreen> createState() => _HYMainScreenState();
}

class _HYMainScreenState extends State<HYMainScreen> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        selectedFontSize: 14,
        unselectedFontSize: 14,
        currentIndex: _currentIndex,
        items: items,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
      ),
    );
  }
}

对pages和items做一个抽取,pages放要显示的页面,items则是底部的BottomNavigationBarItem

import 'package:favorcate/ui/pages/favor/favor.dart';
import 'package:favorcate/ui/pages/home/home.dart';
import 'package:flutter/material.dart';

final List<Widget> pages = [
  HYHomeScreen(),
  HYFavorScreen()
];
final List<BottomNavigationBarItem> items = [
  BottomNavigationBarItem(
    label: "首页",
    icon: Icon(Icons.home),
  ),
  BottomNavigationBarItem(
    label: "收藏",
    icon: Icon(Icons.star),
  ),
];

2.5.2 home.dart

import 'package:flutter/material.dart';

class HYHomeScreen extends StatelessWidget {
  static const String routeName = "/home";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("美食广场"),
      ),
      body: Center(
        child: Text(
          "美食广场",
        ),
      ),
    );
  }
}

2.5.3 favor.dart

import 'package:flutter/material.dart';

class HYFavorScreen extends StatelessWidget {
  static const String routeName = "/favor";
  const HYFavorScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的收藏"),
      ),
      body: Center(
        child: Text("我的收藏"),
      ),
    );
  }
}

运行看一下效果

flutter android 外界纹理 flutter 界面_List_25

2.6 主页

主页需要用到一些数据

2.6.1 json数据

flutter android 外界纹理 flutter 界面_android studio_26


category.json

点击获取json数据meal.json

点击获取json数据解析json数据,需要用到rootBundle,json数据解析成一个一个的对象HYCategoryModel,存储在List中。

flutter android 外界纹理 flutter 界面_android_27

2.6.2 category_model

import 'package:flutter/cupertino.dart';

class HYCategoryModel {
  String id = "";
  String title = "";
  String color = "";
  Color changedColor = const Color.fromARGB(255, 255, 255, 255);

  HYCategoryModel({required this.id, required this.title, required this.color});

  HYCategoryModel.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    title = json['title'];
    color = json['color'];

    /**
     * 1、将color转成十六进制数字
     * 2、透明度加上(或运算符,留下后六位,前两位FF拼上)
     */
    final colorInt = int.parse(color, radix: 16);
    changedColor = Color(colorInt | 0xFF000000);
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['title'] = this.title;
    data['color'] = this.color;
    return data;
  }
}

利用工具生成模型代码

flutter android 外界纹理 flutter 界面_List_28


颜色有些特殊,因为传入的是字符串,这里做了转换

2.6.3 解析json

flutter android 外界纹理 flutter 界面_List_29

import 'dart:convert';

import 'package:flutter/services.dart';

import '../model/category_model.dart';

class JsonParse {
  static Future<List<HYCategoryModel>> getCategoryData() async{
    //1、加载json文件,loadString返回的类型是Future,所以是异步操作
    final jsonString = await rootBundle.loadString("assets/json/category.json");

    /**
     * decode解析,即将json转成map或list;
     * encode则反过来
     */
    //2、将jsonString转成Map/List
    final result = json.decode(jsonString);

    //3、将Map中的内哦荣转成一个个的对象
    final resultList = result["category"];
    List<HYCategoryModel> categories = [];
    for (var json in resultList) {
      categories.add(HYCategoryModel.fromJson(json));
    }
    return categories;
  }
}

2.6.4 主页界面

flutter android 外界纹理 flutter 界面_android studio_30

import 'package:flutter/material.dart';

import '../../../core/model/category_model.dart';
import 'package:favorcate/core/services/json_parse.dart';
import '../../../core/extension/int_extension.dart';
import '../../../core/extension/double_extension.dart';

class HYHomeContent extends StatefulWidget {
  @override
  State<HYHomeContent> createState() => _HYHomeContentState();
}

class _HYHomeContentState extends State<HYHomeContent> {
  List<HYCategoryModel> _categories = [];

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

    //加载json数据
    JsonParse.getCategoryData().then((value) {
      setState(() {
        _categories = value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: EdgeInsets.all(20.px),
      itemCount: _categories.length,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( //固定个数
          crossAxisCount: 2,
          crossAxisSpacing: 20.px,
          mainAxisSpacing: 20.px,
          childAspectRatio: 1.5),
      itemBuilder: (ctx, index) {
        final bgColor = _categories[index].changedColor;
        return Container(
          decoration: BoxDecoration( //圆角
            color: bgColor,
            borderRadius: BorderRadius.circular(12),
            gradient: LinearGradient(  //渐变色
              colors: [
                bgColor.withOpacity(.5), //半透明到全透明
                bgColor
              ]
            )
          ),
          alignment: Alignment.center,
          child: Text(
            _categories[index].title,
            style: Theme.of(context).textTheme.displaySmall?.copyWith(  //copyWith:在原有基础上添加属性
              fontWeight: FontWeight.bold
            ),
          ),
        );
      },
    );
  }
}

2.6.5 界面展示

这里用浏览器展示

flutter android 外界纹理 flutter 界面_android studio_31

2.6.6 HYHomeContent代码优化

将statefulWidgets转换为statelessWidget,可以使用FutureBuilder来完成这项功能,但是不能用于频繁刷新的界面

home_content.dart

import 'package:favorcate/core/model/category_model.dart';
import 'package:flutter/material.dart';

import 'package:favorcate/core/services/json_parse.dart';
import '../../../core/extension/int_extension.dart';
import 'home_category_item.dart';

class HYHomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<HYCategoryModel>>( //泛型类
        future: HYJsonParse.getCategoryData(),
        builder: (ctx, snapshot) {
          if (!snapshot.hasData) return Center(child: CircularProgressIndicator());
          final categories = snapshot.data;
          return GridView.builder(
            padding: EdgeInsets.all(20.px),
            itemCount: categories?.length,
            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( //固定个数
                crossAxisCount: 2,
                crossAxisSpacing: 20.px,
                mainAxisSpacing: 20.px,
                childAspectRatio: 1.5),
            itemBuilder: (ctx, index) {
              return HYHomeCategoryItem(categories![index]);
            },
          );
        }
    );
  }
}

home_category_item.dart

import 'package:flutter/material.dart';

import '../../../core/model/category_model.dart';
class HYHomeCategoryItem extends StatelessWidget {
  final HYCategoryModel _category;

  HYHomeCategoryItem(this._category);

  @override
  Widget build(BuildContext context) {
    final bgColor = _category.changedColor;
    return Container(
      decoration: BoxDecoration( //圆角
          color: bgColor,
          borderRadius: BorderRadius.circular(12),
          gradient: LinearGradient(  //渐变色
              colors: [
                bgColor.withOpacity(.5), //半透明到全透明
                bgColor
              ]
          )
      ),
      alignment: Alignment.center,
      child: Text(
        _category.title,
        style: Theme.of(context).textTheme.displaySmall?.copyWith(  //copyWith:在原有基础上添加属性
            fontWeight: FontWeight.bold
        ),
      ),
    );
  }
}

2.7 菜品页面

2.7.1 网络请求数据

category.json数据采用了解析本地的json数据,那么meal.json数据就采用网络请求的方式获取

配置dio

flutter android 外界纹理 flutter 界面_ide_32


flutter android 外界纹理 flutter 界面_android_33


这里用到了之前学习案例里面的两个dart文件

flutter android 外界纹理 flutter 界面_android_34


http_request.dart

import 'package:dio/dio.dart';
import 'config.dart';

class HttpRequest {
  static final BaseOptions baseOption = BaseOptions(
      baseUrl: HttpConfig.baseURL, connectTimeout: HttpConfig.timeout);
  static final Dio dio = Dio(baseOption);

  static Future<T> request<T>(String url, {method, params, inter}) async {
    //创建单独配置
    final options = Options(method: method);

    //全局拦截器
    //创建默认的拦截器
    Interceptor dInter = InterceptorsWrapper(onRequest: (options, handler) {
      print("请求拦截");
      return handler.next(options);
    }, onError: (error, handle) {
      print("错误拦截");
      return handle.next(error);
    }, onResponse: (response, handler) {
      print("响应拦截");
      handler.next(response);
    });
    List<Interceptor> inters = [dInter];

    //请求单独拦截器
    if(inter != null){
      inters.add(inter);
    }

    //统一添加
    dio.interceptors.addAll(inters);

    //发送网络请求
    try {
      Response response =
          await dio.request(url, queryParameters: params, options: options);
      //返回数据
      return response.data;
    } on DioError catch (e) {
      //返回错误
      return Future.error(e);
    }
  }
}

config.dart

class HttpConfig {
  static const String baseURL = "http://123.207.32.32:8001/api";
  static const int timeout = 10000;
}

meal_request.dart

请求meals数据

import 'package:favorcate/core/model/meal_model.dart';

import 'http_request.dart';
class HYMealRequest {
  static Future<List<HYMealModel>> getMealData() async{
    // 1、发送网络请求
    const url = "/meal";
    final result = await HttpRequest.request(url);
    //2、json转modal
    final mealArray = result["meal"];
    List<HYMealModel> meals = [];
    for (var json in mealArray) {
      meals.add(HYMealModel.fromJson(json));
    }
    return meals;
  }
}

2.7.2 生成MealModel

工具

flutter android 外界纹理 flutter 界面_ide_35


flutter android 外界纹理 flutter 界面_android studio_36


meal_request.dart

import 'package:favorcate/core/model/meal_model.dart';

import 'http_request.dart';
class HYMealRequest {
  static Future<List<HYMealModel>> getMealData() async{
    // 1、发送网络请求
    final url = "/meal";
    final result = await HttpRequest.request(url);
    //2、json转modal
    final mealArray = result["meal"];
    List<HYMealModel> meals = [];
    for (var json in mealArray) {
      meals.add(HYMealModel.fromJson(json));
    }
    return meals;
  }
}

2.7.3 异步数据共享(provider)

flutter android 外界纹理 flutter 界面_android_37


共享meals数据,在viewmodel中创建meal_view_model.dart

flutter android 外界纹理 flutter 界面_android_38


meal_view_model.dart

import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/services/meal_request.dart';
import 'package:flutter/cupertino.dart';

class HYMealViewModel extends ChangeNotifier {
  List<HYMealModel> _meals = [];


  List<HYMealModel> get meals => _meals;

  HYMealViewModel() {
    HYMealRequest.getMealData().then((value) {
      _meals = value;
      notifyListeners();
    });
  }
}

接下来在main.dart,也就是一开始加载App处,添加懒加载

flutter android 外界纹理 flutter 界面_ide_39

main() {
  runApp(
      ChangeNotifierProvider(
        create: (ctx) => HYMealViewModel(),
          child: MyApp(),
      )
  );
}

2.7.4 初步构建meal界面

meal.dart

import 'package:favorcate/core/model/category_model.dart';
import 'package:favorcate/ui/pages/meal/meal_content.dart';
import 'package:flutter/material.dart';

class HYMealScreen extends StatelessWidget {

  static const String routeName = "/meal";

  @override
  Widget build(BuildContext context) {
    //获取参数
    final category = ModalRoute.of(context)?.settings.arguments as HYCategoryModel;

    return Scaffold(
      appBar: AppBar(
        title: Text(category.title),
      ),
      body: Center(
        child: HYMealContent(),
      ),
    );
  }
}

meal_content.dart

import '../../../core/model/category_model.dart';

class HYMealContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //全局路由查找种类
    final category =
        ModalRoute.of(context)?.settings.arguments as HYCategoryModel;

    //Consumer获取
    return Consumer<HYMealViewModel>(builder: (ctx, mealVM, child) {
      //where表示过滤,如果包含当前种类的就保留
      final meals = mealVM.meals
          .where((meal) => meal.categories.contains(category.id))
          .toList();
      return ListView.builder(
          itemCount: meals.length,
          itemBuilder: (ctx, index) {
            return ListTile(
              title: Text(meals[index].title),
            );
          });
    });
  }
}

2.7.5 界面展示

flutter android 外界纹理 flutter 界面_android_40


目前可以获取到数据,并在各个不同的分类中

2.7.6 菜品界面布局

可以采用consumer的方式获取数据,也可以采用selector

meal_content.dart

import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/viewmodel/meal_view_model.dart';
import 'package:favorcate/ui/pages/meal/meal_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:collection/collection.dart';

import '../../../core/model/category_model.dart';

class HYMealContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //全局路由查找种类
    final category =
        ModalRoute.of(context)?.settings.arguments as HYCategoryModel;
    return Selector<HYMealViewModel, List<HYMealModel>>(
      shouldRebuild: (prev, next) {
        //列表不同需要重新执行build,相同相同需要重新build
        return !const ListEquality().equals(prev, next);
      },
      selector: (ctx, mealVM) => mealVM.meals
          .where((meal) => meal.categories.contains(category.id))
          .toList(),
      builder: (ctx, meals, child) {
        return ListView.builder(
          itemCount: meals.length,
          itemBuilder: (ctx, index) {
            return HYMealItem(meals[index]);
          },
        );
      },
    );
    // //Consumer获取
    // return Consumer<HYMealViewModel>(builder: (ctx, mealVM, child) {
    //   //where表示过滤,如果包含当前种类的就保留
    //   final meals = mealVM.meals
    //       .where((meal) => meal.categories.contains(category.id))
    //       .toList();
    //   return ListView.builder(
    //       itemCount: meals.length,
    //       itemBuilder: (ctx, index) {
    //         return ListTile(
    //           title: Text(meals[index].title),
    //         );
    //       });
    // });
  }
}

传入一个个的meal,每一个item的布局在meal_item.dart中构造

flutter android 外界纹理 flutter 界面_ide_41


meal_item.dart

import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/ui/widgets/operation_item.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';

final cardRadius = 12.px;

class HYMealItem extends StatelessWidget {
  final HYMealModel _meal;

  HYMealItem(this._meal);

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: EdgeInsets.all(10.px),
      elevation: 4, //阴影
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.px)),
      child: Column(children: [buildBasicInfo(context),buildOperationInfo()]),
    );
  }

  Widget buildBasicInfo(BuildContext context) {
    return Stack(
      children: [
        ClipRRect(
          //只裁剪上边两个角
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(cardRadius),
            topRight: Radius.circular(cardRadius),
          ),
          child: Image.network(
            _meal.imageUrl,
            width: double.infinity,
            height: 250.px,
            fit: BoxFit.cover,
          ),
        ),
        Positioned(
          right: 10.px,
          bottom: 10.px,
          child: Container(
            width: 300.px,
            decoration: BoxDecoration(
              color: Colors.black54,
              borderRadius: BorderRadius.circular(6.px),
            ),
            padding: EdgeInsets.symmetric(
              horizontal: 10.px,
              vertical: 5.px,
            ),
            child: Text(
              _meal.title,
              style: Theme.of(context)
                  .textTheme
                  .displayMedium
                  ?.copyWith(color: Colors.white),
            ),
          ),
        )
      ],
    );
  }

  Widget buildOperationInfo() {
    return Padding(
      padding: EdgeInsets.all(16.px),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          HYOperationItem(Icon(Icons.schedule), "${_meal.duration}分钟"),
          HYOperationItem(Icon(Icons.restaurant), _meal.complexityStr),
          HYOperationItem(Icon(Icons.favorite), "未收藏")
        ],
      ),
    );
  }
}

难度字段

flutter android 外界纹理 flutter 界面_ide_42


flutter android 外界纹理 flutter 界面_List_43

2.7.7 界面展示

flutter android 外界纹理 flutter 界面_android_44

2.8 菜品详情界面

2.8.1 路由配置

flutter android 外界纹理 flutter 界面_flutter_45

2.8.2 界面布局及数据如何获取

要构建如下界面,主体是Column,里面包含若干个Widget。

flutter android 外界纹理 flutter 界面_List_46


在detail.dart中通过全局查询来获取meal菜品的数据

final meal = ModalRoute.of(context)?.settings.arguments as HYMealModel;

detail.dart

import 'package:favorcate/core/model/meal_model.dart';
import 'package:favorcate/core/viewmodel/favor_view_model.dart';
import 'package:favorcate/ui/pages/detail/detail_floating_button.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'detail_content.dart';

class HYDetailScreen extends StatelessWidget {
  static const String routeName = "/detail";

  @override
  Widget build(BuildContext context) {
    final meal = ModalRoute.of(context)?.settings.arguments as HYMealModel;
    return Scaffold(
      appBar: AppBar(
        title: Text(meal.title),
      ),
      body: HYDetailContent(meal),
      floatingActionButton: HYDetailFloatingButton(meal),
    );
  }
}

detail_content.dart

import 'package:favorcate/core/model/meal_model.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';

class HYDetailContent extends StatelessWidget {
  final HYMealModel _meal;

  HYDetailContent(this._meal);

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: Column(
        children: [
          buildBannerImage(),
          buildMakeTitle(context, "制作材料"),
          buildMakeMaterial(context),
          buildMakeTitle(context, "制作步骤"),
          buildMakeSteps(context),
        ],
      ),
    );
  }

  //1、横幅图片
  Widget buildBannerImage() {
    return Container(
      width: double.infinity,  //这里属性必须设置,如果图片未加载,那么Column对齐
      margin: EdgeInsets.all(5.px),
      child: ClipRRect(
        borderRadius: BorderRadius.circular(12.px),
        child: Image.network(_meal.imageUrl),
      ),
    );
  }

  Widget buildMakeMaterial(BuildContext context) {
    return buildMakeContent(
      context: context,
      child: ListView.builder(
        /**
         * 1、shrinkWrap:
         * true->内容多大就占据多大的高度,false->尽可能占据高度
         * 2、column嵌套LListView,Column需要ListView给出一个指定高度,
         * 这里不是局部滚动,所以不设置Height,而设置shrinkWrap
         */
        shrinkWrap: true,
        //禁止滚动
        physics: NeverScrollableScrollPhysics(),
        padding: EdgeInsets.zero,
        itemCount: _meal.ingredients.length,
        itemBuilder: (ctx, index) {
          return Card(
            color: Theme.of(context).colorScheme.secondary,
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 5.px, horizontal: 10.px),
              child: Text(_meal.ingredients[index]),
            ),
          );
        },
      ),
    );
  }

  Widget buildMakeSteps(BuildContext context) {
    return buildMakeContent(
        context: context,
        child: ListView.separated(
          shrinkWrap: true,
            physics: NeverScrollableScrollPhysics(),
            itemBuilder: (ctx, index) {
              return ListTile(
                leading: CircleAvatar(
                  child: Text("#${index + 1}"),
                ),
                title: Text(_meal.steps[index]),
              );
            },
            separatorBuilder: (cttx, index) {
              return Divider();
            },
            itemCount: _meal.steps.length));
  }

  Widget buildMakeTitle(BuildContext context, String title) {
    return Container(
      padding: EdgeInsets.symmetric(vertical: 10.px),
      child: Text(
        title,
        style: Theme.of(context)
            .textTheme
            .displayLarge
            ?.copyWith(fontWeight: FontWeight.bold, color: Colors.black),
      ),
    );
  }

  Widget buildMakeContent(
      {required BuildContext context, required Widget child}) {
    return Container(
        padding: EdgeInsets.all(8.px),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.px),
          border: Border.all(color: Colors.grey), //边框
          color: Colors.white,
        ),
        width: MediaQuery.of(context).size.width - 30.px, //媒体查询,距离左右15px
        child: child);
  }
}

2.8.3 收藏功能

首先创建共享数据favor,即用户收藏的菜品

favor_view_model.dart

import 'package:flutter/material.dart';
import 'package:favorcate/core/model/meal_model.dart';

class HYFavorViewModel extends ChangeNotifier {
  List<HYMealModel> _favorMeals = [];

  void addMeal(HYMealModel meal) {
    _favorMeals.add(meal);
    notifyListeners();
  }

  void removeMeal(HYMealModel meal) {
    _favorMeals.remove(meal);
    notifyListeners();
  }

  List<HYMealModel> get favorMeals => _favorMeals;

  //判断是否收藏
  bool isFavor(HYMealModel meal) {
    return _favorMeals.contains(meal);
  }

  void handleMeal(HYMealModel meal) {
    if(isFavor(meal)) {
      removeMeal(meal);
    } else {
      addMeal(meal);
    }
  }
}

flutter android 外界纹理 flutter 界面_android_47


还需要在main.dart去添加Provider

main() {
  runApp(MultiProvider(
    providers: [
      ChangeNotifierProvider(
        create: (ctx) => HYMealViewModel(),
      ),
      ChangeNotifierProvider(
        create: (ctx) => HYFavorViewModel(),
      ),
    ],
    child: MyApp(),
  ));
}

收藏按钮点击效果在detail_floating_button.dart中

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../../../core/model/meal_model.dart';
import '../../../core/viewmodel/favor_view_model.dart';

class HYDetailFloatingButton extends StatelessWidget {
  final HYMealModel meal;

  HYDetailFloatingButton(this.meal);

  @override
  Widget build(BuildContext context) {
    return Consumer<HYFavorViewModel>(
      builder: (ctx, favorVM, child) {
        final iconData =
            favorVM.isFavor(meal) ? Icons.favorite : Icons.favorite_border;
        final iconColor = favorVM.isFavor(meal) ? Colors.red : Colors.black;
        return FloatingActionButton(
          child: Icon(
            iconData,
            color: iconColor,
          ),
          onPressed: () {
            favorVM.handleMeal(meal);
          },
        );
      },
    );
  }
}

以及meal界面的点击收藏按钮

Widget buildFavorItem() {
    return Consumer<HYFavorViewModel>(
      builder: (ctx, favorVM, child) {
        final iconData =
            favorVM.isFavor(_meal) ? Icons.favorite : Icons.favorite_border;
        final favorColor = favorVM.isFavor(_meal) ? Colors.red : Colors.black;
        final title = favorVM.isFavor(_meal) ? "收藏" : "未收藏";
        return GestureDetector(
          child: HYOperationItem(
            Icon(
              iconData,
              color: favorColor,
            ),
            title,
            textColor: favorColor,
          ),
          onTap:() {
            favorVM.handleMeal(_meal);
          },
        );
      },
    );
  }

对于这里的布局,封装成一个Widget

flutter android 外界纹理 flutter 界面_List_48


operation_item.dart

import 'package:flutter/material.dart';

import 'package:favorcate/core/extension/int_extension.dart';

class HYOperationItem extends StatelessWidget {
  final Widget _icon;
  final String _title;
  final Color textColor;

  //可选参数不能以下划线开头
  HYOperationItem(this._icon, this._title, {this.textColor = Colors.black});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 80.px,
      padding: EdgeInsets.symmetric(vertical: 12.px),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          _icon,
          SizedBox(
            width: 3.px,
          ),
          Text(
            _title,
            style: TextStyle(color: textColor),
          ),
        ],
      ),
    );
  }
}

2.9 收藏界面

favor.dart

import 'package:favorcate/ui/pages/favor/favor_content.dart';
import 'package:flutter/material.dart';

class HYFavorScreen extends StatelessWidget {
  static const String routeName = "/favor";
  const HYFavorScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("我的收藏"),
      ),
      body: HYFavorContent()
    );
  }
}

favor_content.dart

import 'package:favorcate/core/viewmodel/favor_view_model.dart';
import 'package:favorcate/ui/pages/meal/meal_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class HYFavorContent extends StatelessWidget {
  const HYFavorContent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<HYFavorViewModel>(builder: (ctx, favorVM, child) {
      return ListView.builder(
        itemBuilder: (itemCtx, index) {
          if(favorVM.favorMeals.length == 0) {
            return Center(
              child: Text("未收藏美食"),
            );
          }
          return HYMealItem(favorVM.favorMeals[index]);
        },
        itemCount: favorVM.favorMeals.length,
      );
    });
  }
}

2.9.1 侧边栏

侧边栏包括点餐按钮和过滤按钮,在main.dart中添加drawer

flutter android 外界纹理 flutter 界面_android studio_49


home_drawer.dart

import 'package:favorcate/ui/pages/filter/filter.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';

class HYHomeDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Drawer(
        child: Column(
          children: [
            buildHeaderView(context),
            buildListTile(
              context,
              Icon(Icons.restaurant),
              "进餐",
              () {
                Navigator.of(context).pop();
              },
            ),
            buildListTile(context, Icon(Icons.settings), "过滤", () {
              Navigator.of(context).pushNamed(HYFilterScreen.routeName);
            }),
          ],
        ),
      ),
      width: 250.px,
    );
  }

  Widget buildHeaderView(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(bottom: 20.px),
      color: Colors.orange,
      alignment: const Alignment(0, 0.5),
      width: double.infinity,
      height: 100.px,
      child: Text(
        "开始动手",
        style: Theme.of(context)
            .textTheme
            .displayLarge
            ?.copyWith(fontWeight: FontWeight.bold, color: Colors.black),
      ),
    );
  }

  Widget buildListTile(
      BuildContext context, Widget icon, String title, Function handler) {
    return ListTile(
      leading: icon,
      title: Text(
        title,
        style: Theme.of(context).textTheme.displaySmall,
      ),
      onTap: handler as Function(), //这里指明类型Function
    );
  }
}

appBar的leading属性可以设置点击事件,弹出侧边栏

home_app_bar.dart

import 'package:flutter/material.dart';

class HYHomeAppBar extends AppBar {
  HYHomeAppBar(BuildContext context)
      : super(
          title: const Text("美食广场"),
          leading: IconButton(
            icon: const Icon(Icons.build),
            onPressed: () {
              Scaffold.of(context).openDrawer();
            },
          ),
        );
}

在route.dart中注册路由

//过滤界面
  static final RouteFactory generateRoute = (setting) {
    if (setting.name == HYFilterScreen.routeName) {
      return MaterialPageRoute(
          builder: (ctx) {
            return HYFilterScreen();
          },
          fullscreenDialog: true);
    }
    return null;
  };

2.9.2 过滤界面布局

import 'package:favorcate/core/viewmodel/filter_view_model.dart';
import 'package:flutter/material.dart';
import 'package:favorcate/core/extension/int_extension.dart';
import 'package:provider/provider.dart';

class HYFilterContent extends StatelessWidget {
  const HYFilterContent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [buildChoiceText(context), buildChoiceSelect(context)],
    );
  }

  Widget buildChoiceText(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20.px),
      alignment: Alignment.center,
      child: Text(
        "展示你的选择",
        style: Theme.of(context)
            .textTheme
            .displaySmall
            ?.copyWith(fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget buildChoiceSelect(BuildContext context) {
    return Expanded(
      child: Consumer<HYFilterViewModel>(
        builder: (ctx, filterVM, child) {
          return ListView(
            children: [
              buildListTile(
                "无谷蛋白",
                "展示无谷蛋白食物",
                filterVM.isGlutenFree,
                (value) {
                  filterVM.isGlutenFree = value;
                },
              ),
              buildListTile(
                "不含乳糖",
                "展示不含乳糖食物",
                filterVM.isLactoseFree,
                (value) {
                  filterVM.isLactoseFree = value;
                },
              ),
              buildListTile(
                "普通素食者",
                "展示普通素食者食物",
                filterVM.isVegetarian,
                (value) {
                  filterVM.isVegetarian = value;
                },
              ),
              buildListTile(
                "严格素食者",
                "展示严格素食者食物",
                filterVM.isVegan,
                (value) {
                  filterVM.isVegan = value;
                },
              ),
            ],
          );
        },
      ),
    );
  }

  Widget buildListTile(
      String title, String subtitle, bool value, Function(bool) onChange) {
    return ListTile(
      title: Text(title),
      subtitle: Text(subtitle),
      trailing: Switch(
        value: value,
        onChanged: onChange, //指明类型
      ),
    );
  }
}

2.9.3 共享数据(过滤条件共享、收藏菜品、所有菜品)

这两个共享的数据有类似的代码段,可以考虑创建一个父类,并让这两个子类继承它

flutter android 外界纹理 flutter 界面_flutter_50


在总的main.dart中配置Provider

main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (ctx) => HYFilterViewModel(),
        ),
        //HYMealViewModel 依赖 HYFilterViewModel
        ChangeNotifierProxyProvider<HYFilterViewModel, HYMealViewModel>(
          create: (ctx) => HYMealViewModel(),
          update: (ctx, filterVM, mealVM) {
            mealVM?.updateFilters(filterVM);
            return mealVM as HYMealViewModel;
          },
        ),
        ChangeNotifierProxyProvider<HYFilterViewModel, HYFavorViewModel>(
          create: (ctx) => HYFavorViewModel(),
          update: (ctx, filterVM, favorVM) {
            favorVM?.updateFilters(filterVM);
            return favorVM as HYFavorViewModel;
          },
        ),
      ],
      child: MyApp(),
    ),
  );
}

BaseMealViewModel.dart

import 'package:flutter/cupertino.dart';

import '../model/meal_model.dart';
import 'filter_view_model.dart';

class BaseMealViewModel extends ChangeNotifier {
  List<HYMealModel> _meals = [];

  HYFilterViewModel _filterVM = HYFilterViewModel();

  void updateFilters(HYFilterViewModel filterVM) {
    _filterVM = filterVM;
    notifyListeners(); //这里加上更新界面
  }

  //筛选符合条件的菜品
  List<HYMealModel> get meals {
    return _meals.where((meal) {
      if (_filterVM.isGlutenFree && !meal.isGlutenFree) return false;
      if (_filterVM.isLactoseFree && !meal.isLactoseFree) return false;
      if (_filterVM.isVegetarian && !meal.isVegetarian) return false;
      if (_filterVM.isVegan && !meal.isVegan) return false;
      return true;
    }).toList();
  }

  List<HYMealModel> get originalMeals {
    return _meals;
  }

  set meals(List<HYMealModel> value) {
    _meals = value;
    notifyListeners();
  }
}

2.9.4 界面展示

flutter android 外界纹理 flutter 界面_List_51

flutter android 外界纹理 flutter 界面_android_52


flutter android 外界纹理 flutter 界面_android studio_53

3、项目三:i18n_demo(国际化开发)

如果APP是面向国际用户,那么需要设置哪些呢?先创建新项目,用AndroidStudio打开

flutter android 外界纹理 flutter 界面_flutter_54

3.1.1 添加依赖库

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

3.1.2 添加支持的语言和delegate

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        //支持的语言
        supportedLocales: [
          Locale("zh"),
          Locale("en"),
        ],
        localizationsDelegates: [
          GlobalMaterialLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          HYLocalizationsDelegate.delegate
        ],
        home: HYHomePage());
  }
}

localizations_delegate.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:i18n_demo/i18n/localizations.dart';

class HYLocalizationsDelegate extends LocalizationsDelegate<HYLocalizations> {
  static HYLocalizationsDelegate delegate = HYLocalizationsDelegate();

  //判断当前语言是否为英文或者中文
  @override
  bool isSupported(Locale locale) {
    return ["zh", "en"].contains(locale.languageCode);
  }

  @override
  Future<HYLocalizations> load(Locale locale) {
    return SynchronousFuture(HYLocalizations(locale));
  }

  //当数据发生改变时,是否需要重新build
  @override
  bool shouldReload(covariant LocalizationsDelegate<HYLocalizations> old) {
    return false;
  }
  
}

构建本地对应的文本Map,如中文title字段就表示“首页local

localizations.dart

import 'package:flutter/cupertino.dart';

class HYLocalizations {
  final Locale locale;
  HYLocalizations(this.locale);
  static HYLocalizations of(BuildContext context) {
    return Localizations.of(context, HYLocalizations);
  }
  static Map<String, Map<String, String>> _localizaValues = {
    "en": {
      "title": "Home",
      "hello": "Hello~",
      "pickTime": "Pick a Time~"
    },
    "zh": {
      "title": "首页local",
      "hello": "您好local",
      "pickTime": "选择一个时间local"
    }
  };
  String? get title {
    return _localizaValues[locale.languageCode]!["title"];
  }
  String? get hello {
    return _localizaValues[locale.languageCode]!["hello"];
  }
  String? get pickTime {
    return _localizaValues[locale.languageCode]!["pickTime"];
  }
}

测试

class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // //不能在MyApp里面的build使用,可以拿到语言
    // HYLocalizations(Localizations.localeOf(context));

    return Scaffold(
      appBar: AppBar(
        title: Text(HYLocalizations.of(context).title as String),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(HYLocalizations.of(context).hello as String),
            RaisedButton(
                child: Text(HYLocalizations.of(context).pickTime as String),
                onPressed: () {
                  showDatePicker(
                      context: context,
                      initialDate: DateTime.now(),
                      firstDate: DateTime(2000),
                      lastDate: DateTime(3000));
                })
          ],
        ),
      ),
    );
  }
}

3.1.3 运行截图

当改变语言时,里面控件的文本对应发生了改变

flutter android 外界纹理 flutter 界面_flutter_55


flutter android 外界纹理 flutter 界面_flutter_56


flutter android 外界纹理 flutter 界面_android_57


flutter android 外界纹理 flutter 界面_List_58


flutter android 外界纹理 flutter 界面_android studio_59

3.1.4 异步加载json数据

新建一个assets文件夹,存储json数据

flutter android 外界纹理 flutter 界面_List_60


注释掉之前的json数据,将json数据存储在json文件中,修改localization.dart

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

class HYLocalizations {
  final Locale locale;
  HYLocalizations(this.locale);
  static HYLocalizations of(BuildContext context) {
    return Localizations.of(context, HYLocalizations);
  }
  static Map<String, Map<String, String>> _localizaValues = {
    // "en": {
    //   "title": "Home",
    //   "hello": "Hello~",
    //   "pickTime": "Pick a Time~"
    // },
    // "zh": {
    //   "title": "首页local",
    //   "hello": "您好local",
    //   "pickTime": "选择一个时间local"
    // }
  };
  Future loadJson() async{
    //加载json文件
    final jsonString = await rootBundle.loadString("assets/json/i18n.json");

    //对json进行解析
    Map<String, dynamic> map = json.decode(jsonString);

    _localizaValues = map.map((key, value) {
      return MapEntry(key, value.cast<String, String>());
    });
  }
  String? get title {
    return _localizaValues[locale.languageCode]!["title"];
  }
  String? get hello {
    return _localizaValues[locale.languageCode]!["hello"];
  }
  String? get pickTime {
    return _localizaValues[locale.languageCode]!["pickTime"];
  }
}

重写localizations_delegate.dart中的load方法

@override
  Future<HYLocalizations> load(Locale locale) async{
    final localizations = HYLocalizations(locale);
    await localizations.loadJson();
    return localizations;
  }

也能实现上面的效果

3.1.5 工具 flutter Intl

初始化文件

flutter android 外界纹理 flutter 界面_android_61


在生成的intl_en.arb文件中添加json数据

flutter android 外界纹理 flutter 界面_android_62


接着在main.dart中修改代码

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        //修改此处
        supportedLocales: S.delegate.supportedLocales,
        localizationsDelegates: [
          GlobalMaterialLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate,
          HYLocalizationsDelegate.delegate,
          S.delegate //添加delegate
        ],
        home: HYHomePage());
  }
}
使用插件

flutter android 外界纹理 flutter 界面_flutter_63

添加中文的local

flutter android 外界纹理 flutter 界面_List_64


添加对应的json数据

flutter android 外界纹理 flutter 界面_android_65


当语言为中文时,显示的是中文文本,英文时,则显示英文的文本

4、案例:flutter测试

4.1 单元测试

import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/unit/math_utils.dart';

void main() {
  group("test math util fime", () {
    test("math utils file sum test", () {
      final result = sum(20,30);
      expect(result, 50);
    });
    test("math utils file mul test", () {
      final result = mul(20, 30);
      expect(result, 600);
    });
  });
}

4.2 widget 测试

import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/widget/contacts.dart';

void main() {
  testWidgets("Test Contacts Widget", (WidgetTester tester) async {
    /**
     * 注:scaffold需要包裹MaterialApp,在MaterialApp中初始化
     */
    //注入需要测试的Widget
    await tester.pumpWidget(
        MaterialApp(home: HYContacts(["abc", "cba", "nma", "cab"])));

    //在HYContacts中查找Widget/Text
    final abcText = find.text("abc");
    final cbaText = find.text("cba");
    final icons = find.byIcon(Icons.people);

    //断言
    expect(abcText, findsOneWidget);
    expect(cbaText, findsOneWidget);
    expect(icons, findsNWidgets(4));
  });
}

flutter android 外界纹理 flutter 界面_ide_66

5、混合开发

5.1 概念

1、Flutter想要调用一些原生的能力

  • 相册、相机、位置信息、Map
  • pub.dev第三方插件
  • 获取原生的能力、但没有对应的插件(Dart调用原生的代码)

2、原有的APP产品(IOS、Android)

  • 原有的APP产品(IOS、Android)
  • 新功能(模块)-》Flutter开发
  • 原有模块(重构)-》Flutter开发

5.2 访问相册

添加依赖

image_picker: ^0.8.5+3

测试代码,当点击选择图片时,从相册选择一张图片显示

import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';

main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue, splashColor: Colors.transparent
        ),
        home: HYHomePage()
    );
  }
}

class HYHomePage extends StatefulWidget {
  @override
  State<HYHomePage> createState() => _HYHomePageState();
}

class _HYHomePageState extends State<HYHomePage> {
  File? _imageFile;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(""),
      ),
      body: Center(
        child: Column(
          children: [
            RaisedButton(
              onPressed: _pickImage,
              child: Text("选择照片"),
            ),
            _imageFile == null ? Image.asset("assets/image/test.jpg") : Image.file(_imageFile!),
          ],
        ),
      ),
    );
  }
  void _pickImage() async{
    final ImagePicker _picker = ImagePicker();
    final XFile? file = (await _picker.pickImage(source: ImageSource.gallery));
    setState(() {
      _imageFile = File(file?.path as String);
    });
  }
}