Разработайте функцию входа в систему с помощью Flutter MVVM

Flutter

Несколько дней назад я написал статью о реализации Flutter MVVM.[Открытый исходный код] Может быть только одно расстояние Flutter MVVM от веб-разработки до разработки приложения., сегодня мы используем его для разработки простого интерфейса входа и испытываем удобство использования привязки данных MVVM в процессе разработки.  

эта статьяполный код

 

Функция входа

Включено в интерфейс входаUserName,Passwordполе ввода текста,loginКнопка, отображаемый текст сообщения об успешном выполнении, отображаемый текст сообщения об ошибке и имеющие следующие функциональные точки:

  1. UserName,PasswordКогда длина содержимого любого поля ввода меньше 3 символов,loginкнопка недоступна

  2. нажмитеloginкнопка, используйте содержимое поля ввода, чтобы запросить удаленную службу для проверки входа

    • Отображение информации о пользователе при успешной аутентификации
    • Отображать сообщение об ошибке при сбое проверки
  3. Состояние ожидания отображается во время запроса удаленной службы (кнопкаloginСлова превращаются в круги~)

 

Реализация функции

Создайте проект Flutter (слегка ~)

Добавьте в проект зависимости Flutter MVVM

Найдите в проекте файл pubspec.yaml и добавьте информацию о пакете в раздел зависимостей.

dependencies:
    mvvm: ^0.1.3+4

Для удобства объяснения код, использованный в этой статье, находится вmain.dartВ файле он может быть разделен сам по себе в реальном проекте

написать базовый код

  • Сначала создайте пустую модель представления входа в систему.LoginViewModelи вход в системуLoginView, сначала создайте базовый интерфейс

Класс модели представления должен начинаться сViewModelНаследование класса. Класс представления должен бытьViewНаследование классов и указание моделей представленияLoginViewModel

class LoginViewModel extends ViewModel {
}

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel());

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
            margin: EdgeInsets.only(top: 100, bottom: 30),
            padding: EdgeInsets.all(40),
            child:
                Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              SizedBox(height: 10),
              TextField(
                decoration: InputDecoration(
                  border: UnderlineInputBorder(),
                  labelText: 'UserName',
                ),
              ),
              SizedBox(height: 10),
              TextField(
                obscureText: true,
                decoration: InputDecoration(
                  border: UnderlineInputBorder(),
                  labelText: 'Password',
                ),
              ),
              SizedBox(height: 10),
              Text("Error info.",
                  style: TextStyle(color: Colors.redAccent, fontSize: 16)),
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  child: RaisedButton(
                      onPressed: () {},
                      child: Text("login"),
                      color: Colors.blueAccent,
                      textColor: Colors.white)),
              SizedBox(height: 20),
              Text("Success info.",
                  style: TextStyle(color: Colors.blueAccent, fontSize: 20))
            ])));
  }
}

Подать заявку на стартовую страницу

void main() => runApp(MaterialApp(home: LoginView()));

Эффект после бега в этот момент

 

Реализовать функциональную точку 1

UserName,PasswordКогда длина содержимого любого поля ввода меньше 3 символов,loginкнопка недоступна

Поле ввода текста во Flutter (TextField) подключив контроллерTextEditingControllerуправлять его вводом и выводом.

Сначала мыLoginViewModelсоздать дваTextEditingController, а в представленииLoginViewв использовании$ModelБудуTextEditingControllerприкреплен кUserNameа такжеPasswordполе ввода текста

Часть кода опущена для удобства

class LoginViewModel extends ViewModel {
    final TextEditingController userNameCtrl = TextEditingController();
    final TextEditingController passwordCtrl = TextEditingController();
}

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel());

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              TextField(
                controller: $Model.userNameCtrl, //这里
                decoration: InputDecoration(
                  border: UnderlineInputBorder(),
                  labelText: 'UserName',
                ),
              ),
              SizedBox(height: 10),
              TextField(
                controller: $Model.passwordCtrl, //这里
                obscureText: true,
                decoration: InputDecoration(
                  border: UnderlineInputBorder(),
                  labelText: 'Password',
                ),
              ),
              // ...
            ])));
  }
}

 

Добавить свойства адаптации

дляLoginViewможем отслеживать изменения содержимого двух полей ввода, мы находимся вLoginViewModelДобавьте два свойства адаптации к

При изменении содержимого поля ввода соответствующийTextEditingControllerБудет предоставлено уведомление об изменении, поэтому все, что нам нужно сделать, это адаптировать его к нашему связанному свойству. Готовые методы уже инкапсулированы во Flutter MVVM.propertyAdaptive (API)

class LoginViewModel extends ViewModel {
    final TextEditingController userNameCtrl = TextEditingController();
    final TextEditingController passwordCtrl = TextEditingController();
    
    LoginViewModel() {
        // 使用 #userName 做为键创建适配到 TextEditingController 的属性
        propertyAdaptive<String, TextEditingController>(
            #userName, userNameCtrl, (v) => v.text, (a, v) => a.text = v,
            initial: "");

        propertyAdaptive<String, TextEditingController>(
            #password, passwordCtrl, (v) => v.text, (a, v) => a.text = v,
            initial: "");
    }
}

Теперь мы можемLoginViewИзменения в двух свойствах отслеживаются в

Часть кода опущена для удобства

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel(RemoteService()));

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              Text("Error info.",
                  style: TextStyle(color: Colors.redAccent, fontSize: 16)),
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  // 使用 $.watchAnyFor 来监视 #userName, #password 属性变化
                  child: $.watchAnyFor<String>([#userName, #password],
                      builder: (_, values, child) {
                    // 当任一属性值发生变化时此方法被调用
                    // values 为变化后的值集合
                    var userName = values.elementAt(0),
                        password = values.elementAt(1);
                    return RaisedButton(
                        // 根据 #userName, #password 属性值是否符合要求
                        // 启用或禁用按钮
                        onPressed: userName.length > 2 && password.length > 2
                            ? () {}
                            : null,
                        child: Text("login"),
                        color: Colors.blueAccent,
                        textColor: Colors.white);
                  })),
              // ...
            ])));
  }
}

Беги, чтобы увидеть эффект

Для облегчения обслуживания мы можемLoginViewЛогика для проверки ввода помещена вLoginViewModelсередина.

class LoginViewModel extends ViewModel {
    final TextEditingController userNameCtrl = TextEditingController();
    final TextEditingController passwordCtrl = TextEditingController();
    
    LoginViewModel() {
        propertyAdaptive<String, TextEditingController>(
            #userName, userNameCtrl, (v) => v.text, (a, v) => a.text = v,
            initial: "");

        propertyAdaptive<String, TextEditingController>(
            #password, passwordCtrl, (v) => v.text, (a, v) => a.text = v,
            initial: "");
    }
    
    // 将 LoginView 中 userName.length > 2 && password.length > 2 逻辑
    // 移到 LoginViewModel 中,方便以后变更规则
    bool get inputValid =>
      userNameCtrl.text.length > 2 && passwordCtrl.text.length > 2;
}

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel(RemoteService()));

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              Text("Error info.",
                  style: TextStyle(color: Colors.redAccent, fontSize: 16)),
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  // 使用 $.watchAnyFor 来监视 #userName, #password 属性变化
                  child: $.watchAnyFor<String>([#userName, #password],
                      // $.builder0 用于生成一个无参的builder
                      builder: $.builder0(() => RaisedButton(
                        // 使用 LoginViewModel 中的 inputValid 
                        // 启用或禁用按钮
                        onPressed: $Model.inputValid
                            ? () {}
                            : null,
                        child: Text("login"),
                        color: Colors.blueAccent,
                        textColor: Colors.white)
                  ))),
              // ...
            ])));
  }
}

 

Реализовать функциональную точку 2

нажмитеloginкнопка, используйте содержимое поля ввода, чтобы запросить удаленную службу для проверки входа

Создайте класс удаленного обслуживания

Создайте смоделированный класс удаленного обслуживания для выполнения функции проверки входа. Этот класс обслуживания имеет только один метод входа. Когда userName="tom" password="123", это законный пользователь. В противном случае вход завершается сбоем и появляется сообщение об ошибке. брошенный. А для имитации сетевого эффекта результат будет возвращен с задержкой в ​​3 секунды

class User {
    String name;
    String displayName;
    User(this.name, this.displayName);
}

// mock service
class RemoteService {
    Future<User> login(String userName, String password) async {
        return Future.delayed(Duration(seconds: 3), () {
            if (userName == "tom" && password == "123") 
                return User(userName, "$userName cat~");
            throw "mock error.";
        });
    }
}

 

Добавить асинхронное свойство

дляLoginViewможем отслеживать изменения запроса на вход, мы находимся вLoginViewModelДобавьте асинхронные свойства и внедрите фиктивный сервис для использования.

Готовый метод создания асинхронных свойств инкапсулирован во Flutter MVVM.propertyAsync (API),propertyAsyncне встроенViewModelкласс, для его использования требуетсяLoginViewModel with AsyncViewModelMixin

class LoginViewModel extends ViewModel with AsyncViewModelMixin {
  final RemoteService _service;

  final TextEditingController userNameCtrl = TextEditingController();
  final TextEditingController passwordCtrl = TextEditingController();

  // 注入服务
  LoginViewModel(this._service) {
    
    // 使用 #login 做为键创建一个异步属性
    // 并提供一个用于获取 Future<User> 的方法
    // 我们使用模拟服务的 login 方法,并将 userName、password 传递给它
    propertyAsync<User>(
        #login, () => _service.login(userNameCtrl.text, passwordCtrl.text));
        
        
    propertyAdaptive<String, TextEditingController>(
        #userName, userNameCtrl, (v) => v.text, (a, v) => a.text = v,
        initial: "");

    propertyAdaptive<String, TextEditingController>(
        #password, passwordCtrl, (v) => v.text, (a, v) => a.text = v,
        initial: "");
  }

  bool get inputValid =>
      userNameCtrl.text.length > 2 && passwordCtrl.text.length > 2;
}

существуетLoginViewиспользование асинхронных свойств в

Когда мы создаем асинхронное свойство, помимо предоставления функции привязки на основе этого свойства, Flutter MVVM также предоставляет намgetInvoke (API),invoke (API) а такжеlink (API) метод,getInvokeвозвращает метод для инициирования запроса, иinvokeнапрямую инициирует запрос,linkЭквивалентноgetInvoke, является его псевдонимом

Следует отметить, что при привязке асинхронных свойств Flutter MVVM будет инкапсулировать значение свойства (результат запроса) какAsyncSnapshot<TValue>

class LoginView extends View<LoginViewModel> {
  // 注入服务实例
  LoginView() : super(LoginViewModel(RemoteService()));

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              SizedBox(height: 10),
              // $.$ifFor 来监视 #login 属性值变化
              // 当属性值变化时使用 valueHandle 结果来控制 widget 是否显示
              // snapshot.hasError 表示请求结果中有错误时显示
              $.$ifFor<AsyncSnapshot>(#login,
                  valueHandle: (AsyncSnapshot snapshot) => snapshot.hasError,
                  builder: $.builder1((AsyncSnapshot snapshot) => Text(
                      "${snapshot.error}",
                      style:
                          TextStyle(color: Colors.redAccent, fontSize: 16)))),
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  child: $.watchAnyFor<String>([#userName, #password],
                      builder: $.builder0(() => RaisedButton(
                          // 使用 $Model.link 将发起异步请求方法挂接到事件
                          onPressed:
                              $Model.inputValid ? $Model.link(#login) : null,
                          child: Text("login"),
                          color: Colors.blueAccent,
                          textColor: Colors.white)))),
              SizedBox(height: 20),
              // $.$ifFor 来监视 #login 属性值变化
              // 当属性值变化时使用 valueHandle 结果来控制 widget 是否显示
              // snapshot.hasData 表示请求正确返回数据时显示
              $.$ifFor<AsyncSnapshot<User>>(#login,
                  valueHandle: (AsyncSnapshot snapshot) => snapshot.hasData,
                  // 绑定验证成功后的用户显示名
                  builder: $.builder1((AsyncSnapshot<User> snapshot) => Text(
                      "${snapshot.data?.displayName}",
                      style:
                          TextStyle(color: Colors.blueAccent, fontSize: 20))))
            ])));
  }
}

эффект после бега

Поскольку смоделированная служба задерживается на 3 секунды, в середине будет очень недружественное состояние застоя, и затем мы реализуем обработку состояния ожидания, чтобы сделать его более дружественным.

 

Реализовать функциональную точку 3

Состояние ожидания отображается во время запроса удаленной службы (кнопкаloginСлова превращаются в круги~)

Как упоминалось ранее, Flutter MVVM будет инкапсулировать результат запроса асинхронных свойств вAsyncSnapshot<TValue>,а такжеAsyncSnapshot<TValue>серединаconnectionStateпредоставил нам изменения состояния в процессе запроса, еслиconnectionStateдляwaitingкогда, поставитьloginкнопкаchildПревратите его в круговую анимацию

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel(RemoteService()));

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  child: $.watchAnyFor<String>([#userName, #password],
                      builder: $.builder0(() => RaisedButton(
                          onPressed:
                              $Model.inputValid ? $Model.link(#login) : null,
                          // 使用 $.watchFor 监视 #login 状态变化
                          // waiting 时显示转圈圈〜
                          child: $.watchFor(#login,
                              builder: $.builder1((AsyncSnapshot snapshot) =>
                                  snapshot.connectionState ==
                                          ConnectionState.waiting
                                      ? SizedBox(
                                          width: 20,
                                          height: 20,
                                          child: CircularProgressIndicator(
                                            backgroundColor: Colors.white,
                                            strokeWidth: 2,
                                          ))
                                      : Text("login"))),
                          color: Colors.blueAccent,
                          textColor: Colors.white)))),
              SizedBox(height: 20),
              // ...
            ])));
  }
}

эффект после бега

Внимательные друзья здесь должны заметить небольшую проблему.При первом сбое входа отображается сообщение об ошибке, но только когда запрос на вход инициируется снова и результат возвращается, сообщение об ошибке первого сбоя входа обновляется.Это также не является хороший опыт, нам просто нужно$Model.link(#login)Небольшое изменение здесь и сброс состояния немедленно каждый раз, когда делается запрос.

    // ...
    RaisedButton(
        // 使用 resetOnBefore
        onPressed:
            $Model.inputValid ? $Model.link(#login, resetOnBefore: true) : null,
    // ...

эффект после бега

Для сценария перехода на страницу после успешной проверки сервиса можно указать при создании асинхронного свойстваonSuccessметод для настройки последующих операций, когда асинхронный запрос успешно возвращает результат.(Дополнительные параметры асинхронного свойства можно просмотретьAPI)

 

Повысить производительность

В основном мы достигли ожидаемой функциональности входа в систему, но поскольку мы находимся в$.watchAnyFor<String>([#userName, #password], builder: ..)изbuilderвложенный внутрь метода$.watchFor(#login, ..), так что это вызывает проблему, когда верхний слой#userName, #passwordменяется независимо отbuilderвнутреннее наблюдение#loginБудет ли изменение запускать изменение вместе с верхним слоем одновременно(в двоемbuilderСпособ добавления отладочной информации для просмотра явления), что не соответствовало нашим ожиданиям и привело к ненужному снижению производительности. Решение очень простое, нужно только поставить внутренний вложенный$.watchFor(#login, ..)перейти на верхний слой$.watchAnyFor<String>([#userName, #password], ..)методchildв параметре.

class LoginView extends View<LoginViewModel> {
  LoginView() : super(LoginViewModel(RemoteService()));

  @override
  Widget buildCore(BuildContext context) {
    return Scaffold(
        body: Container(
              // ...
              Container(
                  margin: EdgeInsets.only(top: 80),
                  width: double.infinity,
                  child: $.watchAnyFor<String>([#userName, #password],
                      builder: $.builder2((_, child) => RaisedButton(
                          onPressed:
                              $Model.inputValid ? $Model.link(#login) : null,
                          // 使用从外部传入的 child
                          child: child,
                          color: Colors.blueAccent,
                          textColor: Colors.white)),
                      // 将按钮 child 的构造移到此处
                      child: $.watchFor(#login,
                              builder: $.builder1((AsyncSnapshot snapshot) =>
                                  snapshot.connectionState ==
                                          ConnectionState.waiting
                                      ? SizedBox(
                                          width: 20,
                                          height: 20,
                                          child: CircularProgressIndicator(
                                            backgroundColor: Colors.white,
                                            strokeWidth: 2,
                                          ))
                                      : Text("login"))))),
              SizedBox(height: 20),
              // ...
            ])));
  }
}

Теперь там, где есть изменение, оно будет только обновляться, а лишних обновлений не будет.Пока мы реализовали полноценную функцию входа в систему.

 

наконец

Длина статьи немного длинная, но содержание невелико.В основном она объясняет использование Flutter MVVM и использует привязку данных для уменьшения нашего логического кода и рабочей нагрузки, а также для улучшения удобства сопровождения кода.

полный код