第一步:搭建环境
在开始搭建第一个Flutter应用之前,还需要在电脑里安装Flutter环境,如果还未搭建环境请先移步这里。如果环境已安装完毕,那就可以开始,
第二步:创建应用
首先找到即将使用的文件夹并在当前文件夹打开命令行 flutter create 项目名称如图一
创建完成后按照下面的命令执行即可进入该项目中运行如图二
第三步:了解文件
完成到这里就可以开始后面定制化的开发了,再次之前可以对每个文件夹进行介绍一下,方便后期开发
pubspec.yaml
pubspec.lock
与vue一样,是依赖版本锁
ios/android/web/windows/macos/linux
对应的不同平台所内置的引擎能力,作为一名前端人员就可以看Web,这里面就有一个html模版,里面通过js将dart代码动态加载并渲染。
lib
这里面就是主要开发文件夹,可以在这里创建自己的开发文件,其中main.dart就是主入口文件,
第四步:Flutter 架构概览
Framework框架层
纯dart语言写成的SDK,实现了一套基础库,其中Foundation、Animation、Painting、Gestures都是Google实现的UI、动画渲染、手势交互层面的基础库,Rendering是渲染层,纯抽象布局的一部分,依赖于Foundation、Animation、Painting、Gestures。在这里将会形成一个可渲染对象组成的渲染树,当渲染内容发生变化,这里就会找出变化的内容进行更新,与vue的Render大同小异。Widgets就是Flutter内置的基础组件库。而Material和Cupertinno则是两套不同风格的具体组件库,在开发工程中就回去引用其中的包,如mai.dart中第一行就引用了import ‘package:flutter/material.dart’
Engine引擎层
对 Flutter 的核心 API 进行了底层封装并将功能暴露给框架层这里就是Flutter根据不同应用渲染出结果、调用不同平台的原生基础能力,都是通过这里进行总装,通过调用不同内容进行分发调用。
Embrdder嵌入层
与平台进行整合,将Flutter引擎嵌入对应平台,因为各平台使用的底层语言不同,这里就会产生对应适配,这样Flutter才可以正常渲染。这是Flutter实现跨平台最为核心的一个地方。如果需要对接新平台也将是在这里增加一套新嵌入层。
第五步:语言介绍
常用如下:
变量声明
使用var声明变量、使用const声明常量、可以Object声明一个对象,与TS比较像
函数
//无参数类型-这是不带函数参数或者说参数列表为空
String getDefaultErrorMsg() => 'Unknown Error!';
//无参数类型-等价于上面函数形式,同样是参数列表为空
get getDefaultErrorMsg => 'Unknown Error!';
//必需位置参数类型-这里的exception是必需的位置参数
String getErrorMsg(Exception exception) => exception.toString();
//必需位置参数类型-这里的exception是必需的位置参数
String getErrorMsg(Exception exception) => exception.toString();
//必需位置参数类型-这里的exception是必需的位置参数
String getErrorMsg(Exception exception) => exception.toString();
//必需位置参数类型-这里的exception是必需的位置参数
String getErrorMsg(Exception exception) => exception.toString();
//注意: 可选命名参数必须在必需位置参数的后面
num add(num a, num b, {num c, num d}) {
return a + b + c + d;
}
void add7([num a, num b], {num c, num d}) {
// todo
}
参数默认值(参数默认值只针对可选参数才能添加的。)
num add(num a, num b, num c,{ num d = 5 }, [num e = 5]}) {
return a + b + c + d;
}
mixin
mixin 可以实现类似多重继承的功能,但是实际上和多重继承又不一样。多重继承中相同的函数执行并不会存在 ”父子“ 关系,mixin还可以抽象和重用一系列特性,mixin实际上实现了一条继承链声明,mixin 的顺序代表了继承链的继承顺序,声明在后面的 mixin,一般会最先执行
异步
Future使用Future对象封装了Dart 的异步操作,在定义时进行声明即可,开箱即用。
Stream
Stream 是一系列异步事件的序列。其类似于一个异步的 Iterable,不同的是当你向 Iterable 获取下一个事件时它会立即给你,但是 Stream 则不会立即给你而是在它准备好时告诉你。Stream 提供一个异步的数据序列。数据序列包括用户生成的事件和从文件读取的数据。你可以使用 Stream API 中的 listen() 方法和 await for 关键字来处理一个 Stream。当出现错误时,Stream 提供一种处理错误的方式。Stream 有两种类型:Single-Subscription 和 Broadcast
安利
为了减少学习成本,这里推荐一下Kraken,使用它就是做了个桥接层,让我们使用前端技术进行Flutter开发了。
第四步:实战
、
因为Flutter本就是支持多运用的,因此本次案例选择Chrome来运行查看效果,这样可以快速的运行并查看效果。Flutter是支持热更新的,但是在第一次使用VScode开发Flutter时,有可能VScode可能配置并没有开启,因此需要再设置中进行配置并重载,如图1,再次运行后就可以支持Flutter的热更新了。
开始码代码,下面是lib下main.dart的代码我在下面进行注释,大致了解一下这里每一个方法的作用
import 'package:flutter/material.dart';
void main() { // main函数就是入口函数
runApp(const MyApp()); // MyApp 这里就是下面创建的根组件,runApp就是flutter程序入口,传入的Widget即是我们需要显示的界面Widget,widget就类似于前端的组件
}
class MyApp extends StatelessWidget { // MyApp继承了无状态的Widget,表示是纯UI组件
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) { // build 就相当于Render函数
return MaterialApp( // 表述整个页面的布局
title: 'Flutter Demo', // 这里就是APP名称
theme: ThemeData( // 使用主题
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue, // 主题颜色是蓝色
),
home: const MyHomePage(title: 'Frist Flutter!'), // 内容区域 这里定义了一个MyHomePage的类传参为title 这一块就是图1里面的内容
);
}
}
class MyHomePage extends StatefulWidget { // 创建MyHomePageWidget,继承与有状态Widget
const MyHomePage({super.key, required this.title}); // 接收参数 required表示必传,
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState(); // 创建一个state进行管理计数器部分
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() { // 定义一个方法
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold( // 表示是一个容器
appBar: AppBar( // 这里创建一个头部,如图3
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center( // 这里表述body是一个用居中容器包裹的,只能接受一个组件,放在child上
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column( // 创建一行,可以接受多个组件,放在children中
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center, // 纵向排列方式,这里使用的是居中
children: <Widget>[ // 表示这个数组是一个组件数组
const Text( // 这是创建一个文本
'You have pushed the button this many times:',
),
Text( //
'$_counter', // 使用$ 接受一个变量
style: Theme.of(context).textTheme.headlineMedium, // 设置文本样式
),
],
),
),
floatingActionButton: FloatingActionButton( // 创建一个浮动的按钮
onPressed: _incrementCounter, // 相当于点击事件,
tooltip: 'Increment', // 长按提示内容,Web中效果是鼠标移入提示
child: const Icon(Icons.add), // 按钮接受一个组件 这里直接创建一个icon
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
在上面的代码中,可以看到flutter就是一个一个的组件堆积而成,基于函数式声明式的开发的有一定的开发基础都可以进行开发,这里面比较难得就是需要知道flutter都提供了那些组件以及组件都需要那些参数、作用都是什么,只要了解了这些flutter开发就不在有任何难度,上面提供了flutter的社区链接和开发文档链接,看完有助于开发。
了解完后,看到flutter基于函数式声明式的开发的,跟前端开始有一定差异,那动起手来,多加练习来适应这种开发形式。那就开始开发一些自己页面,只有动手才能加深自己印象,加快学习进度。
以下是本次练习的主要代码
主入口
import 'package:flutter/material.dart';
import 'package:flutter_news/constants/Constants.dart';
import 'events/ThemeEvent.dart';
import 'pages/HomePage.dart';
void main() => runApp(FlutterNews());
class FlutterNews extends StatefulWidget {
@override
_FlutterNewsState createState() => _FlutterNewsState();
}
class _FlutterNewsState extends State<FlutterNews> {
@override
void initState() {
super.initState();
Constants.eventBus.on<ThemeEvent>().listen((event) {
setState(() {
Constants.currentTheme = event.themeModel;
});
});
}
@override
Widget build(BuildContext context) {
var theme = Constants.currentTheme == Constants.dayTheme
? ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
)
: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.black,
);
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: theme,
home: HomePage(),
);
}
}
首页
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_news/constants/Constants.dart';
import 'package:flutter_news/events/ThemeEvent.dart';
import 'package:flutter_news/models/local/Channel.dart';
import 'package:flutter_news/widgets/Newslistwidget.dart';
import 'aboutpage.dart';
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
//首页面所有数据的容器
List<dynamic> newsData;
//初始化频道数据的容器
List<Channel> channels;
TabController _tabController;
@override
initState() {
super.initState();
_initChannelData();
}
@override
dispose() {
_tabController.dispose();
super.dispose();
}
//加载初始化json数据
_initChannelData() {
channels = List<Channel>();
Future<String> data =
DefaultAssetBundle.of(context).loadString("assets/config/channel.json");
data.then((String value) {
setState(() {
List<dynamic> data = json.decode(value);
_tabController = TabController(
vsync: this,
length: data.length,
);
data.forEach((tmp) {
channels.add(Channel.fromJson(tmp));
});
});
});
}
//初始化标题指示条
Widget _initChannelTitle() {
return TabBar(
controller: _tabController,
indicatorColor: Colors.blue[100],
tabs: channels.map((Channel channel) {
return Tab(
text: channel.channelName,
);
}).toList());
}
//初始化列表内容
Widget _initChannelList() {
return TabBarView(
controller: _tabController,
children: channels.map((Channel channel) {
return NewsListWidget(channel: channel);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: channels.length,
child: Scaffold(
appBar: AppBar(
leading: Icon(Icons.title),
title: Text(Strings.appTitle, style: TextStyle(color: Colors.white)),
bottom: _initChannelTitle(),
actions: <Widget>[
IconButton(
icon: Icon(Icons.assignment),
onPressed: (() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => AboutPage()),
);
}),
),
IconButton(
icon: Icon(Icons.autorenew),
onPressed: (() {
Constants.eventBus.fire(
Constants.currentTheme == Constants.dayTheme
? ThemeEvent(Constants.nightTheme)
: ThemeEvent(Constants.dayTheme));
}))
],
),
body: _initChannelList(),
),
);
}
@override
bool get wantKeepAlive => true;
}
列表页面
import 'package:flutter/material.dart';
import 'package:flutter_news/api/Apis.dart';
import 'package:flutter_news/events/BeanEvent.dart';
import 'package:flutter_news/constants/Constants.dart';
import 'package:flutter_news/models/local/Channel.dart';
import 'package:flutter_news/models/network/NewsList.dart';
import 'package:flutter_news/pages/NewsDetailPage.dart';
class NewsListWidget extends StatefulWidget {
final Channel channel;
NewsListWidget({Key key, this.channel}) : super(key: key);
_NewsListState createState() => _NewsListState();
}
class _NewsListState extends State<NewsListWidget>
with AutomaticKeepAliveClientMixin {
//当前页
int _page = 0;
//网络请求接口
API$Neteast _api;
//该频道下的所有新闻数据
List<News> _datas;
ScrollController _listController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_api = API$Neteast();
_datas = [];
_listController = ScrollController();
_listController.addListener(() {
var maxScroll = _listController.position.maxScrollExtent;
var pixels = _listController.position.pixels;
if (maxScroll == pixels) {
_page += 20;
_getNewsList();
}
});
Constants.eventBus.on<BeanEvent<NewsList>>().listen((event) {
if (widget.channel.channelId == event.id) {
setState(() {
NewsList data = event.data;
_datas.addAll(data.datas);
});
}
});
_getNewsList();
}
@override
void dispose() {
_listController.dispose();
super.dispose();
}
Future<Null> _pullToRefresh() async {
_page = 0;
_datas.clear();
_getNewsList();
return null;
}
_getNewsList() {
_api.getNewsList(
widget.channel.channelType, widget.channel.channelId, _page);
}
_onItemClick(int position, News data) {
if (data.url == null || data.url.isEmpty) {
Scaffold.of(context).showSnackBar(SnackBar(
content: new Text('缺少新闻链接'),
duration: Duration(seconds: 1),
));
} else {
Navigator.of(context).push(MaterialPageRoute(
builder: (ctx) => NewsDetailPage(
postId: data.postid,
url: data.url,
title: "",
)));
}
}
Widget _renderRow(int position) {
if (position.isOdd) return Divider();
final index = position ~/ 2;
News data = _datas[index];
return Card(
color: Colors.grey[250],
elevation: 5.0,
child: InkWell(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(data.imgsrc, fit: BoxFit.fitWidth),
Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
data.title,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: data.digest.isEmpty
? const EdgeInsets.all(0.0)
: const EdgeInsets.only(
left: 10.0, right: 10.0, bottom: 10.0),
child: Text(
data.digest,
style: TextStyle(
fontSize: 12.0,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
child: Text(
'时间:${data.ptime}',
style: TextStyle(
fontSize: 12.0,
),
),
),
Padding(
padding:
const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0),
child: Text(
'来源:${data.source}',
style: TextStyle(
fontSize: 12.0,
),
),
)
],
),
onTap: () {
_onItemClick(index, data);
},
),
);
}
@override
Widget build(BuildContext context) {
if (_datas == null || _datas.isEmpty) {
return Center(child: CircularProgressIndicator());
} else {
Widget listView = ListView.builder(
padding: EdgeInsets.all(10.0),
itemCount: _datas.length * 2,
itemBuilder: (context, i) => _renderRow(i),
controller: _listController,
);
return RefreshIndicator(child: listView, onRefresh: _pullToRefresh);
}
}
}
基于webView详情
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_news/constants/constants.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class NewsDetailPage extends StatefulWidget {
final String url;
final String title;
final String postId;
const NewsDetailPage({Key key, this.postId, this.url, this.title})
: super(key: key);
@override
State<StatefulWidget> createState() => NewsDetailPageState();
}
class NewsDetailPageState extends State<NewsDetailPage> {
bool loaded = false;
String detailDataStr;
final flutterWebViewPlugin = FlutterWebviewPlugin();
NewsDetailPageState({Key key});
@override
void initState() {
super.initState();
flutterWebViewPlugin.onStateChanged.listen((state) {
print("state: ${state.type}");
if (state.type == WebViewState.finishLoad) {
setState(() {
loaded = true;
});
}
});
}
@override
Widget build(BuildContext context) {
List<Widget> titleContent = [];
titleContent.add(Text(
widget.title == null || widget.title.isEmpty
? Strings.newsDetail
: widget.title,
style: TextStyle(color: Colors.white)));
if (!loaded) {
titleContent.add(CupertinoActivityIndicator());
}
titleContent.add(Container(width: 50.0));
return WebviewScaffold(
url: widget.url,
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: titleContent,
),
iconTheme: IconThemeData(color: Colors.white),
),
withZoom: false,
withLocalStorage: true,
withJavascript: true,
);
}
}
请求及数据
import 'package:flutter_news/constants/Constants.dart';
import 'package:http/http.dart' as http;
// 使用网上公共接口
class NetWork {
static bool _debug = true;
//网易新闻的host
static String NETEAST_HOST = "https://c.m.163.com/";
static String getHost(int type) {
switch (type) {
case Constants.TYPE_NET_NETEASE_NEWS:
return NETEAST_HOST;
default:
return '';
}
}
/* 基础GET请求 */
static Future<String> get(String url, {Map<String, String> params}) async {
if (params != null && params.isNotEmpty) {
StringBuffer sb = StringBuffer("?");
params.forEach((key, value) {
sb.write("$key" + "=" + "$value" + "&");
});
String paramStr = sb.toString();
paramStr = paramStr.substring(0, paramStr.length - 1);
url += paramStr;
}
http.Response res = await http.get(url, headers: getCommonHeader());
if (_debug) {
print('发起Get请求_____$url|________________${res.body}|');
}
return res.body;
}
/* 基础POST请求 */
static Future<String> post(String url, {Map<String, String> params}) async {
http.Response res =
await http.post(url, body: params, headers: getCommonHeader());
if (_debug) {
print(
'|发起Post请求|_______|$url|______|${params.toString()}|________|${res.body}|');
}
return res.body;
}
static Map<String, String> getCommonHeader() {
Map<String, String> header = Map();
header['User-Agent'] =
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36';
return header;
}
}