【原创不易,转载请注明出处:https://blog.csdn.net/email_jade/article/details/86220592】
切换账号在移动app中应用的场景非常多,记住账号功能在Android或者IOS中有大量的开源代码,今天,我们就用flutter来实现一个能够记录历史登录账号的Demo吧,以下是效果演示:
实现思路:
1. shared_preferences 用来做账号密码的保存
对于账号密码的保存,注意的有两点,一是要有序保存,比如,最后一次登录的账号,要在下次进入的时候直接展示到输入框中,二是要进行去重操作,假如要登录的账号跟之前是同一个,那么不再新增记录了。考虑到顺序,因此我采用的是List,而去重,采用的是List的contain函数,不过注意,这要重载bean的==运算符,不得不说,相对于Java,Dart新增了运算符的重载,使用起来更加方便了。
对于数据的操作,代码详见下:
import 'package:flutter_login/bean/User.dart';
import 'package:shared_preferences/shared_preferences.dart';
///数据库相关的工具
class SharedPreferenceUtil {
static const String ACCOUNT_NUMBER = "account_number";
static const String USERNAME = "username";
static const String PASSWORD = "password";
///删掉单个账号
static void delUser(User user) async {
SharedPreferences sp = await SharedPreferences.getInstance();
List list = await getUsers();
list.remove(user);
saveUsers(list, sp);
}
///保存账号,如果重复,就将最近登录账号放在第一个
static void saveUser(User user) async {
SharedPreferences sp = await SharedPreferences.getInstance();
List list = await getUsers();
addNoRepeat(list, user);
saveUsers(list, sp);
}
///去重并维持次序
static void addNoRepeat(List users, User user) {
if (users.contains(user)) {
users.remove(user);
}
users.insert(0, user);
}
///获取已经登录的账号列表
static Future> getUsers() async {
List list = new List();
SharedPreferences sp = await SharedPreferences.getInstance();
int num = sp.getInt(ACCOUNT_NUMBER) ?? 0;
for (int i = 0; i < num; i++) {
String username = sp.getString("$USERNAME$i");
String password = sp.getString("$PASSWORD$i");
list.add(User(username, password));
}
return list;
}
///保存账号列表
static saveUsers(List users, SharedPreferences sp){
sp.clear();
int size = users.length;
for (int i = 0; i < size; i++) {
sp.setString("$USERNAME$i", users[i].username);
sp.setString("$PASSWORD$i", users[i].password);
}
sp.setInt(ACCOUNT_NUMBER, size);
}
}
2. 将保存的账号进行展示
对于账号的展示,熟悉Android的朋友都知道,这种情况采用PopupWindow比较合适,我最开始也是这样写的,在flutter里面,使用的是showMenu,不过遇到了一个大坑,就是历史账号展示有最大宽度的限制,导致布局看起来特别难看,参考framework的源代码:
popup_menu.dart
.......
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
.......
final Widget child = ConstrainedBox(
constraints: const BoxConstraints(
minWidth: _kMenuMinWidth,
maxWidth: _kMenuMaxWidth,
),
.......
于是,我改变思路,顶层采用Stack布局,账号展示采用Offstage镶嵌ListView布局,详见:
历史账号的展示覆盖在登陆页面之上,采用Offstage布局,可以随时显示和隐藏。这里还会遇到一个问题,就是历史账号展示的位置及大小,在flutter中,我们可以通过globalKey.currentContext.findRenderObject()来测量已有控件的大小及位置,因此,我们可以测量账号输入框的大小及位置。对于历史账号而言,位置top,可以经过输入框的y坐标+输入框的height计算得来,位置left和right,可以通过屏幕的宽和自身的宽计算而来,自身的宽度可以与账号输入框的宽度保持一致,而历史记录的高度,可以看成是items的高度和Divide的高度和,这里可以经过计算得来。由于采用的是ListView,也不用考虑高度超出屏幕范围的处理了。
完成布局后,剩下的事情就比较简单了,就是账号的删除与新增,注意处理数据边界问题。
这里还用到了package_info这个包,主要用来读取app的版本号的。
布局代码详见下:
import 'package:flutter/material.dart';
import 'package:flutter_login/bean/User.dart';
import 'package:flutter_login/util/SharedPreferenceUtil.dart';
import 'package:package_info/package_info.dart';
import 'package:flutter/rendering.dart' show debugPaintSizeEnabled;
void main() {
debugPaintSizeEnabled = false; //调试用
return runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: LoginPage(),
);
}
}
class LoginPage extends StatefulWidget {
@override
State createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State {
GlobalKey _globalKey = new GlobalKey(); //用来标记控件
String _version; //版本号
String _username = ""; //用户名
String _password = ""; //密码
bool _expand = false; //是否展示历史账号
List _users = new List(); //历史账号
@override
void initState() {
super.initState();
_getVersion();
_gainUsers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Stack(
children: [
Center(
child: Container(
width: 500,
child: Flex(direction: Axis.vertical, children: [
Expanded(
child: Container(
child: Icon(
Icons.account_balance,
size: 100,
),
),
flex: 3,
),
_buildUsername(),
_buildPassword(),
_buildLoginButton(),
Expanded(
child: Container(
padding: EdgeInsets.only(bottom: 20),
alignment: AlignmentDirectional.bottomCenter,
child: Text("版本号:$_version"),
),
flex: 2,
),
]),
),
),
Offstage(
child: _buildListView(),
offstage: !_expand,
),
],
),
);
}
///构建账号输入框
Widget _buildUsername() {
return TextField(
key: _globalKey,
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide()),
contentPadding: EdgeInsets.all(8),
fillColor: Colors.white,
filled: true,
prefixIcon: Icon(Icons.person_outline),
suffixIcon: GestureDetector(
onTap: () {
if (_users.length > 1 || _users[0] != User(_username, _password)) {
//如果个数大于1个或者唯一一个账号跟当前账号不一样才弹出历史账号
setState(() {
_expand = !_expand;
});
}
},
child: _expand
? Icon(
Icons.arrow_drop_up,
color: Colors.red,
)
: Icon(
Icons.arrow_drop_down,
color: Colors.grey,
),
),
),
controller: TextEditingController.fromValue(
TextEditingValue(
text: _username,
selection: TextSelection.fromPosition(
TextPosition(
affinity: TextAffinity.downstream,
offset: _username == null ? 0 : _username.length,
),
),
),
),
onChanged: (value) {
_username = value;
},
);
}
///构建密码输入框
Widget _buildPassword() {
return Container(
padding: EdgeInsets.only(top: 30),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(borderSide: BorderSide()),
fillColor: Colors.white,
filled: true,
prefixIcon: Icon(Icons.lock),
contentPadding: EdgeInsets.all(8),
),
controller: TextEditingController.fromValue(
TextEditingValue(
text: _password,
selection: TextSelection.fromPosition(
TextPosition(
affinity: TextAffinity.downstream,
offset: _password == null ? 0 : _password.length,
),
),
),
),
onChanged: (value) {
_password = value;
},
obscureText: true,
),
);
}
///构建历史账号ListView
Widget _buildListView() {
if (_expand) {
List children = _buildItems();
if (children.length > 0) {
RenderBox renderObject = _globalKey.currentContext.findRenderObject();
final position = renderObject.localToGlobal(Offset.zero);
double screenW = MediaQuery.of(context).size.width;
double currentW = renderObject.paintBounds.size.width;
double currentH = renderObject.paintBounds.size.height;
double margin = (screenW - currentW) / 2;
double offsetY = position.dy;
double itemHeight = 30.0;
double dividerHeight = 2;
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5.0),
border: Border.all(color: Colors.blue, width: 2),
),
child: ListView(
itemExtent: itemHeight,
padding: EdgeInsets.all(0),
children: children,
),
width: currentW,
height: (children.length * itemHeight +
(children.length - 1) * dividerHeight),
margin: EdgeInsets.fromLTRB(margin, offsetY + currentH, margin, 0),
);
}
}
return null;
}
///构建历史记录items
List _buildItems() {
List list = new List();
for (int i = 0; i < _users.length; i++) {
if (_users[i] != User(_username, _password)) {
//增加账号记录
list.add(_buildItem(_users[i]));
//增加分割线
list.add(Divider(
color: Colors.grey,
height: 2,
));
}
}
if (list.length > 0) {
list.removeLast(); //删掉最后一个分割线
}
return list;
}
///构建单个历史记录item
Widget _buildItem(User user) {
return GestureDetector(
child: Container(
child: Flex(
direction: Axis.horizontal,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(left: 5),
child: Text(user.username),
),
),
GestureDetector(
child: Padding(
padding: EdgeInsets.only(right: 5),
child: Icon(
Icons.highlight_off,
color: Colors.grey,
),
),
onTap: () {
setState(() {
_users.remove(user);
SharedPreferenceUtil.delUser(user);
//处理最后一个数据,假如最后一个被删掉,将Expand置为false
if (!(_users.length > 1 ||
_users[0] != User(_username, _password))) {
//如果个数大于1个或者唯一一个账号跟当前账号不一样才弹出历史账号
_expand = false;
}
});
},
),
],
),
),
onTap: () {
setState(() {
_username = user.username;
_password = user.password;
_expand = false;
});
},
);
}
///构建登录按钮
Widget _buildLoginButton() {
return Container(
padding: EdgeInsets.only(top: 30),
width: double.infinity,
child: FlatButton(
onPressed: () {
//提交
SharedPreferenceUtil.saveUser(User(_username, _password));
SharedPreferenceUtil.addNoRepeat(_users, User(_username, _password));
},
child: Text("登录"),
color: Colors.blueGrey,
textColor: Colors.white,
highlightColor: Colors.blue,
),
);
}
///获取版本号
void _getVersion() async {
PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
setState(() {
_version = packageInfo.version;
});
});
}
///获取历史用户
void _gainUsers() async {
_users.clear();
_users.addAll(await SharedPreferenceUtil.getUsers());
//默认加载第一个账号
if (_users.length > 0) {
_username = _users[0].username;
_password = _users[0].password;
}
}
}
GitHub地址:https://github.com/jadennn/flutter_login
flutter很好,路还很长,让我们一起奋斗前行!