前言
wanandroid 是一个学习android的站点 , 特别感谢 wanandroid提供的api 。
项目截图
项目地址 : flutter_wanandroid
部分依赖插件如下
状态管理 : riverpod
网络请求 : dio
事件总线 : event_bus (android中已经不推荐使用 ,flutter没找到其他暂时先用这个)
数据库 : sqflite
data 类生成 : freezed
动画 : lottie
json处理 : FlutterJsonBeanFactory + json_serializable
完整的依赖可以去项目中查看 , 这里为了减少文章篇幅就不贴出来了
相关插件的使用
1 ,freezed
使用过compose 应该知道 , kotlin 中有个data class , copyWith 方法和state 配合天下无敌 , 但是dart 语言又没有data class , 但 freezed 可以帮我们实现类似功能
集成之后 , 创建一个freezed注解标记的类 , 如下
part 'collect_state.freezed.dart';
@freezed
class CollectPageState with _$CollectPageState{
factory CollectPageState({ @Default([]) List<CollectDatas> datas , CollectEntity? collectEntity}) = _CollectPageState;
}
然后命令行输入 flutter pub run build_runner build
就会生成另外一个类 , 实现了copyWith 方法
调用copyWith 界面就会刷新
2 ,FlutterJsonBeanFactory
这个插件主要是生成实体类 , idea 安装插件
右键点击FlutterJsonBeanFactory , 然后把json输入会生成实体类
FlutterJsonBeanFactory 插件自动生成代码分析
先分析下插件生成的代码 , 接下来封装网络请求用得上
使用插件生成的实体类会存在map中, key为实体类class name , value 为实体类实例对象
JsonConvert 的 convert 和 convertList 方法 , 非list类型的解析用convert 这个方法 , list的解析用convertList 方法
T? convert<T>(dynamic value) {
return asT<T>(value);
}
List<T?>? convertList<T>(List<dynamic>? value) {
return value.map((dynamic e) => asT<T>(e)).toList();
}
但最终都是调用 asT方法
T? asT<T extends Object?>(dynamic value) {
try {
final String valueS = value.toString();
if (type == "String") {
return valueS as T;
} else if (type == "int") {
//省略大部分类似代码
} else if (type == "double") {
else {
if (_convertFuncMap.containsKey(type)) {
return _convertFuncMap[type]!(value) as T;
}
}
}
}
总结一下asT方法 , 如果是基本类型直接as 返回 , 不是基本类型就从map中查找
MVVM架构设计
在android 中mvvm用的比较多 , 层次也比较清晰 , android 中mvvm架构如下
flutter 中 widget 相当于上图的(Activity/Fragmeent) , 即mvvm的view层 , 负责承载ui的显示 , 其他的都类似
model: 负责提供界面显示的数据 , 数据可以从 database , sp , 服务器中获取
view(Widget): 负责显示ui
ViewModel : 负责连接view 和 model ,存放各种state
处理 model 层
model 层要封装的主要有 http , sp ,database
我把这几个都放在data包下
http封装
1 . json 对应的实体类BaseEntity封装
wanandroid 服务器返回的数据定义为如下
{
"data": ...,
"errorCode": 0,
"errorMsg": ""
}
用FlutterJsonBeanFactory 插件生成一个BaseEntity , 经过处理后的关键代码如下
class BaseEntity<T> {
T? data;
int? errorCode;
String? errorMsg;
BaseEntity();
factory BaseEntity.fromJson(Map<String, dynamic> json) =>
_$BaseEntityFromJson(json);
Map<String, dynamic> toJson() => _$BaseEntityToJson(this);
@override
String toString() {
return jsonEncode(this);
}
}
BaseEntity<T> _$BaseEntityFromJson<T>(Map<String, dynamic> json) {
final BaseEntity<T> baseEntity = BaseEntity();
T? data;
if(json['data'] !=null){
data = JsonConvert.fromJsonAsT<T>(json['data']);
}
if (data != null) {
baseEntity.data = data;
}
final int? errorCode = jsonConvert.convert<int>(json['errorCode']);
if (errorCode != null) {
baseEntity.errorCode = errorCode;
}
final String? errorMsg = jsonConvert.convert<String>(json['errorMsg']);
if (errorMsg != null) {
baseEntity.errorMsg = errorMsg;
}
return baseEntity;
}
Map<String, dynamic> _$BaseEntityToJson(BaseEntity entity) {
final Map<String, dynamic> data = <String, dynamic>{};
data['data'] = entity.data?.toJson();
data['errorCode'] = entity.errorCode;
data['errorMsg'] = entity.errorMsg;
return data;
}
泛型T 可以为 List类型 , 去掉@JsonSerializable() 注解 ,避免代码生成器改变我们处理之后的代码
2.网络请求dio封装
封装一下dio , 用于处理请求的返回的结果 和 请求状态(加载中 , 失败 , 成功) , 下面以封装get请求为例
const successCode = 0;
class DioHolder {
late final Dio _dio = Dio();
static get() => DioHolder()._dio;
DioHolder._() {
_dio.interceptors.add(CookieManager.instance);
}
static DioHolder? _dioHolder;
factory DioHolder() {
return _dioHolder ??= DioHolder._();
}
}
Future<T?> get<T>(String path,
{Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress}) async {
state.setHttpRequestState(HttpRequestState.Loading);
try {
var response = await dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
if (response.statusCode == 200) {
return processResponse<T>(response, path)!;
} else {
state.setHttpRequestState(HttpRequestState.Fail);
return null;
}
} catch (e) {
state.setHttpRequestState(HttpRequestState.Fail);
return null;
}
}
//处理结果
T? processResponse<T>(Response<dynamic> rep, String path) {
if (rep.statusCode != 200) {
state.setHttpRequestState(HttpRequestState.Fail);
return null;
}
var entity = BaseEntity<T>.fromJson(rep.data);
if (entity.errorCode == successCode) {
state.setHttpRequestState(HttpRequestState.Suc);
//把请求的结果缓存在sp中
WanRepository.sp.put(path, entity.data.toString());
} else {
state.setHttpRequestState(HttpRequestState.Fail,
message: entity.errorMsg!);
}
try {
return entity.data;
} catch (e) {
return null;
}
}
dio封装好之后就开始定义请求接口 , 贴出部分代码
Future<List<NaviEntity>?> getNavi() async {
return _dioProxy.get<List<NaviEntity>>(WanUrls.NAVI);
}
Future<ArticleListEntity?> getArticleList({page}) async {
return _dioProxy
.get<ArticleListEntity>("${WanUrls.ARTICLE_LIST}$page/json");
}
cookie 处理
登录成功后会在cookie中返回账号密码,做好cookie持久化存储即可自动登录验证。
添加一个dio的拦截器 , 用来处理cookie
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.statusCode == 200) {
if (response.realUri.toString().contains(WanUrls.LOGIN)) {
if (response.headers.map['set-cookie'] != null) {
_persistCookie(response.headers.map['set-cookie']!);
}
}
if (response.realUri.toString() == WanUrls.LOGOUT) {
_clearCookie();
}
}
super.onResponse(response, handler);
}
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
options.headers[keyCookie] = _cookie;
return super.onRequest(options, handler);
}
在登录成功后保存cookie , 退出登录后清空cookie , 如果cookie 不为空且有效 , 请求时把cookie带上便可自动登录 .
database 数据库处理
wanandroid 站点没有提供文章阅读记录 , 只能在本地处理了
数据库打开然后创建文章表
openDatabase(databaseName, version: 1,
onCreate: (Database _db, int version) async {
await _db.execute('''
create table $tableArticle (
$columnId integer primary key autoincrement,
$columnArticleId integer not null,
$columnTitle text not null,
$columnChapterName text not null,
$columnShareUser text not null,
$columnNiceDate text not null,
$columnLink text not null,
$columnCollect integer not null)
''');
文章表的 增删改查 , db 有个batch 方法 , 用于批处理 , 效率非常高
insert 的时候先把原来的文章删了在插入到末尾 , 获取全部文章然后逆序返回
Future<Article> insert(Article article) async {
var batch= _db.batch();
batch.delete(tableArticle, where: '$columnArticleId = ?', whereArgs: [article.id]);
batch.insert(tableArticle, article.toMap() ,conflictAlgorithm:ConflictAlgorithm.replace);
await batch.commit(continueOnError: true);
return article;
}
Future<List<Article>?> getAll() async {
List<Map<String, dynamic>> maps = await _db.query(tableArticle);
if (maps.isNotEmpty) {
var list =<Article>[];
for (var element in maps) {
var en= Article.fromMap(element);
print("getAll ${en.id}");
list.add(en);
}
//逆序返回
return list.reversed.toList();
}
return null;
}
处理 ViewModel
viewmodel 用于处理逻辑 , 连接界面显示和数据获取
state的处理 , 状态管理用到了 riverpod
riverpod 中有个 StateNotifier , 当对象地址改变后界面也会跟着刷新 , 但使用很麻烦 , 需要封装一下, 就放在viewmodel中 , 以 HomeVm 为例
class HomeVm extends BaseViewModel {
late var refreshController = RefreshController();
late final homePageNotifier = newNotifier(HomePageState());
HomePageState get _homeState => homePageNotifier.state;
set _homeState(HomePageState state) {
homePageNotifier.state = state;
}
_request() async {
var value = await service.getArticleList(page:_getNextPage());
if (value != null) {
var data = <ArticleListDatas>[];
data.addAll(_homeState.datas);
if (value.curPage == 1) {
data.clear();
}
data.addAll(value.datas ?? []);
//会刷新界面
_homeState = _homeState.copyWith(datas: data, articleListEntity: value);
}
}
}
class _HomeArListPageState extends State<HomeArListPage>
with AutomaticKeepAliveClientMixin {
late var vm = HomeVm();
@override
Widget build(BuildContext context) {
super.build(context);
return Consumer(builder: (context, ref, _) {
var data = vm.homePageNotifier.watch(ref).datas;
var httpState = vm.watchHttpState(ref);
return refreshListStatePage(
child: RefreshList(
controller: vm.refreshController,
onRefresh: () async {
await vm.refresh();
},
onLoading: () async {
await vm.loadMore();
},
content: ListView.builder(
itemBuilder: (c, index) {
final entity = data[index];
return commonListItem(
id: entity.id!,
title: entity.title,
shareUser: entity.shareUser,
chapterName: entity.superChapterName,
niceDate: entity.niceDate,
link: entity.link,
itemClick: () {
navToPage(Browser(entity.link!, entity.title!));
});
},
itemCount: data.length,
)),
fail: vm.showFailedPage(),
retry: () {
vm.retry();
});
});
}
}
HomePageState 这个类是使用 freezed 生成的 , 用来实现和kotlin data class类似功能 , 当调用_homeState = _homeState.copyWith 界面就会刷新 .
踩过的坑
jsonToDart 插件
这个插件用于生成json 对应的实体类 , 但这个插件真不推荐使用 , 在这个插件上折腾了好几天时间
后续
这个项目是自用的 , 以后还会完善更新 , 欢迎大家学习
最后再贴下项目地址 flutter_wanandroid