最近在使用flutter开发APP,flutter实现了一套代码同时生成Android和iOS两个平台的APP,可以实现零基础快速上手APP开发,缩短开发周期。但flutter仍处于较快增长期,版本迭代速度快,文档资料相对较少,这里将开发中遇到的一些问题整理下来,备忘+分享。

flutter版本:1.12.13+hotfix.5

微信分享

这里使用fluwx插件实现微信中分享,版本号1.2.1+1。

配置依赖

在pubspec.yaml中增加依赖

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  #other dependencies
  fluwx: ^1.2.1+1

Android配置

如果需要微信回调,需要配置WXEntryActivity和WXPayEntryActivity,如果不使用回调,则不需要特殊配置。详见官方doc

IOS配置

如官方文档所示,需要配置ios的URLSchema, LSApplicationQueriesSchemes or universal link 

Android 微信分享跳转app怎样判断是否安装了此app 微信分享跳转到app_flutter

ios/Runner/Info.plist

<key>CFBundleURLTypes</key>
    <array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>your url name</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>your scheme</string>
        </array>
    </dict>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>weixin</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>you wechat appid</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>wechat</string>
    <string>weixin</string>
    <string>weixinULAPI</string>
</array>

其中CFBundleURLTypes的第一项是为了能够从url跳回APP所设置

ios/Runner/Runner.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <!-- ... other keys -->
  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>webcredentials:your universal link</string>
  </array>
  <!-- ... other keys -->
</dict>
</plist>

这里的universal link要与微信开放平台上的设置保持一致。

Flutter调用

注册微信sdk,如果用于ios,必须指定universalLink

import 'package:fluwx/fluwx.dart' as fluwx;
@override
void initState() {
    fluwx.registerWxApi(
        appId: your wechat appid,
        doOnAndroid: true,
        doOnIOS: true,
        universalLink: your universal link
    );
}

分享,以图文链接到好友为例

fluwx.shareToWeChat(
    fluwx.WeChatShareWebPageModel(
        title: your title, 
        description: your description(can split by \n),
        webPage: 'https://example.com/app/jump.html?pageName=xx&id=xx',
        thumbnail: 'https://example.com/images/share_weixin.png',
        scene: fluwx.WeChatScene.SESSION
        )
    ).then((data) {
    print (data);
});

至此,可实现从APP分享到微信,下一步将从微信点击分享后的链接,唤起APP并直接打开指定页面。

打开APP指定页面

从APP分享出去只是产品宣传的第一步,下一步需要接收到的用户通过点击链接卡片可以打开我们的APP的某个指定页面。当然,用户点击分享链接后,可以打开产品的小程序or网页版,以更好的提升体验,这里仅以跳转APP为例。

很多文档提到改写Activity类来实现跳转链接参数的解析,从而进一步跳转到指定页面,但随着flutter的升级,kotlin的Activity相关API有明显变化,由于笔者并未做过Android开发,所以放弃了硬写的方案,这里使用uni_links来实现参数的解析。

配置依赖

uni_links: ^0.2.0

Android配置

android/app/src/main/AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:theme="@style/LaunchTheme"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize">
    <meta-data
        android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
        android:value="true" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="yourScheme"
            android:host="yourHost"
        />
    </intent-filter>
</activity>

IOS配置

在上面微信配置中已加入相关配置(这里没有使用HTTPS scheme,如果使用的话,还需要一些额外配置,详见uni_link的文档

Flutter调用

uni_link的官方example已经非常详尽,照着来就可以实现,如下是我的实现

在main.dart中实现

class MyAppState extends State<MyApp> with WidgetsBindingObserver {
  StreamSubscription _sub;
  String _latestLink = 'Unknown';
  Uri _latestUri;
  // This widget is the root of your application.
  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  @override
  void dispose() {
    if (_sub != null) _sub.cancel();
    super.dispose();
  }

  Future<void> initPlatformState() async {
    _sub = getLinksStream().listen((String link) {
      if (!mounted) return;
      setState(() {
        _latestLink = link ?? "Unknown";
        _latestUri = null;
        try {
          if (link != null) _latestUri = Uri.parse(link);
        } on FormatException {}
      });
    }, onError: (err) {
      if (!mounted) return;
      setState(() {
        _latestLink = 'Failed to get latest link: $err.';
        _latestUri = null;
      });
    });
    getLinksStream().listen((String link) {
      print('got link: $link');
    }, onError: (err) {
      print('got err: $err');
    });
    String initialLink;
    Uri initialUri;
    try {
      initialLink = await getInitialLink();
      print('initial link: $initialLink');
      if (initialLink != null) initialUri = Uri.parse(initialLink);
    } on PlatformException {
      initialLink = 'Failed to get initial link.';
      initialUri = null;
    } on FormatException {
      initialLink = 'Failed to parse the initial link as Uri.';
      initialUri = null;
    }
    if (!mounted) return;

    setState(() {
      _latestLink = initialLink;
      _latestUri = initialUri;
    });
  }

  @override
  Widget build(BuildContext context) {
    final queryParams = _latestUri?.queryParametersAll?.entries?.toList();
    // print (queryParams);
    // print (queryParams?.length);
    Widget homeWidget = new SplashPage();
    String pageType;
    String taskID;
    if (queryParams != null) {
      for (var param in queryParams) {
        if (param.key == 'pageName') {
          pageType = param.value.length > 0 ? param.value[0] : null;
        }
        if (param.key == 'taskid') {
          taskID = param.value.length > 0 ? param.value[0] : null;
        }
      }
    }
    print (pageType);
    if (pageType == 'report') {
      homeWidget = new ReportPage(taskID: taskID,);
    }
    return MaterialApp(
      routes: {
        Config.routeMain: (context) => MyHomePage()
      },
      debugShowCheckedModeBanner: false,
      title: 'your title',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: homeWidget
    );
  }
}

中转页面

这里写了一个简单的中转页面

<!Doctype html>
<html xmlns=http://www.w3.org/1999/xhtml>
<head>
    <meta http-equiv=Content-Type content="text/html;charset=utf-8">
    <title>Your Title</title>
</head>
<body>
    <style>
        #mobliestart{font-size:40px;}
    </style>
    <a href='yourScheme://yourHost?page=report&taskid=1' id="mobliestart">点击打开APP</a>

    <script type="text/javascript"> 
        function applink(){   
            window.location = 'yourScheme://yourHost?page=report&taskid=1'; 
        }
        applink();
    </script>
</body>
</html>

中转页面设置了自动跳转和点击跳转,由于微信浏览器对scheme link的限制,ios和Android都需要点击右上角的‘浏览器中打开’。ios(iPhone xr,13.3)在Safari中打开后会自动弹出对话框,Android(华为)在系统浏览器和chrome会自动弹出对话框,qq浏览器需要点击页面超链接才会弹出对话框。

Android 微信分享跳转app怎样判断是否安装了此app 微信分享跳转到app_唤起APP_02

Android 微信分享跳转app怎样判断是否安装了此app 微信分享跳转到app_唤起APP_03

 

注:目前仍存在一个问题,如果APP处于后台打开状态,从链接打开后,只是唤起APP,并不会跳转到指定页面,通过打印日志,build函数中解析链接参数的逻辑解析正确,目前这个问题还未进一步解决。