Container width and height related issues

 Container Setting width and height does not take effect


It’s usually caused by the constraints attribute of the parent container. In Flutter, the size of a child component is limited by the constraints attribute of the parent component, for example

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100.0,  
    minHeight: 50.0 
  ),
  child: Container(
    height: 5.0, 
    child: redBox 
  ),
)


In the above code, setting the height of the Container component to 5 pixels will not work because the parent container has already set the minimum height to 50 pixels, so the final height of the Container component will be 50 pixels.


Of course, this is certainly not the effect we want, we just want to make the final height of the Container component is 5 pixels how to do? It’s very simple, you can use UnconstraindBox to lift the limitation of the constraints property of the parent container on the size of the child component. For example:

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100.0, 
    minHeight: 50.0  
  ),
  child: UnconstraintsBox(
    child: Container(
      height: 5.0, 
      child: redBox 
    ),
  ),
)


UnconstrainedBox Allow its sub-components to draw at their own size, we rarely use this component directly, except for some components that come with Material, such as the icon at Appbar which is officially limited to a fixed size and can be unlocked by using this component, but in general we can solve the need by applying a layer of layout components to the outside of the component, such as the following component:

Row()
Column()
Align()
Center()
Flex()
Wrap()
Flow()
Stack()


SignleChildScrollView does not support full screen when less than one screen height.


Actually, it is similar to the above problem, which can be solved by using a layout class component, or in the following way:

Container(
  alignment: Alignment.topLeft,
  child: SingleChildScrollView(),
),


If you look at the source code for Container you’ll see that actually setting the alignment property is the same thing as using the Align component, and the source code also uses the Align component, which is a syntactic sugar, that’s all.


Speaking of syntactic sugar, the Center component is actually syntactic sugar for the Align component as well, and when you don’t pass any parameters to Align , using Center() has exactly the same effect as using Align() . It’s my habit to just use the Align component, no matter what the situation is.

 How to customize AppBar


As I mentioned above, Flutter’s official restriction on AppBar is very strict, even the basic height is written to death, how can this meet the fancy demands of our project? So in my project, except for the SliverAppBar component that comes with the AppBar, I basically didn’t use any other AppBar component.

 There are two ways to customize AppBar :


The first way, using the Column and Expanded components, provides a simple example from my project:

class VideoEditPage extends StatelessWidget {
  get appBar => DecoratedBox(
    decoration: BoxDecoration(
      color: currentTheme.primaryColor,
      boxShadow: tabBoxShadow,
    ),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: navigatorState.pop,
              child: Container(
                width: Screens.appBarHeight,
                height: Screens.appBarHeight,
                margin: EdgeInsets.only(right: setWidth(7)),
                child: Icon(
                  IcoMoon.arrowLeft,
                  size: setWidth(42),
                  color: currentTheme.primaryColorDark,
                ),
              ),
            ),
            Text('text', style: titleStyle),
          ],
        ),
        GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: Routes.pushUploadCoverEditPage,
          child: Container(
            height: Screens.appBarHeight,
            alignment: Alignment.center,
            padding: EdgeInsets.symmetric(horizontal: setWidth(19)),
            child: Text('next', style: titleStyle),
          ),
        ),
      ],
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          children: <Widget>[
            appBar,
            Expanded(child: listView)
          ],
        ),
      ),
    );
  }
}


The second way, using the Stack and Positioned components, example:

class MyApp extends StatelessWidget {
  get body => Stack(
    children: <Widget>[
      appBar,
      Positioned(
        left: 0,
        top: Screens.appBarHeight,
        right: 0,
        bottom: bottomAppBarHeight,
        child: listViewBox,
      ),
      bottomAppBar,
    ],
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SizedBox.expand(child: body),
      ),
    );
  }
}


Container setting borderRadius does not work.


There are two ways to set borderRadius , the first is to use the borderRadius attribute that comes with components such as Container , and the second is, to crop the container directly with a clip component such as ClipRRect . The second is more brute force and performance consuming than the first, but more effective.


For example to TabView container set borderRadius , you will find that can not take effect, and the use of ClipRRect can be solved, my understanding is that ClipRRect will be directly cropped into a rounded shape, and BorderRadius outside the arc of the rounded range is transparent, similar to css in the difference between display:none and opaticy:0 , the actual specific what is the cause of this, I have not to go into detail, copy and paste, can run on the line.

 List height position changes


TabBar switching causes scrolling components such as PageView, ListView, etc. to change position.


Workaround: add the key attribute to the scroll component to hold position information, for example: key: PageStorageKey(1)


In fact, the general ListView is not enough to meet the needs of our daily development of a variety of fancy, we recommend the use of the french NestedScrollView


What more bikes do we need when the French have already given us a lot of weird bugs to work out?


The push page returns, causing the page to be rebuilt and causing the position to change.


It’s a good idea to use StatefulWidget to mix in AutomaticKeepAliveClientMixin to keep the page state, so that when you route pop it doesn’t cause a re build .

class GalleryPage extends StatefulWidget {
  @override
  createState() => GalleryPageState();
}

class GalleryPageState extends State<GalleryPage> with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Scaffold(body: GridView.builder());
  }    

  @override
  bool get wantKeepAlive => true;
}


As shown above, if you go to a new page at GalleryPage page push and then pop return to GalleryPage page, the status will remain the same and the list will not be re build .

 Element display hierarchy issues


You can think of the hierarchical relationship of the widget layout in Flutter as progressive, for example, the hierarchy of child is higher than that of its parent Widget , Column , Row and other components of the same level in children widget , whoever is at the back of whoever is at a higher level, and the same hierarchical relationship as Stack and its children .

 Several approaches to show hiding


The first one is to use IndexedStack component to control the hierarchy, as mentioned above, whoever is behind the sub-component has a higher hierarchy, although there is no z-index in Flutter, but the principle is similar to z-index in css, the bigger the index is, the higher the hierarchy is, of course, the index attribute of IndexedStack is used to control the current display of a child, only one can be displayed. This method is often used in APP home page to switch the bottom navigation.


The second is to hide the widget using a combination of the IgnorePointer and Opacity components, and you can use the AnimationOpacity component to achieve the fadeIn effect that you used to use in JQuery .


The third, using Positioned or Transform.translate to move off-screen and then move back when it needs to be displayed, is ideal for animation switching, such as video progress bars and other effects.


The fourth, the use of Offstage component, the first three are the use of visual effects to hide the elements, in fact, did not change in the layout, and this component is similar to the css display:none , directly so that the elements in the layout of the hidden, will not continue to take up space on the layout.


The last one, in the build method to determine in advance, does not meet the conditions directly do not render, or return an empty box, which is similar to deleting the dom element in HTML, I’m no longer a person, but also show a 🔨, this is the most horrible.


GestureDetector setting onTap does not take effect


Listener The default behavior is HitTestBehavior.deferToChild


If the subcomponent of Listener is a Container and this Container does not set the decoration case, i.e. transparent background color and no border, then when clicking on Container , events such as down、up cannot be triggered.


Similarly, GestureDetector is the encapsulation of Listener , and it is inevitable that events such as onTap cannot be triggered, so the solution is also very simple, and there are the following two solutions:

 


Error after calling setState or markNeedsBuild.

 The first one reports an error.

setState() or markNeedsBuild() called during build


The general solution idea for this prompt is to utilize addPostFrameCallback , for example:

WidgetsBinding.instance.addPostFrameCallback((_){
    _model.setOpacity(opacity);
});

  The second type of error

setState() called after dispose()


General timer in the app back to the desktop is still calling setState or page pop after the destruction of asynchronous tasks are completed, this time called setState will certainly appear the tip, then the solution is also very simple to determine the lifecycle and then the implementation of the reconstruction logic.

if (!mounted) return;
setState(() {
  // do somthing
});


Error reported by setState after dynamically changing the length of the TabBar.


In fact, this problem is definitely caused by the use of SingleTickerProviderStateMixin , and there are two solutions.


The first is to use DefaultTabController to solve the problem, this solution is more suitable for the big man to build the wheel, because you need to write your own TabBar switching effect, it is very troublesome.


The second option is the one I’m currently using, it’s very simple, you just need to replace SingleTickerProviderStateMixin with TickerProviderStateMixin , the relevant code is as follows:

class EntryPage extends StatefulWidget {
  @override
  createState() => _EntryPageState();
}

class _EntryPageState extends State<EntryPage> with TickerProviderStateMixin {

  TabController tabController;

  final tabs = <DanceSort>[
    DanceSort.fromJson({"id": -1, "name": "1"}),
    DanceSort.fromJson({"id": 0, "name": "2"}),
  ];

  Future<void> getTabBar() async {
    final danceSorts = await EntryApi.getDanceSorts();
    if (danceSorts == null) return;
    tabs.addAll(danceSorts);
    tabController.dispose();
    tabController = TabController(length: tabs.length, vsync: this);
    setState(() {});
  }

  @override
  initState() {
    getTabBar();
    tabController = TabController(length: tabs.length, vsync: this);
    super.initState();
  }

  @override
  dispose() {
    tabController.dispose();
    super.dispose();
  }

  get tabBar => TabBar(
    controller: tabController,
    tabs: tabs.map<Tab>((v) => Tab(text: v.name)).toList(),
  );

  get tabBarView => TabBarView(
    controller: tabController,
    children: tabs.map<ListPage>((v) => ListPage(v.id)).toList(),
  );

  @override
  Widget build(BuildContext context) {
    return FloatingScrollView(
      tabBar: tabBar,
      tabBarView: tabBarView,
    );
  }
}


At initState , the local default tab is initialized first, and after requesting the tab data from the server via Api, the original tabController is destroyed and a new tabController is generated, and since TickerProviderStateMixin is used, there is no error report due to Single .


To make it easier to understand, this example uses setState to reconstruct the layout, in fact, it can be completely optimized by using Provider, my project is also using Provider to manage all of them, using Selector to minimize the scope of the construction, it can greatly improve the performance of reconstructing the layout of the problem, for example, the tabBar part above can be replaced by:

get tabBar => Selector<HomeModel, TabController>(
  selector: (context, model) => model.tabController,
  builder: (context, controller, _) => TabBar(
    controller: controller,
    tabs: model.tabs.map<Tab>((v) => Tab(text: v.name)).toList(),
  ),
);


Finally, it should be noted that when deleting a certain tab , the position of tabController must be adjusted in advance, e.g. the following code is not effective:

tabs.removeAt(3);
tabController.dispose();
tabController = TabController(length: tabs.length, vsync: this, initialIndex: 0);
setState(() {});


In the above code, after deleting tab , which is labeled as 3 , and reassigning tabController , the initial position of initialIndex will be 0 .


Assuming that the current index of tab is 3 , then after running the above code, you will find that it won’t take effect and the page doesn’t change, it still stays at the list with the subscript 3 .


So the right thing to do is to adjust the position ahead of time and then reassign the value tabController , for example:

tabs.removeAt(3);
tabController.index = 0;
tabController.dispose();
tabController = TabController(length: tabs.length, vsync: this);
setState(() {});


To add one last point, in the example above, tabBarView is generated as follows:

get tabBarView => TabBarView(
  controller: tabController,
  children: tabs.map<ListPage>((v) => ListPage(v.id)).toList(),
);


ListPage It’s a StatefulWidget with AutomaticKeepAliveClientMixin mixed in to keep the list state, and there may be some glitches here.


For example, after dynamically deleting a certain tab and reassigning tabController , the v.id passed to ListPage from the parent widget remains the value before the deletion, not the new id . This problem occurs because of the use of initState or the constructor assignment to id in State , for example:

class ListPage extends StatefulWidget {
  final int id;
  ListPage(this.id);

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

class _ListPageState extends State<ListPage> with AutomaticKeepAliveClientMixin {
  int id;

  @override
  void initState() {
    id = widget.id;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text(
            'state id $id, '
            'father widget id ${widget.id}, '
            'num $index'
          ),
        );
      }
    );
  }

  @override
  bool get wantKeepAlive => true;
}


After running the above code, delete a certain tab and then setState , you will find that the state id and father widget id displayed in ListTile are not the same, the first time I encountered this situation I was also surprised.


Then I realized that initState or constructor assignments only take effect the first build time, and no matter how many times the parent Widget re setState later, it won’t have any effect on it.


Knowing the cause, the solution is simple: make an assignment in build , for example:

class _ListPageState extends State<ListPage> with AutomaticKeepAliveClientMixin {
  int id;
  
  @override
  Widget build(BuildContext context) {
    super.build(context);
    
    id = widget.id;
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          title: Text(
            'state id $id, '
            'father widget id ${widget.id}, '
            'num $index'
          ),
        );
      }
    );
  }

  @override
  bool get wantKeepAlive => true;
}

  Keyboard related issues

 Keyboard pops up and tops off the layout instead of covering it up


Workaround: Set resizeToAvoidBottomInset: false in scafold and the keyboard will cover the layout instead of topping it.

 What if I just want the keyboard to top off the layout and the layout overflows?


The overflow must be because when there is no keyboard, the overall height is not as high as one screen, and when the keyboard appears, it exceeds the height of one screen. The solution is very simple, first of all, the layout will use SingleChildScrolleView and other scrolling components wrapped around the layout to change the layout to scrollable, so that the keyboard pops up after the layout will not overflow.


Then you can use the WidgetsBindingObserver class to listen to the keyboard pop-up event, every time you pop up the keyboard will automatically trigger the didChangeMetrics hook, you can execute the logic in the hook, for example, the current position of SingleChildScrolleView will be adjusted to the bottom of the relevant code is as follows:

import 'package:flutter/material.dart';

class Demo extends StatefulWidget {
  @override
  createState() => _DemoState();
}

class _DemoState extends State<Demo> with WidgetsBindingObserver {

  final _scrollController = ScrollController();
  final _phoneController = TextEditingController();

  FocusNode _phoneFocusNode = FocusNode();
  FocusScopeNode _focusScopeNode;

  get _phoneTextFiled => TextField(
    controller: _phoneController,
    focusNode: _phoneFocusNode,
    keyboardType: TextInputType.phone,
    maxLength: 11,
    decoration: InputDecoration(
      hintText: 'phone',
      border: InputBorder.none,
      counterText: '',
    ),
  );

  void handlePostFrame() {
    if (!_phoneFocusNode.hasFocus) {
      print('requestFocus');
      _focusScopeNode.requestFocus(_phoneFocusNode);
    }
    print('jumpTo');
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
  }

  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void didChangeMetrics() {
    WidgetsBinding.instance.addPostFrameCallback(handlePostFrame);
    super.didChangeMetrics();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

  Keyboard ejection and retraction causes a page rebuild.


I have a video detail page with close to 10,000 lines of code in my project, all using Provider for state management, and if the keyboard pops up and retracts to cause a rebuild, there can be some strange bugs, such as the current scroll component’s position in the screen changing.


My solution is to use the showBottomSheet method, the page displayed on the TextField covered with a transparent mask, so that the user can not click, and click on the mask, then trigger showBottomSheet , push into a new route, pop up the keyboard, but does not cause a rebuild, put away the keyboard, it will be popped back to the page, in fact, visually always remain in the same page, and the ordinary pop up the keyboard is no difference, and the performance is also very great. The code is as follows:

  get textField => TextField(
    autofocus: true,
    cursorColor: currentTheme.hoverColor,
    cursorWidth: 1.0,
    textInputAction: TextInputAction.done,
    style: TextStyle(
      color: currentTheme.primaryColorLight,
      fontSize: setSp(32),
    ),
    decoration: InputDecoration(
      hintText: '1212',
      hintStyle: TextStyle(fontSize: setSp(28)),
      contentPadding: EdgeInsets.symmetric(horizontal: setWidth(31)),
      filled: true,
      fillColor: currentTheme.primaryColorDark,
      border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(setWidth(30)),
          borderSide: BorderSide.none
      ),
    ),
    onSubmitted: (value) {},
  );

  Widget buildTextFieldPage(BuildContext context) {
    return SizedBox.expand(
      child: Stack(
        alignment: Alignment.bottomLeft,
        children: <Widget>[
          Positioned.fill(
            child: GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: () => Navigator.pop(context),
              child: Container(color: Colors.black.withOpacity(.5)),
            ),
          ),
          buildInput(),
        ],
      ),
    );
  }

  buildInput({hasTextField = true}) {
    Widget child;

    child = hasTextField
        ? Container(
            decoration: BoxDecoration(
              color: currentTheme.backgroundColor,
              borderRadius: BorderRadius.circular(setWidth(31)),
            ),
            child: textField,
          )
        : GestureDetector(
            onTap: () {
              showBottomSheet(
                context: context,
                backgroundColor: Colors.transparent,
                builder: buildTextFieldPage,
              );
            },
            child: Container(
              decoration: BoxDecoration(
                color: currentTheme.backgroundColor,
                borderRadius: BorderRadius.circular(setWidth(31)),
              ),
            ),
          );

    return Container(
      height: setWidth(103),
      padding: EdgeInsets.symmetric(
        vertical: setWidth(20),
        horizontal: setWidth(25),
      ),
      decoration: BoxDecoration(
        border: Border(top: commentDivider),
        color: currentTheme.primaryColor,
      ),
      child: Row(
        children: <Widget>[
          Expanded(child: child),
          Container(
            width: setWidth(66),
            padding: EdgeInsets.only(left: setWidth(25)),
            alignment: Alignment.center,
            child: Icon(
              IcoMoon.send,
              color: currentTheme.hoverColor.withOpacity(.5),
              size: setWidth(42),
            ),
          ),
        ],
      ),
    );
  }

  The relevant effects are as follows:


The above image uses showBottomSheet to pop up the keyboard, but you can actually use showDialog to accomplish the same function by replacing the onTap callback with the following one from GestureDetector :

showDialog(
  context: context,
  builder: buildTextFieldPage,
);


Note that the builder callback for showDialog needs to wrap the page with Material or Scaffold or the style will throw an exception:

Widget buildTextFieldPage(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.transparent,
    body: SizedBox.expand(
      child: Stack(
        alignment: Alignment.bottomLeft,
        children: <Widget>[
          Positioned.fill(
            child: GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: () => Navigator.pop(context),
              child: Container(color: Colors.black.withOpacity(.5)),
            ),
          ),
          buildInput(autoFocus: true),
        ],
      ),
    ),
  );
}

  The related realization is shown below:

 TextField how to adapt the height according to the content


TextField When the maximum number of rows is one, the height of TextField can be limited directly by the parent Container without setting additional attributes, but this doesn’t work well with multiple row heights.


You can see that the comment box in the image above automatically adjusts its height based on the output and limits the maximum number of rows to 4. This is a particularly common requirement.


First, you need to set the maxLine attribute of TextField to null so that TextField will automatically adjust the height according to the content


Next, set decoration: InputDecoration(isDense:true) so that TextField will allow us to limit the maximum height of TexField


Finally, use the parent container to limit the maximum height of TextField , for example

Container(
  constraints: BoxConstraints(maxHeight: 200),
  child: TextField(),
)


With the above settings, the height of TextField is automatically adapted according to the content, and will be limited to the maximum height, but also need to clarify one point, how to ensure that the maximum height is exactly the height of 4 rows?


Since TextField has no attribute that is used to limit the height of each line, this would require us to calculate contentPadding ourselves, for example:

decoration: InputDecoration(
  isDense: true,
  contentPadding: EdgeInsets.symmetric(vertical: 20),
}


TextField setting border does not work


There are 3 kinds of border in TextField, you need to set them accordingly, only one of them can’t be effective:

decoration: InputDecoration(border enabledBorder focusBorder)


ps: After setting the maxLength attribute, you need to set counterText: '' in decoration , otherwise it will come with a style of counting the number of words by default.

 Route Jump Related Issues

 push, pop Common Requirements


For example, there are 4 pages in the browsing history as follows, and the current page is d :

a->b->c->d


If you use Navigator.popUtil(context, ModalRoute.withName('a')) on the current page, you can directly return to the a page and destroy the b and c pages.


You can use Navigator.pushNamedAndRemoveUntil(context, 'e', (route) => false) on the current page to destroy all the history before going to the e page, i.e., the e page becomes the first page, and you can’t continue on the e page. pop returns to the previous page.


How to use context at the initState stage


Flutter has some methods that need to use context , such as Theme.of(context) , Navigator.of(context) , etc. Normally, we need to use context in the build method, but if we use Theme.of(context) directly when defining the attributes of the class, it’s obviously not possible to do so, and the editor will directly indicate a syntax error.


The solution is also very simple, first create a file constants.dart to hold the project’s global variables and declare a navigatorKey in the Contants class:

class Constants {
  static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
}


Then mount navigatorKey under MaterailApp :

MaterialApp(navigatorKey: Constants.navigatorKey)


Finally, the following 3 getter are declared in constants.dart :

NavigatorState get navigatorState => Constants.navigatorKey.currentState;
BuildContext get currentContext => navigatorState.context;
ThemeData get currentTheme => Theme.of(currentContext);


Since all components are mounted under MaterialApp , we can use these 3 getter ‘s when we have introduced constants.dart in any of the components, the usage is as follows:

class LoginPage extends StatelessWidget {
  final box = GestureDetector(
    onTap: navigatorState.pop,
    child: Text('1'),
  );
  final userModel = Provider.of<UserModel>(currentContext, listen: false);
  final primaryColor = currentTheme.primaryColor;
}

  The code above is equivalent:

class LoginPage extends StatelessWidget {
  Widget box;
  UserModel userModel;
  Color primaryColor;
  
  build(context) {
    box = GestureDetector(
      onTap: Navigator.of(context).pop,
      child: Text('1'),
    );
    userModel = Provider.of<UserModel>(context, listen: false);
    primaryColor = Theme.of(context).primaryColor;
  }
}

  Web request related

 How to encapsulate Dio


Network requests everyone basically use Dio , I encountered the first problem in the project on the hands of the first problem is how to Dio encapsulation, the following provides my approach (for reference only):


(1) A directory api is created under the lib directory to store files related to web requests.


(2) Create a api_path.dart in the api directory to store the address configuration information of the project interface, with the following contents:

const baseUrl = 'http://app.lcgod.com/';
const version = '1.0';

class ApiPath {
  static const messages = 'messages';
  static const loginWithSMS = 'login/sms';
  static const loginWithPassword = 'login/password';
  static const loginWithWeChat = 'login/wechat';
  static const loginWithQQ = 'login/qq';
  static const loginWithWeibo = 'login/weibo';
  static const videos = 'videos';
  static const users = 'users';

  static followUser(int id) => '$users/$id/follow_users';
  static videoCollect(int id) => '$videos/$id/collect_users';
  static videoLike(int id) => '$videos/$id/like_users';
  static videoShare(int id) => '$videos/$id/share_users';
  static videoComments(int id) => '$videos/$id/comments';
}


(3) Create a api.dart as a base class for Dio in the api directory with the following contents:

import 'dart:async';
import 'dart:io';
import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:http/io_client.dart';
import 'package:http_client_helper/http_client_helper.dart';

import 'api_path.dart';

export 'login_api.dart';
export 'user_api.dart';

class Api {
  static Dio _dio;

  static void init() {
    HttpClient http = HttpClient();
    http.badCertificateCallback = (cert, host, port) => true;
    IOClient client = IOClient(http);
    HttpClientHelper().set(client);
    _dio = Dio()
      ..options.baseUrl = baseUrl
      ..options.headers['accept'] = 'application/vnd.vhiphop.v$version+json'
      ..interceptors.add(InterceptorsWrapper(onRequest: _handleRequest));
  }

  static RequestOptions _handleRequest(RequestOptions options) {
    String fullPath = options.baseUrl + options.path;

    if (options.method == 'GET' && options.queryParameters.length > 0) {
      List params = [];
      options.queryParameters.forEach((k, v) => params.add('$k=$v'));
      fullPath += '?' + params.join('&').toString();
      print(fullPath);
    }

    final time = DateTime.now().millisecondsSinceEpoch ~/ 1000;
    options.headers.addAll({
      'x-date': '$time',
      'x-token': '123456',
      'x-user-id': Constants.user.id == null ? '' : '${Constants.user.id}',
      'x-user-token': Constants.user.token ?? '',
    });

    return options;
  }

  static final _fetchTypes = <String, Function>{
    'post': _dio.post,
    'put': _dio.put,
    'patch': _dio.patch,
    'delete': _dio.delete,
    'head': _dio.head,
  };

  static Future<dynamic> head(url, {Map<String, dynamic> data}) async {
    return await _fetch('head', url, data);
  }

  static Future<dynamic> get(url, {Map<String, dynamic> data}) async {
    return await _fetch('get', url, data);
  }

  static Future<dynamic> post(url, {Map<String, dynamic> data}) async {
    return await _fetch('post', url, data);
  }

  static Future<dynamic> put(url, {Map<String, dynamic> data}) async {
    return await _fetch('put', url, data);
  }

  static Future<dynamic> patch(url, {Map<String, dynamic> data}) async {
    return await _fetch('patch', url, data);
  }

  static Future<dynamic> delete(url, {Map<String, dynamic> data}) async {
    return await _fetch('delete', url, data);
  }

  static Future<dynamic> _fetch(method, url, data) async {
    try {
      final Response response = method == 'get'
        ? await _dio.get(url, queryParameters: data)
        : await _fetchTypes[method](url, data: data);
      return response.data;
    } catch (e) {
      final error = (e is DioError
          && e.response != null
          && e.response.statusCode == 403)
        ? e.response.data
        : {"message": "1", "status_code": 1001};

      showTip(error['message']);

      throw error;
    }
  }
}


(4) The api directory creates files for specific request logic, such as login_api.dart , where the interface logic for login logic is placed:

import 'package:vhiphop/constants/constants.dart';

import 'api.dart';
import 'api_path.dart';

class LoginApi {
  static Future<dynamic> loginWithPassword({phone, password}) async {
 
    var isError = false;
    final user = await Api.post(ApiPath.loginWithPassword, data: {
      'phone': phone,
      'password': password,
    }).catchError((_) => isError = true);
    return isError ? true : User.fromJson(user);
  }

  static Future<dynamic> loginWithWeChat(code) async {
 
    var isError = false;
    final user = await Api.post(ApiPath.loginWithWeChat, data: {
      'code': code,
    }).catchError((_) => isError = true);
    return isError ? true : User.fromJson(user);
  }

  static Future<dynamic> checkCodeAndGetToken({
    String phone,
    String code
  }) async {
    print(' token');
    var isError = false;
    final user = await Api.get(ApiPath.messages, data: {
      'phone': phone,
      'code': code,
    }).catchError((_) => isError = true);
    return isError ? true : User.fromJson(user);
  }
}

  (5) Calling examples:

Future<void> _handleEnterButtonTap() async {
  if (_isLogging) return;
  if (_code < 1 || _code > 999999) return showTip('111');

  _isLogging = true;
  showLoading('222');
  final user = await LoginApi.loginWithSMS(
    phone: _phone,
    code: _code,
    nation: _nation,
  );
  _isLogging = false;
  if (user == true) return;
  dismissAllToast();

  Constants.user = user;
  Constants.saveUser();
  Routes.pop();
}


  • All the interface request classes will be exported by api.dart and then by constants.dart , then all the files in the project only need to introduce a constants.dart to get each request class.

  • All network requests are handled uniformly when they encounter an error, for example, my code is uniformly using OkToast to pop up a prompt box with specific information.

 Dio Requests for content-type Issues


When using Dio for HTTP requests, the default value of the request header content-type is

application/json; charset=utf-8

 If content-type in the return header is

application/json


Dio will automatically parse the returned json data into the corresponding data type of Dart, instead of manually calling the jsonDecode method, so the client-side, server-side unification of the use of application/json as content-type , he is good and I am good.

 Android can’t make network requests after packaging


I encountered this problem when I first packaged a project using Flutter, and finally realized that I didn’t have permission to make network requests. Similarly, there may be a similar problem when storing and reading local files, which can be solved by setting permissions.

 Found on android/app/src/profile/AndroidManifest.xml


and android/app/src/main/AndroidManifest.xml files by adding the following sub-tag inside the manifest tag:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />


If you need to request permissions dynamically, you can use the permission_handler plugin.


Errors when building in Mac environments

 The tips are as follows:

Automatically assigning platform iOS with version 9.0 on target Runner because no platform was specified. Please specify a platform for this target in your Podfile.


The solution: delete the pod file before the platform #


Since I haven’t done native development, I’m really at a loss when it comes to this kind of build problem. At the beginning, I encountered a few similar errors, and I solved them by searching for answers on the Internet and asking the big guys in the group, which was very troublesome. So later on, when I get an error on my Mac build, I just rebuild the project, copy the logic code into the new project, and then rebuild it again so that I don’t get all sorts of messy and unintelligible errors, and it’s also more efficient.

 How to listen to the App Return to Desktop event


I need to pause the video when the app returns to the desktop, and then resume it after returning to the app from the desktop:

class _DemoState extends State<Demo> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print('app lifecycle state: $state');
    if (state == AppLifecycleState.inactive) {
      _playerModel.pausePlayer();
    } else if (state == AppLifecycleState.resumed) {
      if (_homeModel.isFindPage) _playerModel.startPlayer();
    }
    super.didChangeAppLifecycleState(state);
  }
}


WidgetsBindingObserver is a class that I use a lot, for example, to listen for keyboard pop-up events.

 Some suggestions for standardizing the definition of properties and methods in classes

  •  Members that do not reference other attributes are defined as attributes


  • Members that reference other attributes and take no arguments are defined as getters


  • A member that references another attribute and accepts an argument is defined as function

 Full screen related settings

 Forced vertical screen:

void initState() {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown
  ]);
  super.initState();
}

  Forced landscape:

initState() {
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft,
    DeviceOrientation.landscapeRight
  ]);
  super.initState();
}


Transform 3D Transform


It is recommended to use the Transform component to accomplish the animation effect, for example, Transform.translate and Transform.scale can accomplish the change of position and zoom, and Transform.rotate can accomplish the change of rotation angle.


Transform.rotate Both RotateBox and can perform the rotation function, what is the difference between them?


Rendering a widget with RotateBox is in the layout phase and takes up the actual space once it is rendered, whereas the Transform component is in the drawing phase after the layout. Transform is just a visual effect and takes up the same amount of space as it did before the transform was changed, so re-rendering the Transform.rotate component is less costly than re-rendering RotateBox .


This feature of Flutter’s Transform component is very similar to the CSS transform property, which can be used to improve animation performance.


But when you do fullscreen video, you can use IndexedStack + RotateBox instead of push a horizontal screen route, RotateBox will make the container fill the fullscreen, and IndexedStack can control whether to show fullscreen or not, here if you use Transform you can’t fill the fullscreen, because the container’s width and height have been determined in the layout, so you can only use RotateBox .

 Video Mirror Flip


In my project, I not only use RotatedBox to complete the video full screen function, but also utilize Transform to complete the mirror flip function, written as follows:

Selector<VideoModel, bool>(
  selector: (context, model) => model.isMirror,
  builder: (context, isMirror, child) => Transform(
    alignment: Alignment.center,
    transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateY(isMirror ? math.pi : 0),
    child: child,
  ),
  child: FijkView(
    player: model.player,
    color: Colors.black,
    panelBuilder: (player, context, size, pos) => emptyBox,
  ),
)


The principle is simple, FijkView is a video container provided by fijkplayer, I rotate the video container by 180 degrees along the Y-axis with the center as the center.


setEntry Used to set perspective, otherwise you will not be able to see the stereoscopic transformation of the Y-axis and X-axis.


rotateY This is the same as rotateY in css, i.e. rotate along the y-axis. You can set transform: rotateY(180deg) in css to achieve the same effect.

 Status bar related settings

 Hide the status bar:

import 'package:flutter/services.dart';

void toggleFullscreen() {
  _isFullscreen = !_isFullscreen;
  _isFullscreen
      ? SystemChrome.setEnabledSystemUIOverlays([])
      : SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
}


To change the status bar color, you need to use the plugin: flutter_statusbarcolor , here is an example usage:

 
Future<void> changeStatusColor({Color color: Colors.transparent}) async {
  try {
    await FlutterStatusbarcolor.setStatusBarColor(
      color,
      animate: true,
    );
    FlutterStatusbarcolor.setStatusBarWhiteForeground(true);
    FlutterStatusbarcolor.setNavigationBarWhiteForeground(true);
  } on PlatformException catch (e) {
    debugPrint(e.toString());
  }
}


Here is one usage, my home page contains 4 tabs using the indexStack component, every time I change a tab it changes the value of currentHomeTab but does not trigger a rebuild, and since routing to push or pop triggers a rebuild again, if you need to change to a black status bar when you go to 发现 tab 页 on the home page, you can do it in the following way:

 
@override
Widget build(BuildContext context) {
  if (ModalRoute.of(context).isCurrent && currentHomeTab == '11111') {
    changeStatusColor(color: Colors.black);
  }
}

  Optimize fijkplayer’s second start, progress jumps, etc.


By default, fijkplayer may have performance issues with progress skipping and playback, for which the following optimizations can be made:

_player.setDataSource(_video.src);
await _player.applyOptions(
    FijkOption()
      ..setFormatOption('flush_packets', 1)
      ..setFormatOption('analyzeduration', 1)
      ..setCodecOption('skip_loop_filter', 48)
      ..setPlayerOption('start-on-prepared', 1)
      ..setPlayerOption('packet-buffering', 0)
      ..setPlayerOption('framedrop', 1)
      ..setPlayerOption('enable-accurate-seek', 1)
      ..setPlayerOption('find_stream_info', 0)
      ..setPlayerOption('render-wait-start', 1)
);
await _player.prepareAsync();

 LayoutBuilder Related Practices


How to realize the WeChat Friends Circle, Beili Beili comments of multi-line text put away, expand function


I wrote the following tool class, simple, easy to use I have withered, the principle is to use the first LayoutBuilder to determine whether the specified number of lines, if exceeded then return to Column , if not exceeded then return to the original widget

import 'package:flutter/material.dart';

class ExpandableText extends StatefulWidget {
  final String text;
  final int maxLines;
  final TextStyle style;
  final bool expand;
  final TextStyle markerStyle;
  final String atName;

  const ExpandableText(this.text, {
    Key key,
    this.maxLines,
    this.style,
    this.markerStyle,
    this.expand = false,
    this.atName = '',
  }) : super(key: key);

  @override
  createState() => _ExpandableTextState();

}

class _ExpandableTextState extends State<ExpandableText> {

  bool expand;
  TextStyle style;
  int maxLines;

  @override
  void initState() {
    expand = widget.expand;
    style = widget.style;
    maxLines = widget.maxLines;
    super.initState();
  }

  Widget buildOrdinaryText() {
    final text = widget.text;
    return LayoutBuilder(builder: (_, size) {
      final tp = TextPainter(
        text: TextSpan(text: text, style: style),
        maxLines: maxLines,
        textDirection: TextDirection.ltr,
      );
      tp.layout(maxWidth: size.maxWidth);

      if (!tp.didExceedMaxLines) return Text(text, style: style);

      return Builder(
        builder: (context) => Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(text, maxLines: expand ? null : widget.maxLines, style: style),
            GestureDetector(
              onTap: () {
                expand = !expand;
                (context as Element).markNeedsBuild();
              },
              child: Text(
 
                style: widget.markerStyle,
              ),
            ),
          ],
        ),
      );
    });
  }

  Widget buildAtText() {
    return LayoutBuilder(builder: (_, size) {
      final tp = TextPainter(
        text: TextSpan(text: '  @${widget.text}:', style: style),
        maxLines: maxLines,
        textDirection: TextDirection.ltr,
      );
      tp.layout(maxWidth: size.maxWidth);

      if (!tp.didExceedMaxLines) return Text.rich(
        TextSpan(
          children: [
            TextSpan(text: '1111 '),
            TextSpan(text: '@${widget.atName}', style: widget.markerStyle),
            TextSpan(text: ':${widget.text}'),
          ],
        ),
        style: style,
      );

      return Builder(
        builder: (context) => Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text.rich(
              TextSpan(
                children: [
                  TextSpan(text: '22222 '),
                  TextSpan(text: '@${widget.atName}', style: widget.markerStyle),
                  TextSpan(text: ':${widget.text}'),
                ],
              ),
              maxLines: expand ? null : widget.maxLines,
              style: style,
            ),
            GestureDetector(
              onTap: () {
                expand = !expand;
                (context as Element).markNeedsBuild();
              },
              child: Text(
                
                style: widget.markerStyle,
              ),
            ),
          ],
        ),
      );
    });
  }

  @override
  build(context) => widget.atName == '' ? buildOrdinaryText() : buildAtText();
}

  The call method is as follows:

Container(
  padding: EdgeInsets.only(top: setWidth(6), bottom: setWidth(11)),
  alignment: Alignment.centerLeft,
  child: ExpandableText(
    reply.content,
    maxLines: 4,
    style: commentTextStyle,
    markerStyle: commentMarkerStyle,
    atName: reply.isDirect > 0 ? '' : reply.pNickname,
  ),
),

  The relevant effects are as follows:

 Listen to the actual width and height of the parent widget.


LayoutBuilder’s role is very big, you can use it to listen to the width and height information of a widget, I encountered a demand in the project, need to pop up the BottomSheet according to the height of a widget, and the height of this widget can be slid to change, then LayoutBuilder came in handy, the practice is as follows:


The widget that needs to be listened to is the Body() component, and the Body() component is given a Stack

get body => Stack(
  children: <Widget>[
    Body(),
    BodyLayout(model),
  ],
);

 Then use the BodyLayout component to listen:

import 'package:flutter/material.dart';

import 'package:vhiphop/provider/video/video_model.dart';

class BodyLayout extends StatelessWidget {

  final VideoModel model;
  BodyLayout(this.model);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, BoxConstraints constraints) {
      model.bottomSheetDy = constraints.maxHeight;
      return emptyBox;
    });
  }
}


When the height of the Body() component changes, it triggers the builder callback function of LayoutBuilder , where the height information is passed to model , so I can get the height from the model to set the height of the BottomSheet every time before BottomSheet is popped up.

 Two implementations of the bottom popup animation


This kind of animation is a common effect in Apps, such as the App Share feature, where clicking on the Share button brings up the Share component from the bottom of the page.


The first one, utilizing showModalBottomSheet , the relevant implementation code is as follows:

  void showShareBottomSheet() {
    showModalBottomSheet(
      elevation: 0,
      backgroundColor: currentTheme.highlightColor,
      context: context,
      builder: (context) => Container(
        width: Screens.width,
        decoration: BoxDecoration(color: currentTheme.primaryColor),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Container(
              alignment: Alignment.bottomLeft,
              height: setWidth(59),
              padding: EdgeInsets.only(left: setWidth(42)),
              child: Text(
                'share',
                style: TextStyle(
                  fontSize: setSp(32),
                  color: currentTheme.highlightColor,
                ),
              ),
            ),
            Container(
              height: setWidth(206),
              padding: EdgeInsets.only(top: setWidth(33), left: setWidth(33)),
              alignment: Alignment.topLeft,
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(
                    width: setWidth(.7),
                    color: currentTheme.dividerColor,
                  ),
                ),
              ),
              child: Row(
                children: <Widget>[
                  shareIconOfQQ,
                  shareIconOfQQZone,
                  shareIconOfWeChat,
                  shareIconOfWeChatMoments,
                  shareIconOfMicroBlog,
                ],
              ),
            ),
            Container(
              height: setWidth(206),
              padding: EdgeInsets.only(top: setWidth(33), left: setWidth(33)),
              alignment: Alignment.topLeft,
              child: Row(
                children: <Widget>[
                  shareIconOfLink,
                ],
              ),
            ),
            GestureDetector(
              onTap: () {
                Navigator.pop(context);
              },
              child: Container(
                width: Screens.width,
                height: setWidth(125),
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  border: Border(
                    top: BorderSide(
                      width: setWidth(10),
                      color: currentTheme.backgroundColor,
                    ),
                  ),
                ),
                child: Text(
                  'cancel',
                  style: TextStyle(
                    fontSize: setSp(36),
                    color: currentTheme.highlightColor,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }


Implemented using translate


When I used showModalBottomSheet in my project, I found that the animation was a bit laggy, probably because the test phone was not working, and it only cost 1000 oysters, but we are a stubborn poor people, and we have to find a way to get better performance, and that is translate .


This method has higher performance than the showModalBottomSheet animation, which is very silky smooth in debug mode on my 1000-ocean test machine, but the code implementation is a bit more complicated and needs to rely on the Provider to update, which I prefer.


The entire page is built using Stack , while the bottomSheet and mask box are positioned at the bottom of the page using Positioned :

get body => Stack(
  children: <Widget>[
    page,
    Positioned(
      left: 0,
      bottom: 0,
      right: 0,
      child: bottomSheetBox,
    ),
    Positioned(
      left: 0,
      top: 0,
      right: 0,
      bottom: shareBottomSheetHeight,
      child: bottomSheetBoxMask,
    ),
  ],
);


Then we use a tool class I defined, called AnimatedTranslateBox , I found that there are various animated components in the Animated family, such as AnimatedPadding , AnimatedPositioned and so on, only there is no Translate , I don’t know what the official means, maybe they think Positioned is enough for adjusting the position, but translate has higher animation performance, doesn’t it smell good? It’s okay, we built one by ourselves, the code is as follows:

import 'package:flutter/material.dart';

class AnimatedTranslateBox extends StatefulWidget {
  AnimatedTranslateBox({
    Key key,
    this.dx,
    this.dy,
    this.child,
    this.curve = Curves.linear,
    this.duration = const Duration(milliseconds: 200),
    this.reverseDuration,
  });

  final double dx;
  final double dy;
  final Widget child;
  final Duration duration;
  final Curve curve;
  final Duration reverseDuration;

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

class _AnimatedTranslateBoxState extends State<AnimatedTranslateBox>
    with SingleTickerProviderStateMixin {

  AnimationController controller;
  Animation<double> animation;
  Tween<double> tween;

  void _updateCurve() {
    animation = widget.curve == null
      ? controller
      : CurvedAnimation(parent: controller, curve: widget.curve);
  }

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this,
    );
    tween = Tween<double>(begin: widget.dx ?? widget.dy);
    _updateCurve();
  }

  @override
  void didUpdateWidget(AnimatedTranslateBox oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.curve != oldWidget.curve) _updateCurve();
    controller
      ..duration = widget.duration
      ..reverseDuration = widget.reverseDuration;
    if ((widget.dx ?? widget.dy) != (tween.end ?? tween.begin)) {
      tween
        ..begin = tween.evaluate(animation)
        ..end = widget.dx ?? widget.dy;
      controller
        ..value = 0.0
        ..forward();
    }
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  build(context) => AnimatedBuilder(
    animation: animation,
    builder: (context, child) => widget.dx == null
        ? Transform.translate(
            offset: Offset(0, tween.animate(animation).value),
            child: child,
          )
        : Transform.translate(
            offset: Offset(tween.animate(animation).value, 0),
            child: child,
          ),
    child: widget.child,
  );
}


The call is simple and uses Selector to rely on a boolean value in the model that is used to control show-hide:

get bottomSheetBox => Selector<VideoModel, bool>(
  selector: (context, model) => model.showBottomSheet,
  builder: (context, show, child) => AnimatedOpacity(
    opacity: show ? 1 : 0,
    curve: show ? Curves.easeOut : Curves.easeIn,
    duration: bottomSheetDuration,
    child: AnimatedTranslateBox(
      dy: show ? 0 : bottomSheetHeight,
      curve: show ? Curves.easeOut : Curves.easeIn,
      duration: bottomSheetDuration,
      child: child,
    ),
  ),
  child: Container(
    height: bottomSheetHeight,
    child: bottomSheet,
  ),
);


Whenever the value of dx or dy is changed, the child of AnimatedTranslateBox animates the y-axis or x-axis movement according to the value of dx or dy .

 The related effects are as follows:

 Provider Calling Issues


I found that if the Provider is globally mounted under MaterialApp , it is not possible to make the Provider available until the Home page initialization is complete, for example:

class MyApp extends StatelessWidget {

  final _userModel = UserModel();
  final _homeModel = HomeModel();

  Widget build(BuildContext context) {
    return OKToast(
      dismissOtherOnShow: true,
      child: MultiProvider(
        providers: [
          ChangeNotifierProvider.value(value: _userModel),
          ChangeNotifierProvider.value(value: _homeModel),
        ],
        child: Selector<ThemeModel, ThemeData>(
          selector: (context, model) => model.theme,
          builder: (context, theme, child) => MaterialApp(
            navigatorKey: Constants.navigatorKey,
            debugShowCheckedModeBanner: false,
            theme: theme,
            initialRoute: '/',
            routes: {
              '/': (context) => HomePage(),
            },
          ),
        ),
      ),
    );
  }
}


The above code declares MultiProvider , if you make the following call on the home page:

@override
initState() {
  _model = Provider.of<HomeModel>(context);
  _userModel = Provider.of<UserModel>(context);
  super.initState();
}

  then an error will be reported:

I/flutter ( 8380): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 8380): The following assertion was thrown building Builder:
I/flutter ( 8380): dependOnInheritedWidgetOfExactType<_DefaultInheritedProviderScope<HomeModel>>() or
I/flutter ( 8380): dependOnInheritedElement() was called before _HomePageState.initState() completed.
I/flutter ( 8380): When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
I/flutter ( 8380): widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
I/flutter ( 8380): or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
I/flutter ( 8380): inherited widget.
I/flutter ( 8380): Typically references to inherited widgets should occur in widget build() methods. Alternatively,
I/flutter ( 8380): initialization based on inherited widgets can be placed in the didChangeDependencies method, which
I/flutter ( 8380): is called after initState and whenever the dependencies change thereafter.


Tip: initState must be called before you can use Provider.of to get the model of the ancestor node, what if you have to use it? The solution is very simple, the of method has an attribute value listen , the default value is true , set this value to false , then no dependency with Provider will be established, in fact, I also found in Provider’s manual, it is recommended to set listen to false when calling of in the initState method:

@override
initState() {
  _userModel = Provider.of<UserModel>(context, listen: false);
  _model = Provider.of<HomeModel>(context, listen: false);
  super.initState();
}


How to realize the background image blurring effect of NetEase cloud music, QQ music playing page


Analyze, in fact, this effect is particularly simple, first of all, zoom in on the background image, followed by Gaussian blur on the picture, directly on the code:

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

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

class MyApp extends StatelessWidget {
  final image = Image.asset(
    'assets/images/test.jpg',
    fit: BoxFit.cover,
    width: 200,
    height: 200,
  );
  
  get blurImage => ClipRect(
    child: Stack(
      children: <Widget>[
        Transform.scale(
          scale: 1.5,
          child: image,
        ),
        BackdropFilter(
          filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
          child: Container(
            width: 200,
            height: 200,
            alignment: Alignment.center,
            color: Colors.black.withOpacity(.3),
            child: Text(
              '11',
              style: TextStyle(
                fontSize: 24,
                color: Colors.white,
              ),
            ),
          ),
        ),
      ],
    ),
);
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo app',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(title: Text('blur image demo')),
        body: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  margin: EdgeInsets.only(bottom: 30),
                  child: image,
                ),
                blurImage,
              ],
            ),
          ],
        )
      ),
    );
  }
}

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *