Flutter Future 回撥地獄的一種解決思路

BestNevermore發表於2020-07-07

背景

嘗試使用Flutter來開發雙端應用,實現訂閱功能. 之前用kotlin實現過一次可以參考: koltin回撥地獄的一種解決思路.

實現過程

UI

這個UI比較簡單

Flutter Future 回撥地獄的一種解決思路

訂閱邏輯

同樣使用流式來實現,主動丟擲異常來中斷流.

  void _goBuy(SubscriptModel model) async {
    setState(() {
      _isloading = true;
    });
    // 購買前先確認是不是已經付費過,但是本地訂單丟失了
    await FlutterInappPurchase
        .instance
        // 1. 建立和GP/AppStore的連線
        .initConnection
        // 2. 查詢歷史快取訂單,看看是否已經是vip了.   換機器可以restore,沒必要走網路查詢
        .then((it) {
          if (it == null) {
            throw InitExection;
          }
          return FlutterInappPurchase.instance
              .getAvailablePurchases()
              .then((historyItems) => model.checkHistorySubscript(historyItems))
              .then((item) {
            if (item != null) {
              model.updatePurchased(item);
              throw VipStateException;
            }
          });
        })
        // 3. 獲取可購買產品列表
        .then((it) {
          return FlutterInappPurchase.instance
              .getSubscriptions(List.filled(1, this._productID))
              .then((items) => items
                  .firstWhere((item) => item.productId == this._productID));
        })
        // 4. 購買
        .then((item) {
          return FlutterInappPurchase.instance
              .requestSubscription(this._productID)
              .then((itemStr) => PurchasedItem.fromJSON(jsonDecode(itemStr)));
        })
        // 5.校驗
        .then((purchaseItem) => model.checkSubscript(purchaseItem))
        // 6.客戶端確認訂單
        .then((purchaseItem) {
          model.updatePurchased(purchaseItem);
          SubscriptModel.log(StatisticConstant.SUCCESS);
          FlutterInappPurchase.instance.consumePurchaseAndroid(
              purchaseItem.purchaseToken, purchaseItem.isAcknowledgedAndroid);
        })
        // 異常處理
        .catchError((e) {
          switch (e.runtimeType) {
            case PlatformException:
              String method = e.code;
              if (method == "service_error") {
                _dismissLoading(
                    '''Google services are unavailable. Install "GoogleService" and communicate again.''');
              } else if (method == "buyItemByType") {
                _dismissLoading("");
              }
              break;
            case VipStateException:
              SubscriptModel.data[StatisticConstant.REASON] = 'isVip';
              _dismissLoading('''Already is VIP. No repurchase required.''');
              break;
            case InitExection:
              _dismissLoading(
                  '''Google services are unavailable. Install "GoogleService" and communicate again.''');
              break;
          }
        })
        .whenComplete(() => {_dismissLoading("")});
  }
複製程式碼

這裡實現過程和kotlin原生實現思路一樣,程式碼風格都很接近,接收起來相對容易.同樣實現流式的問題也就轉化換成了:

  1. 怎麼把原生程式碼的回撥轉化成Future流
  2. 怎麼處理原生回撥哪些異常場景.

把原生程式碼的回撥轉化成Future

在kotlin,已經試用使用通道把java回撥轉化成suspend函式. 這裡也是一樣的,只是多了一層dart封裝.例如上面的initConnection方法先申明為一個Future函式

  Future<String> get initConnection async {
    if (_platform.isAndroid) {
      await _setPurchaseListener();
      String result =
          await _channel.invokeMethod('initConnection').catchError((e) {
        return null;
      });
      return result;
    } else if (_platform.isIOS) {
      await _setPurchaseListener();
      final String result =
          await _channel.invokeMethod('canMakePayments').catchError((e) {
        return null;
      });
      return result;
    }
    throw PlatformException(
        code: _platform.operatingSystem, message: "platform not supported");
  }
複製程式碼

iOS和android分別實現initConnection 和 canMakePayments方法. 因為flutter是單執行緒模型,await和kotlin協程的await一樣是非阻塞的,原生實現功能後可以直接利用方法通道,結束await.
方法通道的原生java程式碼:

    @Override
    public void onMethodCall(final MethodCall call, final Result result) {
        if (call.method.equals("initConnection")) {
            billingClient = BillingClient.newBuilder(reg.activity()).setListener(purchasesUpdatedListener)
                    .enablePendingPurchases()
                    .build();
            billingClient.startConnection(new BillingClientStateListener() {
                @Override
                public void onBillingSetupFinished(BillingResult billingResult) {
                    int responseCode = billingResult.getResponseCode();

                    if (responseCode == BillingClient.BillingResponseCode.OK) {
                        result.success("Billing client ready");
                    } else {
                        result.error(call.method, "responseCode: " + responseCode, "");
                    }
                }
            });
        }
    }
複製程式碼

這裡result就是起了通道的作用.

異常機制

Future回撥有個catchError函式,用法和kotlin的Exceptionhandler相似.比較下兩種程式碼

        .catchError((e) {
          switch (e.runtimeType) {
            case PlatformException:
              String method = e.code;
              if (method == "service_error") {
                _dismissLoading(
                    '''Google services are unavailable. Install "GoogleService" and communicate again.''');
              } else if (method == "buyItemByType") {
                _dismissLoading("");
              }
              break;
            case VipStateException:
              SubscriptModel.data[StatisticConstant.REASON] = 'isVip';
              _dismissLoading('''Already is VIP. No repurchase required.''');
              break;
            case InitExection:
              _dismissLoading(
                  '''Google services are unavailable. Install "GoogleService" and communicate again.''');
              break;
          }
        })
複製程式碼

vs

               when (throwable) {
                    is UserCancleException -> {
                        // 使用者取消
                    }
                    is SubscriptProductException -> {
                        // 訂閱購買異常
                    }
                    is InitException -> {
                        // GP初始化異常 一般是沒有裝GP
                    }
                    is RepeateSubscription -> {
                        // 重複購買
                    }
                    is NoProducteException -> {
                        // 找不到要購買的商品資訊
                    }
                    is AcknowException -> {
                        // 訂單確認異常
                    }
複製程式碼

總結

  1. 基於同樣一個業務場景,整體程式碼結構dart和koltin二者相似
  2. 把非同步callback轉化成同步程式碼的方式相似
  3. 異常機制處理相似

後續

  1. 把iOS端留下的問題補充好.
  2. 整理下Flutter的事件模型.

相關文章