本文简单介绍一下旦夕的整体项目结构。
[TOC]
文件夹旁边的中文注释解释了这个文件夹下代码的作用。
一些目录结构可能随项目结构调整而变化,不过大体上区别不大,
lib 整个旦夕源码部分的根目录。
├─common 一些整个项目中会用到的东西,例如图标、应用名、下载链接、主题色、API KEY 等等
├─feature 一些 Feature 的定义。Feature 是旦夕应用内的概念,每个 Feature 可在首页被显示成一项功能。
├─generated 一些由 intl 等插件生成的国际化支持文件,你不应当手动修改这些文件。如果需要,请修改 l10n 目录下的翻译文件。
│ └─intl
├─l10n 翻译文件,包含多种语言。
├─model 一些用到的模型类,如帖子、回复、公告、消息等信息模型。.g.dart 结尾的文件由 json_serializable 生成,你也不应该手动修改。
│ ├─curriculum
│ └─opentreehole
├─page 所有页面的定义。
│ ├─curriculum
│ ├─dashboard
│ ├─opentreehole
│ └─settings
├─provider 一些类似于维护全局变量的类。其中一部分继承了 ChangeNotifier 并在 main.dart 被作为 Provider 挂到了布局树中。
├─repository 负责完成一切网络请求的类。
│ ├─app
│ ├─curriculum
│ ├─fdu
│ └─opentreehole
├─test 用于本地测试的类。你可以直接在其中编写测试代码,调试模式下会在旦夕启动后自动执行,但不要把这些新代码上传到仓库中!
├─util 一些工具类,功能很多很杂。
│ ├─bmob
│ │ └─bmob
│ │ ├─realtime
│ │ ├─response
│ │ ├─table
│ │ └─type
│ ├─builder
│ ├─io
│ ├─js
│ ├─opentreehole
│ ├─scroller_fix
│ └─win32
└─widget 应用中所用到的各种自定义控件,如逻辑复杂的对话框、帖子和回复、课程表、Tag 控件等等。
├─dialogs
├─feature_item
├─libraries
├─opentreehole
│ ├─render
│ └─tag_selector
│ └─flutter_tagging
└─time_table
- 所有 .g.dart 结尾的文件都是自动生成的,你不应该手动修改。如果希望重新生成这些文件,你只需要在根目录下执行
flutter pub run build_runner build
。
如果希望尽快开始,选择一个简单的类、学习一下其中代码逻辑总是好的。
我们在
main.dart
中添加了非常详细的注释。你可以通读这份源代码以迅速了解这一 Flutter 的启动方式和工作流程的大致情况。
相关的说明在 main.dart
的 Note: A Checklist After Creating a New Page
中。
提示
关于页面里的网络请求,我们摸索出了一套完整的逻辑。通过这些模式,你可以更轻松地编写网络请求和它们的异常处理。
一个易懂的例子可以看:
page/dashboard/announcement_notices.dart
,你只需要注意它是如何初始化网络请求(在initState
中)和通过网络请求构建界面(在build
中)的!
注意
如果你是要编写一个显示在首页 Tab 的子页面(也就是和首页、树洞、课表、设置同级别的),你应当有更多的了解。
page/subpage_dashboard.dart
是我们能给出的最简单的例子了。注意看这个文件里两个类:HomeSubpage
和HomeSubpageState
分别继承了什么!
我们以宿舍电量功能为案例,在
lib/feature/dorm_electricity_feature.dart
与lib/repository/fdu/dorm_repository.dart
中添加了非常详细的注释。它们只有 100 行左右。你可以通过通读这两份源代码来迅速了解如何创建新的首页功能。
你通常需要先在 repository
下编写对应的网络请求代码。详见 创建一个新网络请求。
然后,你需要创建一个新的 Feature。首先可以查看一个简单的示例,比如 feature/dorm_electricity_feature.dart
,它是一个功能完整的、带网络请求的 Feature。
另外,Feature 还需要一些额外的声明,这方面的说明在 feature/base_feature.dart
的 Note: A Checklist After Creating a New [Feature]
中。
你可能需要实现一些新的网络请求逻辑。这时,你需要在 repository
下编写对应的网络请求代码。
首先,找到你想实现的网络请求对应的类。例如,如果与 ehall 有关,你应当在 repository/fdu/ehall_repository.dart
中编写。
一个通常的请求方法像这样:
/// Load exam scores of all semesters
///
/// Compared to [EduServiceRepository]'s method of the same name,
/// this method doesn't require a Fudan LAN environment.
///
/// NOTE: Result's [type] is year + semester(e.g. "2020-2021 2"),
/// and [id] doesn't contain the last 2 digits.
Future<List<ExamScore>?> loadAllExamScore(PersonInfo? info) =>
UISLoginTool.tryAsyncWithAuth(
dio!, LOGIN_URL, cookieJar!, info, () => _loadAllExamScore());
Future<List<ExamScore>?> _loadAllExamScore() async {
Response r = await dio!.get(SCORE_DETAIL_URL);
BeautifulSoup soup = BeautifulSoup(r.data.toString());
dom.Element tableBody = soup.find("tbody")!.element!;
return tableBody
.getElementsByTagName("tr")
.map((e) => ExamScore.fromDataCenterHtml(e))
.toList();
}
几个需要注意的地方:
- 由于网络请求都是异步方法,返回值一定是
Future<XXX>
。 - 每个方法,你总是需要编写两次:带下划线
_
的私有方法和不带下划线的公用方法。前者实现网络请求,后者则简单地用Retrier
或UISLoginTool.tryAsyncWithAuth
等方法包装前者,以完成必要的登录和网络重试操作。 - 返回值总是可空类型(也即带问号
?
),如果你发现有人没带问号,请务必顺手加上。这是为了在请求失败、抛出异常后,异常处理方法(通常是一个catchError
)可以不必返回任何值(或者说,返回null
)。如果不是可空类型,这些异常处理方法就会被要求返回一个相同类型的、不是null
的值(在上例中,所有关于它的异常处理方法就不得不返回一个不是null
的List<ExamScore>
,否则就会引发类型转换错误:Null is not a subtype of List<ExamScore>
。),这使得编写一个通用的异常处理操作变得不大可能。
提示
注意到上例的公有方法没有
async
关键字了吗?这不是编写错误。相比于返回了
List<ExamScore>?
对象的_loadAllExamScore
方法,loadAllExamScore
本身就返回了一个类型为Future<List<ExamScore>?>
的对象,因此它自己并不是异步方法,当然也就不用加异步关键字。