In our last article “Research on Mobile Platform Cross-platform Technology Solutions”, we studied RN and Flutter cross-platform technology solutions through simple examples, and made a comprehensive analysis and comparison between them. Subsequently, we incorporated Flutter into our company’s actual project StreamKar, built some basic code, developed a complete page, and further learned and experienced Flutter hybrid development. This article, based on the iOS platform, will document some key points in the practice of Flutter development and provide some help for you to get started with Flutter development.

1. The Dart language

Flutter development requires learning the Dart language first. The good news is that Dart is really easy to learn compared to many languages, and in practice, I think it’s probably even easier than JS. For those of you who are in contact for the first time, I recommend this Chinese document:

Overview of the Dart development language

Only one page, the main language functions are included, can be read in half a day to a day.

2. Integrate existing iOS apps

Since version 1.12, existing native App integration with Flutter has been officially documented.

2.1 Creating a Flutter Module

The code for Flutter is integrated into the native App in the form of a Flutter Module, so first we need to create a Flutter Module:

flutter create --template module my_flutter
Copy the code

2.2 integration

There are two ways to integrate the Flutter Module into the existing App:

  • Integrate through CocoaPods:

    In the header of the project Podfile add:

    flutter_application_path = '.. /my_flutter'
    load File.join(flutter_application_path, '.ios'.'Flutter'.'podhelper.rb')
    Copy the code

    Then add to the target of your Podfile:

    install_all_flutter_pods(flutter_application_path)
    Copy the code

    Here, we run into a slight problem. Since our Podfile and App project files are not in the same path, Therefore, the path of the flutter_export_environment.sh file in the Flutter Build Script automatically generated by pod install was not correct, resulting in compilation. However, the path had to be manually changed each time.

  • Direct integration of module compilations:

    Project members who need to develop and debug Flutter must integrate in the first way, using CocoaPods. However, this will depend on the Flutter environment. There is no need to install the Flutter environment for members of the project who are doing pure native development. Thus, Flutter provides a solution for direct integration of compiled products.

    Under iOS, the compiled product is Framework, which generates the following commands:

    flutter build ios-framework --output=some/path/MyApp/Flutter/
    Copy the code

    This way of integration also provides convenience for automatic packaging continuous integration.

2.3 call

We need to add some code in the native project to call the Flutter page.

The official documentation recommends creating and running the FlutterEngine at App startup, and then passing the engine object to the FlutterViewController to display the Flutter page. In this way, the engine will complete the initialization of the Flutter page quickly, without black screen or flash. However, memory will always be occupied, and relatively large.

Since our Flutter page is a secondary sub-page, we do not create the FlutterViewController at App startup. Instead, we create the FlutterViewController directly where the App is called and display it, so that the Flutter engine is implicitly created and destroyed.

FlutterViewController *vc = [[FlutterViewController alloc] init];
[vc setInitialRoute:@"/feedback"]; / / set the routing [self navigationController pushViewController: vc animated: YES];Copy the code

Another reason for this is that route Settings never take effect when you explicitly create a FlutterEngine. No reason was found.

2.4 Commissioning

  1. Run the App with Xcode.
  2. Enter the Flutter page.
  3. Open the Flutter Module directory in VS Code (with Dart and the Flutter plugin installed).
  4. In the Command Palette, select Debug: Attach to Flutter on Device.

After attaching, you can set breakpoint Debug, Hot Reload, Restart, and view the layout tree, memory, CPU, frame rate, and other information in the DevTools page that pops up.

3. Development of Flutter

We introduced some of the basic concepts of Flutter in the last article. This article will introduce some of the aspects we covered in our project development examples.

3.1 Navigation and Routing

The FlutterViewController is a container that holds a stack of Flutter pages. Manage page navigation, I do a little encapsulation, to achieve the following functions:

  1. Generate navigation bar widgets that are consistent with native.
  2. Supports Push and Pop for Flutter pages, and supports Push native pages from Flutter pages.
  3. Support mixed page stack sideslip back operation.

3.1.1 navigation bar

A common navigation Widget is easy to implement. You can use a CupertinoNavigationBar and use it together with CupertinoPageScaffold.

However, the page navigation bar in our project has a TabBar, which is usually located at the bottom of the Scaffold AppBar and is integrated with the navigation bar. The code is as follows:

Widget build(BuildContext context) {
    return Scaffold(
      appBar: PreferredSize(
        preferredSize: Size.fromHeight(MediaQuery.of(context).padding.top + 45),
        child: AppBar(
          title: Text('Feedback'),
          leading: CupertinoButton(
            child: AssetImage("assets/navigationbar_1_white.png"), onPressed: () => NavigatorHelper.popPage(context); ) , backgroundColor: Color(0xFF682193), bottom: PreferredSize( preferredSize: Size.fromHeight(45), child: Container( height: 45, color: Colors.white, child: TabBar( tabs: <Widget>[Tab(text:'Feedback'), Tab(text: 'My Feedback')],
                controller: _tabController,
                indicator: KKUnderlineTabIndicator(
                  fixedWidth: 18,
                  borderSide: BorderSide(
                    color: Color(0xFFdf2a8b),
                    width: 3,
                  ),
                ),
                labelColor: Color(0xFF333333),
                labelStyle:
                    TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
                unselectedLabelColor: Color(0xFFA8AAB3),
              ),
            ),
          ),
          elevation: 0,
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          SubmitFeedbackWidget(),
          MyFeedbackWidget()
        ],
      ),
    );
  }
Copy the code

We set the height of the navigation bar and TabBar using the PreferredSize. The elevation attribute is set to 0 to eliminate the shadow below the navigation bar.

3.1.2 Push and Pop

MethodChannel implements:

  1. Support for the Push Flutter page.
  2. Support to Push native pages from Flutter pages.
  3. Support Pop Flutter page.
  4. Support Pop to native pages. Note that we can Push a native page from a Flutter page, but try not to enter a new Flutter page from that native page stack. There will be multiple instances of FlutterEngine, resulting in a memory bump. The code is relatively simple:

The Dart end:

  static pushNativePage(String route, dynamic params) {
    final args = {'route': route, 'param': params};
    platform.invokeMethod('pushNativePage', args);
  }

  static pushPage(BuildContext context, Widget page) {
    Navigator.of(context).push(
      MaterialPageRoute(builder: (context) => page),
    );
  }

  static popPage(BuildContext context) {
    if (Navigator.of(context).canPop()) {
      Navigator.of(context).pop();
    } else {
      platform.invokeMethod('popToNativePage'); }}Copy the code

OC side:

    __weak typeof(self) weakSelf = self;
    self.navigateChannel = [FlutterMethodChannel methodChannelWithName:@"sk.flutter.dev/navigate" binaryMessenger:vc.binaryMessenger];
    [self.navigateChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if ([call.method isEqualToString:@"pushNativePage"]) {
            NSDictionary *dic = call.arguments;
            NSString *route = dic[@"route"];
            id param = dic[@"param"];
            if ([route isEqualToString:@"/feedback/detail"]) { KKTVFeedbackDetailViewController *vc = [[KKTVFeedbackDetailViewController alloc] init]; vc.model = param; [appCtx.navController pushViewController:vc animated:YES]; }}else if ([call.method isEqualToString:@"popToNativePage"]) {
            [appCtx.navController popViewControllerAnimated:YES];
        } else if ([call.method isEqualToString:@"canPopFlutterPage"]) {
            UIViewController *vc = appCtx.navController.topViewController;
            if([vc isKindOfClass:[KKFlutterBaseViewController class]]) { ((KKFlutterBaseViewController *)vc).canPop = [call.arguments boolValue]; }}else{ result(FlutterMethodNotImplemented); }}];Copy the code

3.1.3 Sideslip return

The Flutter page stack itself supports the side right slide return gesture, but in our project I found that this did not work and that all Flutter pages in the entire FlutterViewController were returned at once. This is probably why we used FDFullscreenPopGesture. The solution is to disable the native navigation controller gestures before swiping back to the Flutter page:

self.navigationController.interactivePopGestureRecognizer.enabled = NO;
Copy the code

In addition, since we support a mixed stack of Flutter pages and native pages, we listen for the Push and Pop of the Flutter page and control the gesture opening of the native navigation controller through MethodChannel.

The Dart end:

class KKNavigatorObserver extends NavigatorObserver {
  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    bool canPop = route.navigator.canPop();
    platform.invokeMethod('canPopFlutterPage', canPop);
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    if(! route.isFirst) { platform.invokeMethod('canPopFlutterPage'.true); }}}Copy the code

OC side:

- (BOOL)fd_interactivePopDisabled {
    return self.canPop;
}

- (void)setCanPop:(BOOL)canPop { self.navigationController.interactivePopGestureRecognizer.enabled = ! canPop; _canPop = canPop; }Copy the code

3.1.4 routing

The Dart end receives routing information from the native end using window.defaultRouteName.

void main() => runApp(MyApp()); class MyApp extends StatelessWidget { final navObserver = KKNavigatorObserver(); // Listen to the Flutter page navigation Widget _widgetForRoute(String route) {switch (route) {case '/feedback':
        return FeedbackPage();
      case '/route1':
        return MyHomePage();
      case '/route2':
        return SecondRoute();
      case '/route3':
        return ThirdRoute();
      default:
        return Center(child: Text('Unknown route: $route'));
    }
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorObservers: [navObserver],
      title: "Flutter Module", theme: ThemeData(scaffoldBackgroundColor: Color(0xFFF3F5F9)), home: _widgetForRoute(window.defaultRouteName), ); }}Copy the code

3.2 UI and layout

As we can see from the above code, the Scaffold + AppBar + TabBar + TabBarView is the overall structure. The specific page construction uses:

3.2.1 Basic Components

Text, RichText, Image, InkWell, CupertinoButton, CupertinoTextField

These components are fairly generic, but with the following caveats:

  1. The default font for Text on iOS is a bit thicker than the default font for native pages, so I usually set fontWeight to fontweight.normal for Text styles. It’s also important to know that the TextiOS default font is not the same as the iOS default font, but they look very close.

  2. The Cupertino oButton has the highlight state effect when pressed on the iOS platform. However, the size of the cupertino obutton cannot be smaller than 44 x 44.

  3. Clicking the CupertinoTextField input box will trigger the interface to move up automatically with the keyboard. However, it is not clear how to control the height and position of the interface.

  4. To cancel the keyboard when the keyboard is up, the root Widget of the entire interface can be wrapped in a gesture Widget:

    returnGestureDetector( behavior: HitTestBehavior.translucent, onTap: Focusscope.of (context).requestFocus(FocusNode()); }, child: Container(Copy the code

3.2.2 Layout components

Row, Column, Stack, Positioned, Align, Center

3.2.3 Container Components

Container, SizedBox, Padding, Expanded

We can also use mediaQuery.of (context).size to get the screen width, and mediaQuery.of (context).padding to get the size of the top and bottom security zones.

3.2.4 Rolling Components

SingleChildScrollView, ListView

There is a pit where the SingleChildScrollView cannot be rolled in the Column. The solution is to give the SingleChildScrollView package a Expanded component.

Column(
    children: <Widget>[
        Expanded(
            child: SingleChildScrollView(),
        )
    ],
)
Copy the code

3.2.5 Functional Components

ClipRRect, GestureDetector

For those who are beginning to learn about Flutter, I recommend this tutorial on Flutter on the Chinese website. It is not as complex as the official documentation and is clearly stated.

3.3 Network Request

My solution is to call the existing network request library on the native side through MethodChannel. Because the implementation of Flutter still needs to obtain user tokens through MethodChannel to encrypt the request. Listen for server switches through EventChannel. In addition, one less Flutter package can be used to reduce the size of the Flutter package.

3.4 JSON parsing

We used the JSON to Dart site to automatically generate model classes for JSON data. It supports model class nesting and supports model array.

Note that the network request returns a response of type <dynamic, dynamic>. Use map. from to <String, dynamic> and parse it.

FeedbackResult result = FeedbackResult.fromJson(Map.from(response));
Copy the code

3.5 Public View

Custom dialogs, Toast, load views these are public views that I call directly from the native side of the existing implementation with MethodChannel. This maximizes consistency with the native end.

3.6 Refreshing the List

The pull_to_refresh and pull-up loading of lists is more heavily done by the third-party plug-in pull_to_refresh. We then implement custom headers and footers.

RefreshController _refreshController = RefreshController(initialRefresh: true);
      
RefreshConfiguration(
    shouldFooterFollowWhenNotFull: (state) {
        return true;
    },
    hideFooterWhenNotFull: false, headerTriggerDistance: 60, footerTriggerDistance: 60, // Last cell height child: SmartRefresher(Controller: _refreshController,enablePullUp: true,
        onRefresh: _onRefresh,
        onLoading: _onLoad,
        header: KKListHeader(),
        footer: KKListFooter(),
        child: ListView.builder(
            itemExtent: 60,
            itemCount: _data.length,
            itemBuilder: (context, index) {
            ...
            }
        ),
    ),
),

Future _onRefresh() async {
    _data.clear();
    _pageIndex = 0;
    _hasMore = false;
    await _getListData();
    _refreshController.refreshCompleted();
    if (_hasMore)
      _refreshController.resetNoData();
    else
      _refreshController.loadNoData();
}

Future _onLoad() async {
    await _getListData();
    if (_hasMore)
      _refreshController.loadComplete();
    else
      _refreshController.loadNoData();
}
Copy the code

3.7 Keeping the Page Status

Each time TabBar switches tabs, the corresponding TabBarView is destroyed and redrawn. To keep hold page state, we need to use AutomaticKeepAliveClientMixin, and wantKeepAlive is set to true, the last in the build method adding super. Build (context).

class MyFeedbackWidget extends StatefulWidget { @override _MyFeedbackWidgetState createState() => _MyFeedbackWidgetState(); } class _MyFeedbackWidgetState extends the State < MyFeedbackWidget > with AutomaticKeepAliveClientMixin {/ / keep page 1 @ override bool get wantKeepAlive =>true; Override Widget build(BuildContext context) {super.build(context); // Keep the page status 3...... }}Copy the code

3.8 Plug-ins and Resources

Any third-party plug-ins, image resources, and other resources that Flutter relies on need to be declared in the pubspec.yaml file. VS Code will refresh dependencies by editing files to save or clicking the Get Packages button.

3.8.1 statement

dependencies:
  flutter:
    sdk: flutter
  # packages and pluginsCupertino_icons: ^0.1.2 image_picker: ^0.6.2+3 pull_to_refresh: ^1.5.8 flutter:# To add Flutter specific assets to your application, add an assets section, 
  assets:
    - assets/navigationbar_1_white.png
    - assets/select_country_selected.png
    - assets/select_country_unselect.png
Copy the code

For pictures with different resolutions, they should be stored in the specified directory structure:

Assets/my_icon. PNG assets / 2.0 x/my_icon PNG assets / 3.0 x/my_icon PNGCopy the code

Since we only have 2x and 3x images and no single image, each image should be declared in pubspec.yaml file instead of just one image directory.

3.8.2 Resource Sharing

In the case of mixed development, we want to avoid duplication of resources. That is, the source of a Flutter can also call the resources of the source of the Flutter. This requirement can be achieved through the official plugin iOS_platform_images.

Dart native end resources:

import 'package:ios_platform_images/ios_platform_images.dart';

Image(image: IosPlatformImages.load("flutter")),Copy the code

Native end of the Flutter resource:

#import <ios_platform_images/UIImage+ios_platform_images.h>

UIImage* image = [UIImage flutterImageWithName:@"assets/foo.png"];
Copy the code

conclusion

In this hybrid development project we covered as many aspects of Flutter development as possible, leaving some experience and base code to be reused for subsequent pages and other projects.

However, there are still some aspects that are not covered, such as animation, self-painting controls, complex state management, etc., which need to be explored later.

In the previous article, Flutter was introduced and compared in detail from multiple perspectives. This time, we’ll focus on the development experience of Flutter.

Advantages:

  1. The Dart language is easy to learn and use.
  2. Hot Reload greatly improves the development efficiency of interface.
  3. Third-party packages and plug-ins have been relatively rich, basic to meet the needs.

Disadvantages:

  1. There are a lot of widgets, and RN only has about 30 widgets.
  2. Widgets are layered and not concise enough.
  3. There is no official visual interface editor.
  4. Some relatively low-level problems, encountered hard to solve.

For example, we found in this practice that clicking on the status bar on iOS won’t scroll to the top of either SingleChildScrollView or ListView. We tried a lot to follow the official documentation, but it didn’t work. I don’t know where to start.

Generally speaking, FLutter is mature and complete enough. However, it is still developing at a high speed, and it is normal that there are some problems of its own, or not enough feedback from the community. I believe that with the in-depth application of Flutter by major manufacturers at home and abroad, the usability of Flutter will become higher and higher.