状态分类flutter应用中有两种状态短时状态和应用状态短时状态就是只属于单个widget的简单状态应用状态是需要在多个页面共享例如用户登录信息…思考在flutter中如果想要多个widget共享同一份数据就必须把数据提升到这些widget的共同祖先中然后通过参数层层向下传递但是这样很麻烦。更好的方法是使用一个专门的状态管理机制让下层的widget可以直接的调用访问祖先的数据而不需要中间的widget进行传递。Flutter中提供了InheritedWidget来实现这种跨层访问并且provider包对InheritedWidget有进行有好的封装便于我们的使用。ProviderChangeNotifier - 可监听的数据模型ChangeNotifier是Flutter原生提供的一个类它实现了这样一个功能观察者模式当数据改变时通过调用notifyListeners()通知所有监听器ChangeNotifierProvider - 向子树提供模型实例ChangeNotifierProvider是一个widget他的作用是创建并管理一个ChangeNotifier实例并使其对子树中的所有widget可用并且我们需要把他放在所有需要访问该模型的所有widget的共同祖先之上。例如voidmain(){runApp(ChangeNotifierProvider(create:(context)CartModel(),// 创建 CartModel 实例child:constMyApp(),),);}并且当不再需要时provider会自动调用CartModel.dispose()如果需要为多个模型提供那么可以使用MultiProviderMultiProvider(providers:[ChangeNotifierProvider(create:(context)CartModel()),Provider(create:(context)SomeOtherClass()),],child:constMyApp(),)Consumer - 在UI中获取并使用模型有了ChangeNotifierProvider后我们可以在任何子widget中通过Consumer来获取数据实例并在模型变化是自动重建UI。Consumer是一个泛型widget其中的builder函数有三个参数context(上下文)cart(从上层ChangeNotifierProvider中获取到的CartModel实例)child()实例代码如下returnConsumerCartModel(builder:(context,cart,child){returnText(Total price:${cart.totalPrice});},);其他访问方式Provider.of有些时候我们只需要调用模型的方法而不需要模型的数据来重建UI。例如ElevatedButton(onPressed:(){Provider.ofCartModel(context,listen:false).removeAll();},child:Text(清空购物车),)其中listen: false表示我们不像监听任何模型变化因此按钮不会重建通过这些方法UI只需要通过Consumer或Provider.of获取数据而不用关心数据从何而来并且只有依赖数据部分会重建其他部分不变。补充1如果想要整个页面关闭后依旧保留的话应该了解的知识是持久化要实现持久化的话如果需要存储的键值集合相对较少例如用户偏好设置主题登陆状态等等可以使用shared_preferences插件。首先还是flutter pub add shared_preferences其次所有的读写操作都需要先通过SharePreferences.getInstance()获得一个单例实例并且这个方法是异步的因为他需要与原生层通信。所以就是说所有的相关操作全是异步的。所以其实可以像用Dio时一样在某处定义一个单例用来调用。finalprefsawaitSharedPreferences.getInstance();保存数据如果是保存数据的话应该使用setter方法SharedPreferences提供了针对不同数据类型的setter方法setInt(String key, int value)setDouble(String key, double value)setBool(String key, bool value)setString(String key, String value)setStringList(String key, List value)其工作原理是将数据更新到内存中的缓存然后异步写入磁盘。所以说调用后数据立即可以在内存中访问但是完全持久化可能还需要一些时间读取数据读取数据时使用对应的getter方法getInt(String key)getDouble(String key)getBool(String key)getString(String key)getStringList(String key)这些getter返回的类型是可空类型int?如果键值不存在就返回null所以需要结合??来设置默认值。实例代码如下finalcounterprefs.getInt(counter)??0;移除数据使用romove(String key)方法可以删除指定键及其值awaitprefs.remove(counter);如果想清楚所有数据可以使用clear()方法。慎用awaitprefs.clear();局限性仅支持基本类型int,double,bool,String,ListString而不能直接存储自定义对象。如果需要存复杂对象则需要序列化为JSON字符串例如使用jsonEncode后存为String读取时再解析。不适合大量数据因为它是同步读取到内存的如果存储大量数据可能会影响性能。所以如果要存储大量数据应该使用文件或数据库。系统可能会清除应用数据或者用户手动清除。补充2 文件读写相比于shared_preferences只能存简单键值对文件读写可以存任意数据并且适合存储较大的数据或结构化数据。首先要先说明一下文件读写这边是指在移动端上的读写dart:io中的File类不支持Web平台第一步找到正确的本地路径path_providerpath_provider是一个Flutter插件它提供了一种跨平台的方式来获取设备的特殊目标路径。它会根据当前运行的平台Android、IOS、Windows…返回正确的路径。常用目录临时目录Temporary Directory获取方式getTemporaryDirectory()特点系统可以随时清空这个目录比如存储空间不足时适合存放临时缓存文件。文档目录Documents Directory获取方式getApplicationDocumentsDirectory();只有你的应用可以访问用户卸载应用时才会被系统删除适合存放需要长期保留的数据。代码importpackage:path_provider/path_provider.dart;FutureStringget_localPathasync{finaldirectoryawaitgetApplicationDocumentsDirectory();returndirectory.path;}补充这里的get意思是引用这个函数的时候可以像引用变量一样不用添加括号。第二步创建指向文件位置的引用代码FutureFileget_localFileasync{finalpathawait_localPath;returnFile($path/counter.txt);}这边其实有点像创建一个文件指针。第三步将数据写入文件代码FutureFilewriteCounter(int counter)async{finalfileawait_localFile;returnfile.writeAsString($counter);}如果文件不存在writeAsString会创建一个新的文件如果已经存在则会覆盖。第四步从文件读取数据代码FutureintreadCounter()async{try{finalfileawait_localFile;finalcontentsawaitfile.readAsString();returnint.parse(contents);}catch(e){return0;// 如果出错比如文件不存在返回默认值}}补充parse是将字符串类型转换为整型。补充3使用SQLite做数据持久化首先还是添加依赖flutter pub add sqflite path其中sqflte提供了丰富的类和方法用于使用SQLite数据库path提供了大量方法用于定义数据库在磁盘上的存储位置定义数据模型其实就是定义一个类打开数据库打开数据库有两个方法使用sqflitepackage里的getDatabasesPath方法并配合pathpackage里的join方法定义数据库的路径。就是获取数据库的地址然后在这个地址打开这个数据库。finaldatabasePathawaitgetDatabasesPath();finalpathjoin(databasePath,doggie_database.db);或者是 使用sqflitepackage里的openDatabase方法打开数据库。finaldatabaseopenDatabase(path,// 数据库文件路径onCreate:(db,version){// 首次创建时调用// 执行建表语句},version:1,// 数据库版本号);创建数据表在onCreate中我们就可以执行SQL语句创建表onCreate:(db,version){returndb.execute(CREATE TABLE dogs(id INTEGER PRIMARY KEY, name TEXT, age INTEGER),);},这段代码就是db.execute执行一条SQL语句不返回数据。具体的SQL语句可以直接翻看手册SQL中文手册将数据插入数据库数据库的insert方法需要一个MapString, Object?类型的参数键为列名值为要插入的值。所以说我们需要在上面创建的类中添加一个方法toMap()方法MapString,Object?toMap(){return{id:id,name:name,age:age,};}然后编写插入函数FuturevoidinsertDog(Dogdog)async{finaldbawaitdatabase;// 获取数据库实例异步awaitdb.insert(dogs,dog.toMap(),conflictAlgorithm:ConflictAlgorithm.replace,// 冲突处理);}这里的 ‘dogs’ 是数据库表的名字。第三个参数conflictAlgorithm指定当发生主键冲突时的处理方式。ConflictAlgorithm.replace表示如果 id 已存在则替换整条记录相当于先删除后插入。其他选项如ignore、abort等。具体使用方法如下varfidoDog(id:0,name:Fido,age:35);awaitinsertDog(fido);查询所有数据查询操作使用db.query方法它返回一个ListMapString, Object?每个Map代表一行数据键为列名。因为返回的类型是ListMapString, Object?所以return的时候要进行类型转换。查询函数FutureListDogdogs()async{finaldbawaitdatabase;finalListMapString,Object?dogMapsawaitdb.query(dogs);// 将 ListMap 转换为 ListDogreturn[for(final{id:idasint,name:nameasString,age:ageasint}indogMaps)Dog(id:id,name:name,age:age),];}更新一条数据更新数据使用db.update方法。需要指定表明、要更新的数据Map以及一个where子句来定位具体行。更新函数FuturevoidupdateDog(Dogdog)async{finaldbawaitdatabase;awaitdb.update(dogs,dog.toMap(),where:id ?,// SQL 的 WHERE 子句? 是占位符whereArgs:[dog.id],// 占位符的实际值);}update 默认会更新所有匹配的行这里 id 是主键所以最多一行。使用fidoDog(id:fido.id,name:fido.name,age:fido.age7);awaitupdateDog(fido);