阅读视图
《Flutter全栈开发实战指南:从零到高级》- 26 -持续集成与部署
Xcode 26还没有适配SceneDelegate的app建议尽早适配
Xcode 26之前不需要多窗口的很多app没有适配SceneDelegate,升级到Xcode 26后运行没有问题,但是控制台有以下输出:
`UIScene` lifecycle will soon be required. Failure to adopt will result in an assert in the future.
UIApplicationDelegate 中的相关生命周期函数也有弃用标记:
/// Tells the delegate that the application has become active
/// - Note: This method is not called if `UIScene` lifecycle has been adopted.
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);
建议尽早适配
方案举例
以下是我的适配方案,供大家参考
- 兼容iOS13以下版本;
- app只有单窗口场景。
1. 配置Info.plist
Delegate Class Name和Configuration Name 可自定义
2. 配置SceneDelegate
- 创建SceneDelegate class 类名要和Info.plist中配置一致
- appDelegate中实现代理
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){
// name要和Info.plist中配置一致
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions API_AVAILABLE(ios(13.0)){
// 释放资源,单窗口app不用关注
}
3. 新建单例 AppLifecycleHelper 实现AppDelegate和SceneDelgate共享的方法
- iOS 13 及以上需要在
scene: willConnectToSession: options:方法中创建Window,之前仍然在didFinishLaunchingWithOptions:
AppDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[AppLifecycleHelper sharedInstance].launchOptions = launchOptions;
// ... 自定义逻辑
if (@available(iOS 13, *)) {
} else {
[[AppLifecycleHelper sharedInstance] createKeyWindow];
}
}
SceneDelgate:
URL冷启动APP时不调用openURLContexts方法,这里保存URL在DidBecomeActive处理
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions API_AVAILABLE(ios(13.0)){
[[AppLifecycleHelper sharedInstance] createKeyWindowWithScene:(UIWindowScene *)scene];
// 通过url冷启动app,一般只有一个url
for (UIOpenURLContext *context **in** connectionOptions.URLContexts) {
NSURL *URL = context.URL;
if (URL && URL.absoluteString.length > 0) {
self.launchUrl = URL;
}
}
}
AppLifecycleHelper:
- (void)createKeyWindow {
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self setupMainWindow:window];
}
- (void)createKeyWindowWithScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0)) {
UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
[self setupMainWindow:window];
}
- (void)setupMainWindow:(UIWindow *)window {
}
- 实现SceneDelegate后appDelegate 中失效的方法
在AppLifecycleHelper中实现,共享给两个DelegateClass
- (void)applicationDidBecomeActive:(UIApplication *)application {
[[AppLifecycleHelper sharedInstance] appDidBecomeActive];
}
- (void)applicationWillResignActive:(UIApplication *)application {
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
}
/// URL Scheme
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {
}
/// 接力用户活动
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<**id**<UIUserActivityRestoring>> * _Nullable))restorationHandler {
}
/// 快捷方式点击
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler API_AVAILABLE(ios(9.0)) {
}
SceneDelegate部分代码示例:
- (void)sceneDidBecomeActive:(UIScene *)scene API_AVAILABLE(ios(13.0)){
[[AppLifecycleHelper sharedInstance] appDidBecomeActiveWithLaunchUrl:self.launchUrl];
// 清空冷启动时的url
self.launchUrl = nil;
}
这个方法总结下来就是求同存异,由Helper提供SceneDelegate与AppDelegate相同或类似的方法,适合单窗口、且支持iOS 13以下的app;
另外注意URL Scheme冷启动app不会执行openURL需要记录URL,在合适的时机(一般是DidBecomeActive)处理。
UIWindowScene 使用指南:掌握 iOS 多窗口架构
swift中的知识总结(一)
GetX 状态管理实践
下面内容只关注 GetxController / GetBuilder / Obx / 局部状态组件这些部分。
GetX 状态管理实践说明
本文介绍在项目中如何使用 GetxController、GetBuilder、Obx / GetX 等组件来组织业务逻辑和控制 UI 更新。
GetxController 的角色与生命周期
GetxController 用来承载页面或模块的业务状态与逻辑,通常搭配 StatelessWidget 使用,无需再写 StatefulWidget。
- 常用生命周期方法:
- onInit:Controller 创建时调用,做依赖注入、初始请求、订阅等。
- onReady:首帧渲染后调用,适合做需要 UI 已经渲染的操作(弹窗、导航等)。
- onClose:Controller 销毁时调用,用于取消订阅、关闭 Stream、释放资源。
推荐习惯:
- 把原来写在 StatefulWidget
initState / dispose里面的逻辑迁移到 Controller 的onInit / onClose中,UI 层尽量保持“傻瓜视图”。
GetX 中的两种状态管理方案
GetX 内置两类状态管理方式:简单状态管理(GetBuilder)与响应式状态管理(Obx / GetX)。
方案一:简单状态管理(GetBuilder + GetxController)
适用场景:不想使用 Rx 类型(.obs),希望显式控制刷新时机。
-
写法示例:
class CounterController extends GetxController { int count = 0; void increment() { count++; update(); // 手动触发使用该 controller 的 GetBuilder 重建 } }class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Get.put(CounterController()); return Scaffold( body: Center( child: GetBuilder<CounterController>( builder: (c) => Text('Count: ${c.count}'), ), ), floatingActionButton: FloatingActionButton( onPressed: controller.increment, ), ); } } -
特点:
- 无需
.obs,状态是普通字段。 - 只有调用
update()的时候,使用该 Controller 的 GetBuilder 才会重建。 - 适合页面级、大块 UI、不太频繁刷新场景。
- 无需
方案二:响应式状态管理(Obx / GetX + Rx)
适用场景:已经在使用 .obs,或希望局部 UI 随状态变化自动刷新。
-
写法示例:
class CounterController extends GetxController { var count = 0.obs; void increment() => count++; }class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Get.put(CounterController()); return Scaffold( body: Center( child: Obx(() => Text('Count: ${controller.count}')), // 或 // child: GetX<CounterController>( // builder: (c) => Text('Count: ${c.count}'), // ), ), floatingActionButton: FloatingActionButton( onPressed: controller.increment, ), ); } } -
特点:
- 变量通过
.obs变为 Rx 类型(如 RxInt、RxString)。 - 一旦值变化,Obx / GetX 所在的小部件自动重建,无需写
update()。 - 适合高频、小区域更新,如计数器、进度、网络状态、Switch 等。
- 变量通过
两种方案的混用
在同一个项目、同一个 Controller 中,可以同时使用:
- 一部分状态使用普通字段 +
GetBuilder。 - 一部分状态使用
.obs+Obx / GetX。
经验规则:
- 页面大块区域、刷新不频繁 → 优先 GetBuilder。
- 小范围、高频刷新 → 优先 Obx / GetX。
GetBuilder 的生命周期回调
GetBuilder 本质上是一个 StatefulWidget,内部有完整的 State 生命周期,对外通过参数暴露部分回调:[1]
- 常用回调参数:
-
initState:对应 State.initState,适合调用 Controller 方法、发请求等。 -
didChangeDependencies:父级依赖变化时触发,用得不多。 -
didUpdateWidget:父组件重建、参数改变时触发。 -
dispose:组件销毁时触发,适合释放本地资源。
-
示例:
GetBuilder<CounterController>(
initState: (_) {
// widget 创建时执行
},
dispose: (_) {
// widget 销毁时执行
},
builder: (c) => Text('Count: ${c.count}'),
);
建议:
- 页面 /模块的“生命周期逻辑”优先放在 Controller.onInit/onClose。
- 某个局部 Widget 有特别的创建 / 销毁逻辑时,再使用 GetBuilder 的
initState / dispose。
局部状态组件:ValueBuilder 与 ObxValue
对于“只在一个小部件内部使用”的临时状态,可以使用局部状态组件,而不必放入 Controller:
-
ValueBuilder(简单本地状态):
dart ValueBuilder<bool>( initialValue: false, builder: (value, update) => Switch( value: value, onChanged: update, // update(newValue) ), ); -
ObxValue(本地 Rx 状态):
ObxValue<RxBool>( (data) => Switch( value: data.value, onChanged: data, // 相当于 (v) => data.value = v ), false.obs, );
使用建议:
- 仅在该 Widget 内使用且与全局业务无关的状态 → 用 ValueBuilder / ObxValue。
- 会被多个 Widget 或页面共享的状态 → 放入 GetxController,再用 GetBuilder / Obx 监听。
快速选型表
| 需求场景 | 状态写法 | UI 组件 | 刷新方式 |
|---|---|---|---|
| 不想用 Rx,页面级 / 大块区域 | 普通字段 | GetBuilder | 手动 update()
|
已使用 .obs,局部自动刷新 |
.obs(RxXX 类型) |
Obx / GetX | 值变化自动刷新 |
| 单个小 widget 内部的临时简单状态 | 普通字段 | ValueBuilder | 调用 updateFn
|
| 单个小 widget 内部的临时响应式状态 | .obs |
ObxValue | 值变化自动刷新 |
在这种“页面加载时请求 API”的需求里,推荐把请求放在 GetxController 的生命周期 里做,而不是放在页面 Widget 里。
方案一:在 onInit 里请求
适合“只要创建了这个 Controller(进入页面)就立刻请求”的场景。
class ArticleController extends GetxController {
int pageCount = 0; // 简单状态
var likeCount = 0.obs; // 响应式状态
var isFavorite = false.obs;
var loading = false.obs; // 加载状态
var article = Rxn<Article>(); // 文章详情
@override
void onInit() {
super.onInit();
increasePageCount(); // 原来的逻辑
fetchArticle(); // 页面加载时请求 API
}
Future<void> fetchArticle() async {
loading.value = true;
try {
final data = await api.getArticleDetail(); // 这里调用你的 repository / service
article.value = data;
// article 是 Rx,使用 Obx 的地方会自动刷新
// 如果你有依赖简单状态的 GetBuilder,需要的话再调用 update()
// update();
} finally {
loading.value = false;
}
}
void increasePageCount() {
pageCount++;
update(); // 刷新 GetBuilder
}
void like() => likeCount++;
void toggleFavorite() => isFavorite.toggle();
}
页面里依然混用 GetBuilder + Obx:
class ArticlePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(ArticleController());
return Scaffold(
appBar: AppBar(title: const Text('Article Detail')),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 顶部浏览次数(简单状态)
GetBuilder<ArticleController>(
builder: (c) => Text('页面浏览次数:${c.pageCount}'),
),
const SizedBox(height: 16),
// 中间部分:加载中 / 内容(响应式状态)
Obx(() {
if (controller.loading.value) {
return const CircularProgressIndicator();
}
final article = controller.article.value;
if (article == null) {
return const Text('暂无数据');
}
return Text(article.title); // 文章标题
}),
const SizedBox(height: 16),
// 点赞 + 收藏(响应式状态)
Obx(
() => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('点赞:${controller.likeCount}'),
const SizedBox(width: 16),
Icon(
controller.isFavorite.value
? Icons.favorite
: Icons.favorite_border,
color: controller.isFavorite.value ? Colors.red : null,
),
],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: controller.increasePageCount,
child: const Text('增加浏览次数 (GetBuilder)'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: controller.like,
child: const Text('点赞 (Obx)'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: controller.toggleFavorite,
child: const Text('收藏切换 (Obx)'),
),
],
),
],
),
);
}
}
方案二:在 onReady 里请求(需要等页面渲染后)
如果你的 API 请求需要在“首帧 UI 出来之后”再做,比如要先弹一个对话框提示用户,将请求放在 onReady:
@override
void onReady() {
super.onReady();
fetchArticle(); // 首帧渲染完成后请求
}
不再建议的做法
- 不建议再在页面的
initState里请求,而是优先放到 GetxController.onInit / onReady,这样视图层更干净,也更符合 GetX 推荐的结构。