简介:这是一份基于Java+MVC架构的完整毕业设计源码包,主要面向计算机相关专业学生,用于完成天气预报与穿衣搭配APP的课题设计。项目包含PC端后台管理系统与安卓手机客户端,覆盖地区管理、用户登录、天气发布与查询、穿衣指数提示、虚拟形象搭配等模块,并附带MySQL数据库脚本;开发环境兼容MyEclipse/Eclipse/Idea和AndroidStudio,导入即可运行和继续扩展。压缩包共424个文件,类型包括93个Java源文件、86个XML配置与数据文件、31个JSP页面、50个class编译文件,以及Android布局图片、GIF动图等资源,整体大小约2.73MB,目录结构清晰,便于按模块学习。内容呈现服务端与客户端的三层分离实现、XML和JSON通信示例,管理员发布各地天气、用户按所在地区查询的完整流程;结合实体ER设计中的地区、用户属性,可帮助读者快速理解MVC分层思路与Android界面适配方法。已有266人学习,适合毕业设计参考、课程设计练习及新手入门移动端后台开发。
1. 基于Java+MVC开发的天气预报穿衣搭配APP:带PC端和安卓端,到底值不值得做
毕设季最怕两类题:只有概念没有数据的PPT题,以及技术点太散、双端加网络加解析揉在一起没人指路的翻车题。基于Java+MVC开发的天气预报穿衣搭配APP,带PC端和安卓Android端,分量恰好卡在中间:核心只有天气数据获取和穿衣推荐两条线,MVC又把界面与数据切开,PC端Swing和安卓端能共用同一套Model与Controller。它不考算法,却把网络、JSON解析、线程、事件驱动全串了一遍,答辩有架构可讲、有代码可演示,适合有Java基础、想同时做双端界面的读者。
这类题能不能一周做完,取决于前两天有没有立住架构。架构对了,后面只是换皮;架构散了,PC端跑通,安卓端就得重推。下文按我做双端项目的顺序展开。
2. MVC三层先立住:天气数据、推荐规则和界面怎么各干各的
很多人拿到这类题目第一反应是写一个WeatherUtil工具类,把请求、解析、推荐、弹窗全塞进去。PC端能跑,安卓端一移植就发现到处是Swing的类,改都没法改。先花半天把MVC的边界画清楚,后面两天基本就是复制粘贴。
2.1 先定Model字段:WeatherData里该有哪些属性,为什么体感温度是主角
Model层我一般放两个类:WeatherData保存从接口拿到的原始天气,DressAdvice保存推荐结果。字段名不要随手起,先想清楚穿衣推荐到底需要哪些输入,再回头对照接口返回。
public class WeatherData { private String city; // 城市名 private int temp; // 实时气温(℃) private int feelsLike; // 体感温度(℃),穿衣推荐的主依据 private int humidity; // 相对湿度(0~100) private String windDir; // 风向,如"北风" private String windScale; // 风力等级,如"3级" private String condition; // 天气现象,如"晴""小雨""小雪" public WeatherData() {} // 全参构造方法与各字段 getter/setter 省略,IDE 一键生成即可 }字段名的选取原则是:跟第三方天气接口返回的JSON字段尽量对齐。多数免费天气接口都会给出实时温度、体感温度、湿度、风向、风力等级、天气现象这六类信息,只是命名各家不一样,有的叫temp,有的叫temperature,有的是数字、有的是带单位的字符串。我在Model里用自己统一的命名,解析那一步再做映射,Model本身不感知接口差异。
public class DressAdvice { private String top; // 推荐上衣 private String bottom; // 推荐下装 private String extra; // 附加提醒:围巾、雨伞、防风…… public DressAdvice(String top, String bottom, String extra) { this.top = top; this.bottom = bottom; this.extra = extra; } // getter 省略 /** * 穿衣推荐规则:按体感温度从低温向高温分段, * 再用天气现象和风力做兜底增强。 */ public static DressAdvice recommend(WeatherData w) { int t = w.getFeelsLike(); String top, bottom, extra = ""; if (t <= 0) { top = "羽绒服"; bottom = "加绒裤"; extra = "毛衣+围巾"; } else if (t <= 10) { top = "厚呢大衣"; bottom = "厚长裤"; extra = "可加薄毛衣"; } else if (t <= 18) { top = "夹克/风衣"; bottom = "长裤"; extra = "早晚加薄外套"; } else if (t <= 25) { top = "卫衣/长袖"; bottom = "休闲裤"; extra = ""; } else { top = "短袖T恤"; bottom = "短裤/薄裙"; extra = "注意防晒"; } // 雨雪天强制提醒带伞,风力过大追加防风提示 if (w.getCondition().contains("雨") || w.getCondition().contains("雪")) { extra = extra.isEmpty() ? "记得带伞" : extra + ",记得带伞"; } if (w.getWindScale().startsWith("5") || w.getWindScale().startsWith("6")) { extra = extra.isEmpty() ? "风大注意防风" : extra + ",风大注意防风"; } return new DressAdvice(top, bottom, extra); } }这条规则本质上是一张阈值表,分段判断比写一堆散落的if更直观,答辩时也讲得清楚:
| 体感温度区间 | 推荐上衣 | 推荐下装 | 附加提醒 |
|---|---|---|---|
| ≤0℃ | 羽绒服 | 加绒裤 | 毛衣+围巾 |
| 1~10℃ | 厚呢大衣 | 厚长裤 | 可加薄毛衣 |
| 11~18℃ | 夹克/风衣 | 长裤 | 早晚加薄外套 |
| 19~25℃ | 卫衣/长袖 | 休闲裤 | — |
| >25℃ | 短袖T恤 | 短裤/薄裙 | 注意防晒 |
这里有三个容易踩的细节。第一,判断顺序必须从低温往高温走,否则0℃会被后面<=10的分支漏掉或误吞。第二,contains("雨")同时覆盖"小雨""中雨""阵雨",比精确匹配省心。第三,用体感温度而不是实时温度做主依据——同样5℃的气温,刮风和不刮风完全是两套穿衣方案,这是很多人做到后面才补的认知。
2.2 Controller层职责:IWeatherSource接口把"数据从哪来"和"业务怎么算"解耦
Controller层最常见的错误是每个方法里直接new URL(...)发请求,状态、超时、错误处理散落各处。我一般先定义一个数据源接口,让"数据从哪来"这件事变成可以替换的插座。
public interface IWeatherSource { WeatherData fetch(String city) throws Exception; // 按城市名获取实时天气 }public class HttpWeatherSource implements IWeatherSource { private String apiBase; // 接口模板,如 http://某地址/weather?city=%s private int timeoutMs; // 连接和读取超时 public HttpWeatherSource(String apiBase, int timeoutMs) { this.apiBase = apiBase; this.timeoutMs = timeoutMs; } @Override public WeatherData fetch(String city) throws Exception { String url = String.format(apiBase, city); String json = doGet(url, timeoutMs); // 内部用HttpURLConnection,样板代码省略 JSONObject obj = new JSONObject(json); WeatherData d = new WeatherData(); // 字段名按实际接口返回对齐,这里以统一命名示例 d.setCity(obj.getString("city")); d.setTemp(obj.getInt("temp")); d.setFeelsLike(obj.getInt("feelsLike")); d.setHumidity(obj.getInt("humidity")); d.setWindDir(obj.getString("windDir")); d.setWindScale(obj.getString("windScale")); d.setCondition(obj.getString("condition")); return d; } }JSON解析用org.json就够,安卓自带这个库,PC端引一个jar也能用,没必要为了简单字段上重量级框架。doGet里注意两点:连接超时和读取超时要分开设置,很多接口慢在读取而不是连接;读流的时候要按UTF-8读,不然城市名或天气现象会乱码。
public class WeatherController { private final ExecutorService pool = Executors.newSingleThreadExecutor(); private final IWeatherSource source; // 数据源,可替换为Mock private final MainView view; // 界面抽象 private final UiExecutor uiExecutor; // Swing和Android的"回主线程"封装 public WeatherController(IWeatherSource source, MainView view, UiExecutor uiExecutor) { this.source = source; this.view = view; this.uiExecutor = uiExecutor; } public void refresh(String city) { view.showLoading(true); pool.execute(() -> { try { WeatherData data = source.fetch(city); DressAdvice advice = DressAdvice.recommend(data); uiExecutor.post(() -> { view.showWeather(data); view.showAdvice(advice); view.showLoading(false); }); } catch (Exception e) { uiExecutor.post(() -> view.showError("查询失败:" + e.getMessage())); } }); } }这个Controller是整套双端工程的心脏。网络请求放进线程池,不让它阻塞UI;刷新界面通过UiExecutor回到UI线程,Swing和Android各自实现这个接口即可。newSingleThreadExecutor对一次一个查询足够了,如果以后想并发查多个城市,换成newFixedThreadPool(2)就行,不用改其他代码。
注意:Controller不直接依赖Swing或Android的任何类,这是双端复用能成立的根基。答辩时老师问MVC的"解耦"体现在哪,指这条链路讲就行。
2.3 View层只做两件事:显示数据和转发点击,Swing和安卓换皮不换芯
View层需要一个抽象接口,两个端各自实现。接口不要定义太多方法,够用就好。
public interface MainView { void showWeather(WeatherData data); // 刷新温度、体感、天气现象 void showAdvice(DressAdvice advice); // 刷新穿搭推荐文字 void showLoading(boolean loading); // 控制查询按钮/进度条 void showError(String message); // 错误弹窗或Toast }Swing端用JFrame实现,安卓端用Activity实现,两边各自管自己的控件。View还承担一件事:把用户点击查询的动作转成controller.refresh(city)调用,仅此而已。这样看,双端共用的东西其实占了整个工程六成以上——Model、Controller、数据源、推荐规则全是同一批Java代码,只有最外层的控件壳子不同。这就是这类题目的性价比所在。
3. PC端用Swing跑通最小闭环:建工程、接接口、点按钮刷新
先把PC端跑通,安卓端才有可以照抄的"芯"。PC端我选择Swing而不是JavaFX,原因很实在:Swing是JDK自带的,学生机上随便哪个版本都能编译运行,不会被环境问题卡在答辩前一天。
3.1 工程目录按职责分包:一个最小Java工程的初始化步骤
不要用IDE默认的包结构,先按职责把目录建好。命令行直接建:
mkdir -p WeatherDressApp/src/main/java/model mkdir -p WeatherDressApp/src/main/java/controller mkdir -p WeatherDressApp/src/main/java/view/swing mkdir -p WeatherDressApp/src/main/java/datasource建完的目录长这样:
WeatherDressApp/ ├── src/main/java/ │ ├── model/ # WeatherData、DressAdvice │ ├── controller/ # WeatherController │ ├── view/ # MainView接口、swing/MainFrame │ ├── datasource/ # IWeatherSource、HttpWeatherSource │ └── App.java # 入口:读配置,组装Controller和View两个包要特别说清楚。view下面是接口加各个端的实现,PC端只有swing/MainFrame一个类;datasource放数据源实现,以后Mock也放这里。依赖方向必须是View和DataSource指向Controller,Controller指向接口,谁都不许反向依赖。这一步做好了,后面安卓端就是加一个新包的事。
3.2 从Controller到View的刷新链路:UI线程调度的习惯从PC端就养成
血泪经验是:Swing的界面更新必须在事件分发线程(EDT)上做,否则界面卡顿、闪烁甚至直接抛异常。这个坑在PC端不显眼,因为它勉强能跑,但同样的代码搬到安卓就是ANR。所以从PC端开始就要用UiExecutor封装UI线程:
// SwingUiExecutor:把任务抛到事件分发线程 public class SwingUiExecutor implements UiExecutor { @Override public void post(Runnable r) { SwingUtilities.invokeLater(r); } }再实现MainView,把上一步接口里的四个方法落到控件上:
public class MainFrame extends JFrame implements MainView { private JTextField cityInput; private JButton queryButton; private JLabel cityLabel, tempLabel, condLabel; private JTextArea adviceArea; private WeatherController controller; public MainFrame() { setTitle("天气穿衣搭配 - PC端"); setSize(480, 420); setDefaultCloseOperation(EXIT_ON_CLOSE); // 布局代码略,放输入框、按钮、三个标签、一个文本域 controller = new WeatherController( new HttpWeatherSource("http://127.0.0.1:8080/weather?city=%s", 8000), this, new SwingUiExecutor()); queryButton.addActionListener(e -> controller.refresh(cityInput.getText().trim())); } @Override public void showWeather(WeatherData d) { cityLabel.setText(d.getCity()); tempLabel.setText(d.getTemp() + "℃ 体感" + d.getFeelsLike() + "℃"); condLabel.setText(d.getCondition() + " " + d.getWindDir() + d.getWindScale()); } @Override public void showAdvice(DressAdvice a) { adviceArea.setText("上衣:" + a.getTop() + "\n下装:" + a.getBottom() + "\n提醒:" + (a.getExtra().isEmpty() ? "无" : a.getExtra())); } @Override public void showLoading(boolean loading) { queryButton.setEnabled(!loading); } @Override public void showError(String message) { JOptionPane.showMessageDialog(this, message); } }这里的http://127.0.0.1:8080/weather?city=%s是本地调试用的地址,可以先用最简单的HTTP服务顶一下,也可以直接对接第三方接口。8000是超时毫秒数,按8秒起步,校园网环境偏慢就给到10秒。按钮监听里直接调controller.refresh,不做任何业务处理,业务全在Controller里,这句代码就是View分内的事。
3.3 配置文件放什么:API地址、默认城市、超时时间别写死在代码里
接口地址、超时、默认城市这类环境相关的值,写死在代码里是最常见的后悔药。换一台机器、换一个接口,就得翻源码改。我习惯放一个config.properties:
api.base=http://127.0.0.1:8080/weather?city=%s api.timeout=8000 default.city=某市读取代码放在入口处:
Properties props = new Properties(); try (InputStream in = new FileInputStream("config.properties")) { props.load(in); } String apiBase = props.getProperty("api.base"); int timeout = Integer.parseInt(props.getProperty("api.timeout", "8000")); String defaultCity = props.getProperty("default.city", "某市");这样做的两个直接好处:一是答辩现场想切换演示数据源,只改配置文件不用改代码;二是default.city防止输入框为空就去请求,空城市名在多数接口里会返回解析异常。配置读取用Properties是Java自带能力,不需要额外依赖,加载失败就抛异常,启动时快速暴露问题而非运行时才崩。
4. 安卓Android端移植:复用Model和Controller,只重写View层
PC端跑通后,安卓端的工作量比想象中小很多。核心的Model、Controller、IWeatherSource、DressAdvice.recommend全部原样拿过来,要新写的只有Activity、布局和权限配置。这一步做完,整个项目就从"会跑"变成"双端都有"。
4.1 Android工程先补两件事:INTERNET权限和明文HTTP开关
很多人在安卓端第一跑就翻车,不是代码问题,是清单文件少了两样东西。第一是网络权限,第二是明文HTTP开关。Android 9(API 28)开始默认禁止明文HTTP请求,而毕设阶段的接口大多是http而不是https,必须显式放开。
<uses-permission android:name="android.permission.INTERNET" /> <application android:usesCleartextTraffic="true" android:label="@string/app_name" android:theme="@style/Theme.WeatherDress"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>usesCleartextTraffic="true"是整个application级别放开明文,用于毕设足够。如果以后上生产,应该改成networkSecurityConfig只对指定域名放开,但那是后话。权限漏加的表现比较隐蔽,后面避坑章节会专门讲。
4.2 Activity即View:持有Controller,用runOnUiThread刷新控件
安卓端的Activity就是MainView的实现类。布局用一个输入框、一个查询按钮、几个TextView即可,重点看Activity怎么和Controller联动:
public class MainActivity extends AppCompatActivity implements MainView { private EditText etCity; private TextView tempView, adviceView; private WeatherController controller; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); etCity = findViewById(R.id.etCity); tempView = findViewById(R.id.tvTemp); adviceView = findViewById(R.id.tvAdvice); UiExecutor uiExecutor = this::runOnUiThread; controller = new WeatherController( new HttpWeatherSource("http://10.0.2.2:8080/weather?city=%s", 8000), this, uiExecutor); findViewById(R.id.btnQuery).setOnClickListener( v -> controller.refresh(etCity.getText().toString().trim())); } @Override public void showWeather(WeatherData d) { tempView.setText(d.getCity() + " " + d.getTemp() + "℃ 体感" + d.getFeelsLike() + "℃"); // 天气现象与风力可再加一个TextView,或合并到这一行显示 } @Override public void showAdvice(DressAdvice a) { adviceView.setText("上衣:" + a.getTop() + "\n下装:" + a.getBottom() + "\n提醒:" + (a.getExtra().isEmpty() ? "无" : a.getExtra())); } @Override public void showLoading(boolean loading) { findViewById(R.id.btnQuery).setEnabled(!loading); } @Override public void showError(String message) { Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } }this::runOnUiThread是方法引用,正好塞进UiExecutor接口,Android的UI线程调度就被封装掉了。网络请求仍然在Controller的线程池里跑,showWeather这些方法看起来是在主线程执行——实际上就是,因为uiExecutor.post把更新动作切回了主线程。
提示:别在Activity里自己写
new Thread(...).start()再runOnUiThread,那样每个方法都重复调度代码。把调度收敛到UiExecutor里,双端行为才一致。
4.3 RecyclerView展示穿搭卡片:文字和图片资源的绑定方法
如果只想快速交差,用TextView显示推荐文字完全够。但答辩现场,一个带图标的穿搭卡片列表比纯文字有说服力得多。给每件衣服备一张小图放到drawable,比如dress_top_down.png、dress_bottom_pants.png,然后用RecyclerView渲染:
public class DressAdapter extends RecyclerView.Adapter<DressAdapter.VH> { private final List<DressItem> items; public static class DressItem { public final String name; // 穿搭项名称,如"羽绒服" public final int iconRes; // 对应图片资源id public DressItem(String name, int iconRes) { this.name = name; this.iconRes = iconRes; } } public static class VH extends RecyclerView.ViewHolder { ImageView icon; TextView name; public VH(View itemView) { super(itemView); icon = itemView.findViewById(R.id.ivIcon); name = itemView.findViewById(R.id.tvName); } } public DressAdapter(List<DressItem> items) { this.items = items; } @Override public VH onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_dress, parent, false); return new VH(v); } @Override public void onBindViewHolder(VH holder, int position) { DressItem item = items.get(position); holder.icon.setImageResource(item.iconRes); holder.name.setText(item.name); } @Override public int getItemCount() { return items.size(); } }图片映射不要用getIdentifier去反射资源名,那是把错误拖到运行时的写法。在showAdvice里把推荐结果翻译成DressItem列表时,用switch或Map显式映射,比如"羽绒服"对应R.drawable.dress_coat_down,一张图对应一个字符串,清晰且不会在真机上悄悄崩。列表数据的刷新用adapter.notifyDataSetChanged()即可,毕设场景没有局部刷新的性能压力。
5. 双端联调避坑指南:五个翻车现场从模拟器到推荐边界
这套项目翻车最多的地方不在业务逻辑,而在环境、权限和线程这几件事。下面五条是我和周围人做同类双端项目时反复踩的坑,按"现象、原因、解决"记下来。
5.1 模拟器访问PC接口失败:10.0.2.2才是宿主机地址
现象:安卓模拟器里点查询,界面一直转圈或直接提示连接失败,Logcat里能看到Connection refused或SocketException。代码检查了好几遍,地址明明是对的。
原因:模拟器里的127.0.0.1指向模拟器自己,不是你的电脑。PC端接口跑在localhost上,模拟器要访问宿主机,必须用Android模拟器预留的特殊地址10.0.2.2。
解决:安卓端的API地址写http://10.0.2.2:8080/weather?city=%s,而不是127.0.0.1。真机调试时换成电脑在局域网里的IP,比如http://192.168.1.20:8080/...。另外确认电脑把8080端口对外放开,否则真机也会连不上。
5.2 明文HTTP被拦:usesCleartextTraffic与缺失权限一起排查
现象:模拟器上点查询,Logcat报CLEARTEXT communication to 10.0.2.2 not permitted;有时候报的是UnknownHostException,看起来像网络不通。
原因:Android 9开始默认禁止明文HTTP,http://请求直接被系统拦截,代码层面没有任何错误。另一个隐藏问题是忘了加INTERNET权限,表现是网络异常,却不是权限异常,误导性很强。
解决:先确认Manifest里同时有INTERNET权限和android:usesCleartextTraffic="true"。排错时看Logcat关键字:出现Cleartext就是明文开关问题,出现UnknownHost或Permission denied就查权限,两件事一起查一遍,不要只盯着一个改。
5.3 主线程网络请求:ANR与NetworkOnMainThreadException
现象:点击查询后,界面卡住几秒,然后弹"XX已停止运行"或"无响应",Logcat里出现NetworkOnMainThreadException。有时候没崩,但界面明显卡顿,点什么都延迟。
原因:在按钮点击事件里直接写HttpURLConnection请求。Android 9以后主线程发网络请求直接抛异常;更早版本不抛,表现为ANR。Swing端勉强能跑,安卓端是硬性禁止。
解决:所有网络操作必须离开UI线程。用第2章的WeatherController方案——线程池执行请求,UiExecutor.post回主线程更新控件。自己写也不要慌,new Thread(() -> { ... runOnUiThread(...); }).start()是能用的临时方案,只是调度代码散一点。记住一条底线:UI线程只更新控件,不跑网络、不解析大JSON。
5.4 JSON解析全0:字段名对不上,数据进不了Model
现象:界面能显示城市名,但温度是0、天气现象是空串;或者直接JSONException崩溃。接口在浏览器里打开明明有数据。
原因:接口返回的字段名和Model里的字段名对不上,比如接口叫weather而代码读的是condition;或者接口把温度返回成字符串"12",代码用getInt就崩。
解决:第一次接入接口时,先把原始JSON字符串打印出来看,别把解析当黑匣子。把字段列表抄下来,逐个对齐。对数字字段做一层安全读取,因为它可能是数字也可能是字符串:
// 字段可能是数字,也可能是字符串"12",统一安全读取 private static int readInt(JSONObject obj, String key) { Object v = obj.opt(key); if (v instanceof Integer) { return (Integer) v; } if (v instanceof String) { return Integer.parseInt((String) v); } return 0; // 字段缺失时返回0,不抛异常 }解析这件事最怕的就是"浏览器里正常,代码里全0",本质是字段映射问题。用opt系列方法而不是get系列,可以让缺失字段返回默认值而不是直接崩,对答辩演示更友好。
5.5 穿衣推荐"玄学":只看温度不看体感和天气状态的边界坑
现象:5℃大风天推荐"夹克/风衣",用户在室外冻得发抖;小雨天没提醒带伞;体感25℃但湿度95%的闷热天,还是推荐卫衣。推荐结果被身边人说"不准",感觉像玄学。
原因:规则只看实时温度temp,没考虑风、湿度、天气现象;判断顺序或边界写错,比如从高温往低温判断,0℃就会落进错误的分支;雨雪判断用了精确匹配而不是包含匹配。
解决:第一,主依据换成feelsLike体感温度,数据源里没有就按temp减风力修正估算。第二,先判断雨雪天气,再走温度分段,顺序不能反。第三,大风天追加防风提示,湿度高追加闷热提示。最后准备一组测试用例,五个场景各跑一遍:0℃小雪、5℃大风、10℃小雨、18℃雾、30℃晴,把推荐结果逐条核对,比现场拍脑袋调阈值靠谱。
6. 答辩演示的兜底技巧:内置Mock数据源,断网也能完整走通流程
6.1 MockWeatherSource:三个预设场景一键切换
答辩现场最不可控的因素是网络。第三方天气接口偶尔慢、偶尔限流,演示当场转圈是最尴尬的事。所以我从第2章那个IWeatherSource接口里留的伏笔,就在这里收网——做一个不联网的Mock数据源,效果等同"后悔药"。
public class MockWeatherSource implements IWeatherSource { private final WeatherData[] presets = new WeatherData[] { new WeatherData("某市", -2, -5, 70, "北风", "4级", "小雪"), new WeatherData("某市", 12, 10, 85, "东风", "2级", "小雨"), new WeatherData("某市", 28, 31, 60, "南风", "3级", "晴") }; private int index = 0; @Override public WeatherData fetch(String city) { WeatherData d = presets[index % presets.length]; index++; return d; // 每点一次查询切换一组数据,方便现场演示 } }切换数据源只需要改组装Controller那一行:
IWeatherSource source = isDemo ? new MockWeatherSource() : new HttpWeatherSource(apiBase, timeout); controller = new WeatherController(source, this, uiExecutor);isDemo在PC端可以从启动参数-Ddemo=true读,在安卓端可以做成一个常量或设置开关。演示时先用Mock把雪、雨、晴三个场景各点一遍,展示推荐规则对天气现象的响应;如果有网络,再切回真实接口补充演示一次。两个数据源走的是同一条Controller链路,这一点本身就是MVC解耦最好的现场证明。
我现在的习惯是:答辩前一天的最后一件事,不是调UI,而是把本机网络断开,用Mock把整个流程从头点一遍,确认双端都能完整走通;再联网跑一遍真实接口,确认解析没退化。这两遍走完,心里才有底。这个习惯帮我救回过好几次现场翻车,希望帮到你。
本文还有配套的精品资源,点击获取