生活常识

高楼晃动正常吗?买房可能遇到哪些质量问题

文章来源:https://zhidao.baidu.com/special/view?id=7ca55a24626975510400

专业人士称,超高层建筑来回摇动是正常的,有利于楼体安全,高层建筑大部分都是钢结构,对于侧向压力会起到缓冲作用。高层建筑“打太极”,摇摆而“定”,也是对风压的一种缓冲。高层建筑不会摇摆才是危险的,一般来说越高的建筑,摇摆幅度越大。

在建造高层建筑时,风是首先要考虑的要素之一,高空风力增大的原因是高楼林立的“狭管效应”。由于高楼阻挡,如果风的通行面减小一半的话,其穿行速度则会增加约一倍。遇见高楼狭小的通道,风就更猛烈了。因此,超高层建筑顶部一般都会有位移表现,只是幅度不同。楼层越高,幅度越大。因此住在顶楼的居民感觉晃动是正常的。

编程思想-面向协议

[toc]

一、产生背景

1.1 传统面向对象编程(OOP)的弊端/继承的痛点:

继承的痛点:滥用继承的话,后面如果要抽离功能就会牵一发而动全身。即抽离一个功能会导致也要把继承链上涉及的功能都抽出来。显然太麻烦了。

痛点的解决:使用面向协议,将每个单独功能一个协议,复杂功能将协议组合起来即可。如此后续抽离时,抽单独协议和实现类即可(swift中可以将实现写在扩展中,可能不涉及实现类)。

我们将网络库CJNetwork使用面向协议编程后

1、定义协议CQNetworkRequestCompletionClientProtocal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NS_ASSUME_NONNULL_BEGIN

@protocol CQNetworkRequestCompletionClientProtocal <NSObject>

@required
/*
* 请求
*
* @param model 请求相关的信息(包含请求方法、请求地址、请求参数等)real\simulate\local
* @param completeBlock 请求结束的回调(为方便接口的重复利用回调中的responseModel使用id类型)
*
* @return 执行请求的任务
*/
- (NSURLSessionDataTask *)requestModel:(__kindof NSObject<CJRequestModelProtocol> *)model
completeBlock:(void (^)(CJResponeFailureType failureType, CJResponseModel *responseModel))completeBlock;
@end

NS_ASSUME_NONNULL_END

2、使用CQNetworkRequestCompletionClientProtocal

1
2
3
4
5
6
7
NS_ASSUME_NONNULL_BEGIN

@interface CJNetworkClient (Completion) <CQNetworkRequestCompletionClientProtocal>

@end

NS_ASSUME_NONNULL_END

1.2 什么是面向协议编程(POP)?

这个问题,我感觉比较明确的定义就是2015年Apple WWDC中说的一句话很直了的解释了:

1
2
Don't start with a class.
Start with a protocol.

即在程序设计中,不要以一个类开始设计,应该从一个协议开始,应抛弃之前OOP的对象设计理念,设计协议,这样不同的继承链之间也可以使用同一个协议。可以将协议看做一个组件,哪里需要哪里继承协议即可,而且协议是可以多继承的,iOS中的类只能单继承,这也是面向协议相对面向对象的一大优势。

1.3 Objective-C 和Swift的面向协议编程区别

OC和swift面向协议编程一个最大区别是OC的 Protocol 没有默认的实现,需要依赖具体的实现类实现协议定义的方法,而Swift2.0开始提供了Protocol + Extension,协议可以再 Extension中提供默认的实现,这样上层调用可以直接调用协议的默认实现。

严谨来说,OC不是一门面向协议编程的语言,因为 Protocol 只提供定义,而不提供实现,所以叫他 面向接口编程 更合适一些。

面向协议和依赖注入

依赖注入要求我们传入的参数遵循协议,这样做的好处是:

  • 我们可以在外部注入一个通用类,只要它遵循协议。
  • 也可以注入一个可测试的模拟类,便于测试。

参考文章:iOS-面向协议编程(POP)

二、替代继承的方式

参考文章:

1、协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> // 在屏幕中间弹出
> @protocol ShowInCenterProtocol <NSObject>
> - (void)show;
> @end
>
> // 在屏幕底部弹出
> @protocol ShowInBottomProtocol <NSObject>
> - (void)show;
> @end
>
> @class NormalVoicePlayer : NSObject <ShowInCenterProtocol>
>
> @end
>
>

2、用组合替代继承

eg1:想要为一个视图做一个show操作。(设想这个show方法后面可能随时会变,比如show的位置,或者动画)

AlertUtil:使用由AlertView/PopupView+PopupAction来组合,而不使用继承AlertView+为其添加show方法。

eg2:常见的举例还有,文本框TextField的根据历史记录填充、根据@邮箱填充。

3、类别category

1
2
3
@interface NSArray (OBJExtras)
- (void)removingFirstObject;
@end

三、使用面向协议为视图添加抖动功能

举例:为UIImageView和UIButton等添加视图的抖动

1、方法1:添加Category方式

如果你写过 Objective-C, 你很可能会把 shake() 写到一个 UIView 的分类(Category) 中(也就是 Swift 中的拓展 (extension)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//  UIViewExtension.swift

import UIKit

extension UIView {

func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

2、方法2:面向协议方式

但在swift里,我们除了对UIView添加extension外,其实还有另一种更好的方式:创建面向协议的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FoodImageView: UIImageView, Shakeable {
// 其他自定义写在这儿
}

class ActionButton: UIButton, Shakeable {
// 其他自定义写在这儿
}

class ViewController: UIViewController {

@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!

@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}

附上述面向协议的视图里所对的Shakeable 协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  Shakeable.swift

import UIKit

protocol Shakeable { }

// 你可以只为 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {

// shake 方法的默认实现
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}

参考文章:

四、使用面向协议处理reuseIdentifier

tableView的cell注册的基本代码如下

1
2
let foodCellNib = UINib(NibName: "FoodTableViewCell", bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: "FoodTableViewCell")

改进1:FoodTableViewCell字符串改成由类获得

1
2
let foodCellNib = UINib(NibName: String(FoodTableViewCell), bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier: String(FoodTableViewCell))

改进2:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol ReusableView: class {}
// 你可以只为 UIView 添加 ReusableView 方法!
extension ReusableView where Self: UIView {
static var reuseIdentifier: String {
return String(self)
}
}


extension UITableViewCell: ReusableView { }

let foodCellNib = UINib(NibName: “FoodTableViewCell”, bundle: nil)
tableView.registerNib(foodCellNib, forCellReuseIdentifier:FoodTableViewCell.reuseIdentifier)

END

架构模式-④MVC与MVVM

[toc]

一、框架

二、MVC,MVP 和 MVVM 的图示

MVC,MVP 和 MVVM 的图示

三、MVC与MVVM

1、MVC的开发模式

MVC是模型、视图、控制器开发模式,对于iOS SDK,所有的View都是视图层的,它应该独立于模型层,由视图器来控制。所有的用户数据都是模型层,它应该独立于视图。所有的ViewController都是视图器,由它负责控制视图,访问模型数据。

2、关于MVC 和 MVVM 的区别

MVVM 介绍

1.传统的MVC设计模式

mvc_1

MVP设计模式

mvp_1

用户在view上的操作,会通过vm对model进行数据的修改;

数据的修改引起的属性变化会通知到vm上;

vm根据变化进行view UI的更新;

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

img

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

MVVM的代码实现

ViewModel设计的了解,请进入项目:《DvlproadDesignPatternCollect》中的 CJViewModelDemo
该项目采用blockdelegateKVORAC四种方式介绍ViewModel的设计。 详情请查看:CJViewModelDemoViewModelDemos 文件夹下的代码

Flutter 中可查看 001-UIKit-CQDemo-Flutter 项目中的 tsdemodemo_flutter 工程里的 architecture 文件目录

该MVVM设计在其他项目中的使用,可见:CJStandardProject 的 LoginViewModel.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//  LoginViewModel.h
#import <Foundation/Foundation.h>
@protocol LoginViewModelDelegate <NSObject>

/// userName 的有效性发生变化
- (void)logic_checkUserNameWithValid:(BOOL)valid;
/// password 的有效性发生变化
- (void)logic_checkPasswordWithValid:(BOOL)valid;
/// 登录按钮 的有效性发生变化
- (void)logic_checkLoginWithValid:(BOOL)valid;

@end



@interface LoginViewModel : NSObject {

}
@property (nonatomic, weak) id<LoginViewModelDelegate> delegate;
@property (nonatomic, copy, readonly) NSString *userName;
@property (nonatomic, copy, readonly) NSString *password;

- (instancetype)initWithUserName:(NSString *)userName password:(NSString *)password;

#pragma mark - Update
- (void)updateUserName:(NSString *)userName;
- (void)updatePassword:(NSString *)password;

#pragma mark - Do
/// 检查是否可登录
- (NSString *)checkLoginCondition;

/// 执行登录
- (void)loginWitLoginSuccess:(void (^)(NSString *successMessage, DemoUser *user))loginSuccess
loginFailure:(void (^)(NSString *errorMessage))loginFailure;

@end

其他MVVM说明示例

设有如下原始Model

1
2
3
4
5
6
7
8
9
10
@interface Person : NSObject

- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;

@property (nonatomic, readonly) NSString *salutation;
@property (nonatomic, readonly) NSString *firstName;
@property (nonatomic, readonly) NSString *lastName;
@property (nonatomic, readonly) NSDate *birthdate;

@end

则如果仅有此Model的话,我们在PersonViewController中

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

NSString *nameText = nil;
if (self.personModel.salutation.length > 0) {
nameText = [NSString stringWithFormat:@"%@ %@ %@", self.personModel.salutation, self.personModel.firstName, self.personModel.lastName];
} else {
nameText = [NSString stringWithFormat:@"%@ %@", self.personModel.firstName, self.personModel.lastName];
}

self.nameLabel.text = nameText;
}

而如果我们进而增加一个ViewModel,则如果我们PersonViewController中可以简化成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation PersonViewModel

- (instancetype)initWithPerson:(Person *)person {
self = [super init];
if (!self) return nil;

_person = person;
if (person.salutation.length > 0) {
_nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
} else {
_nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName];
}

return self;
}

@end

// 此时我们PersonViewController中可以简化成
- (void)viewDidLoad {
[super viewDidLoad];

self.nameLabel.text = self.personViewModel.nameText;
}

END

架构模式-①概览

[Toc]

前言:架构模式、框架、设计模式的概念

iOS—架构模式、框架、设计模式的理解

架构模式:架构模式的出现是为了管理复杂的应用程序,这样可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC架构MVVM架构属于此类。

框架:这个最好理解了,通常是代码重用。框架与设计模式的概念容易弄混,两者有相似之处,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述规范,它比框架更抽象;框架为已经解决问题的具体实现方法,能直接执行或复用;设计模式是比框架更小的元素,一个框架中往往含有一种或多种设计模式。Xcode自带的FoundationUIKit,以及我们经常使用的AFNetworkingMJExtensionSVProgressHUD就属于这一类。

设计模式:设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范。增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式迭代器模式策略模式就属于这一类。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。

三者关系:架构(动词)>框架>设计模式。

软件通过架构,可以设计出很多不同的框架。在一个框架中,也可以使用很多的设计模式。设计模式不是哪儿哪儿都可以用的,只有当出现了某一特定的问题时,才利用设计模式去解决。设计模式不是用的越多越好,在维护的时候,过多的设计模式会极大的增添维护成本。

一、架构模式

1、架构模式的演变

1.1、MVC

1、架构的设计其实是为了更好的维护和迭代,而不是只考虑眼前当下的开发。

2、最基本的架构是MVC,试想下如果我一个应用的所有功能都是像”关于“功能的,那我还需要其他架构干嘛?显然MVC就已经很足够,而且还显得不冗余(这里指的是设计或者粒度不冗余)。

3、其他的架构都是从MVC演变而来。再讲其他架构的演变之前,我们先明确最原始的MVC代码是如何的。

①、首先M主要负责数据、V主要负责视图、C主要负责数据的视图显示。而这里的M是原始的瘦Model

所以,我们拿常见的”我的”页面来举例,部分原始的MVC代码一般为如下样子:

1
2
3
4
5
6
7
8
9
UserModel


View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = user.sex == 1? "女" :"男";

4、在MVC+瘦Model的演变下,还有MVC+胖Model

胖Model就是瘦Model+部分弱业务逻辑(这些弱业务重复一般都是经常出现,或者说是要求可复用性的,如根据枚举获取性别字符串)。它使得Controller可以从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上.

1
2
3
4
5
6
7
8
9
UserModel
+ (NSString *)sexStringFrom:(int)sex;

View


ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserModel sexStringFrom:user.sex];

1.2、MVP?

5、有时候在MVC+瘦Model的演变下,不是转成MVC+胖Model,而是MVC+瘦Model+Helper

其实这个思路已经很接近MVP了、但是还差提点。MVP还需要为View提供数据

1
2
3
4
5
6
7
8
9
10
UserModel

View

UserHelper
+ (NSString *)sexStringFrom:(int)sex;

ViewController
self.userInfoView.name = user.name;
self.userInfoView.sexString = [UserHelper sexStringFrom:user.sex];

1.3、MVVM

6、关于MVC+瘦Model+Helper,还可以改为MVC+瘦Model+ViewModel

刚才的胖Model只从Controller移植走了一些简单的弱业务。

而ViewModel则干脆把数据的处理全部从Controller移植了出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
UserModel

View

UserViewModel
name
sexString
- (id)initWithName:(NSString *)name sex:(int)sex;


ViewController
self.userInfoView.name = userViewModel.name;
self.userInfoView.sexString = userViewModel.sexString;

1.4、MVCS -> MVVM+S

拿有数据操作的”购物车“页面来说,

假设其除了有网络数据请求外,还有本地数据库处理。(附:购物车是否需要本地数据库,个人认为是基于服务端的压力来考虑,不然一般是放在服务端合适点。压力设想如下,千万级用户,购物车加减商品频繁,假设放在后台,则操作和计算也就频繁,压力也就大了。如果放在前端,则后台只需要负责是否可加减商品的检验,而不用再建购物车数据表和计算价格等,压力就小了。)

每个页面一般都会有网络请求,这些网络请求最原始的时候,是放在ViewController,也就是MVC的C中。

为了避免MVC中的ViewController后期变得十分臃肿。我们肯定要在MVC的基础上额外增加一层来处理这个。

综合考虑后,我们选用MVCS来处理。

其中S代表着Store,且我们这里的S代表着是整个模块的S,而不是每个页面都有一个S。其下层又包含着Storage和Service,即 数据库DB部分网络Network部分

Store类名,个人习惯是使用Manager来命令。如UserManager。

MVCS1

所以,最终我们选用的是 MVCS+MVVM 或者说 MVVM+S。

S层(service 层)提供一种和外界(比如远程服务 API 或文件系统)交互的独立机制。

MVVM+S 纵向的数据流就不用多说,横向的数据流,我们采用自定义的类系统通知。区别在于它有着系统Notification的一对多方便,又有着delegate的接口对接方便。

image-20200917024611311

抛开数据共享问题(如每个请求都要带userToken),每个页面

二、架构分层(Architectural Layering)

架构分层是一种将系统分解为多个逻辑层次的方法,每一层都有特定的职责和功能。

  • 架构分层有助于实现关注点分离(Separation of Concerns),使得系统的不同部分可以独立开发和维护。
  • 典型的分层包括表示层(或用户界面层)、业务逻辑层、数据访问层等。

架构分层图片来源于 《架构分层.graffle》中的【一、架构分层】

其他图片见我的项目 CJStandardProject 中的 Screenshots

其他参考文章

iOS:

三、常见的项目目录结构

常见的项目目录结构:

框架①项目目录结构

项目目录结构规范说明 请点击本链接跳转下文附录进行查看

完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

四、页面与服务类设计

1、核心理念

页面执行自己的动作,调用Service服务类。无需关心数据请求、数据请求结束后是否需要缓存等等。

Service服务中心,内部包含请求和可能的数据存储等处理,不对外暴露。

2、设计举例

页面调用服务类举例

2.1、设计

arc_layer_2

图片来源于 《架构分层.graffle》中的【二、页面与服务类设计】

2.2、服务类中的代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// UserRequest
class UserService {
// 登录-普通
static login_normal(String mobile, String smsCode) {
return UserRequest.login_normal(
mobile: mobile,
smsCode: smsCode,
completeBlock: (UserModel userModel) {
UserInfoManager().loginSuccessWithUserModel(userModel);
},
);
}
}

class UserRequest {
// 登录-普通
static login_normal({
String mobile,
String smsCode,
void Function(UserModel userModel) completeBlock,
}) {
return AppNetworkKit.post(
UrlPath.doLogin,
params: {
"mobile": mobile,
"smsCode": smsCode,
"grantType": "sms_code",
},
).then((ResponseModel responseModel) {
if (responseModel.isSuccess) {
User_manager_bean baseUserModel;
if (responseModel.result != null) {
baseUserModel = UserModel.fromJson(responseModel.result);
} else {
baseUserModel = null;
}
completeBlock(baseUserModel);
} else {
completeBlock(null);
}
});
}
}

附一:项目目录结构规范说明

一个完整的项目目录结构一定至少包含有以下几个方面。

附:完整的 项目目录结构树 请点击链接,跳转到本问附文中查看。

1、程序入口

1
│  ├─ main.dart

2、依赖库package

1
2
3
4
5
├─ package
│ ├─ network
│ └─ route
│ ├─ errors
│ └─ manager

项目依赖库,统一存放在package下。

3、资源文件assets

1
2
3
4
5
6
7
8
9
├─ assets
│ ├─ fonts
│ ├─ images
│ │ ├─ base
│ │ ├─ order
│ │ └─ user
│ └─ json
│ ├─ config
│ └─ mock

资源文件:统一存放在assets下。区分字体fonts、图片images、json(eg城市配置信息)

4、公共组件common

1
2
3
4
5
6
7
8
9
10
├─ lib
│ ├─ common
│ │ ├─ extensions
│ │ │ └─ num_extension.dart
│ │ ├─ styles
│ │ │ └─ colors.dart
│ │ │ └─ typography.dart
│ │ └─ widgets
│ │ ├─ common_button.dart
│ │ └─ common_text.dart

common/:包含通用的小部件、样式和颜色等。

——-widgets:包含通用的小部件,例如按钮、文本、图标等。

——-styles: 包含通用的颜色和字体等。例如,colors.dart 文件中可以定义一些通用的颜色常量,typography.dart 文件中可以定义一些通用的字体样式(RegularTextStyle、BoldTextStyle、MediumTextStyle、DDINPROBoldTextStyle)。

——-extensions:系统类的扩展方法(eg: 20.w_pt_bj)

5、通用工具util

1
2
│  └─ util
│ └─ textsize_util.dart

utils/:包含一些通用的工具函数或类,例如日期格式化、字符串处理、数学计算等。

6、数据模型models

1
2
3
4
5
6
7
8
9
10
11
12
│  ├─ model
│ │ ├─ global_config
│ │ │ ├─ app_config_model.dart
│ │ │ ├─ game_config_model.dart
│ │ │ ├─ global_config_model.dart
│ │ │ └─ web_config_model.dart
│ │ ├─ order
│ │ │ ├─ order_base_model.dart
│ │ │ └─ order_detail_model.dart
│ │ └─ user
│ │ ├─ user_base_model.dart
│ │ └─ user_detail_model.dart

models/:包含应用程序的数据模型,例如用户、订单等。

7、服务类service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
│  ├─ service
│ │ ├─ global_config
│ │ │ ├─ manager
│ │ │ │ └─ global_config_service.dart
│ │ │ ├─ request
│ │ │ │ └─ global_config_request.dart
│ │ │ └─ cache
│ │ │ └─ global_config_cache.dart
│ │ └─ user
│ │ ├─ manager
│ │ │ └─ user_service.dart
│ │ ├─ request
│ │ │ └─ user_info_request.dart
│ │ └─ cache
│ │ └─ user_info_cache.dart

services/:包含应用程序的服务类或者管理中心,例如用户信息中心、配置信息中心等。

——-manager: 管理中心

——-request: 管理中心会需要的请求功能

——-cache: 管理中心可能需要的存储功能

8、模块功能module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
│  ├─ module
│ │ ├─ auth
│ │ │ ├─ pages
│ │ │ │ ├─ login_page
│ │ │ │ │ └─ login_page.dart
│ │ │ │ └─ register_page
│ │ │ │ └─ register_page.dart
│ │ │ ├─ routes
│ │ │ │ ├─ auth_route_handel.dart
│ │ │ │ ├─ auth_route_interceptor.dart
│ │ │ │ └─ auth_route_name.dart
│ │ │ └─ widgets
│ │ │ ├─ code_input_widget.dart
│ │ │ ├─ name_input_widget.dart
│ │ │ └─ tel_input_widget.dart

modules/:包含应用程序的不同模块,例如身份验证、主页和个人资料等。

——-page: 页面

——-routes: 路由

——-widgets: 视图部件

附1、项目目录结构树

项目目录结构(以Flutter为例).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
std_demo
├─ .gitignore
├─ .metadata
├─ README.md
├─ analysis_options.yaml
├─ android
│ ├─ .gitignore
│ ├─ .gradle
│ │ ├─ 7.5
│ │ │ ├─ checksums
│ │ │ │ └─ checksums.lock
│ │ │ ├─ dependencies-accessors
│ │ │ │ ├─ dependencies-accessors.lock
│ │ │ │ └─ gc.properties
│ │ │ ├─ executionHistory
│ │ │ │ └─ executionHistory.lock
│ │ │ ├─ fileChanges
│ │ │ │ └─ last-build.bin
│ │ │ ├─ fileHashes
│ │ │ │ └─ fileHashes.lock
│ │ │ ├─ gc.properties
│ │ │ └─ vcsMetadata
│ │ ├─ buildOutputCleanup
│ │ │ ├─ buildOutputCleanup.lock
│ │ │ └─ cache.properties
│ │ └─ vcs-1
│ │ └─ gc.properties
│ ├─ app
│ │ ├─ build.gradle
│ │ └─ src
│ │ ├─ debug
│ │ │ └─ AndroidManifest.xml
│ │ ├─ main
│ │ │ ├─ AndroidManifest.xml
│ │ │ ├─ java
│ │ │ │ └─ io
│ │ │ │ └─ flutter
│ │ │ │ └─ plugins
│ │ │ │ └─ GeneratedPluginRegistrant.java
│ │ │ ├─ kotlin
│ │ │ │ └─ com
│ │ │ │ └─ example
│ │ │ │ └─ std_demo1
│ │ │ │ └─ MainActivity.kt
│ │ │ └─ res
│ │ │ ├─ drawable
│ │ │ │ └─ launch_background.xml
│ │ │ ├─ drawable-v21
│ │ │ │ └─ launch_background.xml
│ │ │ ├─ mipmap-hdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-mdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xxhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ mipmap-xxxhdpi
│ │ │ │ └─ ic_launcher.png
│ │ │ ├─ values
│ │ │ │ └─ styles.xml
│ │ │ └─ values-night
│ │ │ └─ styles.xml
│ │ └─ profile
│ │ └─ AndroidManifest.xml
│ ├─ build.gradle
│ ├─ gradle
│ │ └─ wrapper
│ │ ├─ gradle-wrapper.jar
│ │ └─ gradle-wrapper.properties
│ ├─ gradle.properties
│ ├─ gradlew
│ ├─ gradlew.bat
│ ├─ local.properties
│ └─ settings.gradle
├─ assets
│ ├─ fonts
│ ├─ images
│ │ ├─ base
│ │ ├─ order
│ │ └─ user
│ └─ json
│ ├─ config
│ └─ mock
├─ ios
│ ├─ .gitignore
│ ├─ Flutter
│ │ ├─ AppFrameworkInfo.plist
│ │ ├─ Debug.xcconfig
│ │ ├─ Generated.xcconfig
│ │ ├─ Release.xcconfig
│ │ └─ flutter_export_environment.sh
│ ├─ Runner
│ │ ├─ AppDelegate.swift
│ │ ├─ Assets.xcassets
│ │ │ ├─ AppIcon.appiconset
│ │ │ │ ├─ Contents.json
│ │ │ │ ├─ Icon-App-1024x1024@1x.png
│ │ │ │ ├─ Icon-App-20x20@1x.png
│ │ │ │ ├─ Icon-App-20x20@2x.png
│ │ │ │ ├─ Icon-App-20x20@3x.png
│ │ │ │ ├─ Icon-App-29x29@1x.png
│ │ │ │ ├─ Icon-App-29x29@2x.png
│ │ │ │ ├─ Icon-App-29x29@3x.png
│ │ │ │ ├─ Icon-App-40x40@1x.png
│ │ │ │ ├─ Icon-App-40x40@2x.png
│ │ │ │ ├─ Icon-App-40x40@3x.png
│ │ │ │ ├─ Icon-App-60x60@2x.png
│ │ │ │ ├─ Icon-App-60x60@3x.png
│ │ │ │ ├─ Icon-App-76x76@1x.png
│ │ │ │ ├─ Icon-App-76x76@2x.png
│ │ │ │ └─ Icon-App-83.5x83.5@2x.png
│ │ │ └─ LaunchImage.imageset
│ │ │ ├─ Contents.json
│ │ │ ├─ LaunchImage.png
│ │ │ ├─ LaunchImage@2x.png
│ │ │ ├─ LaunchImage@3x.png
│ │ │ └─ README.md
│ │ ├─ Base.lproj
│ │ │ ├─ LaunchScreen.storyboard
│ │ │ └─ Main.storyboard
│ │ ├─ GeneratedPluginRegistrant.h
│ │ ├─ GeneratedPluginRegistrant.m
│ │ ├─ Info.plist
│ │ └─ Runner-Bridging-Header.h
│ ├─ Runner.xcodeproj
│ │ ├─ project.pbxproj
│ │ ├─ project.xcworkspace
│ │ │ ├─ contents.xcworkspacedata
│ │ │ └─ xcshareddata
│ │ │ ├─ IDEWorkspaceChecks.plist
│ │ │ └─ WorkspaceSettings.xcsettings
│ │ └─ xcshareddata
│ │ └─ xcschemes
│ │ └─ Runner.xcscheme
│ └─ Runner.xcworkspace
│ ├─ contents.xcworkspacedata
│ └─ xcshareddata
│ ├─ IDEWorkspaceChecks.plist
│ └─ WorkspaceSettings.xcsettings
├─ lib
│ ├─ common
│ │ ├─ extensions
│ │ │ └─ num_extension.dart
│ │ ├─ styles
│ │ │ ├─ colors.dart
│ │ │ └─ typography.dart
│ │ └─ widgets
│ │ ├─ common_button.dart
│ │ └─ common_text.dart
│ ├─ main.dart
│ ├─ model
│ │ ├─ evaluate
│ │ │ └─ evaluate_model.dart
│ │ ├─ global_config
│ │ │ ├─ app_config_model.dart
│ │ │ ├─ game_config_model.dart
│ │ │ ├─ global_config_model.dart
│ │ │ └─ web_config_model.dart
│ │ ├─ goods
│ │ │ ├─ goods_base_model.dart
│ │ │ └─ goods_detail_model.dart
│ │ ├─ order
│ │ │ ├─ order_base_model.dart
│ │ │ └─ order_detail_model.dart
│ │ ├─ user
│ │ │ ├─ user_base_model.dart
│ │ │ └─ user_detail_model.dart
│ │ └─ wish
│ │ ├─ wish_base_model.dart
│ │ └─ wish_detail_model.dart
│ ├─ module
│ │ ├─ auth
│ │ │ ├─ pages
│ │ │ │ ├─ login_page
│ │ │ │ │ └─ login_page.dart
│ │ │ │ └─ register_page
│ │ │ │ └─ register_page.dart
│ │ │ ├─ routes
│ │ │ │ ├─ auth_route_handel.dart
│ │ │ │ ├─ auth_route_interceptor.dart
│ │ │ │ └─ auth_route_name.dart
│ │ │ └─ widgets
│ │ │ ├─ code_input_widget.dart
│ │ │ ├─ name_input_widget.dart
│ │ │ └─ tel_input_widget.dart
│ │ ├─ game
│ │ │ ├─ routes
│ │ │ │ ├─ game_route_handle.dart
│ │ │ │ ├─ game_route_interceptor.dart
│ │ │ │ └─ game_route_name.dart
│ │ │ └─ widgets
│ │ ├─ mall
│ │ │ ├─ pages
│ │ │ │ └─ mall_home_page
│ │ │ │ └─ mall_home_page.dart
│ │ │ ├─ routes
│ │ │ │ └─ mall_route_handle.dart
│ │ │ └─ widgets
│ │ ├─ order
│ │ │ ├─ pages
│ │ │ ├─ routes
│ │ │ └─ widgets
│ │ ├─ profile
│ │ │ ├─ pages
│ │ │ │ └─ profile_page
│ │ │ │ └─ profile_page.dart
│ │ │ └─ widgets
│ │ │ └─ profile_avatar.dart
│ │ ├─ web
│ │ │ └─ routes
│ │ │ ├─ web_route_handel.dart
│ │ │ ├─ web_route_interceptor.dart
│ │ │ └─ web_route_name.dart
│ │ └─ wish
│ │ ├─ pages
│ │ │ ├─ wish_create_add_sku_page
│ │ │ │ └─ wish_create_add_sku_page.dart
│ │ │ ├─ wish_create_page
│ │ │ │ └─ wish_ceate_page.dart
│ │ │ ├─ wish_detail_dark_page
│ │ │ │ └─ wish_detail_dark_page.dart
│ │ │ ├─ wish_detail_light_page
│ │ │ │ └─ wish_detail_light_page.dart
│ │ │ ├─ wish_goods_control_page
│ │ │ │ └─ wish_goods_control_page.dart
│ │ │ └─ wish_home_page
│ │ │ └─ wish_home_page.dart
│ │ └─ widgets
│ │ ├─ wish_create_add_sku_widget1.dart
│ │ └─ wish_create_add_sku_widget2.dart
│ ├─ service
│ │ ├─ global_config
│ │ │ ├─ cache
│ │ │ │ └─ global_config_cache.dart
│ │ │ ├─ manger
│ │ │ │ └─ global_config_service.dart
│ │ │ └─ request
│ │ │ └─ global_config_request.dart
│ │ ├─ order
│ │ │ ├─ cache
│ │ │ │ └─ order_cache.dart
│ │ │ ├─ manager
│ │ │ │ └─ order_service.dart
│ │ │ └─ request
│ │ │ └─ order_request.dart
│ │ ├─ publish
│ │ │ ├─ cache
│ │ │ │ └─ publish_cache.dart
│ │ │ ├─ manager
│ │ │ │ └─ publish_service.dart
│ │ │ └─ request
│ │ │ └─ publish_request.dart
│ │ └─ user
│ │ ├─ cache
│ │ │ └─ user_info_cache.dart
│ │ ├─ manager
│ │ │ └─ user_service.dart
│ │ └─ request
│ │ └─ user_info_request.dart
│ └─ util
│ └─ textsize_util.dart
├─ linux
│ ├─ .gitignore
│ ├─ CMakeLists.txt
│ ├─ flutter
│ │ ├─ CMakeLists.txt
│ │ ├─ generated_plugin_registrant.cc
│ │ ├─ generated_plugin_registrant.h
│ │ └─ generated_plugins.cmake
│ ├─ interceptors
│ │ ├─ game_interceptor.dart
│ │ └─ login_interceptor.dart
│ ├─ main.cc
│ ├─ my_application.cc
│ └─ my_application.h
├─ macos
│ ├─ .gitignore
│ ├─ Flutter
│ │ ├─ Flutter-Debug.xcconfig
│ │ ├─ Flutter-Release.xcconfig
│ │ ├─ GeneratedPluginRegistrant.swift
│ │ └─ ephemeral
│ │ ├─ Flutter-Generated.xcconfig
│ │ └─ flutter_export_environment.sh
│ ├─ Runner
│ │ ├─ AppDelegate.swift
│ │ ├─ Assets.xcassets
│ │ │ └─ AppIcon.appiconset
│ │ │ ├─ Contents.json
│ │ │ ├─ app_icon_1024.png
│ │ │ ├─ app_icon_128.png
│ │ │ ├─ app_icon_16.png
│ │ │ ├─ app_icon_256.png
│ │ │ ├─ app_icon_32.png
│ │ │ ├─ app_icon_512.png
│ │ │ └─ app_icon_64.png
│ │ ├─ Base.lproj
│ │ │ └─ MainMenu.xib
│ │ ├─ Configs
│ │ │ ├─ AppInfo.xcconfig
│ │ │ ├─ Debug.xcconfig
│ │ │ ├─ Release.xcconfig
│ │ │ └─ Warnings.xcconfig
│ │ ├─ DebugProfile.entitlements
│ │ ├─ Info.plist
│ │ ├─ MainFlutterWindow.swift
│ │ └─ Release.entitlements
│ ├─ Runner.xcodeproj
│ │ ├─ project.pbxproj
│ │ ├─ project.xcworkspace
│ │ │ └─ xcshareddata
│ │ │ └─ IDEWorkspaceChecks.plist
│ │ └─ xcshareddata
│ │ └─ xcschemes
│ │ └─ Runner.xcscheme
│ └─ Runner.xcworkspace
│ ├─ contents.xcworkspacedata
│ └─ xcshareddata
│ └─ IDEWorkspaceChecks.plist
├─ package
│ ├─ network
│ └─ route
│ ├─ errors
│ └─ manager
├─ pubspec.lock
├─ pubspec.yaml
├─ test
│ └─ widget_test.dart
├─ web
│ ├─ favicon.png
│ ├─ icons
│ │ ├─ Icon-192.png
│ │ ├─ Icon-512.png
│ │ ├─ Icon-maskable-192.png
│ │ └─ Icon-maskable-512.png
│ ├─ index.html
│ └─ manifest.json
└─ windows
├─ .gitignore
├─ CMakeLists.txt
├─ flutter
│ ├─ CMakeLists.txt
│ ├─ generated_plugin_registrant.cc
│ ├─ generated_plugin_registrant.h
│ └─ generated_plugins.cmake
└─ runner
├─ CMakeLists.txt
├─ Runner.rc
├─ flutter_window.cpp
├─ flutter_window.h
├─ main.cpp
├─ resource.h
├─ resources
│ └─ app_icon.ico
├─ runner.exe.manifest
├─ utils.cpp
├─ utils.h
├─ win32_window.cpp
└─ win32_window.h

END

好文分享:

瘦身-⑥AppDelegate瘦身

必备知识架构-瘦身-⑥AppDelegate瘦身

[toc]

前言

CJModuleManager:CJUIKitDemo-CJBaseUtil-CJModuleManager

1、AppDelegate瘦身

举个更实际的例子,就是你在为AppDelegate瘦身的时候,将AppDelegate上的方法,分散到各自的Manager(UserManager、PushManager、LocationManager等)的时候,就肯定会需要处理这些dele

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

[[CJModuleManager sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; //即一启动,就会去执行所有添加到ModuleManager中所有的 application:application didFinishLaunchingWithOptions:launchOptions 方法

// 设置主窗口,并设置根控制器
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

UIViewController *viewController = [[CTMediator sharedInstance] BBXPLogin_loginViewControllerWithLoginSuccessBlock:loginSuccessBlock getUserInfoSuccessBlock:getUserInfoSuccessBlock];

self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:viewController];

[self.window makeKeyAndVisible];

return YES;
}

// 附:上述AppDelegete.m执行的CJModuleManager.m中方法为
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
for (id<CJModule> module in self.modules) {
if ([module respondsToSelector:_cmd]) {
[module application:application didFinishLaunchingWithOptions:launchOptions];
}
}
return YES;
}

END

新项目实施步骤

[toc]

新项目实施步骤

一、建立项目工程(0.5h)

1、工程配置

1.1、pod处理–Podfile

1
2
3
4
5
pod 'SnapKit'
pod 'Masonry'
pod 'CJBaseUIKit'
pod 'CJFoundation'
pod 'CJBaseHelper'

1.2、混编处理

①、OC 调用 Swift

1、建立XXX-Swift.h

附:如果你是OC工程中第一次创建swift文件,则系统会在自动帮你搭建XXX-Bridging-Header.h的同时,为你生成该文件。即那种情况下,此步可略过

2、Defines Module 设置为YES

image-20201106181320734

3、oc 文件中 #import “XXX-Swift.h”

②、Swift 调用 OC

XXX-Bridging-Header.h

2、工程入口

2.1、入口首页:方便他人进入开发

二、架构设计

1、架构文件管理

  • AppDelegate

    AppDelegate+StartUp.swift

  • CommonUI

  • CommonUtil
  • Service
  • Module
  • Resources

三、功能模块规划

1、登录模块 Page5 24h

1.1、登录模块

Page 5 24h

  • 登录的首页
    • 隐私政策弹窗页
  • 登录方式选择页面
    • 本机号码获取
    • 第三方登录(微信、)
  • 登录的手机号码输入页
  • 登录的手机验证码输入页

1.2、登录后的信息完善

Page

  • 欢迎页 -> 内容介绍页1 -> 内容介绍页2
  • 个人信息完善
    • 个人性别 + 交往喜好(性别+年龄)

1.3、信息完善后的使用引导

Page3 1d

2、主页模块 Page1 4h

2.1、主页框架 Page1 4h

Page1 4h

3、发现模块

Page 17 = 11 + 6 + 2

h 40 = 28 + 8 + 4

3.1、更好使用的信息完善-表 Page11 28h+

涉及的组件开发 8

  • 完善进度组件 2
  • 照片弹窗组件 6
  • 照片列表组件 3

涉及的页面开发(不含请求)20+

  • 昵称完善页 1

  • 生日完善页(日期选择 + 年龄 + 星座) 4

  • 身高完善页 1

  • 家乡完善页 4

  • 学校完善页

  • 职业完善页(选一选+自定义) 4+2

  • 日常完善页

  • 愿望完善页

  • 照片完善页 1

3.2、更好使用的信息完善-里 Page6 8h

3.3、更好使用的信息完善-匹配偏好 Page2 4h

4、我的模块

4.1、我的首页

4.2、资料信息-表

涉及的组件开发 2

  • 完善进度组件 2
  • 照片弹窗组件 已在上述完成
  • 照片列表组件 已在上述完成

涉及的页面开发(不含请求)

  • 表资料 2*8
  • 照片

其他小工具

[toc]

一、截图

二、翻译

  • Bob

  • 沉浸式翻译 immersivetranslate.com

    沉浸式翻译: 双语对照网页翻译 & PDF文档翻译

    沉浸式网页双语翻译扩展,支持PDF翻译,双语Epub电子书制作,Youtube/Netflix/Udemy 等平台双语字幕,支持Deepl/Google等多个翻译

三、投屏(与控制)、录屏

1、投屏(与控制)

参考文章:iphone有线投屏和无线投屏到macbook|电脑上操控手机

应用名 下载地址 好用程度 在电脑上用鼠标操作投屏的ipone
AirServer 最好用
虫洞 https://er.run/ 付费软件(高版本蓝牙有bug)
iMyFone MirrorTo https://www.imyfone.com/screen-mirror/ 付费软件($59.99)
iMyFone MirrorTo 使用简介

2、录屏

五款好用的Mac录屏软件推荐

四、虚拟定位

应用名 应用版本 存放于
AnyGo AnyGo for Mac 7.0.0激活版 百度网盘

聊聊虚拟定位工具新宠儿:AnyGo的原理与识别

macOS 打开软件提示“已损坏”怎么解决?无法确认开发者身份解决方案

六、媒体的转换与合成

1、在线音频视频转换器(格式转化)

https://www.media.io/zh/

在线音频视频转换器

点击下载:

在线音频视频转换器2

2、Mac使用imovie合成音频和视频

Mac使用imovie合成音频和视频

七、免会员、免登录等

1、code-box : 免登录一键复制代码

以访问 UITableView 滑动到最后一个cell自动加载下一页的数据 为例,其需要会员才能解锁文章全部内容。

code-box

其他

第2节:Flutter控件Widget的自定义与封装

[TOC]

前言

如果你不是没有任何开发经验,那么你一定知道任何app里都有可能有重用性比较高的控件。所以对于那么重用性比较高的,或者需要你自定义的控件的,我们需要将它们给封装起来,以便下次或者其他app中继续使用。这也正式本节想要说的内容Flutter中如何封装Widget。

下面我从自己实现一个满意的封装,分别介绍你可能用到的三种封装方式

  • 1、函数式封装
  • 2、以继承 StatefulWidget 的方式封装
  • 3、继承父类式封装(推荐)

下面我们以登录页的文本框的自定义来谈封装。

用户名登录的UI图

一、函数式封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// 蓝色背景按钮(常用于:登录按钮)
/// 方法1:以函数的方法实现
FlatButton blueButton(String text, bool enable, VoidCallback enableOnPressed) {
return FlatButton(
child: Text(text),
splashColor: Colors.transparent,
color: Color(0xff01adfe),
textColor: Colors.white,
highlightColor: Color(0xff1393d7),
disabledColor: Color(0xffd3d3d5),
disabledTextColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0)
),
//onPressed: enable ? enableOnPressed : null,
onPressed: enable ? () {
enableOnPressed();
} : null,
);
}

乍看没什么问题,好像很简洁。但当你也用这种方式来实现文本框的时候,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法1:以函数的方法实现
TextField loginTextField(String placeholder, String prefixIconImageName, ValueChanged<String> onSubmitted) {
return TextField(
//autofocus: shouldAutofocusUserNameTextField,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
prefixIconImageName,
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
// keyboardType: TextInputType.text,
// controller: _usernameController,
// textInputAction: TextInputAction.next,
// focusNode: usernameFocusNode,
// onSubmitted: (text) {
// print("current userName:" + text);
// if (null == currentFocusNode) {
// currentFocusNode = FocusScope.of(context);
// }
// currentFocusNode.requestFocus(passwordFocusNode);
// }
onSubmitted: onSubmitted,
);
}

// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

可见这种函数的方式,没办法处理过多属性的自定义。因为它并不像我们iOS中的UIView,可以对得到的控件在后续再定制。所以,在Flutter中这种函数式的封装不适合,因为它无法满足使用。

附:以下是iOS中的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (CJTextField *)userNameTextField {
if (_userNameTextField == nil) {
UIImage *normalImage = [UIImage imageNamed:@"login_username_gray"];
UIImage *selectedImage = [UIImage imageNamed:@"login_username_blue"];
_userNameTextField = [CJDemoTextFieldFactory textFieldWithNormalImage:normalImage selectedImage:selectedImage];
_userNameTextField.placeholder = NSLocalizedString(@"用户名", nil);
_userNameTextField.returnKeyType = UIReturnKeyNext;
_userNameTextField.clearButtonMode = UITextFieldViewModeWhileEditing;
_userNameTextField.delegate = self;
}
return _userNameTextField;
}

- (UIButton *)loginButton {
if (_loginButton == nil) {
_loginButton = [CJDemoButtonFactory blueButton];
[_loginButton setTitle:NSLocalizedString(@"登录", nil) forState:UIControlStateNormal];
_loginButton.enabled = NO;
[_loginButton addTarget:self action:@selector(loginButtonAction) forControlEvents:UIControlEventTouchUpInside];
}
return _loginButton;
}

二、以继承 StatefulWidget 的方式封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法2:以继承 StatefulWidget 的方式实现
class LoginTextField extends StatefulWidget {
final String placeholder;
final String prefixIconImageName;
final bool autofocus;
final TextEditingController controller;
final TextInputAction textInputAction;
final FocusNode focusNode;
final ValueChanged<String> onSubmitted;
final TextInputType keyboardType;

LoginTextField({
Key key,
this.placeholder,
this.prefixIconImageName,
this.autofocus,
this.keyboardType,
this.controller,
this.textInputAction,
this.focusNode,
this.onSubmitted

}) : super(key: key);

@override
_LoginTextFieldState createState() => _LoginTextFieldState();
}

class _LoginTextFieldState extends State<LoginTextField> {
@override
Widget build(BuildContext context) {
return Container(
child: TextField(
autofocus: widget.autofocus,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: widget.placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
widget.prefixIconImageName,
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: widget.keyboardType,
controller: widget.controller,
textInputAction: widget.textInputAction,
focusNode: widget.focusNode,
onSubmitted: widget.onSubmitted
),
);
}
}

// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

使用的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TextField userNameTextField() {
return TextField(
autofocus: shouldAutofocusUserNameTextField,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: "用户名",
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
userNameValid ? 'lib/Resources/login/login_username_blue.png' : 'lib/Resources/login/login_username_gray.png',
width: 14.0,
height: 15.0,
),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: TextInputType.text,
controller: _usernameController,
textInputAction: TextInputAction.next,
focusNode: usernameFocusNode, //usernameFocusNode
onSubmitted: (text) {
print("current userName:" + text);
if (null == currentFocusNode) {
currentFocusNode = FocusScope.of(context);
}
currentFocusNode.requestFocus(passwordFocusNode);
}
);
}

虽然使用上看似没什么问题,但是整个TextField的继承代码难道你不觉得有更简洁的写法吗?

所以下面将讲解直接继承TextFiled的方法。

三、继承父类式封装(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/// 文本框(常用于:登录用户名、密码文本框)
/// 方法3:以继承 TextField 的方式实现
class LoginTextField extends TextField {
LoginTextField({
Key key,
String text,

String placeholder,

/// prefix icon
bool prefixIconSelected,
String prefixIconNormalImageName,
String prefixIconSelectedImageName,

bool autofocus = false,
bool obscureText = false,
TextInputType keyboardType,
TextEditingController controller,
bool showClear = false,
TextInputAction textInputAction,
FocusNode focusNode,
ValueChanged<String> onSubmitted,
}) : super(
key: key,
autofocus: autofocus,
obscureText: obscureText,
style: TextStyle(color: Colors.black, fontSize: 17.0),
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.0),
//labelText: "用户名",
hintText: placeholder,
//prefixIcon: Icon(Icons.person),
prefixIcon: new Image.asset(
!prefixIconSelected ? prefixIconNormalImageName :prefixIconSelectedImageName,
width: 14.0,
height: 15.0,
),
suffixIcon: !showClear ? null : clearButtonWithOnPressed(controller.clear),
enabledBorder: loginTextFieldDecorationBorder(),
focusedBorder: loginTextFieldDecorationBorder(),
),
keyboardType: keyboardType,
controller: controller,
textInputAction: textInputAction,
focusNode: focusNode,
onSubmitted: onSubmitted
);
}

/// selected Image
class SelectedImage extends Image {
SelectedImage({
Key key,
bool selected,
String normalImageName,
String selectedImageName,
}) :super (
key: key,
image: AssetImage(!selected ? normalImageName :selectedImageName)
);
}


/// 文本框border
InputBorder loginTextFieldDecorationBorder() {
return new OutlineInputBorder(
borderSide: new BorderSide(color: Color(0xffd2d2d2), width: 0.6),
borderRadius: new BorderRadius.circular(6.0)
);
}

使用时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 用户名文本框
LoginTextField userNameTextField() {
return LoginTextField(
placeholder: "用户名",
prefixIconSelected: userNameValid,
prefixIconNormalImageName: 'assets/images/login/login_username_gray.png',
prefixIconSelectedImageName: 'assets/images/login/login_username_blue.png',
autofocus: shouldAutofocusUserNameTextField,
keyboardType: TextInputType.text,
controller: _usernameController,
textInputAction: TextInputAction.next,
focusNode: usernameFocusNode,
onSubmitted: (text) {
print("current userName:" + text);
if (null == currentFocusNode) {
currentFocusNode = FocusScope.of(context);
}
currentFocusNode.requestFocus(passwordFocusNode);
});
}

可见使用继承父类式封装这种方式,不管在封装时候,还是在使用时候,写的代码都是最简洁的。而且后期如果要直接使用系统样式,也只需要改回类名,其他结构和属性都不用动即可

四、强调自定义类的设计规范

在前面,我们已经知道使用继承父类式封装这种方式,不管在封装时候,还是在使用时候,写的代码都是最简洁的。而且后期如果要直接使用系统样式,也只需要改回类名,其他结构和属性都不用动即可

所以,即使是你所定义的类只有一个入参,也一定要遵守使用继承父类式封装的设计规范。

以下以按钮中 textStyle 的传值为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:flutter_baseui_kit/flutter_baseui_kit.dart';

ThemeBGButton(
//width: 300, // 不设置会根据内容自适应
//height: 80, // 不设置会根据内容自适应
bgColorType: ThemeBGType.pink,
title: '红底白字的按钮',
//titleStyle: ButtonThemeUtil.PingFang_FontSize_Bold(18.0), // bad
titleStyle: ButtonBoldTextStyle(fontSize: 18.0), // good
cornerRadius: 20,
//enable: true, // 不设置,默认true
onPressed: () {},
),

附:bad 和 good 两种实现方式的代码分别如下:

bad:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:flutter/material.dart';

// 按钮上的文本样式(按钮上的文字颜色,已通过其他属性设置;不需要 TextStyle 中设置;其他类的文本需要在 TextStyle 设置文本颜色,所以此类最多只提供给按钮使用)
class ButtonThemeUtil {
// 类命名注意:系统有 ButtonTheme 类,别取重名,否则外部取不到

static TextStyle PingFang_FontSize_Medium(double fontSize) {
return TextStyle(
fontFamily: 'PingFang SC',
fontSize: fontSize,
fontWeight: FontWeight.w500,
);
}
}

good:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'package:flutter/material.dart';

// medium 的文本样式
class ButtonMediumTextStyle extends TextStyle {
final double fontSize;
// final Color color;

ButtonMediumTextStyle({
@required this.fontSize,
// this.color,
}) : assert(fontSize != null),
// assert(color != null),
super(
fontFamily: 'PingFang SC',
fontSize: fontSize,
fontWeight: FontWeight.w500,
// color: color,
);
}

所以,综上在Flutter中对于一个Widget的封装,我们采用直接继承其父类的方式来处理,且其具体的写法如上。

End

从登录页进入Flutter的开发规范

[TOC]

前言

良好的规范让你的代码不仅看上来更简洁,也让你的代码更加具有可读性,及大大的减少了后期维护的成本。

以下从几个方面谈谈我对Flutter良好的规范的理解。

一、页面Page规范前后效果对比

我们先从页面开始(以登录页的优化为例),然后逐步的从点到面铺开描述。

目标:页面布局、控件定义、事件处理分离。

1、演示的Page的UI图

用户名登录的UI图

2、规范前的效果

规范前:未进行任何处理的时候,你的代码是这样的。

LoginPage_widgetUnsplit

3、规范后的效果

规范后**:按规范处理后你代码的效果是这样的,

LoginPage_widgetSplit

可见通过处理后Page的整体代码更加简洁了。

二、页面Page开发规范的细则

1、规范一:将每个Widget的定义整理到单独的函数里

写:

Flutter规范_Widget_right

不写:

Flutter规范_Widget_wrong

2、规范二:将各个Widget组成界面的布局放到widgets()方法里处理

写法如下:

Flutter规范_Widgets布局_right

如此通过以上两步后,Widget build中的代码即会变为如下:

Flutter规范_WidgetBuild_right

如此通过以上几个拆分,我们的Page页面就显得很简洁和很好维护了。

3、规范三、Widget的封装

细心的你,可能发现了我们这里有一个系统没有的LoginTextField文本框类。是的,该类是我们自定义封装到Package后来使用的。

对于一个控件怎么封装,详情查看之前的文章《Flutter进阶(2):控件Widget的自定义与封装》

三、页面Page规范小结

  • 1、Widget build方法

build方法里尽量用最少的代码实现整体视图。如下,我们将登录页中的所有Widget通过loginWidgets()函数封装起来。

  • 2、各个Widget组成的视图布局

loginWidgets()方法里实现页面Widgets的布局。而对于Widget的定义为了不让页面布局控件定义因写在一堆,而造成代码一大坨。我们对每个Widget单独提供定义的函数,如userNameTextField()即为定义用户文本框的函数。

  • 3、抽离每个Widget的定义

  • 4、封装单个Widget

四、业务逻辑的开发规范

说完了页面上的Widgets的开发规范,下面就轮到页面上的业务逻辑的开发规范了。

拿登录页需要获取上次登录的账号来说,它的业务是getDefaultLoginAccountAction,下面以混编时候的项目为例

1、未去解耦业务时候的登录页

getDefaultLoginAccountAction在Page中的实现一般为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// LoginPage.dart 中的业务代码
static const callNativeMethodChannel = const MethodChannel('com.dvlproad.ciyouzen/callNativeLoginMethodChannel');
Future<Null> _getDefaultLoginAccountAction() async {
try {
final Map nativeResponse = await callNativeMethodChannel.invokeMethod('getDefaultLoginAccountAction');
final Map nativeResult = callNativeNativeResult(nativeResponse);

setState(() {
userName = nativeResult["userName"];
password = nativeResult["password"];
print(userName + ":" + password);
_usernameController.text = userName;
_passwordController.text = password;
});
} on PlatformException {}
}

相应的它的调用为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// LoginPage.dart 未去解耦时候的登录页
class MyLoginPage extends StatefulWidget {
MyLoginPage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyLoginPageState createState() => _MyLoginPageState();
}

class _MyLoginPageState extends State<MyLoginPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print("Login Page initState");

_getDefaultLoginAccountAction();
}

// other things
}

可见上述的业务处理getDefaultLoginAccountAction中,掺杂了对页面Page的setState操作,这不适合我们以后只对业务或者只对页面做修改的处理。所以下面我们将它们解耦。

2、解耦业务时候的登录页

Flutter的ViewModel

getDefaultLoginAccountAction在ChannelModel中的实现一般为:

1
2
3
4
5
6
7
8
9
10
11
/// LoginChannelModel.dart 文件
Future<Map<String, dynamic>> getDefaultLoginAccountAction() async {
try {
final Map nativeResponse = await callNativeMethodChannel.invokeMethod('getDefaultLoginAccountAction');
final Map nativeResult = callNativeNativeResult(nativeResponse);

return nativeResult;
} on PlatformException {
return null;
}
}

相应的它的调用为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// LoginPage.dart 解耦时候的登录页
class MyLoginPage extends StatefulWidget {
MyLoginPage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyLoginPageState createState() => _MyLoginPageState();
}

class _MyLoginPageState extends State<MyLoginPage> {
@override
void initState() {
// TODO: implement initState
super.initState();
print("Login Page initState");

getDefaultLoginAccountAction().then((nativeResult){
setState(() {
userName = nativeResult["userName"];
password = nativeResult["password"];
print(userName + ":" + password);
_usernameController.text = userName;
_passwordController.text = password;
});
});
}

// other things
}

五、Flutter Channel的数据规范

在使用Flutter与原生项目混编的时候,因为涉及到数据传输,所以我们有必要在前期就对传输数据的进行规范统一。