稀土掘金 稀土掘金

用MobX对Flutter应用程序进行CI/CD和状态管理

MobX是一个可扩展的库,是为了简化前端应用程序的状态管理过程而开发的。在本教程中,您将学习如何使用MobX管理您的Flutter应用程序的状态,然后使用CircleCI为您的应用程序建立一个 持续集成/持续部署(CI/CD)管道。您可以在 这个GitHub仓库中找到为本教程开发的示例项目。

前提条件

在开始之前,您需要对Flutter有一定的了解。如果你需要帮助,你可以关注 Flutter网站上的代码实验。

您需要在您的机器上安装这些项目。

  • Flutter SDK,1.0或更高版本。(这是与 Dart SDK安装一起的)。
  • 一个开发环境。选择其中一个。
    • Android Studio,3.0版或更高版本。
    • IntelliJ IDEA,2017.1版或更高版本。
    • Visual Studio Code。

无论你选择哪个IDE,你都需要安装Dart和Flutter插件。这些插件对于编辑和重构您的Flutter应用程序至关重要。

MobX的简短历史

根据 mobx.js.org,MobX是一个经过战斗考验的库,通过透明地应用功能反应式编程 (TFRP)使状态管理变得简单和可扩展。MobX主要是针对React应用而开发的,它已经发展到支持用其他JavaScript库构建的应用,最近还支持Flutter应用。

在Flutter应用程序中使用MobX进行状态管理

使用MobX管理状态依赖于该库的三个主要概念。

  • 可观察的状态
  • 行动
  • 计算值

可观察的状态是指应用程序中那些容易被改变的属性。这些状态用@observable 注释来声明。例如,待办事项应用程序中的可观察状态包括所有待办事项的列表。该列表还包括其值可能被更新的其他所有属性。

行动是旨在改变可观察状态的值的操作。行动是用@action 注解来声明的。在运行一个动作时,MobX会处理更新应用程序中使用被该动作修改的可观察对象的部分。在一个待办事项应用程序中,动作的一个例子是用一个新的待办事项更新待办事项列表的函数。

计算值类似于可观察的状态,用@computed 注释来声明。计算值不直接依赖于动作。相反,计算值依赖于可观察状态的值。如果一个计算值所依赖的可观察状态被一个动作修改,那么计算值也会被更新。在实践中,开发者常常忽略了计算值的概念,而往往是无意中使用可观察值来代替它们。

比较MobX、BLoC、Redux和setState()的范式

MobX建立在一个简单的理念上,即任何可以从应用状态中导出的东西都应该被导出。这意味着MobX为应用状态中的所有属性提供了覆盖,这些属性已经被定义为有可能发生变化。只有当这些属性发生变化时,MobX才会重建用户界面。这种方法与BLoC、Redux和setState使用的方法不同。BLoC使用流来传播变化,而Redux则是基于一个应用程序拥有一个单一的真理源,它的小部件继承于此。setState() ,提供与MobX类似的简单程度,要求你自己处理状态传播。由于它能够抽象出状态变化的细节,MobX提供了一个相对于其他方法更平滑的学习曲线。

设置一个Flutter项目

为了创建您的新Flutter项目,您将使用Flutter CLI工具。打开你的终端,导航到你的项目目录,运行这个命令。

$ flutter create reviewapp

CLI工具会生成一个模板项目,让您在几秒钟内开始。项目生成后,你可以在你的IDE中打开它。

安装项目的依赖性

你的新项目需要五个主要的依赖项。

  • mobx是MobX的一个Dart移植,用于编写状态修改逻辑。
  • flutter_mobx是MobX的Flutter集成,它提供了Observer widget,可以根据可观察状态的变化自动重建。
  • shared_preferences是一个本地持久化库。
  • mobx_codegen是一个用于MobX的代码生成库,允许使用MobX注释。
  • build_runner是一个独立的库,用于运行代码生成操作。

在你的IDE中打开项目,导航到你的/pubspec.yaml 文件以添加依赖项。用这个片段替换dependencies 部分。

dependencies:
  flutter:
    sdk: flutter
  mobx: ^0.3.5
  flutter_mobx: ^0.3.0+1
  shared_preferences: ^0.5.3+4

然后用这个代码片断替换dev_dependencies 部分。

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.6.5
  mobx_codegen: ^0.3.3+1

现在,在你项目的根目录下运行这个命令,下载依赖项。

$ flutter packages get

你正在构建的东西

在本教程中,您将构建一个简单的评论应用程序,允许用户添加评论和星级,如下面的图片所示。

如何构建 Flutter 示例项目

你要构建的部分中描述的示例项目的工作方式如下。

  • 启动应用程序
  • 从本地偏好中获取评论
  • 用检索到的评论更新用户界面
  • 添加评论
  • 在应用程序状态中更新评论列表
  • 将更新的评论列表保留在偏好设置中

在开始之前,通过在你的项目的/lib 目录中运行这个命令,创建/widgets,/screens, 和/models 文件夹。

$ mkdir widgets screens models

创建数据模型

首先,通过在/lib/models/ 目录中创建一个reviewmodel.dart 文件,为评论定义一个数据模型。把这个代码段添加到其中。

import 'package:meta/meta.dart';
class ReviewModel {
  final String comment;
  final int stars;
  const ReviewModel({@required this.comment, @required this.stars});

  factory ReviewModel.fromJson(Map<String, dynamic> parsedJson) {
    return ReviewModel(
      comment: parsedJson['comment'],
      stars: parsedJson['stars'],
    );
  }

  Map<String, dynamic> toJson(){
    return {
      'comment': this.comment,
      'stars': this.stars,
    };
  }
}

创建用户界面

我们正在构建的示例应用程序需要一种方法让用户与之互动。该应用程序将包含一个评论表单,显示现有的评论列表、评论总数以及每个评论的平均星数。该表格还将让用户添加一个新的评论。

首先,在/lib/screens 目录中创建一个review.dart 文件。添加这个代码片断。

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../widgets/info_card.dart';

class Review extends StatefulWidget {
  @override
  ReviewState createState() {
    return new ReviewState();
  }
}
class ReviewState extends State<Review> {
  final List<int> _stars = [1, 2, 3, 4, 5];
  final TextEditingController _commentController = TextEditingController();
  int _selectedStar;

  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Scaffold(
      appBar: AppBar(
        title: Text('Review App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 12.0),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: <Widget>[
                Container(
                  width: screenWidth * 0.6,
                  child: TextField(
                    controller: _commentController,
                    decoration: InputDecoration(
                      contentPadding: EdgeInsets.all(10),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(10.0),
                      ),
                      hintText: "Write a review",
                      labelText: "Write a review",
                    ),
                  ),
                ),
                Container(
                  child: DropdownButton(
                    hint: Text("Stars"),
                    elevation: 0,
                    value: _selectedStar,
                    items: _stars.map((star) {
                      return DropdownMenuItem<int>(
                        child: Text(star.toString()),
                        value: star,
                      );
                    }).toList(),
                    onChanged: (item) {
                      setState(() {
                        _selectedStar = item;
                      });
                    },
                  ),
                ),
                Container(
                  child: Builder(
                    builder: (BuildContext context) {
                      return IconButton(
                        icon: Icon(Icons.done),
                        onPressed: () {},
                      );
                    },
                  ),
                ),
              ],
            ),
            SizedBox(height: 12.0),
            //contains average stars and total reviews card
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                InfoCard(
                    infoValue: '2',
                    infoLabel: "reviews",
                    cardColor: Colors.green,
                    iconData: Icons.comment),
                InfoCard(
                  infoValue: '2',
                  infoLabel: "average stars",
                  cardColor: Colors.lightBlue,
                  iconData: Icons.star,
                  key: Key('avgStar'),
                ),
              ],
            ),
            SizedBox(height: 24.0),
            //the review menu label
            Container(
              color: Colors.grey[200],
              padding: EdgeInsets.all(10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Icon(Icons.comment),
                  SizedBox(width: 10.0),
                  Text(
                    "Reviews",
                    style: TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
            //contains list of reviews
            Expanded(
              child: Container(
                child: Text("No reviews yet"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

创建自定义小部件

在这个代码片断中,有一个对InfoCard 的引用。InfoCard 是一个自定义小组件,显示评论总数和平均星数。

Infocard custom widget for review app

要创建InfoCard widget,在/lib/widgets 目录下创建一个名为info_card.dart 的文件。添加这个代码段。

import 'package:flutter/material.dart';

class InfoCard extends StatelessWidget {
  final String infoValue;
  final String infoLabel;
  final Color cardColor;
  final IconData iconData;
  const InfoCard(
      {Key key,
      @required this.infoValue,
      @required this.infoLabel,
      @required this.cardColor,
      @required this.iconData,
      })
      : super(key: key);
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Container(
      height: 100,
      width: screenWidth / 2,
      child: Card(
        color: cardColor,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(5.0),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            Icon(
              iconData,
              size: 28.0,
              color: Colors.white,
            ),
            Text(
              "$infoValue $infoLabel",
              style: TextStyle(color: Colors.white),
            ),
          ],
        ),
      ),
    );
  }
}

尽管你在本教程的后面才需要它,但要创建一个ReviewWidget 类。这个类将被用来显示一个单一的评论项目。首先在项目的lib/widgets 目录下创建一个review.dart 文件。添加这个代码片断。

import 'package:flutter/material.dart';
import '../models/reviewmodel.dart';
import '../models/reviews.dart';
import '../widgets/review.dart';
import '../widgets/info_card.dart';

class ReviewWidget extends StatelessWidget {
  final ReviewModel reviewItem;

  const ReviewWidget({Key key, @required this.reviewItem}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Padding(
          padding: EdgeInsets.all(10.0),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: <Widget>[
              Expanded(
                child: Text(
                  reviewItem.comment,
                ),
              ),
              Row(
                children: List(reviewItem.stars).map((listItem) {
                  return Icon(Icons.star);
                }).toList(),
              ),
            ],
          ),
        ),
        Divider(
          color: Colors.grey,
        )
      ],
    );
  }
}

实现MobX

为了在你的应用程序中实现MobX,你需要在应用程序的状态中定义观察变量、动作和计算值。

在应用程序的任何时间点,评论列表、平均星数和评论总数必须是最新的可用值。这意味着它们必须用注释声明,以便MobX可以跟踪它们的变化。

要做到这一点,在你的项目的/lib/models 目录中创建一个文件reviews.dart 。添加这个代码片断。

import 'dart:async';
import 'dart:convert';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import './reviewmodel.dart';
part 'reviews.g.dart';
class Reviews = ReviewsBase with _$Reviews;
abstract class ReviewsBase with Store {
  @observable
  ObservableList<ReviewModel> reviews = ObservableList.of([]);

  @observable
  double averageStars = 0;

  @computed
  int get numberOfReviews => reviews.length;

  int totalStars = 0;

  @action
  void addReview(ReviewModel newReview) {
    //to update list of reviews
    reviews.add(newReview);
    // to update the average number of stars
    averageStars = _calculateAverageStars(newReview.stars);
    // to update the total number of stars
    totalStars += newReview.stars;
    // to store the reviews using Shared Preferences
    _persistReview(reviews);
  }

  @action
  Future<void> initReviews() async {
    await _getReviews().then((onValue) {
      reviews = ObservableList.of(onValue);
      for (ReviewModel review in reviews) {
        totalStars += review.stars;
      }
    });
    averageStars = totalStars / reviews.length;
  }

  double _calculateAverageStars(int newStars) {
    return (newStars + totalStars) / numberOfReviews;
  }

  void _persistReview(List<ReviewModel> updatedReviews) async {
    List<String> reviewsStringList = [];
    SharedPreferences _preferences = await SharedPreferences.getInstance();
    for (ReviewModel review in updatedReviews) {
      Map<String, dynamic> reviewMap = review.toJson();
      String reviewString = jsonEncode(ReviewModel.fromJson(reviewMap));
      reviewsStringList.add(reviewString);
    }
    _preferences.setStringList('userReviews', reviewsStringList);
  }
  
  Future<List<ReviewModel>> _getReviews() async {
    final SharedPreferences _preferences =
        await SharedPreferences.getInstance();
    final List<String> reviewsStringList =
        _preferences.getStringList('userReviews') ?? [];
    final List<ReviewModel> retrievedReviews = [];
    for (String reviewString in reviewsStringList) {
      Map<String, dynamic> reviewMap = jsonDecode(reviewString);
      ReviewModel review = ReviewModel.fromJson(reviewMap);
      retrievedReviews.add(review);
    }
    return retrievedReviews;
  }
}

这段代码声明了两个变量。

  1. reviews 是一个所有用户评论的列表
  2. averageStars 是所有评论中作为观察变量计算的平均星数。它们被计算为观察变量,因为它们的值预计会随着行动的进行而改变。然后,代码定义了 函数,该函数将一个新的评论添加到评论列表中。它还添加了一个 函数,用来自共享偏好的现有数据初始化评论列表,作为更新可观察状态的行动。addReview() initReviews()

尽管numberOfReviews 变量也可以被声明为可观察变量,但我们使用了一个计算值来代替,因为它的值的变化取决于一个动作的结果(更新的可观察状态),而不是直接取决于动作本身。可以把它看作是一种后遗症。最后,我们声明了一个totalStars 变量和函数_calculateAverageStars(),_persistReview(), 和_getReviews() 。这些没有注释,因为它们是不直接更新状态的辅助参数。

运行CodeGen

由于MobX专注于抽象高层实现的细节,该库处理了生成数据存储的过程。相比之下,Redux甚至需要手动编写存储。MobX通过使用其mobx_codegen 库与Dart的build_runner 库来执行代码生成,并在构建存储的脚手架时考虑到所有注释的属性。

进入你的项目的根目录,并运行该命令。

$ flutter packages pub run build_runner build

在你生成商店后,你会在你的/lib/models 目录中发现一个review.g.dart 文件。

使用观察者

即使实现了MobX存储,在你的应用程序的UI中反映状态变化也需要使用flutter_mobx 库中的观察者。观察者是一个小部件,它围绕着观察者或计算值,将其数值的变化呈现在用户界面上。

平均星数、评论数和评论总数的值会随着每个新评论的加入而被更新。这意味着用于呈现这些值的部件被包裹在一个Observer 部件中。要使用观察者小部件,请浏览你的/lib/screens/review.dart 文件。通过使用这段代码修改ReviewState 类。

class ReviewState extends State<Review> {
  final Reviews _reviewsStore = Reviews();
  final TextEditingController _commentController = TextEditingController();  
  final List<int> _stars = [1, 2, 3, 4, 5];
  int _selectedStar;
  @override
  void initState() {
    _selectedStar = null;
    _reviewsStore.initReviews();
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;
    double screenWidth = screenSize.width;
    return Scaffold(
      appBar: AppBar(
        title: Text('Review App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            SizedBox(height: 12.0),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: <Widget>[
                Container(
                  width: screenWidth * 0.6,
                  child: TextField(
                    controller: _commentController,
                    decoration: InputDecoration(
                      contentPadding: EdgeInsets.all(10),
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(10.0),
                      ),
                      hintText: "Write a review",
                      labelText: "Write a review",
                    ),
                  ),
                ),
                Container(
                  child: DropdownButton(
                    hint: Text("Stars"),
                    elevation: 0,
                    value: _selectedStar,
                    items: _stars.map((star) {
                      return DropdownMenuItem<int>(
                        child: Text(star.toString()),
                        value: star,
                      );
                    }).toList(),
                    onChanged: (item) {
                      setState(() {
                        _selectedStar = item;
                      });
                    },
                  ),
                ),
                Container(
                  child: Builder(
                    builder: (BuildContext context) {
                      return IconButton(
                        icon: Icon(Icons.done),
                        onPressed: () {
                          if (_selectedStar == null) {
                            Scaffold.of(context).showSnackBar(SnackBar(
                              content:
                                  Text("You can't add a review without star"),
                              duration: Duration(milliseconds: 500),
                            ));
                          } else if (_commentController.text.isEmpty) {
                            Scaffold.of(context).showSnackBar(SnackBar(
                              content: Text("Review comment cannot be empty"),
                              duration: Duration(milliseconds: 500),
                            ));
                          } else {
                            _reviewsStore.addReview(ReviewModel(
                                comment: _commentController.text,
                                stars: _selectedStar));
                          }
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
            SizedBox(height: 12.0),
            //contains average stars and total reviews card
            Observer(
              builder: (_) {
                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    InfoCard(
                      infoValue: _reviewsStore.numberOfReviews.toString(),
                      infoLabel: "reviews",
                      cardColor: Colors.green,
                      iconData: Icons.comment
                    ),
                    InfoCard(
                      infoValue: _reviewsStore.averageStars.toStringAsFixed(2),
                      infoLabel: "average stars",
                      cardColor: Colors.lightBlue,
                      iconData: Icons.star,
                      key: Key('avgStar'),
                    ),
                  ],
                );
              },
            ),
            SizedBox(height: 24.0),
            //the review menu label
            Container(
              color: Colors.grey[200],
              padding: EdgeInsets.all(10),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Icon(Icons.comment),
                  SizedBox(width: 10.0),
                  Text(
                    "Reviews",
                    style: TextStyle(fontSize: 18),
                  ),
                ],
              ),
            ),
            //contains list of reviews
            Expanded(
              child: Container(
                child: Observer(
                  builder: (_) => _reviewsStore.reviews.isNotEmpty
                      ? ListView(
                          children:
                              _reviewsStore.reviews.reversed.map((reviewItem) {
                            return ReviewWidget(
                              reviewItem: reviewItem,
                            );
                          }).toList(),
                        )
                      : Text("No reviews yet"),
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

这段代码通过从/lib/models/reviews.dart 创建一个Review 类的实例,作为访问商店的手段,附加了第一个修改。然后,它将平均星级和总评论数据所显示的Row ,用一个观察者小部件进行包装。然后,它使用Review 类的reviewStore 实例来引用这些数据。

接下来,占位符 "无评论 "Text widget在商店里的评论列表为空时显示。否则,一个ListView 显示列表中的项目。最后,"完成 "按钮的onPressed() 功能被修改为向商店添加一个新的评论。

在这一点上,你的应用程序几乎已经完成。你的下一步是将评论屏幕导入到你的main.dart 文件的导入部分。打开该文件,并添加这个片段。

$ import './screens/review.dart';

/lib/main.dart ,修改MyApp 类的build() 方法中的home 属性。将home 属性从MyHomePage() 改为Review() 。 这里是代码。

@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Review() //previously MyHomePage(),
  );
}

最后,使用flutter run 命令运行该应用程序。

编写测试样本

为了了解测试如何融入CI/CD管道,你将需要创建一个简单的单元测试和小工具测试。

要编写单元测试,在项目的/test 目录下创建一个名为unit_test.dart 的文件。添加这个代码片断。

import 'package:flutter_test/flutter_test.dart';
import '../lib/models/reviewmodel.dart';
import '../lib/models/reviews.dart';

void main() {
  test('Test MobX state class', () async {
    final Reviews _reviewsStore = Reviews();

    _reviewsStore.initReviews();

    expect(_reviewsStore.totalStars, 0);

    expect(_reviewsStore.averageStars, 0);
    _reviewsStore.addReview(ReviewModel(
      comment: 'This is a test review',
      stars: 3,
    ));

    expect(_reviewsStore.totalStars, 3);
    _reviewsStore.addReview(ReviewModel(
      comment: 'This is a second test review',
      stars: 5,
    ));

    expect(_reviewsStore.averageStars, 4);
  });
}

接下来,用这个代码片断完全替换你项目的test 目录中现有的widget_test.dart 文件的内容,添加小工具测试。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';

void main() {
  testWidgets('Test for rendered UI', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    Finder starCardFinder = find.byKey(Key('avgStar'));

    expect(starCardFinder, findsOneWidget);
  });
}

通过在你的项目根目录下执行flutter test 命令来运行这些测试。

用CircleCI进行CI/CD

除了为你提供一个环境来构建你的项目,持续集成(CI)工具提供了一个可靠和稳定的环境来运行自动测试和自动上传部署工件。在本节中,您将学习如何使用CircleCI为您的Flutter项目设置和使用CI/CD管道。

首先,在您的项目根目录下执行git init 命令,在您的项目中初始化一个本地Git仓库。用git add . 添加你的文件到它。用git commit -m "First commit" 来提交这些文件。然后,在 GitHub 上为你的项目创建一个在线仓库。将 GitHub 仓库作为本地仓库的远程引用,然后在项目的根目录下执行下面的命令,将修改推送到远程仓库。

$ git remote add origin https://link_to_repo && git push -u origin master

创建一个配置文件

在项目的根目录下运行mkdir .circleci 命令,创建一个名为.circleci 的文件夹。创建一个 配置文件,使文件路径的结构像这样/your_project_path/.circleci/config.yml

然后,用这个代码片断填充/.circleci/config.yml 文件。

version: 2
jobs:
  build:    
    docker:
      - image: cirrusci/flutter:v1.5.8 

    branches:
      only: master

    steps:
      - checkout

      - run:
          name: Run Flutter doctor
          command: flutter doctor

      - run:
          name: Run the application tests
          command: flutter test

      - run:
          name: Build the Android version
          command: flutter build apk 

      - store_artifacts:
          path: build/app/outputs/apk/release/app-release.apk

在这个配置文件中, Docker被用来作为 执行器。没有用于Flutter的官方CircleCI Docker镜像,但在DockerHub上有一大串Flutter镜像。最突出的是 cirrusci/flutter镜像。这个镜像的使用频率超过100万次拉取。

配置文件中可选的branches 部分用于过滤部署过程中运行的分支。当没有明确定义时,CircleCI假设master 作为工作的分支。

配置文件定义了要使用的Docker镜像。它还钉了一个与运行你的项目本地副本的Flutter版本相匹配的镜像, (在我的例子中是v1.5.8)。

在步骤部分,配置文件定义了每次在你的项目资源库上运行部署时要执行的每个过程,以及它们的执行顺序。

最后,在上述代码片断的store-artifacts 部分,引用了我们 构建工件的路径。这可以使工件自动上传到你的CircleCI仪表板的Artifacts标签。该工件可以被部署到AWS S3桶或任何其他托管服务。对于生产就绪的Flutter应用程序,您可以在此配置中添加部署到应用程序商店。

设置CircleCI

要将CircleCI与您的项目集成,请进入CircleCI仪表板,并点击添加项目。它在仪表板页面的最左边。接下来,导航到页面的最右边,点击设置项目

Add Projects Screen

在下一个页面,点击开始构建

Project setup interface

你的构建将开始。

Building project

现在,每次提交新的代码都会触发您的Flutter应用程序的自动构建、测试和部署管道。

总结

在这篇文章中,我们介绍了如何用MobX状态管理库管理Flutter应用程序的状态。我们还介绍了如何使用CircleCI为您的应用程序设置CI/CD流水线。

虽然在决定选择Flutter项目的状态管理方法时需要考虑权衡,尤其是大型项目,但MobX为中等规模的项目提供了一个可靠的选择。该库为你做最难的工作,同时给你权力在必要时负责。当您决定为您的下一个Flutter项目进行状态管理时,甚至是重写一个当前的项目时,这是一个很大的好处。

开发团队继续为他们的Flutter项目采用MobX。请查看 该库的GitHub仓库,了解如何为其发展做出贡献。

我确实希望你喜欢这个教程。编码愉快!

玻璃钢生产厂家玻璃钢雕塑是一体成型吗生产玻璃钢雕塑厂家哪里有卖呈贡玻璃钢大型雕塑设计哪里有玻璃钢恐龙雕塑生产厂家冰淇淋商场美陈珠海玻璃钢卡通人物雕塑厂家玻璃钢莲藕形象卡通娃娃雕塑宛城玻璃钢雕塑加工厂家玻璃钢公园雕塑公司日照公仔玻璃钢雕塑山西抽象玻璃钢雕塑设计泡沫玻璃钢景观雕塑小品郑州园林玻璃钢人物雕塑厂家秦皇岛玻璃钢雕塑哪家强玻璃钢动物长颈鹿雕塑玻璃钢单车人物雕塑人物玻璃钢雕塑批发代理句容玻璃钢卡通雕塑设计兰州玻璃钢雕塑制作厂家龙岩玻璃钢伟人像雕塑玻璃钢企鹅雕塑价格商场气球美陈教程贵阳玻璃钢雕塑定做延安仿铜玻璃钢雕塑价格珠海透光玻璃钢雕塑现货北京艺术商场美陈销售企业怀化玻璃钢雕塑制作厂家武汉玻璃钢人物不锈钢雕塑公司砂岩雕塑和玻璃钢雕塑的区别盐城商场春季美陈香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化