Flutter:仿京东项目实战(1)-首页功能实现

在我个人认为学习一门新的语言(快速高效学习) 一定是通过实践,最好的就是做项目,这里我会简单写一个京东的Demo。

Scalfold Widget 是用来描述页面的主结构,一个 MaterialApp 由多个 Scalfold 页面组成,每个 Scalfold 的普遍结果如下:

  • AppBar:顶部导航栏
  • body:中间内容体
  • BottomNavigationBar:底部导航栏

第一天搭建项目框架,实现首页的功能。

第二天实现 分类和商品列表页面juejin.cn/post/704471…

第三天实现 商品详情页功能实现juejin.cn/post/704618…

Flutter-混合工程的持续集成实践juejin.cn/post/704209…

用到的知识点

  1. BottomNavigationBar 底部导航栏基本属性

截屏2021-12-21 上午9.53.18.png

  1. 命名路由:关于更多内容可以参考 Flutter-导航与路由堆栈详解

  2. 屏幕适配:使用了flutter_screenutil插件,具体有以下这些属性

传入设计稿的px尺寸 px px px !
ScreenUtil().setWidth(540)  //根据屏幕宽度适配尺寸
ScreenUtil().setHeight(200) //根据屏幕高度适配尺寸
ScreenUtil().setSp(24)     //适配字体
ScreenUtil().setSp(24, allowFontScalingSelf: true) //适配字体(根据系统的“字体大小”辅助选项来进行缩放)
ScreenUtil().setSp(24, allowFontScalingSelf: false) //适配字体(不会根据系统的“字体大小”辅助选项来进行缩放)
ScreenUtil().pixelRatio       //设备的像素密度
ScreenUtil().screenWidth     //设备宽度
ScreenUtil().screenHeight     //设备高度
ScreenUtil().bottomBarHeight  //底部安全区距离,适用于全面屏下面有按键的
ScreenUtil().statusBarHeight  //状态栏高度 刘海屏会更高  单位dp
ScreenUtil().textScaleFactor //系统字体缩放比例
ScreenUtil().scaleWidth  // 实际宽度的dp与设计稿px的比例
ScreenUtil().scaleHeight // 实际高度的dp与设计稿px的比例
复制代码
  1. 网络请求使用 dio 插件实现,详解可以看官网:github.com/flutterchin…
import 'package:dio/dio.dart';

void main() async {
  var dio = Dio();
  final response = await dio.get('https://google.com');
  print(response.data);
}
复制代码

配置抓包

  • 引入这两个dio 的头文件
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
复制代码
  • 配置抓包代码
//设置只在debug模式下抓包
final kReleaseMode = false;
final Dio dio = Dio();
if (!kReleaseMode){
  //设置代理 抓包用
  (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) {
    client.findProxy = (uri) {
      return "PROXY localhost:8888";
    };
  };
}
复制代码
  1. 轮播图使用 swiper 插件实现

截屏2021-12-20 下午8.51.14.png

SwiperPagination 属性说明:

//如果要将分页指示器放到其他位置,可以修改这个参数
alignment Alignment.bottomCenter 
分页指示器与容器边框的距离
margin EdgeInsets.all(10.0)
分页指示器的样式,fraction自定义样式
builder SwiperPagination.dots
复制代码
  1. 页面框架使用 ListView 实现列表

截屏2021-12-20 下午9.33.30.png

  1. 商品列表用到了 Wrap ,流式布局、自动换行
属性解析
direction:主轴(mainAxis)的方向,默认为水平。
alignment:主轴方向上的对齐方式,默认为start。
spacing:主轴方向上的间距。
runAlignment:run的对齐方式。run可以理解为新的行或者列,如果是水平方向布局的话,run可以理解为新的一行。
runSpacing:run的间距。
crossAxisAlignment:交叉轴(crossAxis)方向上的对齐方式。
textDirection:文本方向。
verticalDirection:定义了children摆放顺序,默认是down,见Flex相关属性介绍。
复制代码

项目的搭建

创建 tabs文件夹,里面添加tabs.dart和4个底导首页(home.dart、category.dart、cart.dart、user.dart)。

这里只是贴出主要代码:

bottomNavigationBar: BottomNavigationBar(
  currentIndex:_currentIndex ,
  onTap: (index){
      setState(() {
         _currentIndex=index;
      });
  },
  type:BottomNavigationBarType.fixed ,
  fixedColor:Colors.red,
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.home),
      label: "首页"
    ),
     BottomNavigationBarItem(
      icon: Icon(Icons.category),
      label: "分类"
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.shopping_cart),
      label: "购物车"
    ),            
    BottomNavigationBarItem(
      icon: Icon(Icons.people),
      label: "我的"
    )
  ],
),
复制代码

配置命名路由

这里为了便于统一管理,创建一个routers文件夹,在里面创建一个router.dart类来管理路由

//配置路由
final Map<String,Function> routes = {
  '/': (context) => Tabs(),arguments,),
};

//固定写法
var onGenerateRoute = (RouteSettings settings) {
  final String? name = settings.name;
  final Function? pageContentBuilder = routes[name];
  if (pageContentBuilder != null) {
    if (settings.arguments != null) {
      final Route route = MaterialPageRoute(
          builder: (context) =>
              pageContentBuilder(context, arguments: settings.arguments));
      return route;
    } else {
      final Route route =
          MaterialPageRoute(builder: (context) => pageContentBuilder(context));
      return route;
    }
  }
};
复制代码

屏幕适配

这里我选择flutter_screenutil实现屏幕适配,flutter_screenutil 默认以750*1334 设计稿为标准 在 pubspec.yaml 添加

flutter_screenutil: ^5.0.0+2
复制代码

然后点击 Pub get 拉取。创建一个services文件夹,在里面创建screen_adapter.dart类,在里面实现常用的功能

import 'package:flutter_screenutil/flutter_screenutil.dart';

class ScreenAdapter {
  //宽、高、字号大小转换
  static height(num value) {
    return ScreenUtil().setHeight(value);
  }

  static width(num value) {
    return ScreenUtil().setWidth(value);
  }
  
  static size(num value) {
    return ScreenUtil().setSp(value);
  }

  //获取设备的物理宽度
  static getScreenWidth() {
    return ScreenUtil().screenWidth; 
  }

  //获取设备的物理高度
  static getScreenHeight() {
    return ScreenUtil().screenHeight; 
  }
  
  //状态栏高度
  static double statusBarHeight = ScreenUtil().statusBarHeight;
  //底部安全区域距离
  static double bottomBarHeight = ScreenUtil().bottomBarHeight;
}
复制代码

具体实现

Simulator Screen Shot - iPhone 12 Pro - 2021-12-20 at 20.25.29.png

页面整体框架使用 ListView 实现,而内容是分下面三大块实现

  • banner区域的轮播图,通过 Swiper 插件实现
  • 猜你喜欢的横向滚动列表,通过 ListView 实现
  • 热门推荐的垂直滚动列表,通过 Wrap 实现

实现banner区域的轮播图

首先引入 flutter_swiper_null_safety: ^1.0.2, 轮播图是网络获取的数据,还需要引入 dio: ^4.0.0 进行网络请求,然后执行 pub get 拉取。

创建轮播图的模型 focus_model.dart,里面的代码实现为

class FocusModel {
  List<FocusItemModel> result=[];
  FocusModel({required this.result});
  FocusModel.fromJson(Map<String, dynamic> json) {
    if (json['result'] != null) {      
      json['result'].forEach((v) {
        result.add(new FocusItemModel.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['result'] = this.result.map((v) => v.toJson()).toList();
    return data;
  }
}

class FocusItemModel {
  String? sId;  //可空类型
  String? title;
  String? status;
  String? pic;
  String? url;

  FocusItemModel({this.sId, this.title, this.status, this.pic, this.url});
  FocusItemModel.fromJson(Map<String, dynamic> json) {
    sId = json['_id'];
    title = json['title'];
    status = json['status'];
    pic = json['pic'];
    url = json['url'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['_id'] = this.sId;
    data['title'] = this.title;
    data['status'] = this.status;
    data['pic'] = this.pic;
    data['url'] = this.url;
    return data;
  }
}
复制代码

获取banner数据

//获取热门推荐的数据
_getBestProductData() async {
  var api = 'https://jdmall.itying.com/api/plist?is_best=1';
  var result = await Dio().get(api);
  var bestProductList = ProductModel.fromJson(result.data);    
  setState(() {
    this._bestProductList = bestProductList.result;
  });
}
复制代码

创建轮播图

Widget _swiperWidget() {
  if (this._focusData.length > 0) {
    return Container(
      child: AspectRatio(
        aspectRatio: 2 / 1,
        child: Swiper(
            itemBuilder: (BuildContext context, int index) {
              String pic = this._focusData[index].pic;
              pic = Config.domain + pic.replaceAll('\', '/');
              return new Image.network(
                "${pic}",
                fit: BoxFit.fill,
              );
            },
            itemCount: this._focusData.length,
            pagination: new SwiperPagination(),
            autoplay: true),
      ),
    );
  } else {
    return Text('加载中...');
  }
}
复制代码

具体代码如下:

List _focusData = [];
@override
void initState() {
  super.initState();
  _getFocusData();
}

@override
Widget build(BuildContext context) {
  return ListView(
    children: <Widget>[
      _swiperWidget(),
    ],
  );
}
复制代码

然后运行项目就实现了这样的效果,轮播图的功能做好了

Simulator Screen Shot - iPhone 12 Pro - 2021-12-20 at 20.46.25.png

实现猜你喜欢的效果

封装一个方法,返回一个Widget

Widget _titleWidget(value) {
  return Container(
    height: ScreenAdapter.height(32),
    margin: EdgeInsets.only(left: ScreenAdapter.width(20)),
    padding: EdgeInsets.only(left: ScreenAdapter.width(20)),
    decoration: BoxDecoration(
        border: Border(
            left: BorderSide(
      color: Colors.red,
      width: ScreenAdapter.width(10),
    ))),
    child: Text(
      value,
      style: TextStyle(color: Colors.black54),
    ),
  );
}
复制代码

获取猜你喜欢网络数据

//获取猜你喜欢的数据
_getHotProductData() async {
  var api = '${Config.domain}api/plist?is_hot=1';
  var result = await Dio().get(api);
  var hotProductList = ProductModel.fromJson(result.data);
  setState(() {
    this._hotProductList = hotProductList.result;
  });
}
复制代码

创建猜你喜欢横向列表

//猜你喜欢
Widget _hotProductListWidget() {
  if (_hotProductList.length > 0) {
    return Container(
      height: ScreenAdapter.height(234),
      padding: EdgeInsets.all(ScreenAdapter.width(20)),
      child: ListView.builder(
        //设置滚动方向
        scrollDirection: Axis.horizontal,
        itemBuilder: (contxt, index) {
          //处理图片
          String sPic = _hotProductList[index].sPic;
          //得到图片URL
          sPic = Config.domain + sPic.replaceAll('\', '/');
          return Column(
            children: <Widget>[
              Container(
                height: ScreenAdapter.height(140),
                width: ScreenAdapter.width(140),
                margin: EdgeInsets.only(right: ScreenAdapter.width(21)),
                child: Image.network(sPic, fit: BoxFit.cover),
              ),
              Container(
                padding: EdgeInsets.only(top: ScreenAdapter.height(10)),
                height: ScreenAdapter.height(44),
                child: Text(
                  "¥${_hotProductList[index].price}",
                  style: TextStyle(color: Colors.red),
                ),
              )
            ],
          );
        },
        itemCount: _hotProductList.length,
      ),
    );
  } else {
    return Text("");
  }
}
复制代码

实现效果

截屏2021-12-20 下午9.10.12.png

实现热门推荐功能

获取热门推荐的数据

_getBestProductData() async {
  var api = '${Config.domain}api/plist?is_best=1';
  var result = await Dio().get(api);
  var bestProductList = ProductModel.fromJson(result.data);    
  setState(() {
    this._bestProductList = bestProductList.result;
  });
}
复制代码

创建商品列表

Widget _recProductListWidget() {
 
  var itemWidth = (ScreenAdapter.getScreenWidth() - 30) / 2;
  return Container(
    padding: EdgeInsets.all(10),
    child: Wrap(
      runSpacing: 10,
      spacing: 10,
      children: this._bestProductList.map((value) {

        //图片
        String sPic = value.sPic == null ? '' : value.sPic;
        sPic = Config.domain+sPic.replaceAll('\', '/');

        return Container(
          padding: EdgeInsets.all(10),
          width: itemWidth,
          decoration: BoxDecoration(
              border: Border.all(
                  color: Color.fromRGBO(233, 233, 233, 0.9), width: 1)),
          child: Column(
            children: <Widget>[
              Container(
                width: double.infinity,
                child: AspectRatio(
                  //防止服务器返回的图片大小不一致导致高度不一致问题
                  aspectRatio: 1 / 1,
                  child: Image.network(
                    "${sPic}",
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              Padding(
                padding: EdgeInsets.only(top: ScreenAdapter.height(20)),
                child: Text(
                  "${value.title}",
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                  style: TextStyle(color: Colors.black54),
                ),
              ),
              Padding(
                padding: EdgeInsets.only(top: ScreenAdapter.height(20)),
                child: Stack(
                  children: <Widget>[
                    Align(
                      alignment: Alignment.centerLeft,
                      child: Text(
                        "¥${value.price}",
                        style: TextStyle(color: Colors.red, fontSize: 16),
                      ),
                    ),
                    Align(
                      alignment: Alignment.centerRight,
                      child: Text( "¥${value.oldPrice}",
                          style: TextStyle(
                              color: Colors.black54,
                              fontSize: 14,
                              decoration: TextDecoration.lineThrough)),
                    )
                  ],
                ),
              )
            ],
          ),
        );
      }).toList(),
    ),
  );
}
复制代码

实现效果

截屏2021-12-20 下午9.11.50.png

Guess you like

Origin juejin.im/post/7043971307867734023