一款集成微信分享,微信登陆,打开微信小程等功能的flutter组件

851 阅读8分钟

这篇文章主要介绍一下自己在实际项目开发中,抽离的一套具有打开微信分享,打开微信登陆,打开微信小程序等功能的Flutter Plugin组件,iOS与Android均已实现对应的原生功能

创作不易,希望各位大佬给个start再走,🙏🙏🙏🙏

github地址

公共pub仓库地址

正片开始

本文主要分为两个部分讲解,flutter部分的实现,iOS部分的实现(由于本人主要做的事iOS开发,安卓部分的实现是我的朋友实现的,所以就不对安卓部分展开讲解)

Flutter部分

这个部分其实不难,主要用到 MethodChannel 这个方法来进行和原生的通讯`

static const MethodChannel _channel = const MethodChannel('flutter_wechate_share_plugin');

  • 注册微信开发平台
  /// register 是和原生端约定好关于微信注册的一个通讯名称, {'appid': appid,'universalLink' : universalLink}, 则是flutter端传递给原生端的参数
  /// 通过 微信 appid 注册应用,universalLink 为 ios 平台的必传参数
  static Future<dynamic> register(String appid, {String universalLink = ''}) async {
    if(appid.length == 0 || appid == null) return;
    var result = await _channel.invokeMethod(
        'register',
        {
          'appid': appid,
          'universalLink' : universalLink
        }
    );
    return result;
  }

  • 分享功能
/*
* arguments 主要格式:
* {
*  "kind": "text",
*  "to": "session", timeline - 分享到朋友圈,favorite - 收藏,session - 分享到好友(默认)
*  "title": "the title of message",
*  "description": "short description of message.",
*  "coverUrl": "https://example.com/path/to/cover/image.jpg",
*  "resourceUrl": "https://example.com/path/to/resource.mp4"
* }
* kind: 分享内容格式,默认为 text -> 文本, image -> 图片, music -> 音乐, video -> 视频, webpage -> 网页
* to: 分享方式,默认为 session -> 会话列表, timeline -> 朋友圈, favorite -> 收藏
* title: 分享的标题
* description: 分享内容描述
* coverUrl: 分享的资源缩略图
* resourceUrl: 分享的资源图
*/
static Future<dynamic> share(Map<String, dynamic> arguments) async {
    arguments['kind'] = arguments['kind'] ?? 'text';
    arguments['to'] = arguments['to'] ?? 'session';
    var result = await _channel.invokeMethod(
        'share',
        arguments
    );
    return result;
  }


  • 打开微信小程序
  /*
  * originalId: 组织id,必传参数
  * path 小程序页面的路径 不填默认拉起小程序首页
  * type 分享小程序的版本 (0-正式,1-开发,2-体验)
  * */
  static Future<dynamic> startMiniProgram(String originalId, {String path, String type = "0"}) async {
    if(originalId.length == 0 || originalId == null) {
      return;
    }
    var result = await _channel.invokeMethod(
        'startMiniProgram',
        {
          'originalId': originalId,
          'path': path,
          'type': type
        }
    );
    return result;
  }

  • 其他功能实现
  /// 检测本地是否安装了微信
  static Future<dynamic> isWechatInstalled() async {
    var result = await _channel.invokeMethod(
        'isWechatInstalled'
    );
    return result == 'true' ? true : false;
  }

  /// 获取微信的版本.
  static Future<dynamic> getApiVersion() async {
    var result = await _channel.invokeMethod(
        'getApiVersion'
    );
    return result;
  }

  /// 打开微信app.
  static Future<dynamic> openWechat() async {
    var result = await _channel.invokeMethod(
        'openWechat'
    );
    return result;
  }
  
  /// 登陆
  /// {
  ///   "scope": "snsapi_userinfo",
  ///   "state": "customestate"
  /// }
  static Future<dynamic> login(Map<String, String> arguments) async {
    arguments['scope'] = arguments['scope'] ?? 'snsapi_userinfo';

    var result = await _channel.invokeMethod(
        'login',
        arguments
    );
    return result;
  }

iOS部分

  • 先来看一个比较核心的问题,iOS端注册关于微信的回调

我们知道要想成功唤起微信客户端,原生端除了注册微信appid外,还需要在AppDelegate的几个系统代理中调用WXApi的一些方法,比如:

#pragma mark - AppDelegate
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)application
              openURL:(NSURL *)url
    sourceApplication:(NSString *)sourceApplication
           annotation:(id)annotation {
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
      restorationHandler:(void (^)(NSArray *_Nonnull))restorationHandler {
    return [WXApi handleOpenUniversalLink:userActivity delegate:self];
}

但是我们的flugin项目是没有Appdelegate的,怎么办呢?在主工程注册?这显然不现实,第一代码侵入性太高,第二回调结果不好处理,在这里,Flutter这个框架给我们提供了一个 FlutterApplicationLifeCycleDelegate 这个代理,实现这个代理后,我们同样可以在plugin项目中拿到Appdelegate中的代理方法的监听实现

  • 实现方案一:拦截Appdelegate的代理方法

截屏2021-05-20 下午5.50.59.png

1.注册成为代理

截屏2021-05-20 下午5.52.43.png

2.实现代理的方法,注册WXApi的对应实现方法

截屏2021-05-20 下午5.54.35.png

大家想想这种方式实现后有没有问题呢?我们知道代理模式是一对一的,我们要是在plugin里面实现了对Appdelegate响应处理的话,要是以后你把这个组件运用到自己的项目中,但是在主工程也想响应这里代理方法的话,那么就会产出冲突,要么主工程响应代理方法,plugin不会响应,要么plugin响应代理方法,主工程又不会响应,怎么着都不行,所以我们这里介绍第二种实现方案!

  • 实现方案二: Runtime + 消息转发的方式进行的监听

  1. UIApplication+Hook类,实现对系统方法的交换
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oldMethod = class_getInstanceMethod([self class], @selector(setDelegate:));
        Method newMethod = class_getInstanceMethod([self class], @selector(flutter_setDelegate:));
        methodExchange([self class], oldMethod, newMethod);
    });
}

static inline void
/// 方法交换
/// @param class 需要交换方法的类
/// @param oldMethod 类的方法
/// @param newMethod 交换后的实现方法
methodExchange(Class class,Method oldMethod,Method newMethod)
{
    BOOL isAddedMethod = class_addMethod(class, method_getName(oldMethod), method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
    if (isAddedMethod) {
        class_replaceMethod(class, method_getName(newMethod), method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod));
    } else {
        method_exchangeImplementations(oldMethod, newMethod);
    }
}

static inline void
/// 获取要交换的两个方法
/// @param class 被交换的类
/// @param selector 实现交换的类
classSelectorHook(Class class,SEL selector)
{
    Method oldMethod = class_getInstanceMethod(class, selector);
    NSString *oldSelName = NSStringFromSelector(method_getName(oldMethod));

    // 在FlutterWechateSharePlugin实现
    Method newMethod = class_getInstanceMethod([FlutterWechateSharePlugin class], NSSelectorFromString([NSString stringWithFormat:@"flutter_%@",oldSelName]));

    methodExchange(class, oldMethod, newMethod);
}

- (void)flutter_setDelegate:(id<UIApplicationDelegate>) delegate
{
    static dispatch_once_t delegateOnceToken;
    dispatch_once(&delegateOnceToken, ^{
        // 交换application的一些方法
        classSelectorHook([delegate class], @selector(application:handleOpenURL:));
        classSelectorHook([delegate class], @selector(application:openURL:options:));
        classSelectorHook([delegate class], @selector(application:openURL:sourceApplication:annotation:));
        classSelectorHook([delegate class], @selector(application:continueUserActivity:restorationHandler:));
    });

    // 此时的flutter_setDelegate为原delegate,保证不影响原始功能
    [self flutter_setDelegate:delegate];
}

  1. 在FlutterWechateSharePlugin类实现,对hook方法的处理
#pragma mark - AppDelegate - Hook 实现
- (BOOL)flutter_application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    
    [FlutterWechateSharePlugin performAppDelegateTarget:[self class] action:_cmd params:application,url, nil];
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)flutter_application:(UIApplication *)application
              openURL:(NSURL *)url
    sourceApplication:(NSString *)sourceApplication
           annotation:(id)annotation {
    
    [FlutterWechateSharePlugin performAppDelegateTarget:[self class] action:_cmd params:application,url,sourceApplication,annotation, nil];
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)flutter_application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
    
    [FlutterWechateSharePlugin performAppDelegateTarget:[self class] action:_cmd params:application,url,options, nil];
    return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)flutter_application:(UIApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
      restorationHandler:(void (^)(NSArray *_Nonnull))restorationHandler {
    
    [FlutterWechateSharePlugin performAppDelegateTarget:[self class] action:_cmd params:application,userActivity,restorationHandler, nil];
    return [WXApi handleOpenUniversalLink:userActivity delegate:self];
}

其中 + (BOOL)performAppDelegateTarget:(Class)cls action:(SEL)sel params:(id)arg, ... 这个方法主要实现了对FlutterWechateSharePlugin类中的消息转发

  • iOS与flutter通信的具体功能实现代码
/// 这个方法是处理oc和flutter通讯的核心方法
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    
    // 注册微信
    if ([METHOD_REGISTERAPP isEqualToString:call.method]) {
        
        NSString *appId = call.arguments[@"appid"];
        NSString *universalLink = call.arguments[@"universalLink"];
        [WXApi registerApp:appId universalLink:universalLink];
        result(nil);
        
    }
    // 判断本地是否安装微信
    else if ([METHOD_ISINSTALLED isEqualToString:call.method]) {
        
        result([NSNumber numberWithBool:[WXApi isWXAppInstalled]]);
    }
    // 打开微信
    else if ([METHOD_OPENWECHAT isEqualToString:call.method]) {
        
        result([NSNumber numberWithBool:[WXApi openWXApp]]);
    }
    // 微信登陆
    else if ([METHOD_LOGINWECHAT isEqualToString:call.method]) {
        
        NSString* scope= call.arguments[@"scope"];
        NSString* state= call.arguments[@"state"];
        SendAuthReq *request = [[SendAuthReq alloc] init];
        request.scope = scope;
        request.state = state;
        [WXApi sendReq:request completion:^(BOOL success) {
            
        }];
    }
    // 分享
    else if ([@"share" isEqualToString:call.method]) {
        
        NSDictionary *arguments = [call arguments];
        
        // kind 为分享的类型,text -> 文本, image -> 图片, music -> 音乐, video -> 视频, webpage -> 网页
        NSString *kind = arguments[@"kind"];
        
        // 分享文字内容
        if ([kind isEqualToString:METHOD_SHARETEXT]) {
            
            [self handleShareTextCall:call result:result];
            
        }
        // 分享图片内容
        else if ([kind isEqualToString:METHOD_SHAREIMAGE]) {
                        
            [self handleShareImageCall:call result:result];
        }
        // 分享音乐
        else if ([kind isEqualToString:METHOD_SHAREMUSIC]) {
            
            [self handleShareMusicCall:call result:result];
        }
        // 分享网页
        else if ([kind isEqualToString:METHOD_SHAREWEBPAGE]) {
            
            [self handleShareWebPageCall:call result:result];
        }
        
    }
    // 打开微信小程序
    else if ([METHOD_STARTMINIPROGRAM isEqualToString:call.method]) {
        
        [self handleStartMiniProgramCall:call result:result];
    }
    else {
    
        result(FlutterMethodNotImplemented);
    }
}

/// 分享文字
/// @param call 分享的内容
/// @param result 分享结果
- (void)handleShareTextCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    
    SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
    // to 是分享的渠道, session -> 会话列表, timeline -> 朋友圈, favorite -> 收藏, 默认是 session -> 会话列表
    NSString *to = call.arguments[@"to"];
    // 分享到朋友圈
    if ([@"timeline" isEqualToString:to]) {
        req.scene = WXSceneTimeline;
    }
    // 添加到收藏列表
    else if ([@"favorite" isEqualToString:to]) {
        req.scene = WXSceneFavorite;
    }
    // 分享到好友
    else {
        req.scene = WXSceneSession;
    }
    
    req.bText = YES;
    req.text = call.arguments[@"text"];
    
    // 验证是否分享成功回调
    [WXApi sendReq:req completion:^(BOOL success){
                    
    }];
    result(nil);
}


/// 分享图片
/// @param call 分享的内容
/// @param result 分享结果
- (void)handleShareImageCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    
    SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
    // to 是分享的渠道, session -> 会话列表, timeline -> 朋友圈, favorite -> 收藏, 默认是 session -> 会话列表
    NSString *to = call.arguments[@"to"];
    // 分享到朋友圈
    if ([@"timeline" isEqualToString:to]) {
        req.scene = WXSceneTimeline;
    }
    // 添加到收藏列表
    else if ([@"favorite" isEqualToString:to]) {
        req.scene = WXSceneFavorite;
    }
    // 分享到好友
    else {
        req.scene = WXSceneSession;
    }
    req.bText = NO;
    WXMediaMessage *message = [WXMediaMessage message];
    message.title = call.arguments[@"title"];
    message.description = call.arguments[@"description"];
    
    WXImageObject *mediaObject = [WXImageObject object];
    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:call.arguments[@"resourceUrl"]]];
    if (imageData != nil) {
        mediaObject.imageData = imageData;
    } else {
        NSString *imageUri = call.arguments[@"url"];
        NSURL *imageUrl = [NSURL URLWithString:imageUri];
        mediaObject.imageData = [NSData dataWithContentsOfFile:imageUrl.path];
    }
    message.mediaObject = mediaObject;
    
    req.message = message;
    [WXApi sendReq:req completion:^(BOOL success){
        
    }];
    result(nil);
}


/// 分享音乐
/// @param call 分享的内容
/// @param result 分享结果
- (void)handleShareMusicCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    
    SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
    // to 是分享的渠道, session -> 会话列表, timeline -> 朋友圈, favorite -> 收藏, 默认是 session -> 会话列表
    NSString *to = call.arguments[@"to"];
    // 分享到朋友圈
    if ([@"timeline" isEqualToString:to]) {
        req.scene = WXSceneTimeline;
    }
    // 添加到收藏列表
    else if ([@"favorite" isEqualToString:to]) {
        req.scene = WXSceneFavorite;
    }
    // 分享到好友
    else {
        req.scene = WXSceneSession;
    }
    req.bText = NO;
    
    // 分享的消息会话模型
    WXMediaMessage *message = [WXMediaMessage message];
    message.title = call.arguments[@"title"];
    message.description = call.arguments[@"description"];
    
    // 音乐的资源路径
    NSString *musicDataUrl = call.arguments[@"resourceUrl"];
    // 音乐的网页链接地址
    NSString *musicUrl = call.arguments[@"url"];
    // 音乐的封面链接地址
    NSString *coverUrl = call.arguments[@"coverUrl"];
    
    // 音乐的数据模型
    WXMusicObject *mediaObject = [WXMusicObject object];
    mediaObject.musicUrl = musicUrl;
    mediaObject.musicDataUrl = musicDataUrl;
    
    NSData *coverImageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:coverUrl]];
    [message setThumbImage:[UIImage imageWithData:coverImageData]];
    
    message.mediaObject = mediaObject;
    
    req.message = message;
    [WXApi sendReq:req completion:^(BOOL success){
        
    }];
    result(nil);
    
}


/// 分享网页
/// @param call 分享的内容
/// @param result 分享结果
- (void)handleShareWebPageCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    
    SendMessageToWXReq *req = [[SendMessageToWXReq alloc] init];
    // to 是分享的渠道, session -> 会话列表, timeline -> 朋友圈, favorite -> 收藏, 默认是 session -> 会话列表
    NSString *to = call.arguments[@"to"];
    // 分享到朋友圈
    if ([@"timeline" isEqualToString:to]) {
        req.scene = WXSceneTimeline;
    }
    // 添加到收藏列表
    else if ([@"favorite" isEqualToString:to]) {
        req.scene = WXSceneFavorite;
    }
    // 分享到好友
    else {
        req.scene = WXSceneSession;
    }
    req.bText = NO;
    // 分享的消息会话模型
    WXMediaMessage *message = [WXMediaMessage message];
    message.title = call.arguments[@"title"];
    message.description = call.arguments[@"description"];
    
    WXWebpageObject *mediaObject = [WXWebpageObject object];
    mediaObject.webpageUrl = call.arguments[@"url"];
    
    [message setThumbImage:[UIImage imageWithData:call.arguments[@"coverUrl"]]];
    message.mediaObject = mediaObject;
    
    req.message = message;
    [WXApi sendReq:req completion:^(BOOL success){
        
    }];
    result(nil);
    
}


/// 打开微信小程序
/// @param call 参数内容
/// @param result 返回结果结果
- (void)handleStartMiniProgramCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    
    WXLaunchMiniProgramReq *req = [[WXLaunchMiniProgramReq alloc] init];
    req.userName = call.arguments[@"originalId"];
    req.path = call.arguments[@"path"];
    // 默认打开正式版本的小程序
    req.miniProgramType = WXMiniProgramTypeRelease;
    NSString *miniProgramType = call.arguments[@"type"];
    if (miniProgramType.length != 0 && miniProgramType != nil) {
        
        NSInteger type = [call.arguments[@"type"] integerValue];
        switch (type) {
            case 0:
            {
                req.miniProgramType = WXMiniProgramTypeRelease;
            }
                break;
                
            case 1:
            {
                req.miniProgramType = WXMiniProgramTypeTest;
            }
                break;
                
            case 2:
            {
                req.miniProgramType = WXMiniProgramTypePreview;
            }
                break;
                
            default:
                break;
        }
        
    }
    [WXApi sendReq:req completion:^(BOOL success){
        
    }];
    result(nil);
}

#pragma mark - WXApiDelegate
- (void)onResp:(BaseResp*)resp {
    NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
    [dictionary setValue:[NSNumber numberWithInt:resp.errCode] forKey:ARGUMENT_KEY_RESULT_ERRORCODE];
    if (resp.errStr != nil) {
        [dictionary setValue:resp.errStr forKey:ARGUMENT_KEY_RESULT_ERRORMSG];
    }
    if ([resp isKindOfClass:[SendMessageToWXResp class]]) {
        // 分享
        [_channel invokeMethod:METHOD_ONSHAREMSGRESP arguments:dictionary];
    } else if ([resp isKindOfClass:[WXLaunchMiniProgramResp class]]) {
        // 打开小程序
        if (resp.errCode == WXSuccess) {
            WXLaunchMiniProgramResp *launchMiniProgramResp =
                (WXLaunchMiniProgramResp *)resp;
            [dictionary setValue:launchMiniProgramResp.extMsg
                          forKey:ARGUMENT_KEY_RESULT_EXTMSG];
        }
        [_channel invokeMethod:METHOD_ONLAUNCHMINIPROGRAMRESP arguments:dictionary];
    }
}

感谢阅读!!,不急不躁!好好学习!天天向上!