Make A Real One

做出一个真正的flutter应用吧,至少能用到些什么!

App-1
App-1
App-2
App-2
通过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