软件工程2019 年 3 月 16 日
Flutter - Ep2
通过豆瓣电影api, 使用Flutter开发native应用。
Make A Real One
做出一个真正的flutter应用吧,至少能用到些什么!
通过
api
获取数据进行渲染,逻辑很简单,不过要挑战的却不少:flutter
如何发送网路请求- 请求完成后如何将数据渲染
render
到界面上 list
列表组件的使用input
如何使用,如何传递值
数据获取
数据来源
rsshub
- 一款rss源定制工具/**
* 网络
*/
- dio // 网络请求 「库」
- async await // 异步编程
……
数据组装
/**
* 数据格式
*/
- xml2json 「库」
- xml
- json
……
数据渲染
/**
* 显示组件
*/
- CardView
- ListView
……
代码
main.dart
/**
* @Author: henryhe
*/
import 'package:flutter/material.dart';
import 'dart:convert';
// english_words package
import 'package:english_words/english_words.dart';
// xml
import 'package:xml2json/xml2json.dart';
// dio
import 'package:dio/dio.dart';
Dio dio = new Dio();
// final api = 'https://rsshub.app/jandan/ooxx';
// dio get
Future<List<Item>> fetch(String api) async {
// final api = 'https://rsshub.app/weibo/keyword/flutter';
// var api = 'https://rsshub.app/instagram/user/henryhe';
// final api = 'https://rsshub.app/douban/movie/playing';
// final api = 'https://rsshub.app/jandan/ooxx';
// final api = 'https://rsshub.app/bbc/chinese';
List<Item> fetchedItem;
// dio load data
final res = await dio.get<String>(api);
final Xml2Json _parser = Xml2Json();
_parser.parse(res.data);
print('====GData rss=====');
print(_parser.toGData());
final resJsonData = _parser.toGData();
XOData _xodata = new XOData.parseJson(json.decode(resJsonData));
print('========xodata========');
// print(_xodata.version);
print('========xodata.child========');
print(_xodata.rss.channel.generator.tvalue);
fetchedItem = _xodata.rss.channel.items;
// TODO: json serialization https://juejin.im/post/5b5d782ae51d45191c7e7fb3
return fetchedItem;
// return List<Item>.from(_xodata.rss.channel.items);
}
class RSS {
final String version;
final Channel channel; // channel class
RSS({this.version, this.channel});
factory RSS.parseJson(Map<String, dynamic> json) {
return RSS(
version: json['version'], channel: Channel.parseJson(json['channel']));
}
}
class Channel {
final OBJCD title;
final OBJT link;
final OBJT generator;
final OBJT webMaster;
final OBJT language;
final OBJT lastBuildDate;
final OBJT ttl;
final List<Item> items; // item list
Channel(
{this.title,
this.link,
this.generator,
this.webMaster,
this.language,
this.lastBuildDate,
this.ttl,
this.items});
factory Channel.parseJson(Map<String, dynamic> json) {
var list = json['item'] as List;
List<Item> itemList = list.map((i) => Item.parseJson(i)).toList();
return Channel(
title: OBJCD.parseJson(json['title']),
link: OBJT.parseJson(json['link']),
generator: OBJT.parseJson(json['generator']),
webMaster: OBJT.parseJson(json['webMaster']),
language: OBJT.parseJson(json['language']),
lastBuildDate: OBJT.parseJson(json['lastBuildDate']),
ttl: OBJT.parseJson(json['ttl']),
// item: json['item'] // item list
items: itemList);
}
}
class Item {
final OBJCD title;
final OBJCD description;
final OBJT guid;
final OBJT link;
Item({this.title, this.description, this.guid, this.link});
factory Item.parseJson(Map<String, dynamic> json) {
return Item(
title: OBJCD.parseJson(json['title']),
description: OBJCD.parseJson(json['description']),
guid: OBJT.parseJson(json['description']),
link: OBJT.parseJson(json['link']));
}
}
class OBJT {
final String tvalue;
OBJT({this.tvalue});
factory OBJT.parseJson(Map<String, dynamic> json) {
return OBJT(
tvalue: json['\$t'],
);
}
}
class OBJCD {
final String cdvalue;
OBJCD({this.cdvalue});
factory OBJCD.parseJson(Map<String, dynamic> json) {
return OBJCD(
cdvalue: json['__cdata'],
);
}
}
class XOData {
final String version;
final String encoding;
final RSS rss;
XOData({this.version, this.encoding, this.rss});
factory XOData.parseJson(Map<String, dynamic> json) {
return XOData(
version: json['version'],
encoding: json['encoding'],
rss: RSS.parseJson(json['rss']));
}
}
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
// final word_pair = WordPair.random();
return new MaterialApp(
title: 'Henry.HE',
theme: new ThemeData(primaryColor: Colors.pink),
home: DefaultTabController(
length: 4,
child: Scaffold(
appBar: new PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: new Container(
color: Colors.pink,
child: new SafeArea(
child: Column(
children: <Widget>[
new Expanded(
child: new Container(),
),
TabBar(
tabs: <Widget>[
// Tab(icon: Icon(Icons.people)),
// Tab(icon: Icon(Icons.local_laundry_service)),
Tab(icon: Icon(Icons.hourglass_empty)),
Tab(icon: Icon(Icons.hot_tub))
],
),
],
),
),
),
// child: TabBar(
// tabs: <Widget>[
// Tab(icon: Icon(Icons.people)),
// Tab(icon: Icon(Icons.local_laundry_service)),
// Tab(icon: Icon(Icons.hourglass_empty)),
// Tab(icon: Icon(Icons.hot_tub))
// ],
// ),
// title: Text('RSSHub.HenryHE.')
// title: null`
),
body: TabBarView(
children: <Widget>[
// Tab(icon: Icon(Icons.people)),
// Tab(icon: Icon(Icons.hourglass_empty))
// RandomWords('https://rsshub.app/jandan/ooxx'),
// RandomWords('https://rsshub.app/jandan/pic'),
RandomWords('https://rsshub.app/weibo/keyword/flutter'),
RandomWords('https://rsshub.app/douban/movie/playing')
],
),
),
),
// home: RandomWords()
// home: Scaffold(
// appBar: AppBar(
// title: Text('Welcome to Flutter.HE')
// ),
// body: Center(
// child: FutureBuilder<XOData>(
// future: fetch(),
// builder: (context, snapshot) {
// if (snapshot.hasData) {
// return Text(snapshot.data.title);
// } else if (snapshot.hasError) {
// return Text("${snapshot.error}");
// }
// // By default, show a loading spinner
// return CircularProgressIndicator();
// },
// )
// )
// // body: Center(
// // child: RandomWords()
// // // child: Text(word_pair.asPascalCase)
// // )
// )
);
// return new MaterialApp(
// title: 'Flutter Demo',
// theme: new 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 press Run > Flutter Hot Reload in IntelliJ). Notice that the
// // counter didn't reset back to zero; the application is not restarted.
// primarySwatch: Colors.green,
// ),
// home: new MyHomePage(title: 'Flutter and Henry.HE'),
// );
}
}
class RandomWords extends StatefulWidget {
final String _api;
RandomWords(this._api);
@override
RandomWordsState createState() => new RandomWordsState(this._api);
}
// random word widget
class RandomWordsState extends State<RandomWords>
with AutomaticKeepAliveClientMixin {
String _api;
// TODO: method
RandomWordsState(this._api);
// final _suggestions = <WordPair>[];
var _suggestions;
List<Item> _allcontent = new List();
final TextEditingController _controller = new TextEditingController();
String searchvalue = '';
bool searchlock = false;
// final Set<WordPair> _saved = new Set<WordPair>();
final Set<Item> _saved = new Set<Item>();
final _biggerFont = const TextStyle(fontSize: 16.0, color: Colors.pink);
List<Item> _list;
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
@override
void initState() {
print('initState==============>');
super.initState();
// get data
foo();
}
foo() async {
_list = await fetch(this._api);
if (_list.length == 0) return;
setState(() {
searchlock = false;
});
print('==================foo==============');
print(_list.length);
setState(() {
_allcontent.addAll(_list);
_suggestions = _allcontent;
// _suggestions = null;
// _suggestions.addAll(_list.take(5));
print('success');
});
}
String getImage(String raw) {
if (this._api.contains('douban')) {
return raw.split('src=')[1].split('>')[0].split('\"')[1];
}
return raw.split('><')[0].split('src=')[1].split('\"')[1];
}
Widget _buildSuggestions() {
print('=========datasource==========');
// print(_suggestions);
// 带search
Widget searchRow = new Container(
padding: EdgeInsets.all(12.0),
child: new Row(
children: <Widget>[
new Expanded(
child: new TextField(
controller: _controller,
decoration: new InputDecoration(
hintText: 'search to weibo',
suffixIcon: new IconButton(
icon: new Icon(Icons.clear),
onPressed: () {
_controller.clear();
setState(() {
searchlock = false;
});
},
),
),
// keyboardAppearance: Brightness.dark,
onChanged: (e) {
// print(e);
setState(() {
searchvalue = e;
});
},
),
),
new IconButton(
icon: new Icon(
Icons.search,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// TODO: search operation
print('pressed search');
print(searchvalue);
searchlock = true;
this._api = 'https://rsshub.app/weibo/keyword/' + searchvalue;
setState(() {
_suggestions = null;
_allcontent = [];
});
print(this._api);
foo(); // update
},
)
],
));
if (_suggestions == null) {
if (this._api.contains('weibo')) {
return new Column(
children: searchlock
? <Widget>[
searchRow,
new Container(
child: new Center(
child: CircularProgressIndicator(),
))
]
: [
searchRow,
],
);
}
// foo();
return Center(child: CircularProgressIndicator());
}
var builder = ListView.builder(
padding: const EdgeInsets.all(12.0), // padding 16
itemCount: _suggestions == null ? 0 : _suggestions.length * 2,
itemBuilder: (context, i) {
// print(i);
if (i.isOdd) return Divider();
final index = i ~/ 2;
print(index);
// reach the bottom
if (index >= _suggestions.length - 2) {
print('==========build new =========');
// foo();
// reach the bottom
// generate 10 more
// _suggestions.addAll(generateWordPairs().take(10));
// _suggestions.addAll(_list.take(10));
// _suggestions = null;
foo();
}
return _buildRowDefine(_suggestions[index]);
// return _buildRow(_suggestions[index]);
// return _buildRowDefine(_suggestions[index]);
},
);
if (this._api.contains('weibo')) {
return new Column(
children: <Widget>[
searchRow,
new Expanded(
child: builder,
),
// builder
],
);
} else {
return builder;
}
}
// Widget _buildRow(WordPair pair) {
// final bool alreadySaved = _saved.contains(pair);
// return ListTile(
// title: Text(
// pair.asPascalCase,
// style: _biggerFont,
// ),
// trailing: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border,
// color: alreadySaved ? Colors.purple : null),
// onTap: () {
// setState(() {
// if (alreadySaved) {
// _saved.remove(pair);
// } else {
// _saved.add(pair);
// }
// });
// },
// );
// }
Widget _buildRowDefine(Item pair) {
final bool alreadySaved = _saved.contains(pair);
if (this._api.contains('douban')) {
// 煎蛋
return new Card(
child: new Column(
children: <Widget>[
new Container(
child: Text(
pair.title.cdvalue,
style: _biggerFont,
),
margin: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 5.0),
),
new Container(
child: new Image.network(
getImage(pair.description.cdvalue).toString(),
))
],
));
}
if (this._api.contains('jandan')) {
// 煎蛋
return new Card(
child: new Column(
children: <Widget>[
new Container(
child: Text(
pair.title.cdvalue,
style: _biggerFont,
),
margin: EdgeInsets.fromLTRB(0.0, 5.0, 0.0, 5.0),
),
new Container(
child: new Image.network(
getImage(pair.description.cdvalue).toString(),
))
],
));
}
return ListTile(
title: Text(
// pair.asPascalCase,
pair.title.cdvalue,
// getImage(pair.description.cdvalue),
style: _biggerFont,
),
trailing: new Icon(alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.pink : null),
onTap: () {
setState(() {
print('tap set');
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
print(_saved);
});
},
);
}
/// @desc: navaigate to saved list
void _pushSaved() {
Navigator.of(context)
.push(new MaterialPageRoute<void>(builder: (BuildContext context) {
print('-=========navi==========-');
// print(_saved);
final Iterable<ListTile> tiles = _saved.map((Item pair) {
return new ListTile(
title: new Text(pair.title.cdvalue, style: _biggerFont));
});
final List<Widget> divided =
ListTile.divideTiles(context: context, tiles: tiles).toList();
return new Scaffold(
appBar: AppBar(
title: const Text('Saved'),
),
body: new ListView(
children: divided,
),
);
}));
}
@override
Widget build(BuildContext context) {
// final wordPair = new WordPair.random();
// return Text(wordPair.asPascalCase);
// return Scaffold(
// appBar: AppBar(
// title: Text('N.Gen By Henry.HE'),
// actions: <Widget>[
// new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
// ],
// ),
// // body: new Image.network('http://wx4.sinaimg.cn/large/006r8Ylqly1fwiglnj9vwj30g40m8ar2.jpg'));
// body: _buildSuggestions());
return _buildSuggestions();
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// 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
_MyHomePageState createState() => new _MyHomePageState();
}
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 += 2;
});
}
@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 new Scaffold(
appBar: new AppBar(
// 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: new Text(widget.title),
),
body: new Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: new Column(
// Column is also 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 paint" (press "p" in the console where you ran
// "flutter run", or select "Toggle Debug Paint" from the Flutter tool
// window in IntelliJ) 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>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
注意
注:
api
获取的数据仅供学习使用,如果可以请自行搭建rsshub
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
作者: Henry He 发表日期:2019 年 3 月 16 日