在带有Android Studio的Android虚拟设备上的flutter应用程序中,StreamControllers出现了一些非常奇怪的行为。当我连接真正的android设备时,该应用程序运行正常。
我正在使用一个用于身份验证表单的块,该表单在更改时侦听,验证值,然后在提交时将其发送到服务器。
我遇到的问题是,电子邮件和密码均已正确添加到流中,并且正在按预期进行验证。
但是,如果我碰到的最后一个字段是密码,则在SubmitForm()中单击Submit时,_email.value的值为null。如果我最后触摸的字段是电子邮件,则_password.value的值为null。同样,这仅在AVD中发生。当我使用真实的电话时,_email和_paswsword的值都符合预期。
我完全删除了Android Studio,然后重新安装,但结果是相同的。我在使用docker时正在运行Hyper V和Windows Hypervisor平台,但是Android并没有关于HAMX的警告。
更新:
我刚刚安装了genymotion,它也可以正常工作,因此似乎是Windows上的AVD问题。我仍然希望AVD能够正常运行,因为精灵运动无法在Hyper V上运行。
系统:
OS Name Microsoft Windows 10 Pro
Version 10.0.17763 Build 17763
Other OS Description Not Available
OS Manufacturer Microsoft Corporation
System Manufacturer MSI
System Model MS-7A68
System Type x64-based PC
System SKU Default string
Processor Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz, 3600 Mhz, 4 Core(s), 8 Logical Processor(s)
BIOS Version/Date American Megatrends Inc. 1.30, 28/06/2017
SMBIOS Version 3.0
Embedded Controller Version 255.255
BIOS Mode UEFI
BaseBoard Manufacturer MSI
BaseBoard Product Z270 TOMAHAWK (MS-7A68)
BaseBoard Version 1.0
Platform Role Desktop
Secure Boot State Off
PCR7 Configuration Binding Not Possible
Windows Directory C:\WINDOWS
System Directory C:\WINDOWS\system32
Boot Device \Device\HarddiskVolume3
Locale United States
Hardware Abstraction Layer Version = "10.0.17763.404"
Time Zone GMT Daylight Time
Installed Physical Memory (RAM) 16.0 GB
Total Physical Memory 16.0 GB
Available Physical Memory 4.49 GB
Total Virtual Memory 21.5 GB
Available Virtual Memory 5.44 GB
Page File Space 5.50 GB
Page File C:\pagefile.sys
Kernel DMA Protection Off
Virtualization-based security Running
Virtualization-based security Required Security Properties
Virtualization-based security Available Security Properties Base Virtualization Support, UEFI Code Readonly, Mode Based Execution Control
Virtualization-based security Services Configured
Virtualization-based security Services Running
Device Encryption Support Reasons for failed automatic device encryption: TPM is not usable, PCR7 binding is not supported, Hardware Security Test Interface failed and device is not InstantGo, Un-allowed DMA capable bus/device(s) detected, TPM is not usable
A hypervisor has been detected. Features required for Hyper-V will not be displayed.
代码:
bloc:
import 'dart:async';
import 'dart:convert';
import 'package:rxdart/rxdart.dart';
import 'package:http/http.dart' as http;
import './auth_validator.dart';
class AuthBloc with AuthValidator {
final _email = BehaviorSubject<String>();
final _password = BehaviorSubject<String>();
final _isLoading = BehaviorSubject<bool>();
final _loginSucceded = BehaviorSubject<bool>();
// stream getters
Stream<String> get email => _email.stream.transform(validateEmail);
Stream<String> get password => _password.stream.transform(validatePassword);
Stream<bool> get isLoading => _isLoading.stream;
Stream<bool> get loginSuccess => _loginSucceded.stream;
Stream<Map> get redirect => Observable.combineLatest2(_loginSucceded,
_isLoading, (log, load) => {'loginSuccess': log, 'isLoading': load});
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// add data to sink onChange
Function(String) get emailChanged => _email.sink.add;
Function(String) get passwordChanged => _password.sink.add;
login(jsonUser) async {
try {
// submit to server
final http.Response response = await http.post(
'http://192.168.1.213:5000/api/users/signin',
body: jsonUser,
headers: {'Content-Type': 'application/json'},
);
final Map<String, dynamic> decodedRes = await json.decode(response.body);
if (response.statusCode == 200) {
_loginSucceded.sink.add(true);
}
return decodedRes;
} catch (e) {
_isLoading.sink.add(false);
}
}
void submitForm() async {
try {
final Map user = {
'email': _email.value,
'password': _password.value
};
final jsonUser = json.encode(user);
print(jsonUser);
await login(jsonUser);
void dispose() {
_email.close();
_password.close();
_isLoading.close();
_loginSucceded.close();
}
} catch (e) {
print('error: $e');
}
}
}
验证者:
import 'dart:async';
class AuthValidator {
final validateEmail = StreamTransformer<String, String>.fromHandlers(handleData: (email, sink) {
if (!email.contains(RegExp(r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"))) {
sink.addError('Please enter a valid email address');
} else {
sink.add(email);
}
});
final validatePassword = StreamTransformer<String, String>.fromHandlers(handleData: (password, sink) {
final goodLength = password.length >= 8;
final hasNumber = password.contains(RegExp(r'[0-9]'));
if (!goodLength) {
sink.addError('Must be at least 8 characters and contains a number');
} else {
sink.add(password);
}
});
}
页面:
import 'package:flutter/material.dart';
import '../blocs/auth_bloc.dart';
class LoginPage extends StatelessWidget {
final authBloc = AuthBloc();
@override
Widget build(BuildContext context) {
// TODO redirect would not work in StreamBuilder block
// manually listening for redirect conidtions here
// once a response is received from the server
authBloc.loginSuccess.listen((data) {
if (data) {
Navigator.pushReplacementNamed(context, '/dashboard');
}
});
return StreamBuilder(
stream: authBloc.isLoading,
initialData: false,
builder: (context, snapshot) {
print(snapshot.data);
return Scaffold(
body: _buildPage(authBloc, snapshot.data),
);
});
}
Widget _buildPage(AuthBloc authBloc, bool isLoading) {
return Container(
margin: EdgeInsets.all(20.0),
child: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
_emailField(authBloc),
_padding(),
_passwordField(authBloc),
_padding(),
_submitButton(authBloc, isLoading)
],
),
),
),
);
}
Widget _circularSpinner() {
return Center(
child: CircularProgressIndicator(),
);
}
Widget _emailField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.email,
builder: (BuildContext context, snapshot) {
return Container(
padding: EdgeInsets.only(top: 10.0),
child: TextField(
onChanged: authBloc.emailChanged,
autofocus: true,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'you@example.com',
labelText: 'Email Address',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
),
);
},
);
}
Widget _passwordField(AuthBloc authBloc) {
return StreamBuilder(
stream: authBloc.password,
builder: (BuildContext context, snapshot) {
return TextField(
onChanged: authBloc.passwordChanged,
obscureText: true,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: '8 characters or more with at least 1 number',
labelText: 'Password',
errorText: snapshot.error,
border: OutlineInputBorder(),
),
);
},
);
}
Widget _padding() {
return Padding(
padding: EdgeInsets.only(top: 20.0),
);
}
Widget _submitButton(AuthBloc authBloc, isLoading) {
final spinner = CircularProgressIndicator();
final button = RaisedButton(
child: Text('Login'),
color: Colors.blue,
onPressed: authBloc.submitForm,
);
return StreamBuilder(
stream: authBloc.submitValid,
builder: (context, snapshot) {
return isLoading ? spinner : button;
});
}
}