In my opinion, learning a new language (fast and efficient learning) must be through practice, and the best way is to do projects. I will simply write a jingdong Demo here.

In the last article, BottomNavigationBar was created, which contains four main interfaces. Today, the second main interface is completed, including the functions of classification page and commodity list.

Knowledge points used

1. Name route parameters

  • The routing tableroutesIncrease in
'/product_list': (context, {arguments}) => ProductListPage(arguments: arguments),
Copy the code
  • Where you need to jump to a page
Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId! });Copy the code
  • Page to jump to
class ProductListPage extends StatefulWidget {
  
  Map arguments;

  ProductListPage({Key? key, required this.arguments}) : super(key: key);

  @override
  _ProductListPageState createState() => _ProductListPageState();
}
Copy the code

2. Configure packet capture

  • Let’s introduce these twodioThe header file
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
Copy the code
  • Configure the packet capture code
// Set packet capture only in debug mode final kReleaseMode = false; final Dio dio = Dio(); if (! Dio.httpclientadapter as DefaultHttpClientAdapter). OnHttpClientCreate = (HttpClient client) {dio.httpClientAdapter as DefaultHttpClientAdapter  client.findProxy = (uri) { return "PROXY localhost:8888"; }; }; }Copy the code
  • The packet capture effect is as follows:

3. Pull-up load pull-down refresh passflutter_easyrefreshimplementation

  • Here are several implementations listed on the Flutter_easyRefresh website:
import 'package:flutter_easyrefresh/easy_refresh.dart'; . EasyRefresh(Child: ScrollView(), onRefresh: () async{.... }, onLoad: () async { .... Custom (slivers: <Widget>[], onRefresh: () async{.... }, onLoad: () async { .... },) // EasyRefresh. Builder (Builder: (context, physics, header, footer) {return CustomScrollView(physics: physics, slivers: <Widget>[ ... header, ... footer, ], ); } onRefresh: () async{ .... }, onLoad: () async { .... },)Copy the code
  • Use in the list of goods
EasyRefresh( child: ListView.builder( itemCount: productList.length, itemBuilder: (context, index) {// Create list contents return createContent(index); }), // drop refresh onRefresh: () async{_page = 1; _getProductListData(false); }, // pull load onLoad: () async {_page += 1; if(! _hasMore){ return; } _getProductListData(true); },)Copy the code
  • For more information: github.com/xuelongqy/f…

4. Keep the page statusAutomaticKeepAliveClientMixin

Flutter does not retain tabbar state after tabar switch. To save memory, widgets are temporary variables. When we use TabBar and switch tabar, initState is called again. Can use AutomaticKeepAliveClientMixin to solve this problem

  • The current class to inherit AutomaticKeepAliveClientMixin
class _CategoryPageState extends State<CategoryPage> with AutomaticKeepAliveClientMixin
Copy the code
  • Implement this method
bool get wantKeepAlive =>true;
Copy the code
  • Adding super. Build (context)
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container();
  }
Copy the code

5. Data and model conversion

This is just a simple data model transformation, which I implemented manually

class ProductItemModel { String? sId; String? title; ProductItemModel({this.sId, this.title,}); ProductItemModel.fromJson(Map<String, dynamic> json) { sId = json['_id']; title = json['title']; } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['_id'] = this.sId; data['title'] = this.title; return data; }}Copy the code

6.ListViewThe use of

7. GridViewThe implementation of grid layout

8. ImageCommonly used method

You can add an Image in the following ways: image. asset: loads a local resource Image Image.network: loads a network resource Image image. file: loads an Image from a local fileCopy the code

9. Internationalization of local projects

flutter_localizations:
    sdk: flutter
Copy the code
import 'package:flutter_localizations/flutter_localizations.dart'; new MaterialApp( localizationsDelegates: [ // ... app-specific localization delegate[s] here GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: [ const Locale('en', 'US'), // English const Locale('he', 'IL'), // Hebrew // ... other locales the app supports ], / /...).Copy the code

Specific support internationalization more solutions can be reference: zhuanlan.zhihu.com/p/145992691

Specific function realization

Achieved effect

Dart global configuration information class

For example, you can store domain names in it

class Config{
  static String domain="https://jdmall.itying.com/";
}
Copy the code

Classification page implementation

The left side of the whole page is realized by ListView, the right side by GridView, and then the right side list is refreshed by clicking on the left side list.

Defining the data model

class CateModel { List<CateItemModel> result = []; CateModel({required this.result}); CateModel.fromJson(Map<String, dynamic> json) { if (json['result'] ! = null) { json['result'].forEach((v) { result.add(new CateItemModel.fromJson(v)); }); } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); if (this.result.length > 0) { data['result'] = this.result.map((v) => v.toJson()).toList(); } return data; } } class CateItemModel { String? sId; //String? Represents the nullable type String? title; Object? status; String? pic; String? pid; String? sort; CateItemModel( {this.sId, this.title, this.status, this.pic, this.pid, this.sort}); CateItemModel.fromJson(Map<String, dynamic> json) { sId = json['_id']; title = json['title']; status = json['status']; pic = json['pic']; pid = json['pid']; sort = json['sort']; } 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['pid'] = this.pid; data['sort'] = this.sort; return data; }}Copy the code

The implementation code

class CategoryPage extends StatefulWidget { CategoryPage({Key? key}) : super(key: key); _CategoryPageState createState() => _CategoryPageState(); } class _CategoryPageState extends the State < CategoryPage > with AutomaticKeepAliveClientMixin {/ / the currently selected int _selectIndex = 0; // List _leftCateList=[]; // List _rightCateList=[]; @override // TODO: Implement wantKeepAlive Cache current page bool get wantKeepAlive =>true; @override void initState() { // TODO: implement initState super.initState(); _getLeftCateData(); _getLeftCateData() async{var API = '${config.domain} API /pcate'; var result = await Dio().get(api); var leftCateList = new CateModel.fromJson(result.data); setState(() { this._leftCateList = leftCateList.result; }); _getRightCateData(leftCateList.result[0].sId); Async {var API = '${config.domain} API /pcate? pid=${pid}'; var result = await Dio().get(api); var rightCateList = new CateModel.fromJson(result.data); setState(() { this._rightCateList = rightCateList.result; }); } // Left list layout Widget _leftCateWidget(leftWidth){if(_leftcatelist. length>0){return Container(width: leftWidth, height: double.infinity, // color: Colors.red, child: ListView.builder( itemCount: _leftCateList.length, itemBuilder: (context,index){ return Column( children: <Widget>[ InkWell( onTap: (){setState(() {_selectIndex= index; _getRightCateData(_leftCateList[index].sid);});} child: Container( width: double.infinity, height: ScreenAdapter.height(84), padding: EdgeInsets.only(top:ScreenAdapter.height(24)), child: Text("${_leftCateList[index].title}",textAlign: Center), color: _selectIndex==index? Color.fromrgbo (240, 246, 246, 0.9): color.white,),), Divider(height: 1)],); },),); } else { return Container( width: leftWidth, height: double.infinity ); }} / / to create the right list Widget _rightCateWidget (rightItemWidth rightItemHeight) {if (_rightCateList. Length > 0) {return Expanded ( flex: 1, child: Container( padding: EdgeInsets.all(10), height: double.infinity, color: Color. FromRGBO (240, 246, 246, 0.9), child: GridView.builder( gridDelegate:SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount:3, childAspectRatio: rightItemWidth/rightItemHeight, crossAxisSpacing: 10, mainAxisSpacing: 10 ), itemCount: _rightCateList. Length, itemBuilder: (context,index){PIC = _rightCateList[index].pic; pic = Config.domain+pic.replaceAll('\', '/'); return InkWell( onTap: (){ Navigator.pushNamed(context, '/product_list', arguments: {'cid': _rightCateList[index].sId! }); }, child: Container( // padding: EdgeInsets.all(10), child: Column( children: <Widget>[ AspectRatio( aspectRatio: 1/1, child: Image.network("${pic}",fit: BoxFit.cover), ), Container( height: ScreenAdapter.height(28), child: Text("${_rightCateList[index].title}"), ) ], ), ), ); },))); } else { return Expanded( flex: 1, child: Container( padding: EdgeInsets.all(10), height: double.infinity, color: Color. FromRGBO (240, 246, 246, 0.9), child: Text("... ))); }} @ override Widget build (BuildContext context) {/ / on the left side of the width of the var leftWidth = ScreenAdapter. GetScreenWidth () / 4. // Width of each item on the right = (total width - left width - left margin of each element on the outside of the GridView - spacing in the middle of the GridView /3 var rightItemWidth=(ScreenAdapter.getScreenWidth()-leftWidth-20-20)/3; If screenAdapter.width = screenAdapter.width (screenAdapter.width); Var rightItemHeight=rightItemWidth+ screenAdapter.height (28); Return Scaffold(appBar: appBar (title: Text(' taxonomy page '),), body: Row(children: <Widget>[ _leftCateWidget(leftWidth), _rightCateWidget(rightItemWidth,rightItemHeight) ], ), ); }}Copy the code

Implementation effect

Product list page

Create item list Model

class ProductModel { List<ProductItemModel> result=[]; ProductModel({required this.result}); ProductModel.fromJson(Map<String, dynamic> json) { if (json['result'] ! = null) { json['result'].forEach((v) { result.add(new ProductItemModel.fromJson(v)); }); } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); if (this.result ! = null) { data['result'] = this.result.map((v) => v.toJson()).toList(); } return data; } } class ProductItemModel { String? sId; //String? Represents the nullable type String? title; String? cid; Object? price; // All types inherit Object String? oldPrice; String? pic; String? sPic; ProductItemModel( {this.sId, this.title, this.cid, this.price, this.oldPrice, this.pic, this.sPic}); ProductItemModel.fromJson(Map<String, dynamic> json) { sId = json['_id']; title = json['title']; cid = json['cid']; price = json['price']; oldPrice = json['old_price']; pic = json['pic']; sPic = json['s_pic']; } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['_id'] = this.sId; data['title'] = this.title; data['cid'] = this.cid; data['price'] = this.price; data['old_price'] = this.oldPrice; data['pic'] = this.pic; data['s_pic'] = this.sPic; return data; }}Copy the code

The implementation code

class ProductListPage extends StatefulWidget {

  Map arguments;

  ProductListPage({Key? key, required this.arguments}) : super(key: key);

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

class _ProductListPageState extends State<ProductListPage> {

  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  //当前页码
  int _page = 1;
  //每次请求返回多少条数据
  int _pageSize = 10;
  //排序
  String _sort = '';
  //是否还有更多
  bool _hasMore = true;
  //每组ID
  int _selectHeaderId = 1;
  //页面列表数据
  List productList = [];
  //搜索关键字
  String _keyWords = '';
  //文本输入框的控制器
  var _initKeywordsController = TextEditingController();

  /*二级导航数据*/
  List _subHeaderList = [
    {"id": 1, "title": "综合", "fileds": "all", "sort": -1,},
    //排序     升序:price_1     {price:1}        降序:price_-1   {price:-1}
    {"id": 2, "title": "销量", "fileds": 'salecount', "sort": -1},
    {"id": 3, "title": "价格", "fileds": 'price', "sort": -1},
    {"id": 4, "title": "筛选"}
  ];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    _getProductListData(false);
  }

  //请求列表数据,异步请求
  _getProductListData(bool isMore) async {

    var api;
    if(_keyWords.isEmpty){
      api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}';
    } else {
      api = '${Config.domain}api/plist?cid=${widget.arguments["cid"]}&page=${_page}&sort=${_sort}&pageSize=${_pageSize}&search=${_keyWords}';
    }

    //设置只在debug模式下抓包
    final kReleaseMode = false;
    final Dio dio = Dio();
    if (!kReleaseMode){
      //设置代理 抓包用
      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) {
        client.findProxy = (uri) {
          return "PROXY localhost:8888";
        };
      };
    }

    var result = await dio.get(api);

    //解析json数据,目前我都还是采用手动解析
    var dataList = ProductModel.fromJson(result.data).result;
    if(dataList.length > 10){
      _hasMore = true;
    }

    setState(() {
      if(isMore){
        productList.addAll(dataList);
      } else {
        productList = dataList;
      }
    });
  }

  //改变分组的处理
  _subHeaderChange(id){
    if(id==4){
      setState(() {
        _selectHeaderId = id;
        _scaffoldKey.currentState!.openEndDrawer();
      });
    } else {
      setState(() {
        _selectHeaderId = id;
        _sort =
        "${_subHeaderList[id - 1]["fileds"]}_${_subHeaderList[id - 1]["sort"]}";
        _page = 1;
        productList = [];
        //改变sort排序
        _subHeaderList[id - 1]['sort'] = _subHeaderList[id - 1]['sort'] * -1;
        _hasMore = true;
        _getProductListData(false);
      });
    }
  }

  //列表的内容
  Widget createContent(index) {
    ProductItemModel itemModel = productList[index];
    String pic = '';
    if(itemModel.pic != null){
      //Config存放全局配置的类
      //由于这个图片链接有问题才这样处理
      pic = Config.domain + itemModel.pic!.replaceAll('\', '/');
    }
    return Column(
      children: [
        InkWell(
          onTap: (){
            //push到下个页面并传参
            Navigator.pushNamed(context, '/product_content', arguments: {'id' : itemModel.sId});
          },
          child: Row(
            children: [
              Container(
                margin: EdgeInsets.only(left: 10),
                width: ScreenAdapter.width(180),
                height: ScreenAdapter.height(180),
                child: Image.network(
                  pic,
                  fit: BoxFit.cover,
                ),
              ),
              Expanded(
                flex: 1,
                child: Container(
                  margin: EdgeInsets.all(10),
                  height: ScreenAdapter.height(180),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        itemModel.title!,
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      Row(
                        children: [
                          Container(
                            alignment: Alignment.center,
                            height: ScreenAdapter.height(36),
                            margin: EdgeInsets.only(right: 10),
                            padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
                            decoration: BoxDecoration(
                              borderRadius: BorderRadius.circular(10),
                              color: Color.fromRGBO(230, 230, 230, 0.9),
                            ),
                            child: Text('4g'),
                          ),
                          Container(
                            alignment: Alignment.center,
                            height: ScreenAdapter.height(36),
                            margin: EdgeInsets.only(right: 10),
                            padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
                            decoration: BoxDecoration(
                              borderRadius: BorderRadius.circular(10),
                              color: Color.fromRGBO(230, 230, 230, 0.9),
                            ),
                            child: Text(
                              '126',
                            ),
                          )
                        ],
                      ),
                      Text(
                        '¥${itemModel.price!.toString()}',
                        style: TextStyle(color: Colors.red, fontSize: 16),
                      )
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  //创建商品列表
  Widget _productListWidget() {
    return Container(
      height: ScreenAdapter.getScreenHeight(),
      padding: EdgeInsets.all(10),
      margin: EdgeInsets.only(top: ScreenAdapter.height(80)),
      //配置刷新
      child: EasyRefresh(
        child: ListView.builder(
            itemCount: productList.length,
            itemBuilder: (context, index) {
              //创建列表内容
              return createContent(index);
            }
            ),
        //下拉刷新
        onRefresh: () async{
          _page = 1;
          _getProductListData(false);
        },
        //上拉加载
        onLoad: () async {
          _page += 1;
          if(!_hasMore){
            return;
          }
          _getProductListData(true);
        },
      ),
    );
  }

  //创建升降序的图标
  Widget _showIcon(id){
    if(id==2 || id==3){
      if(_subHeaderList[id-1]['sort'] == 1){
        return Icon(Icons.arrow_drop_down);
      } else {
        return Icon(Icons.arrow_drop_up);
      }
    }
    return Text('');
  }

  //创建头部分组
  Widget _subHeaderWidget() {
    return Positioned(
      top: 0,
      width: ScreenAdapter.getScreenWidth(),
      height: ScreenAdapter.height(80),
      child: Container(
        //分组底部分割线
        decoration: const BoxDecoration(
            border: Border(
                bottom: BorderSide(
                    color: Color.fromRGBO(233, 233, 233, 0.9), width: 1))),
        child: Row(
          children: _subHeaderList.map((value){
            return Expanded(
                flex: 1,
                child: InkWell(
                  onTap: () {
                    _subHeaderChange(value['id']);
                  },
                  child: Padding(
                    padding: EdgeInsets.fromLTRB(
                        0, ScreenAdapter.height(16), 0, ScreenAdapter.height(16)),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Container(
                          child: Text(
                            value['title'],
                            textAlign: TextAlign.center,
                            style: TextStyle(color: _selectHeaderId == value['id'] ? Colors.red : Colors.black),
                          ),
                        ),
                        _showIcon(value['id'])
                      ],
                    ),
                  ),
                ));
          }).toList(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      //创建导航栏
      appBar: AppBar(
        leading: IconButton(
          onPressed: (){
            Navigator.pop(context);
          },
          icon: Icon(Icons.arrow_back),
        ),
        title: Container(
          //文本输入
          child: TextField(
            controller: this._initKeywordsController,
            autofocus: true,
            decoration: InputDecoration(
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(30),
                borderSide: BorderSide.none
              ),
            ),
            onChanged: (value){
              setState(() {
                //搜索框输入的文字
                _keyWords = value;
              });
            },
          ),
          height: ScreenAdapter.height(68),
          decoration: BoxDecoration(
            color: Color.fromRGBO(233, 233, 233, 0.8),
            borderRadius: BorderRadius.circular(30)
          ),
        ),
        actions: [
          InkWell(
            child: Container(
              width: ScreenAdapter.width(80),
              height: ScreenAdapter.height(68),
              child: Row(
                children: [
                  Text('搜索', style: TextStyle(fontSize: 16),)
                ],
              ),
            ),
            onTap: (){
              //点击搜索框开始搜索,这里只是简单的在综合组搜索
              _subHeaderChange(1);
            },
          )
        ],
      ),
      endDrawer: Drawer(
        child: Container(
          child: Text('实现筛选功能'),
        ),
      ),
      body: !productList.isEmpty ? Stack(
        children: [
          //创建导航栏下分页栏
          _subHeaderWidget(),
          //创建页面
          _productListWidget(),
        ],
      ) : Center(
        child: Text('没有搜索到商品'),
      ),
    );
  }
}
Copy the code

Implementation effect