122
社区成员
发帖
与我相关
我的任务
分享 本次作业采用 Flutter 框架进行编码,以支持跨多平台(win / linux / ios / android / web),实现了在第一次结对作业中设计的原型并成功将 web 端程序部署于云服务器上。
|-- android // android 平台相关
|-- linux // linux 平台相关
|-- test // 测试
|-- web // web 平台相关
|-- assets // 静态资源
|-- lib // 代码
|
|-- entities // 实体类
| |-- athlete.dart // 运动员实体类
| |-- event.dart // 项目实体类
| |-- result.dart // 成绩实体类
|
|-- pages // 页面
| |-- athlete_page.dart // 运动员页面
| |-- learnmore_page.dart // 了解更多页面
| |-- main_page.dart // 主页面
| |-- rank_page.dart // 排名页面
| |-- schedule_page.dart // 赛程页面
|
|-- proxy // 代理
| |-- cors.py // 跨域代理
|
|-- utils // 工具
| |-- global.dart // 全局变量
| |-- infos.dart
| |-- util.dart
|
|-- widgets // 组件
| |-- event_item.dart // 项目组件
| |-- footer.dart // 底部组件
| |-- header.dart // 头部组件
| |-- intro_block.dart // 介绍组件
| |-- navigator.dart // 导航组件
| |-- result_item.dart // 成绩组件
|
|-- main.dart // 入口文件
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 15 | 20 |
| • Estimate | • 估计这个任务需要多少时间 | 15 | 20 |
| Development | 开发 | 1800 | 2000 |
| • Analysis | • 需求分析 (包括学习新技术) | 400 | 400 |
| • Design Spec | • 生成设计文档 | 120 | 120 |
| • Design Review | • 设计复审 | 60 | 60 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
| • Design | • 具体设计 | 180 | 180 |
| • Coding | • 具体编码 | 400 | 600 |
| • Code Review | • 代码复审 | 130 | 130 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 500 | 500 |
| Reporting | 报告 | 160 | 160 |
| • Test Repor | • 测试报告 | 120 | 120 |
| • Size Measurement | • 计算工作量 | 20 | 20 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 20 |
| 合计 | 2000 | 2180 |

主页由一个轮播图与一系列展示图片 / 各项目介绍构成,可以清晰地展示出赛事相关信息











原型设计时考虑了数据的实时性,因此程序将动态从网站后端上实时获取数据并进行解析和展示。
利用 Flutter 提供的 Scaffold 框架,我们将程序分为了以下两部分:
并由顶端的 AppBar 来控制下方的展示内容。我们没有使用 Flutter 默认提供的 AppBar,而是重写了一个具备动画效果、更符合项目需求的 AppBar,并在 lib/main.dart 中搭建起了整个页面的框架。具体页面则放置在 lib/pages 目录下。
对于各个页面,我们尽可能地抽取出了可以共用的组件(如 Header 展示页面标题与 Logo 等),并放置于 lib/widgets 下。
对于程序使用到的实体类,我们进行了封装,并提供了通过网络构造的函数,放置在 lib/entity 目录下。此外,一些全局变量 / 常用工具类的实现放置在 lib/utils 下。
在实现网络请求的过程中,我们发现程序在 web 端出现了跨域访问失败的问题,为此我们实现了一个反向代理服务器 (lib/proxy/cors.py)并部署于云服务器上,以此解决跨域问题。

服务器为该 web app 启用了两个端口

// lib/main.dart
import 'package:cooperating_task/pages/athlete_page.dart';
import 'package:cooperating_task/pages/main_page.dart';
import 'package:cooperating_task/pages/rank_page.dart';
import 'package:cooperating_task/pages/schedule_page.dart';
import 'package:cooperating_task/widgets/navigator.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const SePracticeApp());
}
class SePracticeApp extends StatefulWidget {
const SePracticeApp({super.key});
@override
State<SePracticeApp> createState() => _SePracticeAppState();
}
class _SePracticeAppState extends State<SePracticeApp> {
// 维护当前所在页面的状态
int curPage = 0;
List<Widget> pages = [
const MainPage(),
const RankPage(),
const SchedPage(),
const AthletePage(),
];
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '结对作业',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.white),
useMaterial3: true,
),
home: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(100),
// 使用自定义实现的 navigatorbar
child: NavigatorBar(action: (id) {
setState(() {
curPage = id;
});
}),
),
body: pages[curPage],
),
);
}
}
# lib/proxy/cors.py
import http.server
import http.client
import urllib.request
class ProxyHandler(http.server.BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header(
"Access-Control-Allow-Headers",
"*",
)
self.end_headers()
def do_GET(self):
cors_url = self.headers.get("corsurl")
print(cors_url)
if cors_url is None:
self.send_response(400)
self.end_headers()
self.wfile.write(b"Missing corsurl header")
return
try:
response = urllib.request.urlopen(cors_url)
self.send_response(response.status)
resp = response.read()
for header, value in response.getheaders():
self.send_header(header, value)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS",
)
self.send_header(
"Access-Control-Allow-Headers",
"*",
)
self.end_headers()
self.wfile.write(resp)
except Exception as e:
print("代理请求出错:", e)
self.send_response(500)
self.end_headers()
self.wfile.write("代理请求出错: {}".format(e).encode())
if __name__ == "__main__":
host = "localhost"
port = 4041
server = http.server.HTTPServer((host, port), ProxyHandler)
server.serve_forever()
// lib/pages/schedule_page.dart
import 'package:cooperating_task/utils/global.dart';
import 'package:cooperating_task/widgets/event_item.dart';
import 'package:cooperating_task/widgets/header.dart';
import 'package:flutter/material.dart';
import 'package:cooperating_task/entities/event.dart';
class SchedPage extends StatefulWidget {
const SchedPage({super.key});
@override
State<StatefulWidget> createState() => SchedPageState();
}
class SchedPageState extends State<SchedPage> {
late String date;
late Future<(List<Event>, List<String>)> eventsAndDates;
late String selectedOption;
@override
// 初始化 widget 状态
void initState() {
super.initState();
date = "ALL";
selectedOption = "ALL";
// 初始化 future
eventsAndDates = Event.fromNetwork(
"https://api.worldaquatics.com/fina/competitions/2969/schedule?discipline=")
.then((value) {
value.sort((a, b) {
var res = b.date.compareTo(a.date);
return res == 0 ? a.time.compareTo(b.time) : res;
});
Set<String> dates = {};
for (var e in value) {
dates.add(e.date);
}
return (value, dates.toList());
});
}
// 构建日期选择组件
Widget buildDateSelector(List<String> dates) {
dates = ["ALL", ...dates];
// 构造 drop menu,用以选择日期
var dropMenu = DropdownButton<String>(
value: selectedOption,
items: <DropdownMenuItem<String>>[
const DropdownMenuItem(
value: 'ALL',
child: Text('ALL'),
),
for (var date in dates.sublist(4))
DropdownMenuItem(
value: date,
child: Text(date),
)
],
onChanged: (String? value) {
setState(
() {
selectedOption = value!;
if (value != "ALL") {
date = value;
}
},
);
},
hint: const Text('Select an option'),
style: Theme.of(context).textTheme.titleLarge,
);// drop menu
return Padding(
padding: const EdgeInsets.all(40.0),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 构造日期选择列表
for (var date in dates.sublist(0, 4))
TextButton(
onPressed: () {
setState(() {
this.date = date;
});
},
child: Text(
date,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: Colors.grey.shade700),
),
),
Row(
children: [
Text(
"下拉选择日期 : ",
style: Theme.of(context).textTheme.titleLarge,
),
dropMenu,
],
),
],
),
const Divider(
color: Global.mainThemeBlue,
)
],
),
);
} // build dateselector
// 构建日程表组件
Widget buildSchedList(List<Event> events) {
String curdate = "";
List<Widget> displays = [];
// 对于每个事件
for (var e in events) {
if (e.date != date && date != "ALL") {
continue;
}
// 筛选是否符合日期并添加进列表
if (e.date != curdate) {
displays.add(const Divider());
displays.add(Row(
children: [
Text(
e.date,
style: Theme.of(context).textTheme.displaySmall,
)
],
));
curdate = e.date;
}
displays.add(
EventItem(
event: e,
),
);
}
displays.removeAt(0);
displays.add(const Divider());
// 返回构建好的列表
return Column(
children: displays
.map((e) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
child: e,
))
.toList());
}
@override
// 构建页面
Widget build(BuildContext context) {
// 使用 single child scrollview 作为 parent,使得页面可以滚动
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Header(context: context, title: "Daily Competition Schedule"),
// future builder 用于在网络请求结束前展示进度条
FutureBuilder(
future: eventsAndDates,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("Error: ${snapshot.error.toString()}");
} else if (snapshot.connectionState != ConnectionState.done) {
return SizedBox.fromSize(
size: const Size.fromHeight(50),
child: const Center(child: CircularProgressIndicator()));
} else {
// 网络请求成功,展示日期选择组件与日程表
return Column(
children: [
buildDateSelector(snapshot.data!.$2),
buildSchedList(snapshot.data!.$1),
],
);
}
},
),
],
),
);
}
}
在原型设计完成后不久,我们便开始进行编程方面的实现。由于之前已经有了一些Flutter基础也动手开发过相关案例,于是我们选择使用Flutter作为开发技术,同时Flutter在跨平台移动应用开发方面有很大优势,并且能够提供良好的用户体验。
与我的队友一起进行结对编程是非常有意义的经历。
首先,通过结对编程,我们能够相互促进、相互学习。小陈没有flutter相关基础,在帮助她上手实现的过程,也是对我自身原有知识的考验,同时也促进了队友间的沟通交流,加快学习进程。
其次,正如“众人拾柴火焰高”,两个人的协作实现了1+1>2的效果,思维碰撞会带来更多的灵感和创意。在开发的过程中,我们遇到了不少问题,像…….两个人一起思考,是我获得原来没有的灵感,从而更快更好地解决问题。
通过本次作业,不仅巩固了之前学习的技术,也使我对于学习和开发有了更深的感受。
本次作业的内容量和难度均很有挑战。在原型设计后,我们便进行了讨论。由于小廖有过flutter基础,于是我们选择了flutter来开发我们的网站。
与我的队友一起进行结对编程是非常有意义的经历。我原先并没有flutter的开发基础,在确定工具后,我便开始去进行相关的了解和学习,在这一过程中,小廖提供了很大的帮助,他基于原有的知识,帮助我更快更好地上手。遇到问题时,我们一起讨论,共同思考,极大促进了开发效率,同时增强了搜索和解决问题的能力。此外,两个人的合作在隐形中提高了我们的自觉程度和积极性,同时我们提前规划、学会了注意把握进度,逐渐培养良好的开发习惯。
不同于以前开发的yii博客系统,本次作业复杂很多,涉及很多以前没有接触过的操作包括配置部署等等,通过本次作业也学会了一些新的知识。
小陈同学有着非常好的心态,虽然我们在完成作业的过程中有着不少困难,但她总是十分积极乐观,并积极解决困难,很大程度上助推了我们的进度。遇到问题时,小陈同学也有及时沟通,我们之间的合作因此非常良好透明。这两次结对作业,让我不仅学到了技术,更学到了团队合作的重要性。感谢小陈同学的帮助,让我在这次作业中有了很大的收获。
cx to lwt
小廖是一个非常可靠的队友,在之前就有相关的开发基础,这也使得我们的结对作业完成的不那么艰难。在本次作业中,跟着小廖学到了很多新的知识,受益良多。同时,小廖的执行力强,效率高,使我们的任务完成过程保持在一个张弛有度的状态,避免了ddl战士的拖拉情况;态度积极,善于发现和解决问题,也让我感受到了学习知识该有的状态。