flutter生成源代码_【开源】Flutter Demo 构建一个简单的APP - 活着

本文档展示了如何使用Flutter创建一个生命倒计时应用,记录并计算剩余生命时间。应用利用SharedPreferences存储用户数据,使用ScopedModel进行状态管理,包含时间选择器功能,并通过Grid组件展示时间格子。
摘要由CSDN通过智能技术生成

本帖最后由 zhouzaihang 于 2020-3-7 17:40 编辑

Github 源码链接 https://github.com/zhouzaihang/life_countdown 如果可以的话,在GitHub给个star啊

https://github.com/zhouzaihang/life_countdown

案例介绍起初我们来到这个世界, 是因为我们不得不来, 最后我们离开这个世界, 是因为我们不得不走. 出生和死亡都是我们没有把握的事情, 但是我们能把握的是从出生到死亡这段时间. 中国目前的人均寿命是 76.25 年, 算上程序员通宵透支的生命, 大约是 75 年, 也就是 30 * 30 个月. 本案例是用 Flutter 记录并计算生命的剩余时间:

项目地址: https://github.com/zhouzaihang/life_countdown

实现思路页面分为三块, 第一部分显示时间, 第二部分显示时间格子, 第三部分显示剩余天数

在第一部分右边设置一个按钮, 点击时弹出时间选择器更改出生时间

代码编写

首先新建 life.dart , 编写第一个类 Life. 这个类主要有三个功能:根据生日自动和当前时间计算逝去的生命和剩余的岁月

使用 SharedPreferences 存储数据到本地, 方便下次打开应用的时候自动从本地读取数据

使用 ScopedModel 作为状态管理, 在生日修改后, 更新所有相关的组件import 'package:scoped_model/scoped_model.dart';

import 'package:shared_preferences/shared_preferences.dart';

class Life extends Model {

int life = 900;

String _key;

DateTime _birthDay = DateTime(2000, 1, 1);

DateTime get birthDay => _birthDay;

set birthDay(DateTime value) {

_birthDay = value;

_setBirthday();

notifyListeners();

}

static int dateDifference(DateTime date1, DateTime date2) {

int result = date1.day - date2.day >= 0 ? 0 : -1;

return (date1.year - date2.year) * 12 + date1.month - date2.month + result;

}

Life(String key) {

this._key = key;

_initBirthday();

}

int pastLife() {

return dateDifference(DateTime.now(), this._birthDay);

}

int remainingLife() {

return life - pastLife();

}

Future _initBirthday() async {

final SharedPreferences prefs = await SharedPreferences.getInstance();

String birth = prefs.getString(_key) ?? null;

if (birth != null) {

this._birthDay = DateTime.parse(birth);

}

notifyListeners();

}

Future _setBirthday() async {

final SharedPreferences prefs = await SharedPreferences.getInstance();

return prefs.setString(_key, _birthDay.toString());

}

}构建首页

首先分析下首页的页面可以使用 Material 的 Scaffold 脚手架组件构建, 第一部分使用 APPBar, 第二三部分作为 body 部分. 另外在上面构建life 类的时候, 用到了 ScopedModel 进行状态管理. 所以这里在所有组件的根节点上先创一个 ScopedModel 组件, 使得后面的所有组件都能够直接通过 model 内的 life 对象且, 当对应内容修改时, 能够自动更新视图. 关于状态管理组件 ScopedModel 介绍参考这篇文章

和官方文档.具体实现代码如下:import 'package:flutter/material.dart';

import 'package:life_countdown/life.dart';

import 'package:scoped_model/scoped_model.dart';

void main() {

runApp(ScopedModel(

child: MyApp(),

model: Life("me"),

));

}

class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: 'Life Countdown',

theme: ThemeData(

primaryColor: Colors.white,

),

home: HomePage(),

);

}

}

class HomePage extends StatefulWidget {

HomePage({Key key}) : super(key: key);

@override

_HomePageState createState() => _HomePageState();

}

class _HomePageState extends State {

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(),

body: Padding(),

);

}

}构建 AppBar

第一部分主要是一个居中的日期和一个按钮:appBar: AppBar(

title: Text(

DateTime.now().toString().substring(0, 10),

style: TextStyle(

fontSize: 32,

fontWeight: FontWeight.bold,

fontFamily: "Barriecito"),

),

centerTitle: true,

elevation: 0,

actions: [

IconButton(

icon: Icon(Icons.settings),

onPressed: () {

showDefaultYearPicker(context, ScopedModel.of(context));

})

],

),这里的字体是自定义的, Flutter 使用自定义字体的方法:将文件放在项目目录下, 例如项目的 asset 下:

2. 在 pubspec.yaml 下编写资源名字和相对路径:flutter:

assets:

- asset/iron_man.png

#  - images/a_dot_ham.jpeg

# An image asset can refer to one or more resolution-specific "variants", see

# https://flutter.dev/assets-and-images/#resolution-aware.

# For details regarding adding assets from package dependencies, see

# https://flutter.dev/assets-and-images/#from-packages

# To add custom fonts to your application, add a fonts section here,

# in this "flutter" section. Each entry in this list should have a

# "family" key with the font family name, and a "fonts" key with a

# list giving the asset and other descriptors for the font. For

# example:

fonts:

- family: Barriecito

fonts:

# https://fonts.google.com/specimen/Barriecito?selection.family=Barriecito

- asset: asset/Barriecito-Regular.ttf3. 在相应的地方使用 fontFamily 调用:

Text(

"zhzh.xyz",

style: TextStyle(

fontWeight: FontWeight.bold,

fontFamily: "Barriecito"),

),添加时间选择器

第一部分右边的按钮是可以弹出时间选择器进行生日设置并保存且更新视图, Flutter 内置了时间选择器, 可以使用 showDatePicker 构建, 完整代码如下:void showDefaultYearPicker(BuildContext context, Life life) async {

final DateTime dateTime = await showDatePicker(

context: context,

initialDate: life.birthDay,

firstDate: DateTime(1950, 1),

lastDate: DateTime.now(),

builder: (BuildContext context, Widget child) {

return Theme(

data: ThemeData.dark(),

child: child,

);

},

);

if (dateTime != null && dateTime != life.birthDay) {

life.birthDay = dateTime;

}

}构建 body

Body 由第二部分: 一个网格和第三部分: 一个表示剩余时间的 Text 组件

网格组件

新建 grid.dart 文件, 然后创建一个有状态组件 Grid, 把 life 作为传入组件的必要参数:import 'dart:math';

import 'package:flutter/material.dart';

import 'package:life_countdown/life.dart';

class Grid extends StatefulWidget {

Grid({Key key, @required this.life}) : super(key: key);

final Life life;

@override

_GridState createState() => _GridState();

}

class _GridState extends State {

@override

Widget build(BuildContext context) {

return Column(

children: rowList(),

);

}

}设置一个 List 用于存放网格的随机颜色:static List colors = [

0xFFFFFFFF,

0xAAD4DFE6,

0xAA8EC0E4,

0xAACADBE9,

0xAA6AAFE6,

0xAAA5DFF9,

0xAAFEEE7D,

0xAAFAB1CE,

0xAAFFDA8E

];网格是由一个一个小格子组成, 这里使用 Border 组件实现. 还需要定义一个根据对应的位置生成 Border 的方法:static BorderSide _borderThin =

new BorderSide(width: 0.5, color: Color(0xFFE0E3DA));

static BorderSide _borderBold =

new BorderSide(width: 1.0, color: Color(0xFFE0E3DA));

Border generateBorder(int x, int y) {

BorderSide leftBorder = _borderThin;

BorderSide topBorder = _borderThin;

BorderSide rightBorder = _borderThin;

BorderSide bottomBorder = _borderThin;

if (x == 0) {

topBorder = _borderBold;

} else if (x == this.life.life / 30 - 1) {

bottomBorder = _borderBold;

}

if (y == 0) {

leftBorder = _borderBold;

} else if (y == 29) {

rightBorder = _borderBold;

}

return new Border(

left: leftBorder,

top: topBorder,

right: rightBorder,

bottom: bottomBorder,

);

}网格可以看成是由 30 行组成, 每一行有 30 个 border 组件, 先定义生成一行的方法:List rowDetail(rowIndex) {

List result = new List();

int pastMonth = widget.life.pastLife();

int month = rowIndex * 30;

for (int i = 0; i < 30; i++) {

int currentMonth = month + i;

result.add(Container(

width: width,

height: width,

decoration: BoxDecoration(

border: widget.generateBorder(rowIndex, i),

color: currentMonth < pastMonth

? Color(0xFF5E5E5F)

: currentMonth == pastMonth

? Colors.red

: currentMonth < pastMonth + 10

? Colors.white

: Color(Grid.colors[

rng.nextInt(18) > 8 ? 0 : rng.nextInt(9)])),

));

}

return result;

}再定义生成所有行的代码:List rowList() {

List result = new List();

for (int i = 0; i < widget.life.life / 30; i++) {

result.add(Row(

children: rowDetail(i),

));

}

return result;

}最后把生成的所有行使用 Column 组件包起来就OK了, 最后完整的 grid.dart 如下:import 'dart:math';

import 'package:flutter/material.dart';

import 'package:life_countdown/life.dart';

class Grid extends StatefulWidget {

Grid({Key key, @required this.life}) : super(key: key);

final Life life;

static List colors = [

0xFFFFFFFF,

0xAAD4DFE6,

0xAA8EC0E4,

0xAACADBE9,

0xAA6AAFE6,

0xAAA5DFF9,

0xAAFEEE7D,

0xAAFAB1CE,

0xAAFFDA8E

];

static BorderSide _borderThin =

new BorderSide(width: 0.5, color: Color(0xFFE0E3DA));

static BorderSide _borderBold =

new BorderSide(width: 1.0, color: Color(0xFFE0E3DA));

Border generateBorder(int x, int y) {

BorderSide leftBorder = _borderThin;

BorderSide topBorder = _borderThin;

BorderSide rightBorder = _borderThin;

BorderSide bottomBorder = _borderThin;

if (x == 0) {

topBorder = _borderBold;

} else if (x == this.life.life / 30 - 1) {

bottomBorder = _borderBold;

}

if (y == 0) {

leftBorder = _borderBold;

} else if (y == 29) {

rightBorder = _borderBold;

}

return new Border(

left: leftBorder,

top: topBorder,

right: rightBorder,

bottom: bottomBorder,

);

}

@override

_GridState createState() => _GridState();

}

class _GridState extends State {

@override

Widget build(BuildContext context) {

final width = (MediaQuery.of(context).size.width - 2) / 30;

var rng = new Random();

List rowDetail(rowIndex) {

List result = new List();

int pastMonth = widget.life.pastLife();

int month = rowIndex * 30;

for (int i = 0; i < 30; i++) {

int currentMonth = month + i;

result.add(Container(

width: width,

height: width,

decoration: BoxDecoration(

border: widget.generateBorder(rowIndex, i),

color: currentMonth < pastMonth

? Color(0xFF5E5E5F)

: currentMonth == pastMonth

? Colors.red

: currentMonth < pastMonth + 10

? Colors.white

: Color(Grid.colors[

rng.nextInt(18) > 8 ? 0 : rng.nextInt(9)])),

));

}

return result;

}

List rowList() {

List result = new List();

for (int i = 0; i < widget.life.life / 30; i++) {

result.add(Row(

children: rowDetail(i),

));

}

return result;

}

return Column(

children: rowList(),

);

}

}然后在 main.dart 里引用 grid.dart 代码, 并在调用 Text 组件实现第三部分功能:ScopedModelDescendant(

builder: (BuildContext context, Widget child, Life model) {

return Column(

children: [

Grid(life: model),

Padding(

padding: const EdgeInsets.only(top: 40, bottom: 20),

child: Row(

mainAxisAlignment: MainAxisAlignment.center,

children: [

Padding(

padding: EdgeInsets.only(right: 40),

child: Container(

height: 48,

child: Image.asset("asset/iron_man.png")),

),

Text(

model.remainingLife().toString() +

" / " +

model.life.toString(),

style: TextStyle(

fontSize: 48, fontFamily: "Barriecito")),

],

),

),

Text(

"Stay Hungry \t Stay Foolish",

style: TextStyle(

fontFamily: "Barriecito",

fontWeight: FontWeight.bold,

fontSize: 24),

),

],

);

},

),最后完整的 main.dart 文件的内容如下:import 'package:flutter/material.dart';

import 'package:life_countdown/grid.dart';

import 'package:life_countdown/life.dart';

import 'package:scoped_model/scoped_model.dart';

void main() {

runApp(ScopedModel(

child: MyApp(),

model: Life("me"),

));

}

class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: 'Life Countdown',

theme: ThemeData(

primaryColor: Colors.white,

),

home: HomePage(),

);

}

}

class HomePage extends StatefulWidget {

HomePage({Key key}) : super(key: key);

@override

_HomePageState createState() => _HomePageState();

}

class _HomePageState extends State {

void showDefaultYearPicker(BuildContext context, Life life) async {

final DateTime dateTime = await showDatePicker(

context: context,

initialDate: life.birthDay,

firstDate: DateTime(1950, 1),

lastDate: DateTime.now(),

builder: (BuildContext context, Widget child) {

return Theme(

data: ThemeData.dark(),

child: child,

);

},

);

if (dateTime != null && dateTime != life.birthDay) {

life.birthDay = dateTime;

}

}

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(

DateTime.now().toString().substring(0, 10),

style: TextStyle(

fontSize: 32,

fontWeight: FontWeight.bold,

fontFamily: "Barriecito"),

),

centerTitle: true,

elevation: 0,

actions: [

IconButton(

icon: Icon(Icons.settings),

onPressed: () {

showDefaultYearPicker(context, ScopedModel.of(context));

})

],

),

body: Padding(

padding: const EdgeInsets.all(1.0),

child: ScopedModelDescendant(

builder: (BuildContext context, Widget child, Life model) {

return Column(

children: [

Grid(life: model),

Padding(

padding: const EdgeInsets.only(top: 40, bottom: 20),

child: Row(

mainAxisAlignment: MainAxisAlignment.center,

children: [

Padding(

padding: EdgeInsets.only(right: 40),

child: Container(

height: 48,

child: Image.asset("asset/iron_man.png")),

),

Text(

model.remainingLife().toString() +

" / " +

model.life.toString(),

style: TextStyle(

fontSize: 48, fontFamily: "Barriecito")),

],

),

),

Text(

"Stay Hungry \t Stay Foolish",

style: TextStyle(

fontFamily: "Barriecito",

fontWeight: FontWeight.bold,

fontSize: 24),

),

],

);

},

),

),

);

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值