Flutter 与 iOS 原生项目混编配置(二)

用户的关注列表

主要涉及基类封装、Native与Flutter间相互通信、网络数据请求、列表展示、上下拉刷新,以及MVVM设计模式。

  • Native 实现

    @objc private func pushToFlutter() {
          let flutterVC = CJMFlutterViewController.share()
          flutterVC.setInitialRoute("Follow")
          let channel = FlutterMethodChannel.init(name: "flutter_ios/follow", binaryMessenger: flutterVC.binaryMessenger)
          channel.setMethodCallHandler { [weak flutterVC] (methodCall, result) in
              switch methodCall.method {
              case "backAction":
                  /// 退出FlutterViewController
                  flutterVC?.navigationController?.popViewController(animated: true)
              case "userIconAction":
                  /// 跳转到用户中心
                  if let params = methodCall.arguments as? [String : Int] {
                      DCURLRouter.cjmPushVC(router: RouterURL.userProfile.rawValue, params: params)
                  }
              default: break
              }
          }
          UIViewController.current()?.navigationController?.pushViewController(flutterVC, animated: true)
      }
    

    CJMFlutterViewController

    import UIKit
    
    class CJMFlutterViewController: FlutterViewController {
      class func share() -> CJMFlutterViewController {
          let vc = CJMFlutterViewController.init()
          vc.splashScreenView = {
              let view = UIView.init(frame: UIScreen.main.bounds)
              view.backgroundColor = kBackgroundColor
              return view
          }()
          return vc
      }
      
      public var eventChannelName : String? {
          didSet{
              guard let name = eventChannelName else { return }
              let eventChannel = FlutterEventChannel.init(name: name, binaryMessenger: self.binaryMessenger)
              eventChannel.setStreamHandler(self)
          }
      }
      
      override func setInitialRoute(_ route: String) {
          super.setInitialRoute(route)
          /// 配置公共参数
          let eventChannel = FlutterEventChannel.init(name: "ios_flutter/commonParams", binaryMessenger: self.binaryMessenger)
          eventChannel.setStreamHandler(self)
      }
      
      override func viewDidLoad() {
          super.viewDidLoad()
      }
      
      override func didMove(toParent parent: UIViewController?) {
          super.didMove(toParent: parent)
          if parent == nil {
              DispatchQueue.main.async {
                  self.engine?.viewController = nil
                  self.engine?.destroyContext()
              }
          }
      }
      deinit {
          log.debug("\(#file)  ===>  \(#function)")
      }
    }
    extension CJMFlutterViewController : FlutterStreamHandler {
      /// 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数 EventSink是用来传数据的载体。
      func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
          if let arg = arguments as? String,
              arg == "commonParams" {
              /// 以回调的形式,给flutter传递网络请求的公共参数
              let headerParams = CJMApiManager.empty.headers
              events(headerParams ?? [:])
          }
          return nil
      }
      
      /// flutter不再接收
      func onCancel(withArguments arguments: Any?) -> FlutterError? {
          return nil
      }
    }
    
  • Flutter 实现
    main.dart

    import 'package:flutter/material.dart';
    import 'dart:ui';
    import 'package:flutter/rendering.dart';
    import 'package:flutter_module/class/follow/follow.dart';
    
    void main() {
      debugPaintSizeEnabled = false;
      runApp(_widgetForRoutes(window.defaultRouteName));
    }
    
    Widget _widgetForRoutes(String route) {
      switch (route) {
       case 'Follow':  return Follow();
        default:
          return Center(child: Text('Unknow route: $route', textDirection: TextDirection.ltr,));
      }
    }
    

    pubspec.yaml

    dependencies:
    flutter:
      sdk: flutter
     # The following adds the Cupertino Icons font to your application.
     # Use with the CupertinoIcons class for iOS style icons.
     cupertino_icons: ^0.1.3
     fluttertoast: ^7.0.2    #提示框
     dio: ^3.0.10            #网络请求
     rxdart: ^0.24.0         #响应式编程
     pull_to_refresh: ^1.5.7 #上下拉刷新
    

    Follow.dart

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_module/common/base/base_container.dart';
    import 'follow_listView.dart';
    
    class Follow extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return MaterialApp(
          home: FollowPage(),
          color: Colors.blue,
        );
      }
    }
    
    // 继承基类 BaseContainer
    class FollowPage extends BaseContainer {
      @override
      BaseContainerState<BaseContainer> getState() {
        // TODO: implement getState
        return FollowPageState();
      }
    }
    
    // 继承基类 BaseContainerState
    class FollowPageState extends BaseContainerState<FollowPage> {
    
      // 创建一个channel(类似flutter给iOS发通知)
      static const methodChannel = const MethodChannel('flutter_ios/follow');
      // 点击flutter页面的用户头像给Navite发送消息的回调(带参数用户id)
      Function(int userId) onUserIconButtonHandle;
    
      @override
      void dispose() {
        super.dispose();
        /// 销毁组件
      }
    
      @override
      void initState() {
        super.initState();
        /// 初始化组件
      }
    
      @override
      backItemAction() async {
        // 返回上一级控制器(原生控制器)
        await methodChannel.invokeMethod('backAction');
      }
    
      @override
      Widget setContentView(BuildContext context) {
        final size = MediaQuery.of(context).size;
        return Container(
            height: size.height - 80,
            alignment: Alignment.center,
            child: FollowListView(
                onUserIconButtonHandle: (userId){
                  _userIconAction(userId);
                })
        );
      }
      /// 用户点击事件监听
      _userIconAction(int userId) async {
        /// 给原生控制器发送通知,跳转到用户中心控制器
        await methodChannel.invokeMethod('userIconAction', {'userId':userId});
      }
    }
    

    FollowListView.dart

    import 'package:flutter/material.dart';
    import 'package:flutter_module/common/https/https.dart';
    import 'follow_list_item.dart';
    import 'follow_viewModel.dart';
    import 'follow_items_entity.dart';
    import 'package:pull_to_refresh/pull_to_refresh.dart';
    
    class FollowListView extends StatefulWidget {
      /// 用户头像点击事件的回调
      final Function(int userId) onUserIconButtonHandle;
      /// 初始化函数
      const FollowListView({
        Key key,
        @required this.onUserIconButtonHandle
      }) : super(key:key);
    
      @override
      State<StatefulWidget> createState() {
        // TODO: implement createState
        return FollowListViewState();
      }
    }
    
    class FollowListViewState extends State<FollowListView> {
      /// ListView的数据源
      List<FollowItemsDataPostListList> list = List();
      /// 刷新组件
      RefreshController _refreshController = RefreshController(initialRefresh: false);
      /// 处理数据的viewModel
      FollowViewModel _viewModel = FollowViewModel();
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        /// 当Hppts类中接收到从Native传递过来的公共参数时,通知此处开始做网络请求
        Https().initRequest = (){
          _onRefreshHeader();
        };
      }
    
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return SmartRefresher(
          enablePullDown: true,
          enablePullUp: true,
          controller: _refreshController,
          onRefresh: _onRefreshHeader,
          onLoading: _onRefreshFooter,
          header: WaterDropHeader(
            completeDuration: Duration(milliseconds: 300),
            waterDropColor: Colors.red,
            complete: Center(child: Text('加载完成', style: TextStyle(color: Colors.white),),),
          ),
          footer: CustomFooter(
            builder: (BuildContext context, LoadStatus status){
              String text;
              Widget body;
              if(status == LoadStatus.idle) {
                text = "上拉加载更多";
              }else if(status == LoadStatus.loading) {
                text = "加载中...";
              }else if(status == LoadStatus.failed) {
                text = "数据加载失败!";
              }else if(status == LoadStatus.canLoading) {
                text = "松手立马加载";
              }else{
                text = "到底了";
              }
              body = Text(text, style: TextStyle(color: Colors.white));
              return Container(
                height: 55.0,
                child: Center(child: body,),
              );
            },
          ),
          child: ListView.separated(
            padding: EdgeInsets.all(0),
            itemCount: this.list.length ?? 0,
            itemBuilder: (context, idx) {
              return FollowListItemView(
                itemModel: this.list[idx],
                onFollowButtonHandle: (){
                  var itemModel = this.list[idx];
                  this._viewModel.followUser(
                      '${itemModel.id}',
                      (itemModel.attentionUser == '1' ? false : true), (isSuccess){
                    if(isSuccess){
                      setState(() {
                        this.list[idx].attentionUser = (itemModel.attentionUser == '1' ? '2' : '1');
                      });
                    }
                  });
                },
                onUserIconButtonHandle: this.widget.onUserIconButtonHandle,
              );
            },
            separatorBuilder: (context, idx) {
              return Divider( height: 0 );
            },
            physics: const AlwaysScrollableScrollPhysics(),
          ),
        );
     }
    
     @override
      void dispose() {
        super.dispose();
        _refreshController.dispose();
      }
      /// 下拉刷新数据请求
      _onRefreshHeader() async {
        await this._viewModel.load((data) {
          if (_refreshController.isRefresh) { _refreshController.refreshCompleted(); }
          if (_refreshController.isLoading) { _refreshController.loadComplete(); }
          if(data != null) {
            this.list.clear();
            setState(() {
              this.list.addAll(data);
            });
          }
        });
      }
      /// 上拉刷新数据请求
      _onRefreshFooter() async {
        await this._viewModel.loadMore((data) {
          if (_refreshController.isLoading) { _refreshController.loadComplete(); }
          if(data != null) {
            if (data.length > 0) {
              _refreshController.loadComplete();
            } else {
              _refreshController.loadNoData();
            }
           setState(() {
              this.list.addAll(data);
            });
          }
        });
       }
    }
    

    FollowListItemView

    import 'package:flutter/material.dart';
    import 'package:flutter_module/common/base/base_fluttertoast.dart';
    import 'follow_items_entity.dart';
    
    class FollowListItemView extends StatefulWidget {
      /// 数据模型
      final FollowItemsDataPostListList itemModel;
      /// 关注按钮事件回调
      final Function onFollowButtonHandle;
      /// 用户头像事件回调
      final Function(int userId) onUserIconButtonHandle;
    
      const FollowListItemView({
        Key key,
        this.itemModel,
       this.onFollowButtonHandle,
        this.onUserIconButtonHandle
      }) : super(key: key);
    
      @override
      State<StatefulWidget> createState() {
        return FollowListItemViewState();
      }
    }
    
    class FollowListItemViewState extends State<FollowListItemView> {
      @override
       Widget build(BuildContext context) {
         // TODO: implement build
          return Container(
            height: 60,
            padding: EdgeInsets.all(0),
            alignment: Alignment.center,
            child: Row(
              children: <Widget>[
                Padding(padding: EdgeInsets.only(left: 35),),
    
                GestureDetector(
                  onTap: (){
                    this.widget.onUserIconButtonHandle(this.widget.itemModel.id);
                  },
                  child: ClipRRect(
                   borderRadius: BorderRadius.circular(20.0),
                    child: Image.network(
                      this.widget.itemModel.userAvatar,
                      width: 40,
                      height: 40,
                      fit: BoxFit.fill,
                    ),
                ),
              ),
    
                Padding(padding: EdgeInsets.only(left: 12),),
    
                Expanded(
                  child: Text(
                    "${this.widget.itemModel.nick}",
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.white,
                      decoration: TextDecoration.none,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
               ),
    
                Padding(padding: EdgeInsets.only(left: 10),),
    
                Container(
                  width: 80,
                 height: 24,
                  child: IconButton(
                      padding: EdgeInsets.all(0),
                      icon: Image(
                        image: AssetImage(
                            this.widget.itemModel.attentionUser == "1"
                                ? 'resource/follow_has_attention@3x.png'
                                : 'resource/follow_attention@3x.png'
                        ),
                        fit: BoxFit.cover,
                      ),
                      onPressed: this.widget.onFollowButtonHandle
                  ),
                ),
    
                Padding(padding: EdgeInsets.only(left: 35),)
             ],
            )
       );
      }
    }
    

    FollowViewModel

    import 'follow_items_entity.dart';
    import 'package:flutter_module/common/base/base_fluttertoast.dart';
    import 'package:flutter_module/common/https/https.dart';
    
    class FollowViewModel {
      // 是否下拉刷新
      bool isDown = true;
      // 请求页数
      int pageNo = 1;
      // 每页条数
      int pageSize = 1;
    
      /// 下拉加载
      load(Function(List<FollowItemsDataPostListList>) handle) async {
        this.isDown = true;
        this.pageNo = 1;
        await _request(handle);
      }
    
      /// 加载更多
      loadMore(Function(List<FollowItemsDataPostListList>) handle) async {
        this.isDown = false;
        this.pageNo += 1;
        await _request(handle);
      }
    
      /// 请求列表数据
      _request(Function(List<FollowItemsDataPostListList>) handle) async {
        String urlStr = 'http://dev-api.chaojimei.cn/user/attentionList';
        Map<String, dynamic> params = {"pageNo" : this.pageNo, "pageSize" : this.pageSize};
        await Https().post(urlStr, params, (response) {
          if(response != null) {
            var entity = FollowItemsEntity().fromJson(response);
            handle(entity.data.postList.lists);
          }else{
            BaseFluttertoast.show(response['msg']);
            handle(null);
          }
        }, (obj) {
          if(obj != null) { BaseFluttertoast.show('$obj'); }
          handle(null);
        });
      }
    
      /// 关注、取消关注
      followUser(String userId, bool isFollow, Function(bool) handle) async{
        String urlStr = "http://dev-api.chaojimei.cn/user/attentionUser";
        Map<String,dynamic> params = {'userId' : userId, 'operationType' : (isFollow ? "1" : "2")};
        await Https().post(urlStr, params, (response) {
          if(response != null) {
            BaseFluttertoast.show(isFollow ? '关注成功' : "取消关注成功");
            handle(true);
          }else{
            BaseFluttertoast.show(isFollow ? "关注失败" : "取消关注失败");
            handle(false);
          }
        }, (obj) {
          if(obj != null) { BaseFluttertoast.show('$obj'); }
          handle(false);
        });
      }
    }
    

    网络请求类Https(单例)

    import 'package:dio/dio.dart';
    import 'dart:io';
    import 'dart:convert';
    import 'package:flutter/services.dart';
    
    /// 成功回调
    typedef OnSuccess = void Function(Map response);
    /// 失败回调
    typedef OnFailure = void Function(Object obj);
    
    class Https {
      // 注册一个通知 (用于接收iOS的通知)
      static const eventChannel = const EventChannel('ios_flutter/commonParams');
    
      Map<String,dynamic> headers = {};
    
      String contentType;
    
      // 单列
      factory Https() => _share();
    
      static Https get instance => _share();
    
      static Https _instance;
    
      Https._internal() {
        // 监听事件,同事可发送参数
        eventChannel.receiveBroadcastStream('commonParams').listen(_configure);
      }
    
      static Https _share() {
        if(_instance == null){
          _instance = new Https._internal();
        }
        return _instance;
      }
    
      Function initRequest;
    
      /// 配置公共参数
      void _configure(Object obj) {
        var params = Map<String, dynamic>.from(obj);
        print('$params');
        contentType = params['Content-Type'] ?? "application/x-www-form-urlencoded";
        params.forEach((key, value) {
          if(key != 'Content-Type') { headers[key] = value; }
        });
       initRequest();
      }
    
      post(String url, Map<String,dynamic> params, OnSuccess onSuccess, OnFailure onFailure) async{
        try {
          Response response = await new Dio().post(
            url,
            data: FormData.fromMap(params),
            onSendProgress: (int progress, int total) {
              print('$progress   =====   $total');
            },
            options: Options(
                method: 'Post',
                sendTimeout: 1000 * 15,
                contentType: contentType,
               headers: headers,
            ),
          );
    
          if (response.statusCode == HttpStatus.ok) {
            print('======$url======$params============ $response');
            Map data = json.decode(response.toString());
            if(data["resultCode"] == "0000") {
              onSuccess(data);
            }else{
              onFailure(response);
            }
          }else{
            print('$response   =====  response');
            onFailure(response);
          }
        } catch (exception) {
          print('$exception   =====  exception');
          onFailure(exception);
        }
      }
    }
    

    基类封装
    BaseContainer

    import 'package:flutter/material.dart';
    import 'base_navigation_bar.dart';
    
    abstract class BaseContainer extends StatefulWidget {
      // 标题
      final String title;
      // 导航条高度(默认80)
      final double barHeight;
    
      const BaseContainer({
        Key,key,
        this.title,
        this.barHeight = 80}) : super(key:key);
    
      @override
      State<StatefulWidget> createState() {
        // TODO: implement createState
        return getState();
      }
    
      // 子类实现
      BaseContainerState getState();
    }
    
     abstract class BaseContainerState<T extends BaseContainer> extends State<T> {
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return _buildWidgetDefault();
      }
    
      /// 子类实现,构建各自页面UI控件 相当于setContentView()
      Widget setContentView(BuildContext context);
    
      // 导航条返回按钮事件
      backItemAction();
    
      /// 构建默认布局
      Widget _buildWidgetDefault() {
        final size = MediaQuery.of(context).size;
        return new Scaffold(
          body: Container(
            color: Colors.red,
            constraints: BoxConstraints.expand(),
            child: Column(
              children: <Widget>[
                NavigationBar(
                  title: this.widget.title,
                  height: this.widget.barHeight,
                  backCallback: backItemAction,
                ),
    
                Container(
                  color: Colors.black,
                  height: size.height - 80,
                  alignment: Alignment.center,
                  child: setContentView(context),
               ),
              ],
            ),
          ),
        );
      }
    }
    

    NavigationBar

    import 'package:flutter/material.dart';
    
    class NavigationBar extends StatefulWidget {
      // 标题
      final String title;
      // 高度
      final double height;
    
      // 导航条返回按钮事件回调
      final VoidCallback backCallback;
      // 初始方法
      const NavigationBar({
        Key,
        key,
        this.title,
        this.height,
        @required this.backCallback}) : super(key:key);
    
      @override
      State<StatefulWidget> createState() {
        // TODO: implement createState
        return NavigationBarState();
      }
    }
    
    class NavigationBarState extends State<NavigationBar> {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return Container(
            color: Colors.black,
            height: this.widget.height,
            child: Stack(
              alignment: Alignment.center,
              children: <Widget>[
                Positioned(
                  bottom: 10,
                  height: 20,
                  child: Text("关注",style: TextStyle(fontSize: 15, color: Colors.white, decoration: TextDecoration.none),),
                ),
                Positioned(
                  bottom: 12,
                  left: 28.5,
                  height: 16,
                  child: IconButton(
                    // 点击事件
                   onPressed: this.widget.backCallback,
                    // 必须的参数 一般是 Icon 或者是 ImageIcono
                    icon: Image(
                      image: AssetImage('resource/common_navigationBar_backIcon_white@2x.png'),
                   ),
                    padding: EdgeInsets.all(0),
                    alignment: Alignment.centerLeft,
                    color: Colors.white,
                  ),
                )
              ],
            )
        );
      }
    }
    
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271