Recently, the project integrated many functional modules of Flutter, and there was no article produced for a long time. Therefore, I plan to write this article to summarize and record some problems in Flutter development. Demo address: github.com/weibindev/f…
Ps: The data in demo is read from JSON file under assets\data\ folder, so it does not involve network request encapsulation, project architecture and other related knowledge. This demo focuses on the implementation of point-to-point structure.
The overall effect is as follows:
Overall structure analysis
There is nothing to say about the entrance of the store on the home page, which is mainly the entrance of our order function and the display of the number of goods in the shopping cart.
Here we mainly analyze the structure of the point single interface.
According to the above picture, the analysis is as follows:
- 1: the top search box, equivalent to
Android
In thestatusBar
+toolbar
- 2: on the left side of the first class commodity classification column, some columns will have the situation of the second class classification
- 3: The column of secondary commodity classification is to further classify a large category of commodities
- 4: List of commodities classified as level 1 or level 2. Click a single commodity entry to enter the commodity details page
- 5: Bottom shopping cart, it is located at the top of the entire ordering interface, all functions of this interface will not block the shopping cart (with
overlays
Property.)
1,2,3,4 can be viewed as a whole, and 5 can be viewed as a whole.
Bottom shopping cart implementation
As for the bottom shopping cart, MY initial implementation idea was to use Overlay, which is described as follows in the source code
/// A [Stack] of entries that can be managed independently.
///
/// Overlays let independent child widgets "float" visual elements on top of
/// other widgets by inserting them into the overlay's [Stack]. The overlay lets
/// each of these widgets manage their participation in the overlay using
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
///
/// See also:
///
/// * [OverlayEntry].
/// * [OverlayState].
/// * [WidgetsApp].
/// * [MaterialApp].
class Overlay extends StatefulWidget {
Copy the code
Overlay is a Stack component. You can insert an OverlayEntry into the Overlay so that its separate child window is suspended on top of other components. Using this feature, you can Overlay the bottom shopping cart component on top of other components.
However, there are many problems in the actual use process. We need to accurately control the display and hiding of the floating control for Overlay package, otherwise people will quit the interface and our shopping cart will still be displayed at the bottom. I think it’s better for things like Popupindow and global custom Dialog.
Is there a component in Flutter that can easily manage a bunch of subcomponents?
When writing the Flutter application, the entry to our application is performed through the runApp(MyApp()) of the main() function. MyApp usually builds a MaterialApp component
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'I need something', home: HomePage(), ); }}Copy the code
The routing between different interfaces will be managed by Navigator, such as navigator.push and navigator.pop. Why is the MaterialApp sensitive to the actions of the Navigator?
The MaterialApp constructor has a field called navigatorKey
class MaterialApp extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKey;
/// omit some code
}
class _MaterialAppState extends State<MaterialApp> {
@override
Widget build(BuildContext context) {
Widget result = WidgetsApp(
key: GlobalObjectKey(this),
navigatorKey: widget.navigatorKey,
navigatorObservers: _navigatorObservers,
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
/// omit some code}}Copy the code
Looking deeper, it is passed to the navigatorKey in the WidgetsApp constructor, which creates a global NavigatorState by default when the component is initialized. Then manage the state of the Navigator created in build(BuildContext Context).
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
// NAVIGATOR
GlobalKey<NavigatorState> _navigator;
void _updateNavigator() {// Not specifying navigatorKey in the MaterialApp initializes a global NavigatorState _navigator = widget. NavigatorKey?? GlobalObjectKey<NavigatorState>(this); } override Widget build(BuildContext context) {// Build a Navigator component and put the navigatorKey in it. This allows the Navigator stack to manipulate Widget Navigator;if(_navigator ! = null) { navigator = Navigator( key: _navigator, // If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. initialRoute: WidgetsBinding.instance.window.defaultRouteName ! = Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null ? Navigator.defaultGenerateInitialRoutes : (NavigatorState navigator, String initialRouteName) { return widget.onGenerateInitialRoutes(initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); }}}Copy the code
At this point you can basically figure out how to implement the bottom shopping cart function.
Yes, we can customize a Navigator on the ordering interface to manage the jump of routes searching for goods, details of goods, shopping cart list of goods, etc., and other things are controlled by Navigator of our MaterialApp.
The following is the approximate implementation of the functional code:
class OrderPage extends StatefulWidget {
@override
_OrderPageState createState() => _OrderPageState();
}
class _OrderPageState extends State<OrderPage> {
/// Manage the keys of the single-function Navigator
GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
// The listener returns the key, performs stack processing on the route in the custom Navigator, and closes the OrderPage
navigatorKey.currentState.maybePop().then((value) {
if (!value) {
NavigatorUtils.goBack(context);
}
});
return Future.value(false);
},
child: Stack(
children: <Widget>[
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
// Build the content layer
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300)); }return null;
},
),
Positioned(
bottom: 0,
right: 0,
left: 0.// Shopping cart component, located at the bottom
child: ShopCart(),
),
// Add a small ball animation to the shopping cartThrowBallAnim(), ], ), ); }}Copy the code
Use of page transition animation Hero
The effect can be seen in the original GIF.
Using Hero is very simple, you need to associate the two components with the Hero component wrapped, and specify the same tag parameters, code as follows:
/ / / the list item
InkWell(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Hero(
tag: widget.data,
child: LoadImage(
'${widget.data.img}',
width: 81.0,
height: 81.0, fit: BoxFit.fitHeight, ), ), ), onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => GoodsDetailsPage(data: widget.data))); });Copy the code
/ / / for details
Hero(
tag: tag,
child: LoadImage(
imageUrl,
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
)
Copy the code
Do you feel like you’re done and the Hero effect will come out? Under normal circumstances, there will be an effect, but here we have no effect, with the ordinary route jump a everything, this is why?
We have an effect in the MaterialApp, but the custom Navigator has no effect, so it must be the Navigator of the MaterialApp that does some configuration.
Once again, as you can see from the Source of the MaterialApp, when it is initialized, new a HeroController is added in the constructor parameter, navigatorObservers
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
@override
void initState() {
super.initState();
_heroController = HeroController(createRectTween: _createRectTween);
_updateNavigator();
}
@override
void didUpdateWidget(MaterialApp oldWidget) {
super.didUpdateWidget(oldWidget);
if(widget.navigatorKey ! = oldWidget.navigatorKey) {// If the Navigator changes, we have to create a new observer, because the
// old Navigator won't be disposed (and thus won't unregister with its
// observers) until after the new one has been created (because the
// Navigator has a GlobalKey).
_heroController = HeroController(createRectTween: _createRectTween);
}
_updateNavigator();
}
List<NavigatorObserver> _navigatorObservers;
void _updateNavigator() {
if(widget.home ! =null|| widget.routes.isNotEmpty || widget.onGenerateRoute ! =null|| widget.onUnknownRoute ! =null) {
_navigatorObservers = List<NavigatorObserver>.from(widget.navigatorObservers) .. add(_heroController); }else {
_navigatorObservers = const<NavigatorObserver>[]; }}/ / /...
}
Copy the code
This is finally added to Observers, the Navigator construct parameter of the WidgetsApp build
navigator = Navigator(
key: _navigator,
// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever
// is in [widget.initialRoute].
initialRoute: WidgetsBinding.instance.window.defaultRouteName ! = Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes(initialRouteName);
},
onUnknownRoute: _onUnknownRoute,
// The HeroController of the MaterialApp will be added
observers: widget.navigatorObservers,
);
Copy the code
So we just need to do the same thing in the Navigator we define:
Observers (children: <Widget>[Navigator(key: navigatorKey, // [HeroController()], onGenerateRoute: (settings) {if (settings.name == '/') {
return PageRouteBuilder(
opaque: false,
pageBuilder:
(childContext, animation, secondaryAnimation) =>
_buildContent(childContext),
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
transitionDuration: Duration(milliseconds: 300),
);
}
returnnull; },), toy (bottom: 0, right: 0, left: 0, child: ShopCart(),), // add goods into the shopping cart animation ThrowBallAnim(),],Copy the code
Implementation of Gaussian blur
The gray area of the bottom shopping cart has a Gaussian blur effect
The control in a Flutter is BackdropFilter, which can be used as follows:
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
Copy the code
If you don’t edit it, the Gaussian blur will spread out to the full screen. It should look like this:
ClipRect(
BackdropFilter(
filter: ImageFilter.blur(sigmaX, sigmaY),
child: ...)
)
Copy the code
Ps: In fact, BackdropFilter source code has a more detailed description, I suggest you go to see
The realization of commodity column classification
Commodity column classification of the general point is one, two level menu page switch processing of PageView.
You can view the part of the box on the right of the figure as a PageView, and click the TAB on the left is a vertical page switch operation on PageView. If there is no secondary TAB under the corresponding TAB, then the current page is a ListView.
If there are two tabs, the current page is TabBar+PageView linkage, the direction of the PageView is horizontal
If the above description is not very clear, it doesn’t matter, I have prepared a general structure diagram, which clearly describes the relationship between them:
One more thing to note is that we don’t want the Widgets to reload every time we switch tabs, which would be very bad for the user’s experience, and we want to keep the page status as a page for pages that have already been loaded. This use AutomaticKeepAliveClientMixin can do it.
class SortRightPage extends StatefulWidget {
final int parentId;
final List<Sort> data;
SortRightPage(
{Key key,
this.parentId,
this.data})
: super(key: key);
@override
_SortRightPageState createState() => _SortRightPageState();
}
class _SortRightPageState extends State<SortRightPage>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
if (widget.data == null || widget.data.isEmpty) {
if(Widget.parentid == -1) {// Package Pagereturn DiscountPage();
} else{// List of itemsreturn SubItemPage(
key: Key('subItem${widget.parentId}'), id: widget.parentId ); }}else{// Secondary classificationreturn SubListPage(
key: Key('subList${widget.parentId}'),
data: widget.data
);
}
}
@override
bool get wantKeepAlive => true;
}
Copy the code
The end of the
Well, the article is almost there. For more details, you can go to Github to see the demo I wrote. The user interaction processing is quite appropriate in the demo.