用户的关注列表
主要涉及基类封装、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.dartimport '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
getState() { // TODO: implement getState return FollowPageState(); } } // 继承基类 BaseContainerState class FollowPageState extends BaseContainerState { // 创建一个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
createState() { // TODO: implement createState return FollowListViewState(); } } class FollowListViewState extends State { /// ListView的数据源 List 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
createState() { return FollowListItemViewState(); } } class FollowListItemViewState extends State { @override Widget build(BuildContext context) { // TODO: implement build return Container( height: 60, padding: EdgeInsets.all(0), alignment: Alignment.center, child: Row( children: [ 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/[email protected]' : 'resource/[email protected]' ), 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
) handle) async { this.isDown = true; this.pageNo = 1; await _request(handle); } /// 加载更多 loadMore(Function(List ) handle) async { this.isDown = false; this.pageNo += 1; await _request(handle); } /// 请求列表数据 _request(Function(List ) handle) async { String urlStr = 'http://dev-api.chaojimei.cn/user/attentionList'; Map 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 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
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 .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 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); } } } 基类封装
BaseContainerimport '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
createState() { // TODO: implement createState return getState(); } // 子类实现 BaseContainerState getState(); } abstract class BaseContainerState extends State { @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: [ 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
createState() { // TODO: implement createState return NavigationBarState(); } } class NavigationBarState extends State { @override Widget build(BuildContext context) { // TODO: implement build return Container( color: Colors.black, height: this.widget.height, child: Stack( alignment: Alignment.center, children: [ 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/[email protected]'), ), padding: EdgeInsets.all(0), alignment: Alignment.centerLeft, color: Colors.white, ), ) ], ) ); } }