Flutter 入門與實戰(五十): Provider實現不相關頁面狀態共享

島上碼農發表於2021-08-13

這是我參與8月更文挑戰的第13天,活動詳情檢視:8月更文挑戰

前言

我們之前講述了 Provider 實現狀態管理的兩種方式:

那就來了一個問題,兩個不相關的頁面可以實現狀態共享嗎?比如我們的動態新增和編輯頁面,屬於同一個層級,如果不考慮利用他們的父級元件完成狀態共享,那麼兩個頁面就是平行不相關的,但是他們很多方法和資料都是可以共用的。對於這種情況,Provider 提供了一種方式來實現狀態共享,那就是兩個頁面共用同一個狀態物件。

MyChangeNotifier variable;

ChangeNotifierProvider.value(
  value: variable,
  child: ...
)
複製程式碼

使用 ChangeNofitierProvider.value 方法可以利用一個已有的物件提供狀態管理,而且這個已有的物件可以供其他元件共用。

改造狀態管理類

新增和編輯頁面的很多方法和資料是可以共用的,例如:

  • handleTextFieldChanged:文字輸入值改變時更新對應表單內容;
  • handleClear:點選清除按鈕時清除對應表單的內容;
  • handleImagePicked:圖片選擇控制元件選定圖片後更新圖片;
  • _imageFile:選擇的圖片檔案。
  • _formData:表單的配置是共用的,但是新增頁面沒有初始值,而編輯頁面需要使用已有動態填充。

這裡我們既然要保證新增和編輯頁面共用一個狀態物件,那就需要提供類的共享物件,自然這就需要使用到靜態的單例物件。我們把上一篇的 DynamicAddModel 改為 DynamicShareModel,使用單例定義:

class DynamicShareModel with ChangeNotifier {
  DynamicShareModel._privateConstructor();

  static final DynamicShareModel sharedDynamicModel =
      DynamicShareModel._privateConstructor();
  
  //...
}
複製程式碼

同時,對於新增和編輯頁面,進入前在渲染介面時都應該清除表單資料和快取的動態物件,否則會導致新增頁面和編輯頁面會使用舊資料渲染介面(比如編輯完之後,進入新增頁面會渲染剛剛編輯的表單資料)。因此,需要提供一個清除快取狀態資料的方法:

clearState() {
  _currentDynamic = null;
  _imageFile = null;
  _formData.forEach((key, form) {
    form['value'] = '';
    (form['controller'] as TextEditingController)?.text = '';
  });
}
複製程式碼

然後就是完善新增和編輯的差異方法:

  • getDynamic:使用動態 id 獲取當前的動態物件,成功後通知編輯介面渲染表單——按已有的動態填充表單。
  • handleAdd:新增動態的網路提交和處理方法。
  • handleEdit:編輯動態的網路提交和處理方法。
  • _validateForm::表單校驗方法,對於新增頁面需要額外校驗是否選擇了圖片。

原始碼比較長,就不貼了,已經上傳至:狀態管理程式碼

頁面程式碼改造

這個時候新增頁面進入後我們需要先清除狀態快取,因此需要由生命週期管理了,需要把新增頁面變為 StatefulWidget,然後在 initState 中清除狀態快取。

@override
void initState() {
  super.initState();

  context.read<DynamicShareModel>().clearState();
}
複製程式碼

同時,在 DynamicAddWrapper 這個包裹類上,需要使用 ChangeNotifierProvider.value 的方式來和編輯頁面共享共一個DynamicShareModel狀態管理單例物件。

class DynamicAddWrapper extends StatelessWidget {
  DynamicAddWrapper({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: DynamicShareModel.sharedDynamicModel,
      child: _DynamicAdd(),
    );
  }
}
複製程式碼

編輯頁面也類似,只是需要在 initState 中先清除狀態快取,然後再請求動態詳情:

@override
void initState() {
  super.initState();
  context.read<DynamicShareModel>().clearState();
  context.read<DynamicShareModel>().getDynamic(widget.dynamicId);
}
複製程式碼

編輯頁面和新增頁面的表單幾乎一樣,只是提交按鈕名稱和提交成功後處理邏輯不同。

// 新增頁面
 DynamicForm(
  watchState.formData,
  readState.handleTextFieldChanged,
  readState.handleClear,
  '提交',
  () {
    readState.handleAdd().then((success) {
      if (success) {
        context.read<DynamicModel>().add(readState.currentDynamic);
        Navigator.of(context).pop();
      }
    });
  },
  readState.handleImagePicked,
  imageFile: watchState.imageFile,
),
);

//編輯頁面
DynamicForm(
  watchState.formData,
  readState.handleTextFieldChanged,
  readState.handleClear,
  '儲存',
  () {
    readState.handleEdit().then((success) {
      context.read<DynamicModel>().update(watchState.currentDynamic);
      Navigator.of(context).pop();
    });
  },
  readState.handleImagePicked,
  imageFile: watchState.imageFile,
  imageUrl: watchState.currentDynamic.imageUrl,
);
複製程式碼

就這樣,我們就完成了兩個頁面和共享狀態管理的實現。

執行結果

執行結果和我們的預期一致,實際除錯的時候也可以試著不清楚快取會怎麼樣。實際開發過程中很容易不小心出現忘記清除快取的情況,因此,從程式碼維護性角度而言,共享狀態也未必是最佳的選擇,具體情況具體分析。

螢幕錄製2021-08-12 下午10.34.10.gif

總結

本篇介紹了使用 ProviderChangeNotifierProvider.value的方式來在不相關的頁面間共享狀態資料。步驟為:

  • 建立兩個頁面都能訪問到的同一個物件,通常是使用單例或容器的方式。
  • 建立一個包裹Widget,使用ChangeNotifierProvider.value包裹頁面,以便訪問到共享的狀態物件。
  • 如果頁面之間共享的資料可能因為另一個頁面的操作而改變就需要看是否需要清除狀態快取(有些可能是需要同步而不是清除)。
  • 頁面元件引用狀態完成介面渲染和互動。

本篇僅為示例,實際寫起來的過程中感覺還是有些彆扭,其實狀態共享會更適合對同一個物件的不同操作的場景,例如從詳情頁點選編輯進入編輯頁面這種情況。本篇將新增和編輯放一起做狀態共享雖然提高了程式碼複用性,但是降低了可維護性和破壞了單一職責原則,因此實際開發過程中,可以根據實際情況來決定要不要共享狀態。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章