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.
Set up the project framework on the first day and realize the functions of the home page: juejin.cn/editor/draf…
The next day to implement the classification and product list page: juejin.cn/post/704471…
Continuous Integration practices for Flutter- hybrid Engineering: juejin.cn/post/704209…
Dart version 2.15 is available: mp.weixin.qq.com/s/g-1uCl3up…
The previous two articles have respectively completed the functions of the home page, classification and commodity list page. This article has completed the functions of the commodity details page. The following knowledge points are used here:
Knowledge points used
1. Provider status management
What is Provider state management?
When we want to share state (data) between multiple pages (components/widgets), or between multiple sub-components of a page (widgets/widgets), we can use state management in Flutter to manage uniform state (data). Realize direct value transfer and data sharing between different components. Provider is the state management mode introduced by the Official Flutter team.
Specific usage:
- configuration
The provider: ^ the 6.0.1
. - Create a new folder called Provider and put our state management classes in the Provider folder
- Create counter. Dart in the provider
- Dart creates a new class inheritance
minxins
的ChangeNotifier
The following code
import 'package:provider/provider.dart'; class Counter with ChangeNotifier { int _count; Counter(this._count); void add() { _count++; notifyListeners(); //2 } get count => _count; / / 3}Copy the code
notifyListeners(); This method notifies widgets that use the Counter object to refresh
- Go to Main.dart and modify the code to add
MultiProvider
class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return ScreenUtilInit( DesignSize: Size(750, 1334), Builder :()=> MultiProvider(providers:[ChangeNotifierProvider(create: (_) => Counter()), ], child: MaterialApp( localizationsDelegates: [GlobalMaterialLocalizations. Delegate, / / specified localized string and the value of some other GlobalCupertinoLocalizations. Delegate, Corresponding GlobalWidgetsLocalizations Cupertino style. / / the delegate / / specify the default text orientation, from left to right or from right to left, supportedLocales: [ Locale("en"), Locale("zh") ], initialRoute: '/', onGenerateRoute: onGenerateRoute))); }}Copy the code
- Gets the value, and sets the value
import 'package:provider/provider.dart'; import '.. /.. /provider/Counter.dart'; Widget build(BuildContext context) { final counter = Provider.of<Counter>(context); return Scaffold( floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: (){ counter.add(); },), the body: the Text (" counter value: ${counter. Count} ")); }Copy the code
Use provider.of (context).count to get the value of _count. Provider.of(context) is the equivalent of finding the Counter it manages (1);
With the Provider of (context). The add (); Call the add() method on Counter();
2. The eventBus broadcasting
-
Configuration event_bus: ^ 2.0.0
-
Create the unified management of the event_bus.dart class
Import 'package:event_bus/event_bus.dart'; EventBus EventBus = new EventBus(); // if you want to receive data of the same type, define a variable dynamic obj; // if you want to receive data of the same type, define a variable dynamic obj; EventFn(this.obj); }Copy the code
- Introduce the eventBus.dart class above on the page where you want to broadcast the event and configure the following code
Eventbus. fire(new EventFn(' data '));Copy the code
- Introduce the above where you need to listen for broadcasts
event_bus.dart
Class then configure the following code
void initState() { super.initState(); Eventbus. on<EventFn>().listen((event){print(event); }); }Copy the code
- Event_bus Cancels event listening
@override void dispose() { super.dispose(); // Unsubscribe eventBusfn.cancel (); }Copy the code
3. Flutter_inappwebview loads web pages
- configuration
Flutter_inappwebview: ^ 5.3.2
- Importing package files
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Copy the code
- Initialize properties
InitialUrl: the initialUrl to be loaded. InitialOptions: the initial URL to be loaded; InitialOptions: Initial WebView options to be used. The initial WebView option to use. GestureRecognizers: Specifies which gestures should be consumed by the WebView. InitialData: initial InAppWebViewInitial data. Initial data for InAppWebViewInitialData to load, such as an HTML string. InitialFile: The initial asset file to be loaded. InitialHeaders: The initial header file to be used. The initial header file to be used. ContextMenu: contextMenu, containing custom menu items. Context menu, containing custom menu items.Copy the code
- Commonly triggered events
OnLoadStart: Event that is triggered when the WebView starts loading a URL. OnLoadStop: Event triggered when the WebView finishes loading a URL. OnLoadHttpError: Event raised when the WebView main page receives an HTTP error. OnConsoleMessage: Event that is triggered when the WebView receives a JavaScript console message such as console.log, console.error, etc. ShouldOverrideUrlLoading: Give the host application a chance to control when the URL in the current WebView is about to be loaded. OnDownloadStart: An event emitted when the WebView recognizes a downloadable file. OnReceivedHttpAuthRequest: when the WebView receives the HTTP authentication request event trigger. The default behavior is to cancel the request. OnReceivedServerTrustAuthRequest: when the WebView trust needs to perform server authentication certificate (validation) is triggered when the event. OnPrint: An event that is triggered when window.print() is called from the JavaScript side. The default behavior is to cancel the request. OnCreateWindow: Event that is triggered when WebView needs to perform server trust verification (certificate verification). OnCreateWindow: This event is emitted when InAppWebView requests the host application to create a new window, such as when it tries to open a target="_blank" link or when window.open() is called by the JavaScript side.Copy the code
- Simple to use
Expanded( child: InAppWebView( initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")), onProgressChanged: (InAppWebViewController Controller, int progress){if (progress / 100 > 0.9999) {setState(() {this._flag = false; }); }}))Copy the code
More detailed usage can be found in this article: juejin.cn/post/686929…
4. DefaultTabController and TabController
Both of these can implement the top navigation TAB. The difference is that TabController is usually used in a stateful component, while DefaultTabController is usually used in a stateless component. There is no drop-down refresh. I’m using DefaultTabController on this page.
TabController introduction
- Common attributes
- Common Methods
This section describes TabBar properties
Const TabBar({Key Key, @required this.tabs,// Must be implemented, set the tabs to be displayed, This. Controller, this.isScrollable = false, IndicatorWeight = 2.0,// Select the height of the underline, the higher the height, IndicatorPadding = EdgeInsets. Zero, this.indicator,// This. IndicatorSize,// Select the length of the underline This. labelColor,// Set the selected font color, in tabs the font styles have the highest priority this.labelStyle,// Set the selected font styles, Tabs inside the font style of highest priority enclosing labelPadding, enclosing unselectedLabelColor, / / set the selected font color, the size of Tabs inside the font style of highest priority enclosing unselectedLabelStyle, / / set the selected font styles, the size of Tabs this.dragStartBehavior = dragStartBehavior.start, this.ontap,// Click events})Copy the code
The use of DefaultTabController
return DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: ScreenAdapter.width(400), child: TabBar( indicatorColor: Colors.red, indicatorSize: TabBarIndicatorSize.label, labelColor: Colors.red, unselectedLabelColor: Colors.white, tabs: [Tab(child: Text(' product ', style: TextStyle(fontSize: 18,),),], 18),),), the Tab (child: Text (' evaluation 'style: TextStyle (fontSize: 18),),),),),),), the body: the Stack (children: [ TabBarView(children: [ ProductContentFirst(_productContentList), ProductContentSecond(_productContentList), ProductContentThrid(), ]), ], ), ))Copy the code
ShowModalBottomSheet Bottom panel
ModalBottomSheet bottom panel, which essentially pops up a new page, is similar to ActionSheet
ModalBottomSheet’s properties:
- Context: BuildContext
- Builder: WidgetBuilder
- BackgroundColor: backgroundColor
- Elevation: the shadow
- Shape: shape
- BarrierColor: Hides the background color
- Isdistransmissible: Whether clicking on a covered background can disappear
- EnableDrag: Slide disappear
Instructions for BoxDecoration
BoxDecoration is usually used to set borders, shadows, gradients and other effects on widgets. Common attributes are as follows:
Implementation effect
Concrete implementation code
Create product_content_model. Dart class
class ProductContentModel { late ProductContentitem result; ProductContentModel({ required this.result, }); ProductContentModel.fromJson(Map<String, dynamic> json) { result = ProductContentitem.fromJson(json['result']); } Map<String, dynamic> toJson() { final _data = <String, dynamic>{}; _data['result'] = result.toJson(); return _data; }} class ProductContentitem {// Can be null field set to empty String? sId; String? title; String? cid; Object? price; Object? oldPrice; Object? isBest; Object? isHot; Object? isNew; late List<Attr> attr; // Cannot be empty Object? status; late String pic; // Cannot be empty String? content; String? cname; int? salecount; String? subTitle; int count=1; ProductContentitem({ this.sId, this.title, this.cid, this.price, this.oldPrice, this.isBest, this.isHot, this.isNew, required this.attr, this.status, required this.pic, this.content, this.cname, this.salecount, this.subTitle, }); ProductContentitem.fromJson(Map<String, dynamic> json) { sId = json['_id']; title = json['title']; cid = json['cid']; price = json['price']; oldPrice = json['old_price']; isBest = json['is_best']; isHot = json['is_hot']; isNew = json['is_new']; attr = List<dynamic>.from(json['attr']).map((e) => Attr.fromJson(e)).toList(); status = json['status']; pic = json['pic']; content = json['content']; cname = json['cname']; salecount = json['salecount']; subTitle = json['sub_title']; } Map<String, dynamic> toJson() { final _data = <String, dynamic>{}; _data['_id'] = sId; _data['title'] = title; _data['cid'] = cid; _data['price'] = price; _data['old_price'] = oldPrice; _data['is_best'] = isBest; _data['is_hot'] = isHot; _data['is_new'] = isNew; _data['attr'] = attr.map((e) => e.toJson()).toList(); _data['status'] = status; _data['pic'] = pic; _data['content'] = content; _data['cname'] = cname; _data['salecount'] = salecount; _data['sub_title'] = subTitle; return _data; } } class Attr { late String cate; late List<String> list; Attr({ required this.cate, required this.list, }); Attr.fromJson(Map<String, dynamic> json) { cate = json['cate']; list = List<String>.from(json['list']); } Map<String, dynamic> toJson() { final _data = <String, dynamic>{}; _data['cate'] = cate; _data['list'] = list; return _data; }}Copy the code
The frame page for the product details page
class ProductContentPage extends StatefulWidget { final Map arguments; ProductContentPage({Key? key, required this.arguments}) : super(key: key); @override _ProductContentPageState createState() => _ProductContentPageState(); } class _ProductContentPageState extends State<ProductContentPage> { List _productContentList=[]; @override void initState() { // TODO: implement initState super.initState(); _getContentData(); _getContentData() async{var API ='${config.domain} API/pContent? id=${widget.arguments['id']}'; print(api); var result = await Dio().get(api); var productContent = new ProductContentModel.fromJson(result.data); setState(() { _productContentList.add(productContent.result); }); } @override Widget build(BuildContext context) {return DefaultTabController(length: 3, child: Scaffold( appBar: AppBar( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: ScreenAdapter.width(400), child: TabBar( indicatorColor: Colors.red, indicatorSize: TabBarIndicatorSize.label, labelColor: Colors.red, unselectedLabelColor: Colors.white, tabs: [ Tab( child: Text(' merchandise ', style: TextStyle(fontSize: 18,),), Tab(child: Text(' detail ', style: TextStyle(fontSize: 18,),), [IconButton(onPressed: pressed: pressed: pressed: pressed: pressed: pressed: pressed: pressed: pressed: pressed: pressed ShowMenu (context: context, position: RelativeRect.fromLTRB(ScreenAdapter.width(600), ScreenUtil().statusBarHeight+40, 10, 0), items: [ PopupMenuItem( child: Row(children: [Icon(Icons. Home), SizedBox(width: 10,), Text(' home ')],),), PopupMenuItem(child: Row(children: "Icon (the Icons. Search, SizedBox (width: 10,), Text (" search")],),)]);}, Icon: Icon (the Icons. More_horiz)),,), the body: _productContentList.length > 0 ? Stack( children: [ TabBarView(children: ProductContentSecond(_productContentList), Offer tourists a small glimpse of space. // ProductContentThrid(),]), // add to the shopping cart at the bottom of the page, immediately buy ___ (width: screenadapter.width (750), height: ScreenAdapter.width(100)+ScreenAdapter.bottomBarHeight+10, bottom: 0, child: Container( decoration: BoxDecoration( border: Border( top: BorderSide( width: 1, color: Colors.black26 ) ), color: Colors.white ), child: Container( margin: EdgeInsets.only(top: 10, bottom: ScreenAdapter.bottomBarHeight), child: Row( children: [ Container( width: 100, height: ScreenAdapter.height(100), child: Column( children: [ Icon(Icons.shopping_cart, size: ScreenAdapter. Width (38),), Text (' shopping cart ', style: TextStyle (fontSize: ScreenAdapter. Size (24))),,),), Expanded (flex: 1, child: CircleButton(color: color.fromrgbo (253, 1, 0, 0.9), text: 'Add to ', callBack: Expanded(flex: 1, child: CircleButton(color: color.fromrgbo (255, 165, 0, 0.9)), Expanded(flex: 1, child: CircleButton(color: color.fromrgbo (255, 165, 0, 0.9), text: 'buy now', the callBack: () {print (' buy now ');},))],),),),),),) : LoadingWidget ())); }}Copy the code
The product page of the product details page
class ProductContentFirst extends StatefulWidget { final List _productContentList; ProductContentFirst(this._productContentList, {Key? key}) : super(key: key); @override _ProductContentFirstState createState() => _ProductContentFirstState(); } class _ProductContentFirstState extends State<ProductContentFirst> { late ProductContentitem _productContent; List _attr = []; String _selectedValue=''; var cartProvider; @override void initState() { // TODO: implement setState super.initState(); _productContent = widget._productContentList[0]; _attr = _productContent.attr; _selectedValue = _attr.first.list.first; _attrBottomSheet(){showModalBottomSheet(context: context, Builder: (context){return Stack(children: [ Container( padding: EdgeInsets.only(left: 10), child: ListView( children: [ Column( mainAxisAlignment: MainAxisAlignment.center, children: _getAttrWidget(), ), Divider(), Container( margin: EdgeInsets.only(top: 10), height: ScreenAdapter. Height (80), child: Row(children: <Widget>, style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(width: 10), CartNum(this._productContent) ], ), ) ], ), ), Positioned( bottom: 0, width: ScreenAdapter.width(750), height: ScreenAdapter.height(76)+ScreenAdapter.bottomBarHeight, child: Container( color: Colors.white, padding: EdgeInsets.only(bottom: ScreenAdapter.bottomBarHeight), child: Row( children: <Widget>[ Expanded( flex: 1, child: Container( margin: EdgeInsets.fromLTRB(10, 0, 0, 0), child: CircleButton( color: Color.fromrgbo (253, 1, 0, 0.9), text: "Add to shopping ", callBack: // Close the bottom filter property Navigator. Of (context).pop(); // close the bottom filter property Navigator. / / call the Provider to update this data. CartProvider. UpdateCartList (); Fluttertoast. ShowToast (MSG: 'add to cart success, toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER,); }, ), ), ), Expanded( flex: 1, child: Container( margin: Edgeinset.fromltrb (10, 0, 10, 0), child: CircleButton(color: color.fromrgbo (255, 165, 0, 0.9), text: "Buy now", the callBack: () {print (' buy now ');},),),),),),); }); } List<Widget> _getAttrWidget(){ List<Widget> attrList = []; _attr.forEach((attrItem) { attrList.add( Wrap( children: [ Container( width: ScreenAdapter.width(100), child: Padding( padding: EdgeInsets.only(top: ScreenAdapter.height(28)), child: Text('${attrItem.cate}:', style: TextStyle(fontWeight: FontWeight.bold),textAlign: TextAlign.left), ), ), Container( width: ScreenAdapter.width(580), child: Wrap( children: _getAttrItemWidget(attrItem), ), ) ], ) ); }); return attrList; } List<Widget> _getAttrItemWidget(attrItem) { List<Widget> attrItemList = []; attrItem.list.forEach((item) { attrItemList.add(Container( margin: EdgeInsets.all(10), child: Chip( label: Text("${item}"), padding: EdgeInsets.all(10), ), )); }); return attrItemList; } @override Widget build(BuildContext context) { this.cartProvider = Provider.of<Cart>(context); String PIC = config.domain + this._productContent.pic; pic = pic.replaceAll('\', '/'); Return Container(padding: EdgeInsets. All (10), child: ListView(children: [AspectRatio(AspectRatio: 16/9, child: Image.network(pic, fit: BoxFit.cover,), ), Container( padding: EdgeInsets.only(top: 10), child: Text(_productContent.title!, style: TextStyle(color: Colors.black87, fontSize: ScreenAdapter.size(36), fontWeight: FontWeight.bold),), ), Container( padding: EdgeInsets.only(top: 10), child: Text( _productContent.subTitle!, style: TextStyle( color: Colors.black54, fontSize: ScreenAdapter.size(28)) ) ), SizedBox(height: 10,), Container( child: Row(children: [Expanded(child: Row(children: [Text(' price: ')), Text('¥${_productcontent.price}',style: TextStyle( color: Colors.red, fontSize: ScreenAdapter.size(46))), ], ) ), Expanded( flex: 1, child: Row(mainAxisAlignment: mainAxisAlignment. End, children: [Text('¥${_productcontent.oldprice}',style: TextStyle( color: Colors.black38, fontSize: ScreenAdapter.size(28), decoration: TextDecoration lineThrough),],),),),), / / screening _attr length > 0? Container (margin: EdgeInsets. Only (top: 10), height: ScreenAdapter.height(80), child: InkWell( onTap: () { _attrBottomSheet(); }, child: Row( children: <Widget>[Text(" selected: ", style: TextStyle(fontWeight: fontweight.bold)), Text("${_selectedValue}")],),) : Divider(), Container(height: screenadapter.height (80), child: Row(children: <Widget>[Text(" Divider: ", style: TextStyle (fontWeight: fontWeight. Bold)), Text (" free shipping ")],),), Divider (),,,); }}Copy the code
This is the TAB in the code, which is also generated by returning data from the interface
Product Details page
class ProductContentSecond extends StatefulWidget { final List _productContentList; const ProductContentSecond(this._productContentList, {Key? key}) : super(key: key); @override _ProductContentSecondState createState() => _ProductContentSecondState(); } class _ProductContentSecondState extends State<ProductContentSecond> { var _flag=true; var _id; @override void initState() { // TODO: implement initState super.initState(); _id = widget._productContentList[0].sId; } @override Widget build(BuildContext context) { return Container( child: Column( children: [ _flag ? LoadingWidget() : Text(''), Expanded( child: InAppWebView( initialUrlRequest: URLRequest(url: Uri.parse("https://jdmall.itying.com/pcontent?id=${_id}")), onProgressChanged: InAppWebViewController Controller, int Progress){if (progress / 100 > 0.9999) {setState(() {this._flag = false;}); }},))],),); }}Copy the code
I’ll put the entire project code on Github later.