结对第二次作业——编程实现

182000214廖文焘 2024-03-26 15:04:08

结对第二次作业——编程实现

目录

  • 结对第二次作业——编程实现
  • 作业描述与目录
  • 描述
  • 目录结构
  • gitcode 地址
  • PSP 表格
  • 服务器访问链接
  • 成品展示
  • 主页
  • 时间表
  • 运动员信息
  • 国家奖牌 Rank
  • 了解更多
  • 过程讨论
  • 分工
  • git 使用的协商
  • CORS 问题
  • 博客撰写
  • 实现细节与功能图
  • 实现细节
  • 数据源
  • 程序结构与文件管理
  • CORS 的解决方式
  • 功能结构图
  • 关键代码
  • 程序的整体结构
  • CORS 代理服务器
  • 页面实现(以 Schedule Page 为例)
  • 心得与评价
  • lwt
  • cx
  • lwt -> cx
  • cx -> lwt

作业描述与目录

描述

​ 本次作业采用 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                 // 入口文件

gitcode 地址

clareses / pair-project

PSP 表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划1520
• Estimate• 估计这个任务需要多少时间1520
Development开发18002000
• Analysis• 需求分析 (包括学习新技术)400400
• Design Spec• 生成设计文档120120
• Design Review• 设计复审6060
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)1010
• Design• 具体设计180180
• Coding• 具体编码400600
• Code Review• 代码复审130130
• Test• 测试(自我测试,修改代码,提交修改)500500
Reporting报告160160
• Test Repor• 测试报告120120
• Size Measurement• 计算工作量2020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划2020
合计20002180

服务器访问链接

cooperating_task

成品展示

主页

img

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

时间表

img

  • 采用与官方类似的布局设计,可以通过日期筛选出想查看的日程表
  • 每个条目在点击后才会请求服务器以获取详细信息
  • 详细信息展示了国别 / 运动员信息 / 分数等信息

运动员信息

img

  • 同样使用点击后从官网动态加载数据的方式,减少不必要的网络开销
  • 使用列表直观地展示运动员信息,包括姓名 / 国别 / 生日 / 性别

国家奖牌 Rank

img

  • 国家奖牌榜,实现与运动员信息大致相同,清晰明了地展示了各国获奖牌数量

了解更多

img

过程讨论

分工

img


img

git 使用的协商

img

CORS 问题

img


img


img

博客撰写

img

实现细节与功能图

实现细节

数据源

​ 原型设计时考虑了数据的实时性,因此程序将动态从网站后端上实时获取数据并进行解析和展示。

程序结构与文件管理

​ 利用 Flutter 提供的 Scaffold 框架,我们将程序分为了以下两部分:

  • 程序顶端的 AppBar
  • 下方的展示内容

​ 并由顶端的 AppBar 来控制下方的展示内容。我们没有使用 Flutter 默认提供的 AppBar,而是重写了一个具备动画效果、更符合项目需求的 AppBar,并在 lib/main.dart 中搭建起了整个页面的框架。具体页面则放置在 lib/pages 目录下。

​ 对于各个页面,我们尽可能地抽取出了可以共用的组件(如 Header 展示页面标题与 Logo 等),并放置于 lib/widgets 下。

​ 对于程序使用到的实体类,我们进行了封装,并提供了通过网络构造的函数,放置在 lib/entity 目录下。此外,一些全局变量 / 常用工具类的实现放置在 lib/utils 下。

CORS 的解决方式

​ 在实现网络请求的过程中,我们发现程序在 web 端出现了跨域访问失败的问题,为此我们实现了一个反向代理服务器 (lib/proxy/cors.py)并部署于云服务器上,以此解决跨域问题。

img

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

  • port 8000 是一个简单的 HTTP 文件服务器,将请求的页面返回
  • port 4041 是为了解决跨域访问问题而设计的代理服务器

功能结构图

img

关键代码

程序的整体结构

// 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],
      ),
    );
  }
}

CORS 代理服务器

# 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()

页面实现(以 Schedule Page 为例)

// 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),
                  ],
                );
              }
            },
          ),
        ],
      ),
    );
  }
}

心得与评价

lwt

在原型设计完成后不久,我们便开始进行编程方面的实现。由于之前已经有了一些Flutter基础也动手开发过相关案例,于是我们选择使用Flutter作为开发技术,同时Flutter在跨平台移动应用开发方面有很大优势,并且能够提供良好的用户体验。

与我的队友一起进行结对编程是非常有意义的经历。

首先,通过结对编程,我们能够相互促进、相互学习。小陈没有flutter相关基础,在帮助她上手实现的过程,也是对我自身原有知识的考验,同时也促进了队友间的沟通交流,加快学习进程。

其次,正如“众人拾柴火焰高”,两个人的协作实现了1+1>2的效果,思维碰撞会带来更多的灵感和创意。在开发的过程中,我们遇到了不少问题,像…….两个人一起思考,是我获得原来没有的灵感,从而更快更好地解决问题。

通过本次作业,不仅巩固了之前学习的技术,也使我对于学习和开发有了更深的感受。

cx

本次作业的内容量和难度均很有挑战。在原型设计后,我们便进行了讨论。由于小廖有过flutter基础,于是我们选择了flutter来开发我们的网站。

与我的队友一起进行结对编程是非常有意义的经历。我原先并没有flutter的开发基础,在确定工具后,我便开始去进行相关的了解和学习,在这一过程中,小廖提供了很大的帮助,他基于原有的知识,帮助我更快更好地上手。遇到问题时,我们一起讨论,共同思考,极大促进了开发效率,同时增强了搜索和解决问题的能力。此外,两个人的合作在隐形中提高了我们的自觉程度和积极性,同时我们提前规划、学会了注意把握进度,逐渐培养良好的开发习惯。

不同于以前开发的yii博客系统,本次作业复杂很多,涉及很多以前没有接触过的操作包括配置部署等等,通过本次作业也学会了一些新的知识。

lwt -> cx

小陈同学有着非常好的心态,虽然我们在完成作业的过程中有着不少困难,但她总是十分积极乐观,并积极解决困难,很大程度上助推了我们的进度。遇到问题时,小陈同学也有及时沟通,我们之间的合作因此非常良好透明。这两次结对作业,让我不仅学到了技术,更学到了团队合作的重要性。感谢小陈同学的帮助,让我在这次作业中有了很大的收获。

cx -> lwt

cx to lwt

小廖是一个非常可靠的队友,在之前就有相关的开发基础,这也使得我们的结对作业完成的不那么艰难。在本次作业中,跟着小廖学到了很多新的知识,受益良多。同时,小廖的执行力强,效率高,使我们的任务完成过程保持在一个张弛有度的状态,避免了ddl战士的拖拉情况;态度积极,善于发现和解决问题,也让我感受到了学习知识该有的状态。

...全文
69 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧