Flutter Provider状态管理---八种提供者使用分析

前言

在我们上一篇文章中对Provider进行了介绍以及类结构的说明,最后还写了一个简单的示例,通过上一章节我们对Provider有了一个基本的了解,这一章节我们来说说Provider的8种提供者以及他们的使用区别。

Provider

Provider是最基本的Provider组件,可以使用它为组件树中的任何位置提供值,但是当该值更改的时候,它并不会更新UI,下面我们给出一个示例

第一步:创建模型
class UserModel {

  String name = "Jimi";

  void changeName() {
    name = "hello";
  }
}
第二步:应用程序入口设置
return Provider<UserModel>(
  create: (_) => UserModel(),
  child: MaterialApp(
    debugShowCheckedModeBanner: false,
    home: ProviderExample(),
  ),
);
第三步:使用共享数据

关于Consumer后面将消费者在提及,我们这里只需要知道有两个消费者,第一个用于展示模型的数据,第二个用于改变模型的数据。

  • 第一个Comsumer是用于读取模型的数据name
  • 第二个Consumer用于改变模型的数据name
import 'package:flutter/material.dart';
import 'package:flutter_provider_example/provider_example/user_model.dart';
import 'package:provider/provider.dart';

class ProviderExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ProviderExample"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Consumer<UserModel>(
              builder: (_, userModel, child) {
                return Text(userModel.name,
                    style: TextStyle(
                        color: Colors.red,
                        fontSize: 30
                    )
                );
              },
            ),
            Consumer<UserModel>(
              builder: (_, userModel, child) {
                return Padding(
                  padding: EdgeInsets.all(20),
                  child: ElevatedButton(
                    onPressed: (){
                      userModel.changeName();
                    },
                    child: Text("改变值"),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

继续阅读Flutter Provider状态管理---八种提供者使用分析

Flutter 状态管理框架 Provider 和 Get 分析

状态管理一直是 Flutter 开发中一个火热的话题。谈到状态管理框架,社区也有诸如有以 GetProviderRiverpod 为代表的多种方案,它们有各自的优缺点。面对这么多的选择,你可能会想:「我需要使用状态管理么?哪种框架更适合我?」本文将从作者的实际开发经验出发,分析状态管理解决的问题以及思路,希望能帮助你做出选择。

为什么需要状态管理?

首先,为什么需要状态管理?根据笔者的经验,这是因为 Flutter 基于 声明式 构建 UI ,使用状态管理的目的之一就是解决「声明式」开发带来的问题。

「声明式」开发是一种区别于传原生的方式,所以我们没有在原生开发中听到过状态管理,那如何理解「声明式」开发呢?

继续阅读Flutter 状态管理框架 Provider 和 Get 分析

Flutter Riverpod 全面深入解析,为什么官方推荐它?

随着 Flutter 的发展,这些年 Flutter 上的状态管理框架如“雨后春笋”般层出不穷,而近一年以来最受官方推荐的状态管理框架无疑就是 Riverpod ,甚至已经超过了 Provider ,事实上 Riverpod 官方也称自己为 “Provider,但与众不同”。

Provider 本身用它自己的话来说是 “InheritedWidget 的封装,但更简单且复用能力更强。” ,而 Riverpod 就是在 Provider 的基础上重构了新的可能。

关于过去一年状态管理框架的对比可以看 《2021 年的 Flutter 状态管理:如何选择?》本文主要是带你解剖 RiverPod 的内部是如何实现,理解它的工作原理,以及如何做到比 Provider 更少的模板和不依赖 BuildContext

继续阅读Flutter Riverpod 全面深入解析,为什么官方推荐它?

Flutter 3.16中WillPopScope过期使用PopScope来代替

Flutter 3.16WillPopScope 过期了,需要使用 PopScope 来代替。

针对 PopScopecanPop 参数,官方文档解释如下:

/// Manages system back gestures.
///
/// The [canPop] parameter can be used to disable system back gestures. Defaults
/// to true, meaning that back gestures happen as usual.
///
/// The [onPopInvoked] parameter reports when system back gestures occur,
/// regardless of whether or not they were successful.
///
/// If [canPop] is false, then a system back gesture will not pop the route off
/// of the enclosing [Navigator]. [onPopInvoked] will still be called, and
/// `didPop` will be `false`.
///
/// If [canPop] is true, then a system back gesture will cause the enclosing
/// [Navigator] to receive a pop as usual. [onPopInvoked] will be called with
/// `didPop` as `true`, unless the pop failed for reasons unrelated to
/// [PopScope], in which case it will be `false`.

canPopfalse,则执行系统返回时会被拦截,并且调用 onPopInvoked 方法,同时 didPopfalse,此时进行逻辑判断,如果需要返回则执行 Navigator.of(context).pop();

注意此时 onPopInvoked 又会被调用,并且 didPoptrue

参考Demo: github.com

示例代码如下:

修改之前的代码( WillPopScope )如下:

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This sample demonstrates showing a confirmation dialog before navigating
// away from a page.

import 'package:flutter/material.dart';

void main() => runApp(const NavigatorPopHandlerApp());

class NavigatorPopHandlerApp extends StatelessWidget {
  const NavigatorPopHandlerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/home',
      routes: <String, WidgetBuilder>{
        '/home': (BuildContext context) => const _HomePage(),
        '/two': (BuildContext context) => const _PageTwo(),
      },
    );
  }
}

class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Page One'),
            TextButton(
              onPressed: () {
                Navigator.of(context).pushNamed('/two');
              },
              child: const Text('Next page'),
            ),
          ],
        ),
      ),
    );
  }
}

class _PageTwo extends StatefulWidget {
  const _PageTwo();

  @override
  State<_PageTwo> createState() => _PageTwoState();
}

class _PageTwoState extends State<_PageTwo> {
  void _showBackDialog() {
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Are you sure?'),
          content: const Text(
            'Are you sure you want to leave this page?',
          ),
          actions: <Widget>[
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('Nevermind'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('Leave'),
              onPressed: () {
                Navigator.pop(context);
                Navigator.pop(context);
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        onWillPop: () {
          _showBackDialog();
          return false;
        },
        child: Scaffold(
            body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text('Page Two'),
              TextButton(
                onPressed: () {
                  _showBackDialog();
                },
                child: const Text('Go back'),
              ),
            ],
          ),
        )));
  }
}

修改之后的代码( PopScope )如下:

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This sample demonstrates showing a confirmation dialog before navigating
// away from a page.

import 'package:flutter/material.dart';

void main() => runApp(const NavigatorPopHandlerApp());

class NavigatorPopHandlerApp extends StatelessWidget {
  const NavigatorPopHandlerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/home',
      routes: <String, WidgetBuilder>{
        '/home': (BuildContext context) => const _HomePage(),
        '/two': (BuildContext context) => const _PageTwo(),
      },
    );
  }
}

class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Page One'),
            TextButton(
              onPressed: () {
                Navigator.of(context).pushNamed('/two');
              },
              child: const Text('Next page'),
            ),
          ],
        ),
      ),
    );
  }
}

class _PageTwo extends StatefulWidget {
  const _PageTwo();

  @override
  State<_PageTwo> createState() => _PageTwoState();
}

class _PageTwoState extends State<_PageTwo> {
  void _showBackDialog() {
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Are you sure?'),
          content: const Text(
            'Are you sure you want to leave this page?',
          ),
          actions: <Widget>[
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('Nevermind'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
            TextButton(
              style: TextButton.styleFrom(
                textStyle: Theme.of(context).textTheme.labelLarge,
              ),
              child: const Text('Leave'),
              onPressed: () {
                Navigator.pop(context);
                Navigator.pop(context);
              },
            ),
          ],
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('Page Two'),
            PopScope(
              canPop: false,
              onPopInvoked: (bool didPop) {
                if (didPop) {
                  return;
                }
                _showBackDialog();
              },
              child: TextButton(
                onPressed: () {
                  _showBackDialog();
                },
                child: const Text('Go back'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

参考链接


Flutter的Don't use BuildContext's across async gaps警告解决方法

问题

Flutter开发中遇到Don't use BuildContext's across async gaps警告

有问题的源码

    if (await databaseHelper.isDataExist(task.title)) {
      showDialog(
        context: context,
        builder: (BuildContext context) {
          return AlertDialog(
            title: Text("已存在同名配置"),
            content: Text("是否覆盖已有的配置?"),
            actions: <Widget>[
              ElevatedButton(
                child: const Text("取消"),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
              ElevatedButton(
                child: const Text("确认"),
                onPressed: () async {
                  Navigator.of(context).pop();
                  databaseHelper.updateDatabaseByTitle(task);
                },
              ),
            ],
          );
        },
      );
    }

问题原因

“不要在异步间隙(async gaps)中使用 BuildContext” 是一个Flutter中的常见警告消息,通常表示你正在尝试在异步操作中访问 BuildContext,这是一个不推荐的做法,因为它可能引发不确定的行为或错误。

如果在将上下文传递给AlertDialog后导航堆栈发生更改,并且尝试使用旧上下文再次导航,则会出现错误。

问题分析

Context的含义

Flutter中的 BuildContext 和 Context 是相同的,BuildContext 是 Context 的别名。这两个术语用来表示小部件树中的位置信息和上下文环境,用于在构建小部件树和访问资源(例如主题、本地化、导航等)时提供上下文信息。

在Flutter中,BuildContext 或 Context 表示的是一个由小部件树组成的层次结构中的位置。每个小部件都有一个与之相关的 BuildContext,这个上下文包含有关小部件的信息,例如其位置、父级小部件、主题数据等等。

尽管 Context 和 BuildContext 是相同的类型,但通常我们更倾向于使用 BuildContext 这个术语,因为它更明确地表示它是与构建过程相关的上下文。

BuildContext的作用

BuildContext 类型通常用于以下操作:

  • 访问父级小部件:你可以使用 BuildContext 访问小部件树中的父级小部件,这对于在小部件之间传递数据和状态非常有用。
  • 获取主题数据:通过 BuildContext 可以访问当前主题的数据,如颜色、字体、间距等。
  • 获取本地化信息:你可以使用 BuildContext 获取本地化信息,以根据用户的语言偏好来显示文本。
  • 导航:BuildContext 通常用于导航操作,如推送新路由或弹出对话框。
  • 构建小部件:BuildContext 是在小部件的 build 方法中传递的,它告诉小部件在小部件树中的位置。

BuildContext 和 Context 都代表了小部件树中的位置和上下文信息,它们在构建和交互中扮演着关键的角色,但它们实际上是相同的概念的不同表达方式。因此,你可以放心地将它们视为等同的,使用其中一个作为标识符,以便更清晰地表示其作用。

特殊情况

然而,在某些情况下,你可能需要在异步操作中访问 BuildContext,例如在异步回调中执行 UI 操作。这通常是不安全的,因为异步操作可能会在 BuildContext 不再有效的情况下执行,从而引发错误。

解决方法


使用

if (context.mounted) Navigator.of(context).pop();

不要在异步间隙中直接使用 BuildContext,因为它可能会导致不安全的操作。使用提供的方法来安全地查找小部件并在异步操作中访问它们的上下文。这可以帮助你避免潜在的问题和错误。

官方说明如下:

Details

DON’T use BuildContext across asynchronous gaps.

Storing BuildContext for later usage can easily lead to difficult to diagnose crashes. Asynchronous gaps are implicitly storing BuildContext and are some of the easiest to overlook when writing code.

When a BuildContext is used, its mounted property must be checked after an asynchronous gap.

BAD:

void onButtonTapped(BuildContext context) async {
  await Future.delayed(const Duration(seconds: 1));
  Navigator.of(context).pop();
}

GOOD:

void onButtonTapped(BuildContext context) {
  Navigator.of(context).pop();
}

GOOD:

void onButtonTapped() async {
  await Future.delayed(const Duration(seconds: 1));

  if (!context.mounted) return;
  Navigator.of(context).pop();
}

参考链接


使用 dart extension

如何使用

dart extension 的使用场景是无法修改原类的时候,通过扩展的方式来增加原类的方法,也可以增加 getter,setters,and operators。

比如

int.parse('42')

如果 String 有 pareseInt 方法,我们可以这样写

'42'.parseInt()

为了达到这个目标,需要写一个 Extension

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

然后就可以使用了。

// Import a library that contains an extension on String.
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

处理冲突

注意到前面加的 NumberParsing 了吗?这个是为 extension 起的名字。起名字的作用是有冲突的时候可以方便的控制显隐。比如 NumberParsing2 也定义了 parseInt 方法与 NumberParsing 的 parseInt 冲突,如果只想要 NumberParsing 的 parseInt,只需要把 NumberParsing2 隐藏就好了。

// Defines the String extension method parseInt().
import 'string_apis.dart';

// Also defines parseInt(), but hiding NumberParsing2
// hides that extension method.
import 'string_apis_2.dart' hide NumberParsing2;

// ···
// Uses the parseInt() defined in 'string_apis.dart'.
print('42'.parseInt());

或者可以 给 NumberParsing2 经起个别名

import 'string_apis.dart';
import 'string_apis_2.dart' as rad;

print(NumberParsing('42').parseInt());

// Use the ParseNumbers extension from string_apis_3.dart.
print(rad.NumberParsing('42').parseInt());

// Only string_apis_3.dart has parseNum().
print('42'.parseNum());

泛型 extension

可以只给 int 类型的 list 加 sum 方法,其它类型的 list 不能使用 sum。

// Import a library that contains an extension on String.
import 'string_apis.dart';
// ···
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.
["1", "2", "3"].sum();  //Error: The method 'sum' isn't defined for the class 'List<String>'

[1,2,3].sum(); // ok, output 6

要注意的问题

dart extension 不可以用于 dynamic 类型

dynamic d = '2';
print(d.parseInt()); // Runtime exception: NoSuchMethodError

dart extension 权限很大,这也意味着,很可能被过度使用。所以在使用的时候一定要再三权衡,一定要可读性,可维护性优先。

上面的代码是比较简单的,但是如果类型比较复杂,不明确指定类型,类似如下的形式,也是可能会报错的:

class Provider {

 late final d;

 Provider() {
    this.d = '';
    this.d.parseInt(); // Runtime exception: NoSuchMethodError
  }

}

参考链接


Google I/O 2023 - Flutter 3.10 发布,快来看看有什么更新吧

虽然本次 I/O 的核心 keynote 主要是 AI ,但是按照惯例依然发布了新的 Flutter 稳定版,不过并非大家猜测的 4.0,而是 3.10 ,Flutter 的版本号依然那么的出人意料。

Flutter 3.10 主要包括有对 Web、mobile、graphics、安全性等方面的相关改进,核心其实就是:

  • iOS 默认使用了 Impeller
  • 一堆新的 Material 3 控件袭来
  • iOS 性能优化,Android 顺带可有可无的更新
  • Web 可以无 iframe 嵌套到其他应用

继续阅读Google I/O 2023 - Flutter 3.10 发布,快来看看有什么更新吧

Unable to find a target named `RunnerTests` in project `Runner.xcodeproj`, did find `Runner`

Flutter 2.x 升级到 3.10.1 版本之后,原来正常编译的项目,iOS环境下(Xcode Version 13.2.1 (13C100)),编译报错:

$ ios %  pod install
Analyzing dependencies
[!] Unable to find a target named `RunnerTests` in project `Runner.xcodeproj`, did find `Runner`.

[!] Automatically assigning platform `iOS` with version `11.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.

继续阅读Unable to find a target named `RunnerTests` in project `Runner.xcodeproj`, did find `Runner`

iOS开发:报错The sandbox is not in sync with the Podfile.lock …的解决方法

问题

具体在Xcode中的报错提示如下所示:

Showing Recent Messages
diff: /Users/admin/Documents/WorkSpace/Flutter/portal/ios/Pods/Manifest.lock: No such file or directory
error:The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.

继续阅读iOS开发:报错The sandbox is not in sync with the Podfile.lock …的解决方法

MMKV编译报错Invalid `Podfile` file: undefined method `exists?' for File:Class.

尝试在 Flutter 上使用 MMKV1.2.16)的时候,编译报错,如下:

Launching lib/main.dart on iPod touch (7th generation) in debug mode...
Updating minimum iOS deployment target to 11.0.
Upgrading Podfile
Running pod install...
CocoaPods' output:
↳

    [!] Invalid `Podfile` file: undefined method `exists?' for File:Class.

     #  from /Users/xxxx/MMKV/flutter/example/ios/Podfile:35
     #  -------------------------------------------
     #    plugin_deps_file = File.expand_path(File.join(flutter_application_path, '..', '.flutter-plugins-dependencies'))
     >    if not File.exists?(plugin_deps_file)
     #      is_module = true;
     #  -------------------------------------------

    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:335:in `rescue in block in from_ruby'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:329:in `block in from_ruby'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:50:in `instance_eval'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:50:in `initialize'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:327:in `new'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:327:in `from_ruby'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-core-1.12.0/lib/cocoapods-core/podfile.rb:293:in `from_file'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-1.12.0/lib/cocoapods/config.rb:205:in `podfile'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-1.12.0/lib/cocoapods/command.rb:160:in `verify_podfile_exists!'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-1.12.0/lib/cocoapods/command/install.rb:46:in `run'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/claide-1.1.0/lib/claide/command.rb:334:in `run'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-1.12.0/lib/cocoapods/command.rb:52:in `run'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/gems/cocoapods-1.12.0/bin/pod:55:in `<top (required)>'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/bin/pod:25:in `load'
    /usr/local/Cellar/cocoapods/1.12.0/libexec/bin/pod:25:in `<main>'

Error running pod install
Error launching application on iPod touch (7th generation).
$ ruby -v
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin20]
$ brew install ruby
==> Downloading https://formulae.brew.sh/api/formula.jws.json
######################################################################## 100.0%
==> Downloading https://formulae.brew.sh/api/cask.jws.json
######################################################################## 100.0%
Warning: ruby 3.2.1 is already installed and up-to-date.
To reinstall 3.2.1, run:
  brew reinstall ruby

$ ruby -v          
ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin20]

$ brew link ruby   
Warning: Refusing to link macOS provided/shadowed software: ruby
If you need to have ruby first in your PATH, run:
  echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc

For compilers to find ruby you may need to set:
  export LDFLAGS="-L/usr/local/opt/ruby/lib"
  export CPPFLAGS="-I/usr/local/opt/ruby/include"

For pkg-config to find ruby you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/ruby/lib/pkgconfig"
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.7.7, on macOS 11.7.4 20G1120 darwin-x64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] IntelliJ IDEA Ultimate Edition (version 2018.3.6)
[✓] VS Code (version 1.76.2)
[✓] Connected device (3 available)
[✓] HTTP Host Availability

• No issues found!

这个问题是由于 cocoapods 升级到 1.12 版本之后,依赖的 ruby 升级到 3.2 版本,其中的 File.exists 函数被替换成 File.exist,导致编译异常。

刚刚开始以为是 Flutter 的原因,结果发现 Flutter 3.7.7版本已经修复这个问题。

尝试了半天,才发现是 MMKV 的问题,主要是 MMKViOS 工程下 flutter/example/ios/Podfileflutter/example/mmkvpodhelper.rb里面的代码需要进行适配。

# from ruby 3.2  File.exists is broken, we need compat function
def mmkv_file_exists(file)
  is_exist = false
  if File.methods.include?(:exists?)
    is_exist = File.exists? file
  else
    is_exist = File.exist? file
  end
  return is_exist
end

使用上面的代码替换 File.exists 即可。

参考链接