In this second part of the series, we’ll focus on how to build the scaffolding of a common Flutter App and quickly develop a complete Flutter App.

Friendly tip: All the code for this article is on GSYGithubAppFlutter, and the sample code for this article is found there. After reading this article, you should be able to achieve the following effect easily. See chapter 1 for more details.

Article summary Address:

The complete Combat Combat article series column

A column in the World Series outside Flutter

preface

The content structure of this paper is as follows, which is mainly divided into three parts: basic control, data module and other functions. In each module, in addition to the functional implementation involved, the problems I encountered in the implementation process will be explained together. The ultimate goal of this series is to make you feel the pleasure of Flutter! So let’s start with pleasure!

First, the basic control

The so-called foundation, probably is cutting wood work!

1, Tabbar control implementation

Tabbar pages are often in demand, while in Flutter: Scaffold + AppBar + Tabbar + TabbarView is the most simple implementation Tabbar page, but after add keepAlive AutomaticKeepAliveClientMixin for page, Problems such as #11895 started to be the cause of the Crash, but until the SDK version of the Flutter V0.5.7 was fixed, the problem was still not completely solved, so the implementation was finally modified. (Fixed in 1.9.1 stable)

Scaffolds are combined with Appbar + Tabbar + PageView to solve these problems. First, as a Tabbar Widget, it must be a StatefulWidget, so let’s implement its State first:

Class _GSYTabBarState extends the State < GSYTabBarWidget > with SingleTickerProviderStateMixin {/ / /... omitted non-critical code @ override voidinitState() { super.initState(); / / / / / / initialization is created when the controller through with SingleTickerProviderStateMixin achieve animation effects. _tabController = new TabController(vsync: this, length: _tabItems.length); } @override voiddispose() {/// Destroy controller when page is destroyed _tabcontroller.dispose (); super.dispose(); } @override Widget build(BuildContext context) {/// Bottom TAbBar modereturnNew Scaffold(/// You can set the Scaffold to slide out of the chassis on the side: _drawer). /// You can set the Scaffold to the floatingActionButton without setting the floatingActionButton: AppBar: new appBar (backgroundColor: _backgroundColor, title: _title,), /// page body, PageView, used to host the page corresponding to the Tab body: new PageView(/// must have a controller, synchronized with the tabBar controller controller: _pageController, /// each TAB corresponds to the page body, is a List<Widget> children: _tabViews, onPageChanged: (index) {/// The page touch function slides back to synchronize the TAB selected state _tabController.animateto (index); },), /// bottomNavigationBar: new Material(color: _backgroundColor, /// /tabBar child: New TabBar(/// must have a controller that is synchronized with pageView controller) controller: _tabController, /// each TAB item is a List<Widget> tabs: _tabItems, /// TAB bottom bar color indicatorColor: _indicatorColor,),); }}Copy the code

As shown in the code above, this is the effect of a page with a TabBar at the bottom. TabBar and PageView are synchronized by _pageController and _tabController. By SingleTickerProviderStateMixin Tab animated transitions (ps if there is a need multiple nested animation effects, you may need to TickerProviderStateMixin), we can see from the code:

  • When manually sliding PageView left and right, call _TabController.animateto (index) with onPageChanged callback; Synchronize the TabBar status.

  • In _tabItems, listen for clicks on each TabBarItem, and synchronize PageView status with _pageController.

The above code is missing the click on the TabBarItem because it is placed in an external implementation. Of course, you can also encapsulate the control internally and pass the configuration data directly to the display, which can be encapsulated according to individual needs.

The external call code is as follows: when each Tabbar is clicked, jumpTo the page via pagecontroller.jumpto. Each page needs to jumpTo the coordinate of: current screen size times index.

class _TabBarBottomPageWidgetState extends State<TabBarBottomPageWidget> {

  final PageController pageController = new PageController();
  final List<String> tab = ["Dynamic"."Trends"."I"]; // render the bottom Tab_renderTab() {
    List<Widget> list = new List();
    for(int i = 0; i < tab.length; I++) {list.add(new FlatButton(onPressed: () {/// jumpTo page with jumpTo when each Tabbar is clicked /// each page needs to jumpTo coordinate: current screen size * index index. topPageControl.jumpTo(MediaQuery .of(context) .size .width * i); }, child: new Text( tab[i], maxLines: 1, ))); }returnlist; } /// Render the corresponding Tab page_renderPage() {
    return[ new TabBarPageFirst(), new TabBarPageSecond(), new TabBarPageThree(), ]; } @override Widget build(BuildContext context) {/// Scaffold Tabbar page with Scaffoldreturn new GSYTabBarWidget(
        type: gsyTabbarwidget.bottom_tab, /// render TAB tabItems: _renderTab(), /// render page tabViews: _renderPage(), topPageControl: pageController, backgroundColor: Colors.black45, indicatorColor: Colors.white, title: new Text("GSYGithubFlutter")); }}Copy the code

If you stop here, you’ll notice that the child page of the StatefulWidget will call initState again each time the page is clicked to switch. It’s definitely not what we want, so at this moment you need AutomaticKeepAliveClientMixin.

Each Tab corresponding StatefulWidget State, need through the with AutomaticKeepAliveClientMixin, @override bool get wantKeepAlive => true; , you can actually not rebuild the effect, the effect is shown below.

Since the bottom Tab page has been implemented, let’s do the top Tab page as well. The following code differs from the bottom Tab:

  • The bottom TAB is thereScaffoldbottomNavigationBarIn the.
  • The top TAB is onAppBarbottomUnder the title bar.

We also added the isScrollable: True attribute to the top TabBar to achieve the usual top Tab effect, as shown in the image below.

    returnNew Scaffold(/// You can set the Scaffold to slide out of the chassis on the side: _drawer). /// You can set the Scaffold to the floatingActionButton without setting the floatingActionButton: /// appBar: new appBar (backgroundColor: _backgroundColor, title: _title, ///tabBar control bottom: New TabBar(/// at the top, the TabBar is a sliding mode isScrollable:trueController: _tabController, /// each TAB item is a List<Widget> tabs: _tabItems, /// TAB bottom bar color indicatorColor: _indicatorColor,),), /// page body, PageView, used to host TAB page body: New PageView(/// must have a controller that is synchronized with the tabBar controller controller: _pageController, /// each TAB corresponds to the page body, which is a List<Widget> children: OnPageChanged: (index) {_tabController.animateto (index); },),);Copy the code

In a TabBar page, you will usually see: GlobalKey stateOne = new GlobalKey (); CurrentState object, you can call the public method in PageOneState. Note that the globalKey instance needs to be globally unique.

2. Refresh the list up and down

Without a doubt, a must-have control.

The RefreshIndicator is provided in the Flutter as a built-in drop-down refresh control. At the same time, we add a ScrollController to the ListView as a slide listener, and add an Item at the end to load more Loading display as the slide.

As shown in the code below, the RefreshIndicator control can be used to simply complete the pull-down refresh. One thing to note here is: You can use GlobalKey

to provide RefreshIndicator RefreshIndicatorState. Such external can through GlobalKey call GlobalKey. CurrentState. The show (); , actively display the refresh status and trigger onRefresh.

Pull-up loading is implemented in the code by _getListCount() method, which increases the actual number of items to be rendered to the ListView based on the original data. Finally, the ScrollController listens to the bottom and triggers onLoadMore.

Using the _getListCount() method, as shown in the following code, you can also configure common effects such as empty pages, headers, and so on. Internally, I changed the number of actual items and rendered items to achieve more configuration effects.

Class _GSYPullLoadWidgetState extends State<GSYPullLoadWidget> {///·· final ScrollController _scrollController = new ScrollController(); @override voidinitState() {/ / / add slide monitoring _scrollController. AddListener (() {/ / / whether the current position of sliding is reach the bottom of the trigger to load more callbacksif (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        if(this.onLoadMore ! = null && this.control.needLoadMore) { this.onLoadMore(); }}}); super.initState(); } // return the actual number of lists based on the configuration status /// Actually you can do more processing here /// / such as multiple headers, whether you need empty pages, whether you need more display loading._getListCount() {/// Whether a header is requiredifNeedHeader) {/// If the header is needed, use the Widget Item 0 as the ListView header /// / If the number of lists is greater than 0, you need to add 2 to the total number of list data because more options are loaded in the header and bottomreturn (control.dataList.length > 0) ? control.dataList.length + 2 : control.dataList.length + 1;
    } else{/// If no header is required, return a fixed number of 1 for empty page rendering when no data is availableif (control.dataList.length == 0) {
        return1; } /// If there is data, because the part loads more options, need to add 1 to the total number of data in the listreturn(control.dataList.length > 0) ? control.dataList.length + 1 : control.dataList.length; }} /// Return the actual list rendering according to the configuration status Item _getItem(int index) {if(! control.needHeader && index == control.dataList.length && control.dataList.length ! = 0) {/// If the header is not required and the data is not 0, render loads more items when index equals the length of the data (because index starts at 0).return _buildProgressIndicator();
    } else if(control.needHeader && index == _getListCount() - 1 && control.dataList.length ! = 0) {/// If the header is required and the data is not 0, the render loads more items when the index is equal to the actual render length - 1 (because the index starts at 0)return _buildProgressIndicator();
    } else if(! Control.needheader && Control.datalist. Length == 0) {/// Render empty pages if the header is not needed and the data is 0return _buildEmpty();
    } else{/// callback outside the normal rendering Item, if needed here, can directly return the relative position indexreturn itemBuilder(context, index);
    }
  }

  @override
  Widget build(BuildContext context) {
    returnNew RefreshIndicator(///GlobalKey, RefreshIndicator State, refresh key: Future onRefresh: onRefresh, child: New ListView.builder(/// / keeps the ListView scrolling in any situation, fixes compatibility issues with the RefreshIndicator. Physics: const AlwaysScrollableScrollPhysics (), / / / according to the state returns Kong Jian itemBuilder: (the context, the index) {return_getItem(index); }, /// return itemCount: _getListCount(), /// Slide listener controller: _scrollController,),); } /// empty page Widget_buildEmpty() {///···} /// Pull up to load more widgets_buildProgressIndicator() {///···}}Copy the code

3, Loading frame

In the previous section, we implemented more of the effect of slip-loading by showing the Loading state requirement. The default system provides CircularProgressIndicator etc, but the pursuit of how we may be limited to this, here recommend a third-party Loading library: flutter_spinkit, through simple configuration can use abundant Loading pattern.

Continuing with the _buildProgressIndicator method implementation in the previous section, flutter_spinkit allows you to quickly implement a more different Loading style.

// pull up to load more widgets_buildProgressIndicator() {/// Do you need to display more loading widgets bottomWidget = (control.needloadMore)? New Row (mainAxisAlignment: mainAxisAlignment center, children: < widgets > / / / / loading box new SpinKitRotatingCircle (color: Color(0xFF24292E)), new Container(width: 5.0,),"Loading...", style: TextStyle(color: color (0xFF121917), fontSize: 14.0, fontWeight: fontweight.bold,),)]) new Container();returnNew Padding(Padding: const edgeinset.all (20.0), child: new Center(child: bottomWidget,),); }Copy the code

4. Vector icon library

Vector icon is essential to the author, compared to the general PNG image file, vector icon in the development process: easy to define the color, and arbitrary size without blurring. The vector Icon library is implemented by introducing THE TTF font library file. The Icon control in the Flutter can be used to load the corresponding IconData for display.

The Icons class built-in by default in Flutter provides a rich set of Icons that can be used directly from the Icons object, and I recommend Papa Ali’s IconFont. Add font library support to pubspec.yaml and create an IconData reference to the font library name in the code.

Fonts: -family: wxcIconFont fonts: -asset: static/font/iconfont. TTF... The new Column (mainAxisAlignment: mainAxisAlignment center, children: < widgets > [new Icon (the Icons. The list, the size: 16.0), the new Text ("Trends")],),), / / / use iconfont new Tab (child: new Column (mainAxisAlignment: mainAxisAlignment center, the children: <Widget>[new Icon(IconData(0xe6d0, fontFamily:"wxcIconFont"), size: 16.0), new Text("I")],),)Copy the code

5. Route jump

Page jump in the Flutter is realized by Navigator, and route jump is divided into: jump with parameters and jump without parameters. Skip without parameters is relatively simple. By default, skip through the routing table of the MaterialApp. For jumps with parameters, the parameters are passed through the construction method of the jump page. Common jumps have the following uses:

You can set arguments to pushNamed from the new version, which can be obtained on the new page via modalRoute.of (context).settings.arguments.

PushNamed (context, routeName); / / / jump new page and replace, such as login page jump page Navigator. PushReplacementNamed (context, routeName); / / / jump to the new route, and then shut down before all the pages of a given routing the Navigator. PushNamedAndRemoveUntil (context,'/calendar', ModalRoute.withName('/')); Navigator. Push (context, new MaterialPageRoute(Builder: (context) = > new NotifyPage ())), then ((res) {/ / / get back});Copy the code

As you can see, the Navigator’s push returns a Future, which is called when the page returns. This means that you can return a parameter via the Pop of the Navigator, and then process the results of the page in the listener.

@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route);
}
Copy the code

Second, data module

Data is king, but it should not be next door wang.

1. Network request

Dio is the most popular among the current Flutter network request encapsulation. Dio encapsulates data conversion, interceptor, request return and so on in the network request. As shown in the following code, fast network requests can be made by simple encapsulation of Dio. It is really simple. For more information, you can check the official Dio documentation, which will not be expanded here.

/// create a network request object. Dio dio = new dio (); Response response; Try {/// make a request /// make a request /// make a request /// make a request /// make a request /// make a request /// make a request // make a request // make a request // make a request // make a request // make a request // make a request // make a request // make a request // make a request // make a request Response = await dio.request(URL, data: params, options: option); } on DioError catch (e) {/// HTTP error is an object returned by a DioError catch}Copy the code

2. Json serialization

Json serialization is a little bit special in Flutter, unlike JS. For example, with the Dio network request return above, if the return data format is configured to JSON, it will actually be a Map. The key-value of the Map is not very convenient in the development process, so you need to transform the Map again to the actual Model entities.

So jSON_Serializable plug-in is born, Chinese Json has a tutorial for it, here is the main supplement to explain the specific use of logic.

dependencies:
  # Your other regular dependencies hereJson_annotation: ^ 0.2.2 dev_dependencies:# Your other dev_dependencies hereBuild_runner: ^ 0.7.6 json_serializable: ^ 0.3.2Copy the code

As shown in the delivered code:

  • After creating your entity Model, inherit Object and then tag the class name with @jsonSerializable ().

  • Delegate the fromJson method to the implementation of templation.g. art with _$TemplateSerializerMixin. The introduction of *.g.art, _$* SerializerMixin and _$*FromJson is related to the name of dart, where the Model is located, and the name of the Model class. The specific visible code comments and the following pictures.

  • Finally, the transformation object is generated automatically through the compilation of the flutter packages pub run build_runner build. (Manual compilation after completion of personal habits)

import 'package:json_annotation/json_annotation.dart'; /// associate the file and allow the Template to access private methods in template-g.art. /// Template-g.art is a file generated by the command. The name is xx.g.art, where xx creates the abstract class _ in the current DART file name /// template-g.art$TemplateSerializerMixin, which implements _$TemplateFromJsonMethods part'Template.g.dart'; JsonSerializable() @jsonSerializable ('xx.g.dart'By default, _ is generated based on the current class name, such as AA$AASerializerMixin/// So the current class name is Template, and the generated abstract class is _$TemplateSerializerMixin
class Template extends Object with _$TemplateSerializerMixin{ String name; int id; @jsonkey (name: name)"push_id")
  int pushId;

  Template(this.name, this.id, this.pushId);

  ///'xx.g.dart'By default, _ is generated based on the current class name, such as AA$AAeFromJson/// so the current class name is Template and the generated abstract class is _$TemplateFromJson
  factory Template.fromJson(Map<String, dynamic> json) => _$TemplateFromJson(json);
}


Copy the code

After the above operation, the generated code under templatem.g. art is as follows, so that we can use templatem.fromjson and toJson methods to transform the entity and map, combined with json.decode and json.encode. You can happily convert between strings, maps, and entities.

part of 'Template.dart';

Template _$TemplateFromJson(Map<String, dynamic> json) => new Template(
    json['name'] as String, json['id'] as int, json['push_id'] as int);

abstract class _$TemplateSerializerMixin {
  String get name;
  int get id;
  int get pushId;
  Map<String, dynamic> toJson() =>
      <String, dynamic>{'name': name, 'id': id, 'push_id': pushId};
}


Copy the code

Note: The new version of json serialization has been partially modified, the code is simpler, see demo

3, Redux

Redux is not a new concept in the front end, but as a global state management machine, it works best with Flutter. In case you haven’t heard of it, Don’t worry: It manages and synchronizes State across controls. So Flutter_redux is waiting for you to conquer it.

We all know that State and setState are implemented to render and change stateFulWidgets in Flutter. What would be the effect of using Flutter_redux?

For example, the user information is stored in the Redux Store. The benefits are as following: If a page modifies the current user information, all controls bound to the State will be automatically synchronized by Redux, and the State can be shared across pages.

More details of Redux will not be expanded, and will be introduced in detail later. Next, we will talk about the use of Flutter_redux. In Redux, the concepts of Action, Reducer and Store are mainly introduced.

  • Action is used to define a request for data changes.
  • Reducer is used to generate new states based on actions
  • Store Stores and manages state, listens to actions, allocates actions to the Reducer automatically, and updates state based on the reducer execution results.

So in the following code, we first create a State to store the objects that need to be saved. The key code is UserReducer.

Class GSYState {/// User information User userInfo; /// Constructor GSYState({this.userinfo}); } /// Create the Reducer GSYState appReducer(GSYState state, action) for Store by using the Reducer {returnGSYState(/// associate the userInfo and action in GSYState with UserReducer userInfo: UserReducer(state.userinfo, action),); }Copy the code

The following is the implementation of the UserReducer used above, where the reducer’s processing logic is bound to defined actions using the TypedReducer. Finally, run the combineReducers command to return Reducer

objects to the upper Store.

/ / / redux combineReducers, Final UserReducer = combineReducers<User>([TypedReducer<User, UpdateUserAction>(_updateLoaded), ]); // updateloaded // updateloaded // updateloaded // updateloaded // Return User _updateLoaded(User User, action) {User = action.userinfo;returnuser; } // make an UpdateUserAction to initiate a change to userInfo /// define the class name as you like, Class UpdateUserAction {final User userInfo; UpdateUserAction(this.userInfo); }Copy the code

The store is officially introduced into the Flutter. The store created is referenced into the Flutter through the StoreProvider.

void main() { runApp(new FlutterReduxApp()); } class FlutterReduxApp extends StatelessWidget {// State final Store = new Store<GSYState>(appReducer, initialState: new GSYState(userInfo: User.empty())); FlutterReduxApp({Key key}) : super(key: key); @Override Widget build(BuildContext Context) {/// Apply store via StoreProviderreturnnew StoreProvider( store: store, child: new MaterialApp( home: DemoUseStorePage(), ), ); }}Copy the code

In the DemoUseStorePage below, bind the State to the Widget using the StoreConnector; The state object can be obtained from storeProvider.of; You can update the State by dispatch an Action.

Class DemoUseStorePage extends StatelessWidget {@Override Widget build(BuildContext Context) {/// Associate with StoreConnector The User GSYStatereturnNew StoreConnector<GSYState, User>(/// return userInfo from GSYState to Converter: (store) => store.state.userInfo, /// Return the actual rendered control in userInfo Builder: (Context, userInfo) {returnnew Text( userInfo.name, style: Theme.of(context).textTheme.display1, ); }); }} ····· /// The data in state can be accessed from any location via storeProvider.of (context) (with context under the StoreProvider) StoreProvider.of(context).state.userInfo; State StoreProvider. Of (context).dispatch(new UpdateUserAction(newUserInfo));Copy the code

Is that a little quiet to see? No matter who is quiet, but the practicality of Redux should be more attractive than quiet, as a pursuit of the program ape, more hands and what can not take the mountain is not? See GSYGithubAppFlutter for a more detailed implementation.

4. Database

In GSYGithubAppFlutter, the database uses the sqFlite wrapper, which is essentially sqLite syntax. If you are interested, check out the full code demodb.dart. The sqFlite documentation provides a simple way to define a Provider to operate on the database:

  • The Provider defines table names and database field constants for creating tables and field operations.

  • Provide mapping between databases and data entities, such as transformation between database objects and User objects;

  • The Provider is called to determine whether the table is created, and then the database object is returned for user query.

If the network request is combined with the closure, when the database is needed, the database is returned first, and then the network request method is returned through the next method. Finally, the external can invoke the next method to execute the network request. As follows:

    UserDao.getUserInfo(userName, needDb: true).then((res) {/// the database resultif(res ! = null && res.result) {setState(() {
          userInfo = res.data;
        });
      }
      returnres.next; }).then((res) {/// network resultif(res ! = null && res.result) {setState(() { userInfo = res.data; }); }});Copy the code

3. Other functions

Other features, just because of the unexpected title.

1, return key listening

The Flutter, nested through the WillPopScope, can be used to listen to the logic that handles the Android return key. WillPopScope does not listen for the return button, as the name suggests. It is a callback that is triggered when the current page is about to be pop.

The Future returned by the onWillPop callback determines whether or not the pop is responding. The code below realizes that when pressing the return key, a prompt box will pop up, and press to exit the App.

Class HomePage extends StatelessWidget {Future<bool> _dialogExitApp(BuildContext Context) {return showDialog(
        context: context,
        builder: (context) => new AlertDialog(
              content: new Text("Exit or not"),
              actions: <Widget>[
                new FlatButton(onPressed: () => Navigator.of(context).pop(false), child:  new Text("Cancel")),
                new FlatButton(
                    onPressed: () {
                      Navigator.of(context).pop(true);
                    },
                    child: new Text("Sure")))); } // This widget is the root of your application. @override Widget build(BuildContext context) {returnWillPopScope(onWillPop: () {/// if returnedreturn new Future.value(false); Popped will not be processed if returnedreturn new Future.value(true); /// Here popped the confirmation box with the showDialog, and on return with navigation.of (context).pop(true); Decide whether to quitreturn_dialogExitApp(context); }, child: new Container(), ); }}Copy the code

2, front and back monitoring

WidgetsBindingObserver contains various controls the life cycle of a notice, the didChangeAppLifecycleState Taiwan before and after state monitoring can be used to do.

Class _HomePageState extends State<HomePage> with WidgetsBindingObserver {class _HomePageState extends State<HomePage> with WidgetsBindingObserver { / / / rewrite WidgetsBindingObserver didChangeAppLifecycleState @ override in the void didChangeAppLifecycleState (AppLifecycleState State) {/// Judge App front and background switchover by stateif (state == AppLifecycleState.resumed) {

    }
  }

  @override
  Widget build(BuildContext context) {
    returnnew Container(); }}Copy the code

3, keyboard focus processing

Touching and retracting the keyboard is also a common requirement. As shown in the code below, GestureDetector + FocusScope can meet this requirement.

Class _LoginPageState extends State<LoginPage> {@Override Widget build(BuildContext Context) {/// Define the touch layerreturnNew GestureDetector (/ / / transparent and response processing behaviors: HitTestBehavior. Translucent, onTap: () {focusscope.of (context).requestFocus(new FocusNode()); }, child: new Container( ), ); }}Copy the code

4. Startup page

IOS start page, in the IOS/Runner/Assets. Xcassets/LaunchImage imageset /, have Contents. The json file and start the images, place your start page in the directory, and modify the Contents. The json. The specific size can be Google.

Android start page, the Android/app/SRC/main/res/drawable/launch_background. XML has been written in the start page, the < item > < bitmap > partially blocked, just open the shield, Change your launch image to launch_image and place it in each mipMap folder. Be sure to provide files relative to the size in each folder.

Since then, the second chapter finally ended! (/ / / del / / /)

Resources to recommend

  • Making: github.com/CarGuo/
  • Open source Flutter complete project:Github.com/CarGuo/GSYG…
  • Open source Flutter multi-case study Project:Github.com/CarGuo/GSYF…
  • Open source Fluttre Practical Ebook Project:Github.com/CarGuo/GSYF…

Recommended for complete open source projects:

  • GSYGithubAppWeex
  • GSYGithubApp React Native