Flutter 简易新闻项目

目标

使用flutter快速开发 Android 和 iOS 的简易的新闻客户端
API使用的是 showapi(易源数据) 加载热门微信文章

效果对比

Android iOS
image
image
image
image
image
image

简介

这是一个建议的新闻客户端 页面非常简单

通过网络请求加载 分类数据 和 分类详情数据 (key都在代码里了,轻量使用~)

UI上几乎是没有任何特点

使用BottomNavigationBar 分成3个控制器

首页使用DefaultTabController管理内容

相关依赖:

http: "^0.11.3"                   #网络请求
cached_network_image: "^0.4.1"    #图片加载
cupertino_icons: ^0.1.2           #icon
flutter_webview_plugin: ^0.1.6    #webview
shared_preferences: ^0.4.2        #持久化数据
url_launcher: ^3.0.3              #调用系统浏览器

代码

使用单例来保存数据

由于分类原则上是没有变化的,我这里就使用单例来保存从API请求的分类数据,减少请求次数(API请求次数有限)

class UserSinglen {
   List<WeType> allTypes = [];
  static final UserSinglen _singleton = new UserSinglen._internal();
  factory UserSinglen() {
    return _singleton;
  }
  UserSinglen._internal();
}

使用Shared保存数据

保存当前选中的分类

class Shared {
  //保存分类
  static Future saveSelectedType(List<String> list) async {
    SharedPreferences pre = await SharedPreferences.getInstance();
    await pre.setStringList("selectedTypeIds",list);
    print(list);
    return;
  }

  //获取已选择的分类
  static Future<List<String>> getSelectedType() async {
    SharedPreferences pre = await SharedPreferences.getInstance();
    try {
      List<String> typeIds = pre.getStringList("selectedTypeIds");
      print("typeids = $typeIds");
      return typeIds;
    } catch (e) {
      return null;
    }
  }
}

BottomNavigationBar的使用

构建NavigationIcon

为底部的icon封装,方便找到对应的控制器

class NavigationIcon {
  NavigationIcon({
    Widget icon,
    Widget title,
    TickerProvider vsync,
  })  : item = new BottomNavigationBarItem(
          icon: icon,
          title: title,
        ),
        controller = new AnimationController(
          duration: kThemeAnimationDuration,
          vsync: vsync,
        );

  final BottomNavigationBarItem item;
  final AnimationController controller;
}

构建当前控制器

当前控制器是Stateful类型,刷新页面
初始化3个控制器

class Index extends StatefulWidget {
    const Index({ Key key }) : super(key: key);

  @override
  State<StatefulWidget> createState() => new IndexState();
}

class IndexState extends State<Index> with TickerProviderStateMixin {
  List<NavigationIcon> navigationIcons;
  List<StatefulWidget> pageList;
  int currentPageIndex = 0;
  StatefulWidget currentWidget;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    navigationIcons = <NavigationIcon>[
      new NavigationIcon(
        icon: new Icon(Icons.home),
        title: new Text("首页"),
        vsync: this,
      ),
      new NavigationIcon(
        icon: new Icon(Icons.category),
        title: new Text("分类"),
        vsync: this,
      ),
      new NavigationIcon(
        icon: new Icon(Icons.info),
        title: new Text("关于"),
        vsync: this,
      )
    ];

    pageList = <StatefulWidget>[
      new Home(key: new Key("home"),),
      new Category(key: new Key("category"),),
      new About(),
    ];

    for(NavigationIcon view in navigationIcons) {
      view.controller.addListener(rebuild());
    }

    currentWidget = pageList[currentPageIndex];
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    for (NavigationIcon view in navigationIcons) {
      view.controller.dispose();
    }
  }

rebuild(){
  print("rebuild");
  setState(() {});
}

  @override
  Widget build(BuildContext context) {
    final BottomNavigationBar bottomNavigationBar = new BottomNavigationBar(
      items: navigationIcons.map(((i) => i.item)).toList(),
      currentIndex: currentPageIndex,
      fixedColor: Config.mainColor,
      type: BottomNavigationBarType.fixed,
      onTap: (i) {
        setState(() {
          navigationIcons[i].controller.reverse();
          currentPageIndex = i;
          navigationIcons[i].controller.forward();
          currentWidget = pageList[i];
        });
      },
    );

    return new MaterialApp(
      theme: Config.themeData,
      home: new Scaffold(
        body: Center(
          child: currentWidget,
        ),
        bottomNavigationBar: bottomNavigationBar,
      ),
    );
  }
}

首页

首页实时获取存储在本地的已选择分类,与单例中的所有分类做对比,获取对应的类型id
(shared_preferences只能存储基本数据类型)

class Home extends StatefulWidget {
  const Home({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    // TODO: implement createState
    return new HomeState();
  }
}

class HomeState extends State<Home> {
  bool _isloading = true;
  List<WeType> _list;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    API.featchTypeListData((List<WeType> callback) {
      Shared.getSelectedType().then((onValue) {
        print(onValue);
        if (onValue != null) {
          print("lentch = ");
          print(onValue.length);
          setState(() {
            _isloading = false;
            _list = callback.where((t) => onValue.contains(t.id)).toList();
          });
        }
      });
    }, errorback: (error) {
      print("error:$error");
    });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return _isloading
        ? new Scaffold(
            appBar: new AppBar(
              title: new Text("loading..."),
            ),
            body: new Center(
              child: new CircularProgressIndicator(
                backgroundColor: Colors.black,
              ),
            ),
          )
        : new DefaultTabController(
            length: _list.length,
            child: new Scaffold(
                appBar: new AppBar(
                  title: new Text("新闻"),
                  bottom: new TabBar(
                      isScrollable: true,
                      labelColor: Colors.white,
                      unselectedLabelColor: Colors.black,
                      tabs: _list.map((f) => new Tab(text: f.name)).toList()),
                ),
                body: new TabBarView(
                  children:
                      _list.map((f) => new Content(channelId: f.id)).toList(),
                )),
          );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
  }
}

分类

这个页面也很简单,将已选择的分类id存进shared_preferences中就行

class Category extends StatefulWidget {
  const Category({Key key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => new CategoryState();
}

class CategoryState extends State<Category> {
  List<WeType> _list = [];
  bool _isloading = true;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    API.featchTypeListData((List<WeType> callback) {
      Shared.getSelectedType().then((onValue) {
        setState(() {
          _isloading = false;
          _list = callback
              .map((f) => new WeType(f.id, f.name, onValue.contains(f.id)))
              .toList();
        });
      });
    }, errorback: (error) {
      print("error:$error");
    });
  }

  _onTapButton(WeType type) {
    setState(() {
      _list = _list.map((WeType f) {
        if (f.id == type.id) {
          f.isSelected = !f.isSelected;
        }
        return f;
      }).toList();

      Shared.saveSelectedType(
          _list.where((t) => t.isSelected).map((f) => f.id).toList());
    });
  }

  _selectAll() {
    print("all");
    var r = _list.where((t) => t.isSelected).toList();
    bool res = r.length < _list.length ? true : false;

    setState(() {
      _list = _list.map((f) => new WeType(f.id, f.name, res)).toList();
      Shared.saveSelectedType(
          _list.where((t) => t.isSelected).map((f) => f.id).toList());
    });
  }

  @override
  Widget build(BuildContext context) {
    return _isloading
        ? CircularProgressIndicator()
        : new Scaffold(
            appBar: new AppBar(
              title: new Text("分类"),
              actions: <Widget>[
                new FlatButton(
                  onPressed: _selectAll,
                  child: new Center(
                      child: new Text(
                    "全选",
                    style: new TextStyle(color: Colors.white),
                  )),
                )
              ],
            ),
            body: new GridView.count(
              // Create a grid with 2 columns. If you change the scrollDirection to
              // horizontal, this would produce 2 rows.
              crossAxisCount: 3,
              crossAxisSpacing: 0.0,
              childAspectRatio: 2.0,
              // Generate 100 Widgets that display their index in the List
              children: _list
                  .map((f) => new FlatButton(
                        padding: const EdgeInsets.all(10.0),
                        onPressed: () {
                          _onTapButton(f);
                        },
                        child: new Center(child: _Button(f.name, f.isSelected)),
                      ))
                  .toList(),
            ));
  }
}

class _Button extends StatelessWidget {
  final String title;
  final bool isSelected;
  _Button(this.title, this.isSelected);


  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        new Container(
          child: new Center(
            child: new Text(title),
          ),
          decoration: new BoxDecoration(
            color:  isSelected ? Colors.blue[200] : Colors.white,
            borderRadius: new BorderRadius.all(
              const Radius.circular(20.0),
            ),
            border: new Border.all(
                color: isSelected ? Colors.white : Colors.blue[100],//边框颜色
                width: 1.0,//边框宽度
              ),
          ),
        ),
        new Container(
          child: new Center(
            child: isSelected
                ? new Icon(
                    Icons.check,
                    color: Colors.white,
                  )
                : null,
          ),

        ),
      ],
    );

  }
}

代码地址

Flutter-news
欢迎点赞

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容