一、效果图
项目地址:https://github.com/sparkerandroid/wanandroid_flutter
项目使用的接口:玩安卓、Gank,开放的api。感谢。
二、项目概览
dao:通过http,负责接口的调用;
model:数据解析;
navigator:页面导航;
pages:页面;
util:工具类,比如webView的封装;
widget:自定义组件;
constants文件:定义了项目中使用到的常量,比如接口的请求地址;
main文件:入口文件;
三、项目分析
3.1、项目入口 - main.dart
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'wanAndroid',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyNavigator(),
);
}
}
顶级函数main,作为Flutter项目的入口函数。该函数是必须的,如果没有该函数,则项目是无法运行的。
除此之外,使用了MyApp作为项目的根组件。在Flutter中,一个应用程序的结构就是一个树(具体可以说是wdiget或者element树),需要在入口设置树的根。
Flutter为我们提供了Material风格的各种组件,比如上面使用的MaterialApp,还有像Scaffold等。我们可以直接使用这样的组件快速高效的开发出Material风格的app。当然,你也可以自定义。建议使用这些Flutter提供的Material风格组件来编写自己的应用。为啥?
首先,这和Google一直提倡的Material风格吻合;
其次,Flutter提供的Material组件,比如Scaffold组件,该组件其实已经为我们搭建了一个Material app的原型了,包括如下属性:AppBar、body、drawer、bottomNavigationBar、floatingActionButton等等。可以快速搭建app,无需重复造轮子。
如上,根Widget是MyApp,而home代表了主页面。
3.2、主框架搭建
Scaffold + BottomNavigationBar + PageView + PageController + Drawer;
实现效果参照第一部分的图1。
Scaffold作为主页面的骨架,在其基础之上添加:
- 底部导航栏(BottomNavigationBar);
- 侧滑菜单(Drawer);
- 底部导航对应的页面(PageView) - 比如点击公众号Tab,则切换显示公众号页面;
- 底部导航和页面之间的联动(PageController) - 滑动页面,对应底部导航的切换 或者 点击底部Tab,切换页面;
实现很简单,不再详细分析了,一看就懂的。
class NavigatorState extends State<MyNavigator> {
String appBarTitle = "首页";
int _currentIndex = 0;
PageController _pageController = PageController(initialPage: 0);
@override
Widget build(BuildContext context) {
// AppBar - 顶部标题栏;PageView - 中间显示的Page页面;BottomNavigationBar - 底部导航栏
return Scaffold(
appBar: AppBar(
title: Text(appBarTitle),
leading: Builder(builder: (context) {
return IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
);
}),
),
drawer: MyDrawer(),
body: PageView(
controller: _pageController,
children: <Widget>[HomePage(), GankMzPage(), PublicSubscriptionPage()],
onPageChanged: (index) {
setState(() {
this._currentIndex = index;
_setAppBarTitle();
});
},
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
onTap: (index) {
// 点击跳转对应的PageView
_pageController.jumpToPage(index);
setState(() {
this._currentIndex = index;
_setAppBarTitle();
});
},
currentIndex: _currentIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("首页"),
backgroundColor: Colors.white),
BottomNavigationBarItem(
icon: Icon(Icons.apps),
title: Text("福利"),
backgroundColor: Colors.white),
BottomNavigationBarItem(
icon: Icon(Icons.map),
title: Text("公众号"),
backgroundColor: Colors.white)
]),
);
}
各个组件的具体用法可参阅Flutter中文网。
3.3、公众号页面详解
- 页面搭建
这里涉及到了Scaffold组件的另一种搭建页面的方式 - TabBar + TabBarView + TabController(负责TabBar与TabBarView之间的联动)。实现效果参照第一部分的图三。
Scaffold(
appBar: PreferredSize(
child: AppBar(
bottom: TabBar(
tabs: _tabs,
controller: _tabController,
isScrollable: true,
),
),
preferredSize: Size.fromHeight(50)),
body: TabBarView(
children: _tabData.map((item) {
return SubscriptionPage(
subscriptionId: item?.id,
);
}).toList(),
controller: _tabController,
),
);
其中,页面的顶部导航实现只需为AppBar设置bottom属性即可,属性的value即为TabBar。
点击每个Tab,对应的页面如何实现呢?
Flutter为我们提供了TabBarView。只需根据对应的Tab去显示对应的页面即可。这个我们封装了SubscriptionPage Widget,用于显示每个公众号下的历史数据。
正如上面所说,TabController是连接TabBar和TabBarView之间的桥梁。有了TabController,就很容易实现两者之间的联动了。用法如上代码所示,这里不再详述。
- 数据获取
数据的获取,我们单独放在Dao层。该文件夹下的各个Dao类负责对应页面的数据网络获取工作。涉及到http、convert等库的基本使用。
以获取公账号的Tab数据为例:
class PublicSubscriptionDao {
static Future<PublicSubscriptionModel> getSubscriptions() async {
http.Response response = await http.get(Apis.PUBLIC_SUBSCRIPTION);
if (response != null && response.statusCode == 200) {
Utf8Decoder utf8decoder = new Utf8Decoder();
return PublicSubscriptionModel.fromJson(
json.decode(utf8decoder.convert(response.bodyBytes)));
} else {
return null;
}
}
}
import 'package:http/http.dart' as http; // 网络通信
import 'dart:convert';// 数据解析
这里用到了两个库用于数据的获取及解析。
为了解决中文乱码问题,这里使用了Utf8Decoder。
- 数据解析
class Data {
int courseId;
int id;
String name;
int order;
int parentChapterId;
bool userControlSetTop;
int visible;
Data(
{this.courseId,
this.id,
this.name,
this.order,
this.parentChapterId,
this.userControlSetTop,
this.visible});
Data.fromJson(Map<String, dynamic> json) {
courseId = json['courseId'];
id = json['id'];
name = json['name'];
order = json['order'];
parentChapterId = json['parentChapterId'];
userControlSetTop = json['userControlSetTop'];
visible = json['visible'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['courseId'] = this.courseId;
data['id'] = this.id;
data['name'] = this.name;
data['order'] = this.order;
data['parentChapterId'] = this.parentChapterId;
data['userControlSetTop'] = this.userControlSetTop;
data['visible'] = this.visible;
return data;
}
}
这里涉及到了Dart相关的知识点:命名参数、命名构造函数。
通过上一步骤的数据获取,这里我们拿到了一个Map<String, dynamic>类型的数据。我们只需解析出对应的字段即可了。
- 数据填充
1)定义数据集合
List<Tab> _tabs = [];
2)获取数据
void _getSubscriptions() {
_tabs?.clear();
PublicSubscriptionDao.getSubscriptions().then((subscriptions) {
_tabData = subscriptions?.data ?? [];
setState(() {// 通知Flutter Framework 刷新
subscriptions?.data?.forEach((item) {
_tabs.add(new Tab(text: item.name));
});
});
}).catchError((e) {});
}
3)填充数据
AppBar(
bottom: TabBar(
tabs: _tabs, // 刷新之后,加载tab数据
controller: _tabController,
isScrollable: true,
),
)
......
3.4、自定义Widget
Flutter推荐按照组合而非继承的方式进行自定义Widget。
在3.3小节,第一部分对于TabBarView,我们自定义了一个SubscriptionPage。自定义的好处在于可以封装公用逻辑,页面看起来也整洁不少。
目前,自定义的Widget包括WebView、SubscriptionPage以及Drawer。详细可参阅相应的源码。
3.5、其他相关问题
按照目前搭建的App,还存在一个问题,就是每次底部Tab切换的时候都会重新加载当前页面。解决这个问题只需按如下操作:
class HomeState extends State<HomePage>
with AutomaticKeepAliveClientMixin<HomePage>{
......
@override
bool get wantKeepAlive => true;//重写该方法,返回值为true即可
}
四、总结
Flutter的思想很多和RN比较相似,但是总体的感觉是Flutter的编程体验好于RN,无论的写代码还是调试。Dart的Debug - JIT 和 发布 - AOT模式,这个非常棒。
本项目为入门级项目,还有很多需要优化的地方。共同学习。