切換賬號在移動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<User> list = await getUsers();
list.remove(user);
saveUsers(list, sp);
}
///保存賬號,如果重複,就將最近登錄賬號放在第一個
static void saveUser(User user) async {
SharedPreferences sp = await SharedPreferences.getInstance();
List<User> list = await getUsers();
addNoRepeat(list, user);
saveUsers(list, sp);
}
///去重並維持次序
static void addNoRepeat(List<User> users, User user) {
if (users.contains(user)) {
users.remove(user);
}
users.insert(0, user);
}
///獲取已經登錄的賬號列表
static Future<List<User>> getUsers() async {
List<User> 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<User> 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<StatefulWidget> createState() {
return _LoginPageState();
}
}
class _LoginPageState extends State<LoginPage> {
GlobalKey _globalKey = new GlobalKey(); //用來標記控件
String _version; //版本號
String _username = ""; //用戶名
String _password = ""; //密碼
bool _expand = false; //是否展示歷史賬號
List<User> _users = new List(); //歷史賬號
@override
void initState() {
super.initState();
_getVersion();
_gainUsers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
body: Stack(
children: <Widget>[
Center(
child: Container(
width: 500,
child: Flex(direction: Axis.vertical, children: <Widget>[
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<Widget> 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<Widget> _buildItems() {
List<Widget> 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: <Widget>[
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很好,路還很長,讓我們一起奮鬥前行!