首頁 > 軟體

Flutter 佇列任務的實現

2022-06-14 14:02:14

前言

在電商的應用中,最常見的就是在首頁或完成某事件之後,彈出一堆的活動/廣告。假如重疊彈出,很醜,給使用者的體驗也不好,所以一般都會依次依條件的彈出。

下面講講我是怎麼實現一個方便的佇列任務管理

佇列

任務佇列,那當然要有個佇列。這個佇列的任務內容應該是返回FutureFunction,因為我需要得到他處理完成的結果,比如等待彈窗關閉時return,才表示這個任務被完成。

typedef TaskEventFunction = Future Function();

class TaskQueue {
    List<TaskEventFunction> _actionQueue = [];
}

新增任務進佇列

然後是加入佇列的方法add

void add(TaskEventFunction task) {
  _actionQueue.add(task);
}

然後想到,佇列是不是最好去重?或者可選去重。

那一個Function如何去重呢,我們可以使用它的hashCode,同一個FunctionhashCode相同

taskQueue.add(testFunction);

Future testFunction() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  return Future.value(true);
}

即我們可以按上面這樣定義,同一個類同一個範例下同一個Function,就是相同的hashCode。若是以下這種寫法則hashCode不一樣,每次add都相當於一個新的Function

taskQueue.add(() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  return Future.value(true);
});

假如不需要去重,可以用第二種。也可以主動指定taskId來選擇哪些需要去重(即使內容不一樣),哪些不需要。

修改一下add,增加taskId。同時hashCode應該作為主要的key,修改一下佇列型別。 最終如下:

/// 任務編號佇列
List<String> _actionQueue = [];

/// 任務佇列
Map<String, TaskEventFunction> _actionMap = {};

/// 指定taskId 為 -1 則不用去重
String add(TaskEventFunction task, {
  String? taskId,
}) {
  String? id = taskId;
  id ??= task.hashCode.toString();
  bool isContains = false;
  if (taskId != '-1') {
    isContains = _actionQueue.contains(id);
  } else {
    id = task.hashCode.toString();
  }
  if (!isContains) {
    _actionQueue.add(id);
    _actionMap[id] = task;
  }
  return id;
}

-1時則不去重,也把最終的taskId返回。

移除佇列指定任務

有新增任務的方法,那也需有移除任務的方法

/// 這裡需注意,add的時taskId為-1,那就直接把task傳入,add時有返回,可以對應
bool remove(TaskEventFunction task, {String? taskId}) {
  String? id = taskId;
  id ??= task.hashCode.toString();
  if (_actionQueue.contains(id)) {
    _actionMap.remove(id);
    return _actionQueue.remove(id);
  }
  return false;
}

taskId/hashCode為準。

判斷是否包含對應任務

使用者可以自己判斷是否已包含對應的任務

/// 是否佇列中包含該任務
/// [task] 與 [taskId] 應該只傳一個
bool containers({TaskEventFunction? task, String? taskId}) {
  assert(task != null || taskId != null);
  String? id = taskId;
  id ??= task.hashCode.toString();
  return _actionQueue.contains(taskId);
}

執行佇列任務

任務佇列的進出基本成型,開始處理任務。很簡單,取出任務,然後執行任務,最後移除任務

void startLoop() async {
  if (dealing || _actionQueue.isEmpty) {
    return;
  }
  dealing = true;
  String taskId = _actionQueue.first;
  TaskEventFunction? callback = _actionMap[taskId];
  if (callback == null) {
    _actionQueue.remove(taskId);
    return;
  }

  try {
    await callback();
  } catch (e) {
    log('_actionQueue 出錯 $e');
  } finally {
    _actionQueue.remove(taskId);
    _actionMap.remove(taskId);
    dealing = false;
    if (_actionQueue.isNotEmpty) {
      startLoop();
    }
  }
}

這裡加了個dealing,表示任務正在處理中的狀態,正在處理的話,不允許執行下一個任務。

在執行完成(或失敗)後,自動觸發下一個任務。add中也可以加入startLoop,使新增任務後自動啟動任務迴圈。

任務條件

基本的任務佇列已經完成。不過每個任務其實一般都會有個條件,確認符合當前場景才執行。比如說的確是新人且在首頁,才顯示新人優惠彈窗。

條件可以是整個任務佇列統一的條件,也可以是某個任務特定的條件

typedef TaskConditionFunction = FutureOr<bool> Function();

這裡型別用FutureOr<bool>,有可能需要等待一下才能確認條件。任務對應的條件,也根據taskId來儲存。

/// 任務條件
Map<String, TaskConditionFunction> _actionCondition = {};

/// 總條件
TaskConditionFunction? _condition;

新增任務時加入條件

TaskEventFunction add(TaskEventFunction task, {
  String? taskId,
  TaskConditionFunction? condition,
}) {
    ...
    
    if (condition != null && !_actionCondition.containsKey(id)) {
      _actionCondition[id] = condition;
    }

}

設定總條件,或補充條件

/// 設定允許執行條件
void setAcceptConditions(TaskConditionFunction condition,
    {String? taskId}) {
  if (taskId == null) {
    _condition = condition;
  } else {
    _actionCondition[taskId] = condition;
  }
}

執行任務前判斷條件是否滿足

    bool canNext = await _nextConditions(taskId);
    if (canNext) {
      try {
        await callback();
      } catch (e) {
        log('_actionQueue 出錯 $e');
      } finally {
        _actionQueue.remove(taskId);
        _actionMap.remove(taskId);
        dealing = false;
        if (_actionQueue.isNotEmpty) {
          startLoop();
        }
      }
    } else {
      // 不滿足條件一般後續的也不會執行
      dealing = false;
    }
Future<bool> _nextConditions([String? id]) async {
  String taskId = id ?? _actionQueue.first;
  bool canNext = true;
  var taskCondition = _actionCondition[taskId];
  if (_condition != null) {
    canNext = await _condition!.call();
  }
  if (canNext && taskCondition != null) {
    canNext = await taskCondition();
  }

  return Future.value(canNext);
}

原則上應該既滿足總條件也滿足任務條件

使用和總結

範例化TaskQueue

TaskQueue taskQueue = TaskQueue();

新增任務並開始任務

taskQueue.add(testFunction, condition: () async {
  await Future.delayed(const Duration(milliseconds: 200));
  return Future.value(true);
});
taskQueue.startLoop();

Future testFunction() async {
  return showDialog(...);
}

注意設定條件要返回bool

還可以加入類似延時等操作,跟條件一樣設定即可。太久的操作不要像上面一樣寫在condition中,可能執行完之後又不滿足了,根據具體情況考慮。

一般來說類似彈窗的returnawait showDialog就可以等待彈窗頁面結束,再進行下一個。

跨頁面還是當前頁面的控制,只需要考慮是全域性範例TaskQueue還是頁面內範例TaskQueue

這樣一個使用簡單好用的任務佇列就實現好了,更多相關Flutter 佇列任務內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com