flutter 一文带你了解GetX

GetX

Getx:https://github.com/jonataslaw/getx

目前✨ 1.9k

但是在flutter.io中已经2.5k了,收到了广大开发者的喜爱,废话不多说直接进入正题:

GetX的计数器示例

Flutter默认创建的 "计数器 “项目有100多行(含注释),为了展示Get的强大功能,我将使用 GetX 重写一个"计数器 Plus版”,实现:

  • 每次点击都能改变状态
  • 在不同页面之间切换
  • 在不同页面之间共享状态
  • 将业务逻辑与界面分离

而完成这一切只需 26 行代码(含注释)

  • 步骤1.在你的MaterialApp前添加 “Get”,将其变成GetMaterialApp。
void main() => runApp(GetMaterialApp(home: Home()));
  • 注意:这并不能修改Flutter的MaterialApp,GetMaterialApp并不是修改后的MaterialApp,它只是一个预先配置的Widget,它的子组件是默认的MaterialApp。你可以手动配置,但绝对没有必要。GetMaterialApp会创建路由,注入它们,注入翻译,注入你需要的一切路由导航。如果你只用Get来进行状态管理或依赖管理,就没有必要使用GetMaterialApp。GetMaterialApp对于路由、snackbar、国际化、bottomSheet、对话框以及与路由相关的高级apis和没有上下文(context)的情况下是必要的。

  • 注2: 只有当你要使用路由管理(Get.to(), Get.back()等)时才需要这一步。如果你不打算使用它,那么就不需要做第1步。

  • 第二步:
    创建你的业务逻辑类,并将所有的变量,方法和控制器放在里面。
    你可以使用一个简单的".obs "使任何变量成为可观察的。

class Controller extends GetxController{
  var count = 0.obs;
  increment() => count++;
}
  • 第三步:
    创建你的界面,使用StatelessWidget节省一些内存,使用Get你可能不再需要使用StatefulWidget。
class Home extends StatelessWidget {

  @override
  Widget build(context) {

    // 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
    final Controller c = Get.put(Controller());

    return Scaffold(
      // 使用Obx(()=>每当改变计数时,就更新Text()。
      appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

      // 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
      body: Center(child: RaisedButton(
              child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
      floatingActionButton:
          FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
  }
}

class Other extends StatelessWidget {
  // 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
  final Controller c = Get.find();

  @override
  Widget build(context){
     // 访问更新后的计数变量
     return Scaffold(body: Center(child: Text("${c.count}")));
  }
}

这是一个简单的项目,但它已经让人明白Get的强大。随着项目的发展,这种差异将变得更加显著。

Get的设计是为了与团队合作,但它也可以让个人开发者的工作变得更简单。

加快开发速率,在不损失性能的情况下按时交付一切。Get并不适合每一个人,但如果你认同这句话,Get就是为你准备的!

三大功能

状态管理

目前,Flutter有几种状态管理器。但是,它们中的大多数都涉及到使用ChangeNotifier来更新widget,这对于中大型应用的性能来说是一个很糟糕的方法。你可以在Flutter的官方文档中查看到,ChangeNotifier应该使用1个或最多2个监听器,这使得它们实际上无法用于任何中等或大型应用。

Get 并不是比任何其他状态管理器更好或更差,而是说你应该分析这些要点以及下面的要点来选择只用Get,还是与其他状态管理器结合使用。

Get不是其他状态管理器的敌人,因为Get是一个微框架,而不仅仅是一个状态管理器,既可以单独使用,也可以与其他状态管理器结合使用。

Get有两个不同的状态管理器:简单的状态管理器(GetBuilder)和响应式状态管理器(GetX)。

响应式状态管理器

响应式编程可能会让很多人感到陌生,因为觉得它很复杂,但是GetX将响应式编程变得非常简单。

  • 你不需要创建StreamControllers.
  • 你不需要为每个变量创建一个StreamBuilder。
  • 你不需要为每个状态创建一个类。
  • 你不需要为一个初始值创建一个get。

使用 Get 的响应式编程就像使用 setState 一样简单。

让我们想象一下,你有一个名称变量,并且希望每次你改变它时,所有使用它的小组件都会自动刷新。

这就是你的计数变量。

var name = 'Jonatas Borges';

要想让它变得可观察,你只需要在它的末尾加上".obs"。

var name = 'Jonatas Borges'.obs;

而在UI中,当你想显示该值并在值变化时更新页面,只需这样做。

Obx(() => Text("${controller.name}"));

这就是全部,就这么简单。

关于状态管理的更多细节

你会对GetX的能力有一个很好的了解。

路由管理

如果你想免上下文(context)使用路由/snackbars/dialogs/bottomsheets,GetX对你来说也是极好的,来吧展示:

在你的MaterialApp前加上 “Get”,把它变成GetMaterialApp。

GetMaterialApp( // Before: MaterialApp(
  home: MyHome(),
)

导航到新页面


Get.to(NextScreen());

用别名导航到新页面。查看更多关于命名路由的详细信息这里


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。

Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。

Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。

Get.offAll(NextScreen());

注意到你不需要使用context来做这些事情吗?这就是使用Get路由管理的最大优势之一。有了它,你可以在你的控制器类中执行所有这些方法,而不用担心context在哪里。

依赖管理

Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到与你的Bloc或Controller相同的类,无需Provider context,无需inheritedWidget。

Controller controller = Get.put(Controller()); // 而不是 Controller controller = Controller();
  • 注意:如果你使用的是Get的状态管理器,请多注意绑定api,这将使你的界面更容易连接到你的控制器。

你是在Get实例中实例化它,而不是在你使用的类中实例化你的类,这将使它在整个App中可用。
所以你可以正常使用你的控制器(或类Bloc)。

提示: Get依赖管理与包的其他部分是解耦的,所以如果你的应用已经使用了一个状态管理器(任何一个,都没关系),你不需要全部重写,你可以使用这个依赖注入。

controller.fetchApi();

想象一下,你已经浏览了无数条路由,现在你需要拿到一个被遗留在控制器中的数据,那你需要一个状态管理器与Provider或Get_it一起使用来拿到它,对吗?用Get则不然,Get会自动为你的控制器找到你想要的数据,而你甚至不需要任何额外的依赖关系。

Controller controller = Get.find();
//是的,它看起来像魔术,Get会找到你的控制器,并将其提供给你。你可以实例化100万个控制器,Get总会给你正确的控制器。

然后你就可以恢复你在后面获得的控制器数据。

Text(controller.textFromApi);

实用工具

国际化

翻译

翻译被保存为一个简单的键值字典映射。
要添加自定义翻译,请创建一个类并扩展翻译

import 'package:get/get.dart';

class Messages extends Translations {
  @override
  Map> get keys => {
        'zh_CN': {
          'hello': '你好 世界',
        },
        'de_DE': {
          'hello': 'Hallo Welt',
        }
      };
}

使用翻译

只要将.tr追加到指定的键上,就会使用Get.localeGet.fallbackLocale的当前值进行翻译。

Text('title'.tr);

语言

传递参数给GetMaterialApp来定义语言和翻译。

return GetMaterialApp(
    translations: Messages(), // 你的翻译
    locale: Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
    fallbackLocale: Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
);

改变语言

调用Get.updateLocale(locale)来更新语言环境。然后翻译会自动使用新的locale。

var locale = Locale('en', 'US');
Get.updateLocale(locale);

系统语言

要读取系统语言,可以使用window.locale

import 'dart:ui' as ui;

return GetMaterialApp(
    locale: ui.window.locale,
);

改变主题

请不要使用比GetMaterialApp更高级别的widget来更新主题,这可能会造成键重复。很多人习惯于创建一个 "ThemeProvider "的widget来改变应用主题,这在GetX™中是绝对没有必要的。

你可以创建你的自定义主题,并简单地将其添加到Get.changeTheme中,而无需任何模板。

Get.changeTheme(ThemeData.light());

如果你想在 "onTap "中创建类似于改变主题的按钮,你可以结合两个GetX™ API来实现。

  • 检查是否使用了深色的 "Theme "的API,以及 "Theme "更改API。
  • Theme Change API,你可以把下面的代码放在onPressed里。
Get.changeTheme(Get.isDarkMode? ThemeData.light(): ThemeData.dark());

.darkmode被激活时,它将切换到light主题,当light主题被激活时,它将切换到dark主题。

其他高级API

// 给出当前页面的args。
Get.arguments

//给出以前的路由名称
Get.previousRoute

// 给出要访问的原始路由,例如,rawRoute.isFirst()
Get.rawRoute

// 允许从GetObserver访问Rounting API。
Get.routing

// 检查 snackbar 是否打开
Get.isSnackbarOpen

// 检查 dialog 是否打开
Get.isDialogOpen

// 检查 bottomsheet 是否打开
Get.isBottomSheetOpen

// 删除一个路由。
Get.removeRoute()

//反复返回,直到表达式返回真。
Get.until()

// 转到下一条路由,并删除所有之前的路由,直到表达式返回true。
Get.offUntil()

// 转到下一个命名的路由,并删除所有之前的路由,直到表达式返回true。
Get.offNamedUntil()

//检查应用程序在哪个平台上运行。
GetPlatform.isAndroid
GetPlatform.isIOS
GetPlatform.isMacOS
GetPlatform.isWindows
GetPlatform.isLinux
GetPlatform.isFuchsia

//检查设备类型
GetPlatform.isMobile
GetPlatform.isDesktop
//所有平台都是独立支持web的!
//你可以知道你是否在浏览器内运行。
//在Windows、iOS、OSX、Android等系统上。
GetPlatform.isWeb


// 相当于.MediaQuery.of(context).size.height,
//但不可改变。
Get.height
Get.width

// 提供当前上下文。
Get.context

// 在你的代码中的任何地方,在前台提供 snackbar/dialog/bottomsheet 的上下文。
Get.contextOverlay

// 注意:以下方法是对上下文的扩展。
// 因为在你的UI的任何地方都可以访问上下文,你可以在UI代码的任何地方使用它。

// 如果你需要一个可改变的高度/宽度(如桌面或浏览器窗口可以缩放),你将需要使用上下文。
context.width
context.height

// 让您可以定义一半的页面、三分之一的页面等。
// 对响应式应用很有用。
// 参数: dividedBy (double) 可选 - 默认值:1
// 参数: reducedBy (double) 可选 - 默认值:0。
context.heightTransformer()
context.widthTransformer()

/// 类似于 MediaQuery.of(context).size。
context.mediaQuerySize()

/// 类似于 MediaQuery.of(context).padding。
context.mediaQueryPadding()

/// 类似于 MediaQuery.of(context).viewPadding。
context.mediaQueryViewPadding()

/// 类似于 MediaQuery.of(context).viewInsets。
context.mediaQueryViewInsets()

/// 类似于 MediaQuery.of(context).orientation;
context.orientation()

///检查设备是否处于横向模式
context.isLandscape()

///检查设备是否处于纵向模式。
context.isPortrait()

///类似于MediaQuery.of(context).devicePixelRatio。
context.devicePixelRatio()

///类似于MediaQuery.of(context).textScaleFactor。
context.textScaleFactor()

///查询设备最短边。
context.mediaQueryShortestSide()

///如果宽度大于800,则为真。
context.showNavbar()

///如果最短边小于600p,则为真。
context.isPhone()

///如果最短边大于600p,则为真。
context.isSmallTablet()

///如果最短边大于720p,则为真。
context.isLargeTablet()

///如果当前设备是平板电脑,则为真
context.isTablet()

///根据页面大小返回一个值。
///可以给值为:
///watch:如果最短边小于300
///mobile:如果最短边小于600
///tablet:如果最短边(shortestSide)小于1200
///desktop:如果宽度大于1200
context.responsiveValue()

可选的全局设置和手动配置

GetMaterialApp为你配置了一切,但如果你想手动配置Get。

MaterialApp(
  navigatorKey: Get.key,
  navigatorObservers: [GetObserver()],
);

你也可以在GetObserver中使用自己的中间件,这不会影响任何事情。

MaterialApp(
  navigatorKey: Get.key,
  navigatorObservers: [
    GetObserver(MiddleWare.observer) // Here
  ],
);

你可以为 "Get "创建_全局设置。只需在推送任何路由之前将Get.config添加到你的代码中。
或者直接在你的GetMaterialApp中做。

GetMaterialApp(
  enableLog: true,
  defaultTransition: Transition.fade,
  opaqueRoute: Get.isOpaqueRouteDefault,
  popGesture: Get.isPopGestureEnable,
  transitionDuration: Get.defaultDurationTransition,
  defaultGlobalState: Get.defaultGlobalState,
);

Get.config(
  enableLog = true,
  defaultPopGesture = true,
  defaultTransition = Transitions.cupertino
)

你可以选择重定向所有来自Get的日志信息。
如果你想使用你自己喜欢的日志包,并想查看那里的日志。

GetMaterialApp(
  enableLog: true,
  logWriterCallback: localLogWriter,
);

void localLogWriter(String text, {bool isError = false}) {
  // 在这里把信息传递给你最喜欢的日志包。
  // 请注意,即使enableLog: false,日志信息也会在这个回调中被推送。
  // 如果你想的话,可以通过GetConfig.isLogEnable来检查这个标志。
}

局部状态组件

这些Widgets允许您管理一个单一的值,并保持状态的短暂性和本地性。
我们有Reactive和Simple两种风格。
例如,你可以用它们来切换TextField中的obscureText,也许可以创建一个自定义的可扩展面板(Expandable Panel),或者在"Scaffold "的主体中改变内容的同时修改BottomNavigationBar中的当前索引。

ValueBuilder

StatefulWidget的简化,它与.setState回调一起工作,并接受更新的值。

ValueBuilder(
  initialValue: false,
  builder: (value, updateFn) => Switch(
    value: value,
    onChanged: updateFn, // 你可以用( newValue )=> updateFn( newValue )。
  ),
  // 如果你需要调用 builder 方法之外的东西。
  onUpdate: (value) => print("Value updated: $value"),
  onDispose: () => print("Widget unmounted"),
),

ObxValue

类似于ValueBuilder,但这是Reactive版本,你需要传递一个Rx实例(还记得神奇的.obs吗?自动更新…是不是很厉害?)

ObxValue((data) => Switch(
        value: data.value,
        onChanged: data, // Rx 有一个 _callable_函数! 你可以使用 (flag) => data.value = flag,
    ),
    false.obs,
),

有用的提示

.observables (也称为_Rx_ Types)有各种各样的内部方法和操作符。

.obs的属性实际值,不要搞错了!
我们避免了变量的类型声明,因为Dart的编译器足够聪明,而且代码
看起来更干净,但:

var message = 'Hello world'.obs;
print( 'Message "$message" has Type ${message.runtimeType}');

即使message _prints_实际的字符串值,类型也是RxString
所以,你不能做message.substring( 0, 4 )
你必须在_observable_里面访问真正的value
最常用的方法是".value", 但是你也可以用…

final name = 'GetX'.obs;
//只有在值与当前值不同的情况下,才会 "更新 "流。
name.value = 'Hey';

// 所有Rx属性都是 "可调用 "的,并返回新的值。
//但这种方法不接受 "null",UI将不会重建。
name('Hello');

// 就像一个getter,打印'Hello'。
name() ;

///数字。

final count = 0.obs;

// 您可以使用num基元的所有不可变操作!
count + 1;

// 注意!只有当 "count "不是final时,这才有效,除了var
count += 1;

// 你也可以与数值进行比较。
count > 2;

/// booleans:

final flag = false.obs;

// 在真/假之间切换数值
flag.toggle();


/// 所有类型。

// 将 "value "设为空。
flag.nil();

// 所有的toString()、toJson()操作都会向下传递到`value`。
print( count ); // 在内部调用 "toString() "来GetRxInt

final abc = [0,1,2].obs;
// 将值转换为json数组,打印RxList。
// 所有Rx类型都支持Json!
print('json: ${jsonEncode(abc)}, type: ${abc.runtimeType}');

// RxMap, RxList 和 RxSet 是特殊的 Rx 类型,扩展了它们的原生类型。
// 但你可以像使用普通列表一样使用列表,尽管它是响应式的。
abc.add(12); // 将12添加到列表中,并更新流。
abc[3]; // 和Lists一样,读取索引3。


// Rx和值是平等的,但hashCode总是从值中提取。
final number = 12.obs;
print( number == 12 ); // prints > true

///自定义Rx模型。

// toJson(), toString()都是递延给子代的,所以你可以在它们上实现覆盖,并直接打印()可观察的内容。

class User {
    String name, last;
    int age;
    User({this.name, this.last, this.age});

    @override
    String toString() => '$name $last, $age years old';
}

final user = User(name: 'John', last: 'Doe', age: 33).obs;

// `user`是 "响应式 "的,但里面的属性却不是!
// 所以,如果我们改变其中的一些变量:
user.value.name = 'Roi';
// 小部件不会重建! 
// 对于自定义类,我们需要手动 "通知 "改变。
user.refresh();

// 或者我们可以使用`update()`方法!
user.update((value){
  value.name='Roi';
});

print( user );

GetView

我很喜欢这个Widget,很简单,很有用。

它是一个对已注册的Controller有一个名为controller的getter的const Stateless的Widget,仅此而已。

 class AwesomeController extends GetxController {
   final String title = 'My Awesome View';
 }

  // 一定要记住传递你用来注册控制器的`Type`!
 class AwesomeView extends GetView {
   @override
   Widget build(BuildContext context) {
     return Container(
       padding: EdgeInsets.all(20),
       child: Text( controller.title ), // 只需调用 "controller.something"。
     );
   }
 }

GetWidget

大多数人都不知道这个Widget,或者完全搞不清它的用法。
这个用例非常少见且特殊:它 "缓存 "了一个Controller,由于_cache_,不能成为一个 “const Stateless”(因为_cache_,所以不能成为一个const Stateless)。

那么,什么时候你需要 "缓存 "一个Controller?

如果你使用了GetX的另一个 "不常见 "的特性 Get.create()

Get.create(()=>Controller()) 会在每次调用时生成一个新的Controller
Get.find()

你可以用它来保存Todo项目的列表,如果小组件被 “重建”,它将保持相同的控制器实例。

GetxService

这个类就像一个 “GetxController”,它共享相同的生命周期(“onInit()”、“onReady()”、“onClose()”)。
但里面没有 “逻辑”。它只是通知GetX的依赖注入系统,这个子类不能从内存中删除。

所以这对保持你的 "服务 "总是可以被Get.find()获取到并保持运行是超级有用的。比如
ApiServiceStorageServiceCacheService

Future main() async {
  await initServices(); /// 等待服务初始化.
  runApp(SomeApp());
}

/// 在你运行Flutter应用之前,让你的服务初始化是一个明智之举。
因为你可以控制执行流程(也许你需要加载一些主题配置,apiKey,由用户自定义的语言等,所以在运行ApiService之前加载SettingService。
///所以GetMaterialApp()不需要重建,可以直接取值。
void initServices() async {
  print('starting services ...');
  ///这里是你放get_storage、hive、shared_pref初始化的地方。
  ///或者moor连接,或者其他什么异步的东西。
  await Get.putAsync(() => DbService().init());
  await Get.putAsync(SettingsService()).init();
  print('All services started...');
}

class DbService extends GetxService {
  Future init() async {
    print('$runtimeType delays 2 sec');
    await 2.delay();
    print('$runtimeType ready!');
    return this;
  }
}

class SettingsService extends GetxService {
  void init() async {
    print('$runtimeType delays 1 sec');
    await 1.delay();
    print('$runtimeType ready!');
  }
}

实际删除一个GetxService的唯一方法是使用Get.reset(),它就像"热重启 "你的应用程序。

所以如果你需要在你的应用程序的生命周期内对一个类实例进行绝对的持久化,请使用GetxService

响应式原理

简单的使用已经介绍完毕,那么它是如何实现响应式的呢?

Rx()

首先在使用Obx()之前,我们会定义一个obs

final count = "日志".obs

那么这个count的类型是RxString

extension StringExtension on String {
  /// Returns a `RxString` with [this] `String` as initial value.
  RxString get obs => RxString(this);
}

我们看下RxInt的内部代码:

class RxInt extends _BaseRxNum {
...
}

RxInt()继承了_BaseRxNum,在看下_BaseRxNum内部:

abstract class _BaseRxNum extends _RxImpl {
  _BaseRxNum(T initial) : super(initial);
  }

最终找到了一个叫_RxImpl类,这里边实现了通知的各种操作问题。

首先看下代码:

abstract class _RxImpl implements RxInterface {
  _RxImpl(T initial) {
    _value = initial;
  }
  StreamController subject = StreamController.broadcast();
  final _subscriptions = HashMap, StreamSubscription>();

  T _value;

  bool get canUpdate => _subscriptions.isNotEmpty;

 
  T call([T v]) {
    if (v != null) {
      value = v;
    }
    return value;
  }

  void refresh() {
    subject.add(value);
  }


  void update(void fn(T val)) {
    fn(_value);
    subject.add(_value);
  }

  
  void nil() {
    subject.add(_value = null);
  }

  /// Same as `toString()` but using a getter.
  String get string => value.toString();

  @override
  String toString() => value.toString();

  /// Returns the json representation of `value`.
  dynamic toJson() => value;

  /// This equality override works for _RxImpl instances and the internal
  /// values.
  @override
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
  bool operator ==(dynamic o) {
    // Todo, find a common implementation for the hashCode of different Types.
    if (o is T) return value == o;
    if (o is RxInterface) return value == o.value;
    return false;
  }

  @override
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
  int get hashCode => _value.hashCode;

  /// Closes the subscriptions for this Rx, releasing the resources.
  void close() {
    _subscriptions.forEach((observable, subscription) => subscription.cancel());
    _subscriptions.clear();
    subject.close();
  }

  /// This is an internal method.
  /// Subscribe to changes on the inner stream.
  void addListener(Stream rxGetx) {
    if (_subscriptions.containsKey(rxGetx)) {
      return;
    }
    _subscriptions[rxGetx] = rxGetx.listen((data) {
      subject.add(data);
    });
  }

  bool firstRebuild = true;

  set value(T val) {
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;
    subject.add(_value);
  }

  /// Returns the current [value]
  T get value {
    if (getObs != null) {
      getObs.addListener(subject.stream);
    }
    return _value;
  }

  Stream get stream => subject.stream;

  StreamSubscription listen(void Function(T) onData,
          {Function onError, void Function() onDone, bool cancelOnError}) =>
      stream.listen(onData, onError: onError, onDone: onDone);

  void bindStream(Stream stream) {
    _subscriptions[stream] = stream.listen((va) => value = va);
  }

  Stream map(R mapper(T data)) => stream.map(mapper);
}

我们摘出来主要的函数get value/set value/addListener

void addListener(Stream rxGetx) {
    if (_subscriptions.containsKey(rxGetx)) {
      return;
    }
    _subscriptions[rxGetx] = rxGetx.listen((data) {
      subject.add(data);
    });
  }

  bool firstRebuild = true;

  set value(T val) {
    if (_value == val && !firstRebuild) return;
    firstRebuild = false;
    _value = val;
    subject.add(_value);
  }

  T get value {
    if (getObs != null) {
      getObs.addListener(subject.stream);
    }
    return _value;
  }

继承_RxImpl的子类,在获取value的时候把自己的流的监听给了getObs,现在的全局变量getObsObx()的,千言万语不如一张图:

flutter 一文带你了解GetX_第1张图片

当Obx()build的时候全局变量的值是Obx()的obs,然后在widget.builder()执行controller.value读取value进行添加,添加函数中对controller.obs进行了监听,做到了响应式。每个Obx()都有自己的Obx(),做到了局部刷新。

更多代码和例子见仓库: https://github.com/ifgyong/flutter-example/tree/master/lib/tips/get

你可能感兴趣的:(flutter,flutter,ios,android)