日期: 2021 年 7 月 19 日

iOS各种权限状态的获取及注意事项

前言

关于系统权限的获取,相信大家都不陌生,可是其中蕴含的知识确实不少。
怎样向用户索取权限是非常重要的。例如LBS类的应用,如果在索取权限时遭到用户的拒*,那么该应用基本等同于无用了,更坏的是,点击“不允许”是很轻松的,而要撤销这个决定则不太容易,用户至少需要以下五步,一次性成功获取权限的重要性不言而喻,


关于这一点,好的设置可以概括为这样:

除非当前确实需要,否则不要向用户索取权限。
索取权限时要让用户明确的了解授权后的好处是什么。

权限分类

  • 联网权限
  • 相册权限
  • 相机、麦克风权限
  • 定位权限
  • 推送权限
  • 通讯录权限
  • 日历、备忘录权限

联网权限

引入头文件 @import CoreTelephony;
应用启动后,检测应用中是否有联网权限

  1. typedef NS_ENUM(NSUInteger, CTCellularDataRestrictedState) {
  2. kCTCellularDataRestrictedStateUnknown,//权限未知
  3. kCTCellularDataRestricted,//权限被关闭,
  4. kCTCellularDataNotRestricted//权限开启
  5. };
  6. 使用时需要注意的关键点:
  7. CTCellularData 只能检测蜂窝权限,不能检测WiFi权限。
  8. 一个CTCellularData实例新建时,restrictedState是kCTCellularDataRestrictedStateUnknown,
  9. 之后在cellularDataRestrictionDidUpdateNotifier里会有一次回调,此时才能获取到正确的权限状态。
  10. 当用户在设置里更改了app的权限时,cellularDataRestrictionDidUpdateNotifier会收到回调,如果要停止监听,
  11. 必须将cellularDataRestrictionDidUpdateNotifier设置为nil
  12. 赋值给cellularDataRestrictionDidUpdateNotifier的block并不会自动释放,
  13. 即便你给一个局部变量的CTCellularData实例设置监听,当权限更改时,还是会收到回调,所以记得将block置nil
  14. CTCellularData *cellularData = [[CTCellularData alloc]init];
  15. cellularData.cellularDataRestrictionDidUpdateNotifier = ^(CTCellularDataRestrictedState state)
  16. { //获取联网状态 switch (state)
  17. {
  18. case kCTCellularDataRestricted: NSLog(@”Restricrted”); break;
  19. case kCTCellularDataNotRestricted: NSLog(@”Not Restricted”); break;
  20. //未知,*次请求
  21. case kCTCellularDataRestrictedStateUnknown: NSLog(@”Unknown”); break;
  22. default: break;
  23. };
  24. };

查询应用是否有联网功能

  1. CTCellularData *cellularData = [[CTCellularData alloc]init];
  2. CTCellularDataRestrictedState state = cellularData.restrictedState;
  3. switch (state) {
  4. case kCTCellularDataRestricted: NSLog(@”Restricrted”); break;
  5. case kCTCellularDataNotRestricted: NSLog(@”Not Restricted”); break;
  6. case kCTCellularDataRestrictedStateUnknown: NSLog(@”Unknown”); break; default: break;
  7. }

注意:当应用被设置为不联网,使用的时候,系统会自动弹出警告“xxxx 已被关闭网络”点击可以去设置,自动跳转到设置中心里。
iOS10 国行机*次安装App时会有一个权限弹框弹出,在允许之前是没有网络的,网上对于现状已有描述和解决方法:
(1)在引导页中诱导出网络权限弹框,这样就不会影响到之后应用的网络请求。
(2)允许用户手动重新请求。出现数据空白时,如果在空白页面上有“重新加载”的按钮。
(3) 允许用户手动重新请求。出现数据空白时,如果在空白页面上有“重新加载”的按钮。

相册权限–iOS 9.0之前

导入头文件@import AssetsLibrary;
检查是否有相册权限

  1. ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
  2. switch (status) {
  3. case ALAuthorizationStatusAuthorized: NSLog(@“Authorized”); break;
  4. case ALAuthorizationStatusDenied: NSLog(@“Denied”); break;
  5. case ALAuthorizationStatusNotDetermined: NSLog(@“not Determined”); break;
  6. case ALAuthorizationStatusRestricted: NSLog(@“Restricted”); break; default: break;
  7. }

相册权限–iOS 8.0之后

导入头文件@import Photos;
检查是否有相册权限

  1. PHAuthorizationStatus photoAuthorStatus = [PHPhotoLibrary authorizationStatus];
  2. switch (photoAuthorStatus) {
  3. case PHAuthorizationStatusAuthorized: NSLog(@“Authorized”); break;
  4. case PHAuthorizationStatusDenied: NSLog(@“Denied”); break;
  5. case PHAuthorizationStatusNotDetermined: NSLog(@“not Determined”); break;
  6. case PHAuthorizationStatusRestricted: NSLog(@“Restricted”); break; default: break;}

获取相册权限

  1. [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
  2. if (status == PHAuthorizationStatusAuthorized)
  3. { NSLog(@“Authorized”); }
  4. else{ NSLog(@“Denied or Restricted”);
  5. } }];

相机和麦克风权限

导入头文件@import AVFoundation;
检查是否有相机或麦克风权限

  1. AVAuthorizationStatus AVstatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];//相机权限
  2. AVAuthorizationStatus AVstatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];//麦克风权限
  3. switch (AVstatus) {
  4. //允许状态
  5. case AVAuthorizationStatusAuthorized: NSLog(@”Authorized”); break;
  6. //不允许状态,可以弹出一个alertview提示用户在隐私设置中开启权限
  7. case AVAuthorizationStatusDenied: NSLog(@”Denied”); break;
  8. //未知,*次申请权限
  9. case AVAuthorizationStatusNotDetermined: NSLog(@”not Determined”); break;
  10. //此应用程序没有被授权访问,可能是家长控制权限
  11. case AVAuthorizationStatusRestricted: NSLog(@”Restricted”); break; default: break;
  12. }

获取相机或麦克风权限

  1. [AVCaptureDevice requestAccessForMediaType:
  2. AVMediaTypeVideo completionHandler:^(BOOL granted) {//相机权限
  3. if (granted) { NSLog(@“Authorized”); }
  4. else{ NSLog(@“Denied or Restricted”); }}];
  5. [AVCaptureDevice requestAccessForMediaType:
  6. AVMediaTypeAudio completionHandler:^(BOOL granted)
  7. {//麦克风权限
  8. if (granted) { NSLog(@“Authorized”); }
  9. else{ NSLog(@“Denied or Restricted”);
  10. }}];

定位权限

导入头文件@import CoreLocation;
由于iOS8.0之后定位方法的改变,需要在info.plist中进行配置;

配置文件

检查是否有定位权限

  1. BOOL isLocation = [CLLocationManager locationServicesEnabled];
  2. if (!isLocation) { NSLog(@”not turn on the location”);}
  3. CLAuthorizationStatus CLstatus = [CLLocationManager authorizationStatus];
  4. switch (CLstatus) {
  5. case kCLAuthorizationStatusAuthorizedAlways: NSLog(@”Always Authorized”); break;
  6. case kCLAuthorizationStatusAuthorizedWhenInUse: NSLog(@”AuthorizedWhenInUse”); break;
  7. case kCLAuthorizationStatusDenied: NSLog(@”Denied”); break;
  8. case kCLAuthorizationStatusNotDetermined: NSLog(@”not Determined”); break;
  9. case kCLAuthorizationStatusRestricted: NSLog(@”Restricted”); break; default: break;
  10. }

获取定位权限

  1. 这里有一个细节要注意, CLLocationManager 实例必须是全局的变量,否则授权提示弹框会一闪而过,不会一直显示。
  2. manager = [[CLLocationManager alloc] init];
  3. manager.delegate= self;
  4. [manager requestAlwaysAuthorization];//一直获取定位信息
  5. [manager requestWhenInUseAuthorization];//使用的时候获取定位信息

在代理方法中查看权限是否改变

  1. – (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
  2. {
  3. switch (status) {
  4. case kCLAuthorizationStatusAuthorizedAlways: NSLog(@“Always Authorized”); break;
  5. case kCLAuthorizationStatusAuthorizedWhenInUse: NSLog(@“AuthorizedWhenInUse”); break;
  6. case kCLAuthorizationStatusDenied: NSLog(@“Denied”); break;
  7. case kCLAuthorizationStatusNotDetermined: NSLog(@“not Determined”); break;
  8. case kCLAuthorizationStatusRestricted: NSLog(@“Restricted”); break; default: break;
  9. }}

推送权限

检查是否有通讯权限

  1. UIUserNotificationSettings *settings = [[UIApplication sharedApplication] currentUserNotificationSettings];
  2. switch (settings.types) {
  3. case UIUserNotificationTypeNone: NSLog(@”None”); break;
  4. case UIUserNotificationTypeAlert: NSLog(@”Alert Notification”); break;
  5. case UIUserNotificationTypeBadge: NSLog(@”Badge Notification”); break;
  6. case UIUserNotificationTypeSound: NSLog(@”sound Notification'”); break; default: break;
  7. }

获取推送权限

  1. UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge categories:nil];
  2. [[UIApplication sharedApplication] registerUserNotificationSettings:setting];

通讯录权限

iOS9.0之前
导入头文件 @import AddressBook;
检查是否有通讯录权限

  1. ABAuthorizationStatus ABstatus = ABAddressBookGetAuthorizationStatus();
  2. switch (ABstatus) {
  3. case kABAuthorizationStatusAuthorized: NSLog(@”Authorized”); break;
  4. case kABAuthorizationStatusDenied: NSLog(@”Denied'”); break;
  5. case kABAuthorizationStatusNotDetermined: NSLog(@”not Determined”); break;
  6. case kABAuthorizationStatusRestricted: NSLog(@”Restricted”); break; default: break;
  7. }
  8. 获取通讯录权限
  9. ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
  10. ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
  11. if (granted)
  12. { NSLog(@”Authorized”);
  13. CFRelease(addressBook);
  14. }else{ NSLog(@”Denied or Restricted”);
  15. }});
  16. iOS9.0及以后
  17. 导入头文件 **@import Contacts;**
  18. 检查是否有通讯录权限
  19. CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
  20. switch (status) {
  21. case CNAuthorizationStatusAuthorized: { NSLog(@”Authorized:”); } break;
  22. case CNAuthorizationStatusDenied:{ NSLog(@”Denied”); } break;
  23. case CNAuthorizationStatusRestricted:{ NSLog(@”Restricted”); } break;
  24. case CNAuthorizationStatusNotDetermined:{ NSLog(@”NotDetermined”); } break;
  25. }
  26. 查询是否获取通讯录权限
  27. CNContactStore *contactStore = [[CNContactStore alloc] init]; [contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  28. if (granted) { NSLog(@”Authorized”); }
  29. else{ NSLog(@”Denied or Restricted”); }
  30. }];

日历、备忘录权限

导入头文件
检查是否有日历或者备忘录权限

  1. typedef NS_ENUM(NSUInteger, EKEntityType) { EKEntityTypeEvent,//日历 EKEntityTypeReminder //备忘 };
  2. EKAuthorizationStatus EKstatus = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent];
  3. switch (EKstatus) {
  4. case EKAuthorizationStatusAuthorized: NSLog(@”Authorized”); break;
  5. case EKAuthorizationStatusDenied: NSLog(@”Denied'”); break;
  6. case EKAuthorizationStatusNotDetermined: NSLog(@”not Determined”); break;
  7. case EKAuthorizationStatusRestricted: NSLog(@”Restricted”);
  8. break;
  9. default: break;
  10. }
  11. 查询是否获取日历或备忘录权限
  12. EKEventStore *store = [[EKEventStore alloc]init];
  13. [store requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError * _Nullable error) {
  14. if (granted) {
  15. NSLog(@”Authorized”); }
  16. else{ NSLog(@”Denied or Restricted”); }

ios 权限提示语_iOS中各个权限功能提示弹框

1. 麦克风权限

单存检测是否有麦克风权限,并不会弹出是否允许弹出权限提示框

#import

/**

判断当前是有语音权限,但是不会弹出是否允许弹出权限

(需要在info中配置)Privacy – Microphone Usage Description 允许**访问您的语音,来用于**功能?

@return YES:有权限,NO:没权限

*/

– (BOOL)JX_Device_Permission_AudioAuth {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];

if (authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted) {
return NO;

}

return YES;

}复制代码

检测是否有权限,如果没有授权过,会弹出是否允许提示框

#import

/**

判断当前是有语音权限,会弹出是否允许弹出权限

(需要在info中配置)Privacy – Microphone Usage Description 允许**访问您的语音,来用于**功能?

*/

– (void)JX_Device_Permission_Check_AudioAuth {
AVAudioSession *session = [AVAudioSession sharedInstance];

if ([session respondsToSelector:@selector(requestRecordPermission:)]){
[session performSelector:@selector(requestRecordPermission:) withObject:^(BOOL granted) {
// do something

}];

}

}复制代码

2. 访问相册权限

检测是否有访问相册权限,并不会弹出是否允许访问相册权限提示框

#import

#import

/**

判断相册权限开关,但是不会弹出是否允许弹出权限

(需要在info中配置)Privacy – Photo Library Additions Usage Description 允许**访问您的相册,来用于**功能

@return YES:有权限,NO:没权限

*/

– (BOOL)JX_Device_Permission_PhotoLibraryAuth {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
PHAuthorizationStatus authStatus = [PHPhotoLibrary authorizationStatus];

if(authStatus == PHAuthorizationStatusDenied || authStatus == PHAuthorizationStatusRestricted) {
return NO;

}

} else if ([[UIDevice currentDevice].systemVersion floatValue] >= 6.0 && [[UIDevice currentDevice].systemVersion floatValue] < 8.0) {
ALAuthorizationStatus authStatus = [ALAssetsLibrary authorizationStatus];

if(authStatus == ALAuthorizationStatusDenied || authStatus == ALAuthorizationStatusRestricted) {
return NO;

}

}

return YES;

}复制代码

检测是否有权限,如果没有授权过,会弹出是否允许提示框

#import

/**

判断相册权限开关,会弹出是否允许弹出权限

(需要在info中配置)Privacy – Photo Library Additions Usage Description 允许**访问您的相册,来用于**功能

*/

– (void)JX_Device_Permission_Check_PhotoLibraryAuth{
BOOL auth = [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary];

if (!auth) return;

[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { //弹出访问权限提示框

if (status == PHAuthorizationStatusAuthorized) { // 有权限

dispatch_async(dispatch_get_main_queue(),^{
// do something

// 一般操作

self.imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;

[self presentViewController:self.imagePickerController animated:YES completion:nil];

});

} else {
dispatch_async(dispatch_get_main_queue(),^{ // 无权限

// do something

});

}

}];

}复制代码

3. 访问相机权限

检测是否有相机拍照权限,并不会弹出是否允许提示框

#import

/**

判断相机权限开关,但是不会弹出是否允许弹出权限

(需要在info中配置)Privacy – Camera Usage Description 允许**访问您的相机,来用于**功能

@return YES:有权限,NO:没权限

*/

– (BOOL)JX_Device_Permission_CameraAuth {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];

if (authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted) {
return NO;

}

return YES;

}复制代码

检测是否有过授权,如果没有授权过,会弹出是否允许提示框

#import

/**

判断相机权限开关,会弹出是否允许弹出权限

(需要在info中配置)Privacy – Camera Usage Description 允许**访问您的相机,来用于**功能

*/

– (void)JX_Device_Permission_Check_CameraAuth {
BOOL auth = [UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera];

if (!auth) permission(NO);

NSString *mediaType = AVMediaTypeVideo;//读取媒体类型

[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(),^{
if (granted) { // 授权成功

// do something

// 一般会做的操作,跳转到系统的相机

self.imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera;

[self presentViewController:self.imagePickerController animated:YES completion:nil];

} else { // 拒*授权

// do something

}

});

}];

}复制代码

4. 推送权限(远程、本地)

检测是否有推送权限,并不会弹出是否允许提示框

/**

推送权限开关

@return YES:有权限,NO:没权限

*/

– (BOOL)JX_Device_Permission_NotificationAuth {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0f) {
UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];

if (UIUserNotificationTypeNone == setting.types) {
return NO;

}

} else {
UIRemoteNotificationType type = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];

if(UIRemoteNotificationTypeNone == type){
return NO;

}

}

return YES;

}复制代码

检测是否有过授权,如果没有授权过,会弹出是否允许提示框

说明:这里仅仅是检测是否授权过,弹出提示框操作。至于远程注册一些流程请参见另一篇博客。iOS 通知权限(远程通知、本地通知)

#ifdef NSFoundationVersionNumber_iOS_9_x_Max

#import

#endif

/**

判断通知权限开关,会弹出是否允许弹出权限(远程、本地)

*/

– (void)JX_Device_Permission_Check_NotificationAuth {
if ([[UIDevice currentDevice].systemVersion floatValue] >= 10.0) {
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
// do something

// 对granted 进行判断,是否允许权限

});

}];

} else if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
UIUserNotificationSettings *setting = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge categories:nil];

[[UIApplication sharedApplication] registerUserNotificationSettings:setting];

BOOL auth = [self JX_Device_Permission_NotificationAuth];

// 对auth 进行判断,是否允许权限

}

}复制代码

ios 权限提示语_iOS11权限变化,权限都要弹框提示,而且需要说出功能用途

iOS11访问权限列表

隐私数据对应key值提示语

相册(读)NSPhotoLibraryUsageDescription”XXX”需要您的同意,才能访问相册

相册(写)NSPhotoLibraryAddUsageDescription”XXX”需要您的同意,才能添加照片

相机NSCameraUsageDescription”XXX”需要您的同意,才能访问相机

麦克风NSMicrophoneUsageDescription”XXX”需要您的同意,才能访问麦克风

位置NSLocationUsageDescription”XXX”需要您的同意,才能访问位置

在使用期间访问位置NSLocationWhenInUseUsageDescription”XXX”需要您的同意,才能在试用期间访问位置

始终访问位置NSLocationAlwaysUsageDescription”XXX”需要您的同意,才能始终访问位置

NSLocationAlwaysAndWhenInUseUsageDescription 申请Always权限,以便应用在前台和后台(suspend 或 terminated)都可以获取到更新的位置数据

日历NSCalendarsUsageDescription”XXX”需要您的同意,才能访问日历

提醒事项NSRemindersUsageDescription”XXX”需要您的同意,才能访问提醒事项

运动与健身NSMotionUsageDescription”XXX”需要您的同意,才能访问运动与健身

健康更新NSHealthUpdateUsageDescription”XXX”需要您的同意,才能访问健康更新

健康分享NSHealthShareUsageDescription”XXX”需要您的同意,才能访问健康分享

蓝牙NSBluetoothPeripheralUsageDescription”XXX”需要您的同意,才能访问蓝牙

媒体资料库NSAppleMusicUsageDescription”XXX”需要您的同意,才能访问媒体资料库

在iOS11中访问相册权限

iOS11以前:

NSPhotoLibraryUsageDescription:访问相册和存储照片到相册(读写),会出现用户授权。

iOS11之后:

NSPhotoLibraryUsageDescription:无需添加。默认开启访问相册权限(读),无需用户授权。

NSPhotoLibraryAddUsageDescription: 添加内容到相册。(写),会出现用户授权。

iOS11中地理定位权限

iOS 8 – iOS 10 版本:

NSLocationWhenInUseUsageDescription 表示应用在前台的时候可以搜到更新的位置信息。(在使用期间访问位置)

NSLocationAlwaysUsageDescription 申请Always权限,以便应用在前台和后台(suspend 或 terminated)都可以获取到更新的位置数据。(始终访问位置)

NSLocationUsageDescription 位置

iOS 11 版本:

NSLocationAlwaysAndWhenInUseUsageDescription 申请Always权限,以便应用在前台和后台(suspend 或 terminated)都可以获取到更新的位置数据(NSLocationWhenInUseUsageDescription 也必须有)

在plist文件中修改访问相册和地位定位权限,必须兼容老版本iOS系统.

权限都要弹框提示隐私数据对应key值提示语必须说出指明用途,否则上线会被拒*

 

iOS11隐私权限变动

iOS11隐私信息访问列表

iOS11访问权限列表

隐私数据 对应key值 提示语
相册(读) NSPhotoLibraryUsageDescription “XXX”需要您的同意,才能访问相册
相册(写) NSPhotoLibraryAddUsageDescription “XXX”需要您的同意,才能添加照片
相机 NSCameraUsageDescription “XXX”需要您的同意,才能访问相机
麦克风 NSMicrophoneUsageDescription “XXX”需要您的同意,才能访问麦克风
位置 NSLocationUsageDescription “XXX”需要您的同意,才能访问位置
在使用期间访问位置 NSLocationWhenInUseUsageDescription “XXX”需要您的同意,才能在试用期间访问位置
始终访问位置 NSLocationAlwaysUsageDescription “XXX”需要您的同意,才能始终访问位置
日历 NSCalendarsUsageDescription “XXX”需要您的同意,才能访问日历
提醒事项 NSRemindersUsageDescription “XXX”需要您的同意,才能访问提醒事项
运动与健身 NSMotionUsageDescription “XXX”需要您的同意,才能访问运动与健身
健康更新 NSHealthUpdateUsageDescription “XXX”需要您的同意,才能访问健康更新
健康分享 NSHealthShareUsageDescription “XXX”需要您的同意,才能访问健康分享
蓝牙 NSBluetoothPeripheralUsageDescription “XXX”需要您的同意,才能访问蓝牙
媒体资料库 NSAppleMusicUsageDescription “XXX”需要您的同意,才能访问媒体

iOS电池条颜色修改

订阅专栏
iOS9之前:
[[UIApplicationsharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];

但9之后这种方式就不行了。。怎么设置呢?
1. [ UINavigationBar appearance ] setBarStyle : UIBarStyleBlack ]; // 电池条,白色
2. 在plist文件中,加入这么一行:

%title插图%num

然后再运行,你会发现电池条变成白色了哦Y(^_^)Y

iOS基础控件的学习

1.UIControl 控制视图的事件处理(控件中会使用到)

基于触摸 事件处理:

UIControlEventTouchDown 用户按下时触发

UIControlEventTouchDownRepeat 点击计数大于1时触发

UIControlEventTouchDragInside 当触摸在控件内拖动时触发

UIControlEventTouchDragOutside 当触摸在控件之外拖动时触发
UIControlEventTouchDragEnter 当触摸从控件之外拖动到控件内部时触发
UIControlEventTouchDragExit 当触摸从控件之内拖动到控件外部时触发
UIControlEventTouchUpInside 控件之内触摸抬起时触发
UIControlEventTouchUpOutside 控件之外触摸抬起时触发
UIControlEventTouchCancel 触摸取消事件,设备被上锁(锁屏)或者电话呼叫打断

UIControlEventTouchValueChanged 当控件的值发生改变时。用于滑块、分段控件等控件(UISliser UISegmentControl UIPageControl )

基于编辑 事件处理:

UIControlEventEditingDidBegin 文本控件中开始编辑时
UIControlEventEditingChanged 文本控件中的文本被改变时
UIControlEventEditingDidEnd 文本控件中编辑结束时
UIControlEventEditingDidOnExit 文本控件内通过按下回车键结束编辑时

UIControlEventAllTouchEvents 所有触摸事件
UIControlEventAllEditingEvents 文本编辑的所有事件

UIControlEventAllEvents 所有事件

2.button

巧用button的selected方法,可以把按钮设置成开关一样。

if ( button.selected ) {
button.selected = NO;

}else{
button.selected = YES;

}

3.UIImageView

在imageView上加子控件时,记得为imageview设置一个属性,imageView.userInteractionEnable = YES;

4.UITextField

常用属性:

placeholder 提示用户输入内容文本

borderStyle 设置风格,默认没有风格,需要设置

clearsOnBeginEditing 用户编辑时是否clear内容,默认为NO

adjustsFontSizeToFitWidth 自适应调整字体大小,默认为NO

(UIImage)background 设置背景,需要将textField实例的风格设置为None

(UIImage)disabledBackground 设置textField不可用时的背景图片

(Bool)editing 设置是否可编辑

clearButtonMode 清除按钮的模式,默认不出现

leftView 自定义左视图

leftViewMode 自定义左视图出现的模式

inputView 不用系统键盘,自定义键盘

inputAccessoryView 系统键盘和自定义键盘共存

autocapitalizationType 自动大写类型

autocorrectionType 检查拼写是否正确

keyboardType 修改键盘类型

returnKeyType 修改返回类型

secureTextEntry 是否安全输入,比如用户输入密码

太麻烦了,直接上代码:

#import “ViewController.h”

#import “AlertView.h”

@interface ViewController ()<UIAlertViewDelegate,UIActionSheetDelegate,UITextFieldDelegate>

@end

@implementation ViewController

– (void)viewDidLoad {
[superviewDidLoad];

// 1.AlertView

UIButton *button1 = [UIButtonbuttonWithType:UIButtonTypeRoundedRect];

button1.frame =CGRectMake(10, 60, 140, 40);

button1.backgroundColor = [UIColorredColor];

[button1 setTitle:@”show Alert View”forState:UIControlStateNormal];

[button1 addTarget:selfaction:@selector(showAlertView)forControlEvents:UIControlEventTouchUpInside];

[self.viewaddSubview:button1];

// 2.ActionSheet

UIButton *button2 = [UIButtonbuttonWithType:UIButtonTypeRoundedRect];

button2.frame =CGRectMake(160, 60, 140, 40);

button2.backgroundColor = [UIColorredColor];

[button2 setTitle:@”show Action Sheet”forState:UIControlStateNormal];

[button2 addTarget:selfaction:@selector(showActionView)forControlEvents:UIControlEventTouchUpInside];

[self.viewaddSubview:button2];

// 3.进度环

UIActivityIndicatorView *activityView = [[UIActivityIndicatorViewalloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];

activityView.center =CGPointMake(60, 160);

activityView.color = [UIColorredColor];

activityView.hidesWhenStopped =NO;//停止转动后是否消失,,默认是YES

[activityViewstartAnimating];

[self.viewaddSubview:activityView];

[NSTimer scheduledTimerWithTimeInterval:3 target:selfselector:@selector(timerTest:)userInfo:activityView repeats:NO];

// 4.滑动条UISlider

UISlider *slider = [[UISlideralloc]initWithFrame:CGRectMake(30, 210, 230, 20)];//高度是默认的,你设置的没用,没法设置它

slider.value = 0.5; //设置当前值

slider.maximumValue = 1; //设置*大值

slider.minimumValue = 0; //设置*小值

slider.tintColor = [UIColorredColor];//设置颜色

[slider addTarget:selfaction:@selector(sliderAction:)forControlEvents:UIControlEventValueChanged];

[self.viewaddSubview:slider];

[NSTimer scheduledTimerWithTimeInterval:1 target:selfselector:@selector(sliderTest:)userInfo:slider repeats:YES];

// 5.分段控件UISegmentControl

NSArray *array = @[@”aaa”,@”bbb”,@”ccc”];

UISegmentedControl *segmented = [[UISegmentedControlalloc]initWithItems:array];

segmented.frame =CGRectMake(50, 260,300,40);

segmented.selectedSegmentIndex = 1;

segmented.tintColor = [UIColorredColor];//设置颜色

[segmented addTarget:selfaction:@selector(segmentAction:)forControlEvents:UIControlEventValueChanged];

[self.viewaddSubview:segmented];

[NSTimer scheduledTimerWithTimeInterval:3 target:selfselector:@selector(segmentTest:)userInfo:segmented repeats:YES];

// 6.UIPageControl分页控制(通常与UIScrollView连用,提示用户当前显示的页数)

UIPageControl *pageControl = [[UIPageControlalloc]initWithFrame:CGRectMake(60, 320, 100, 40)];

// pageControl.backgroundColor = [UIColor grayColor];//灰色背景

pageControl.numberOfPages = 10;

pageControl.currentPage = 1;

pageControl.currentPageIndicatorTintColor = [UIColorredColor];

pageControl.PageIndicatorTintColor = [UIColorgrayColor];

[pageControl addTarget:selfaction:@selector(pageAction:)forControlEvents:UIControlEventValueChanged];

[self.viewaddSubview:pageControl];

// 7.UITextField

UITextField *tf = [[UITextFieldalloc]initWithFrame:CGRectMake(60, 380, 200, 40)];

tf.tag = 110;

tf.delegate =self;

tf.textColor = [UIColorredColor];

tf.placeholder =@”用来提示用户”;

// tf.adjustsFontSizeToFitWidth = YES;

// tf.clearsOnBeginEditing = YES;//在编辑之前先清空内容

tf.clearButtonMode =UITextFieldViewModeWhileEditing;//当正在编辑的时候显示*右边的清除按钮

tf.borderStyle =UITextBorderStyleRoundedRect;//圆角

// tf.keyboardType = UIKeyboardTypeNumberPad;//设置键盘type。数字键盘

// tf.secureTextEntry = YES;//设置密码

tf.returnKeyType =UIReturnKeySearch;//设置键盘回车键的类型(搜索)

//自定义左视图

UIView *view = [[UIViewalloc]initWithFrame:CGRectMake(0, 0, 20, 20)];

view.backgroundColor = [UIColorredColor];

tf.leftView = view;

tf.leftViewMode =UITextFieldViewModeAlways;

// 自定义系统键盘

UIView *scView = [[UIViewalloc]initWithFrame:CGRectMake(0, 0, 320, 44)];

scView.backgroundColor = [UIColorredColor];

tf.inputAccessoryView = scView;

// tf.inputView = scView;

[self.viewaddSubview:tf];

// 8.UILable

UILabel *label = [[UILabelalloc]initWithFrame:CGRectMake(60, 450, 200, 40)];

label.text =@”abndnfkdabndnfkdfabndnfkdfabndnfkdff”;//文本

label.textColor = [UIColorredColor];//文本颜色

label.textAlignment =NSTextAlignmentCenter;//对齐方式

label.backgroundColor = [UIColorwhiteColor];//背景色

// label.shadowColor = [UIColor blueColor];//偏移颜色

// label.shadowOffset = CGSizeMake(-1, 1);//偏移量

// label.font = [UIFont fontWithName:<#(NSString *)#> size:<#(CGFloat)#>]//字体

label.font = [UIFontsystemFontOfSize:14];

label.numberOfLines = 1;//*多显示几行

label.lineBreakMode =NSLineBreakByTruncatingMiddle;//文本太多显示不完时,在哪里打省略号

[label sizeToFit];根据文本自动调整label的宽度和高度

[self.viewaddSubview:label];

}

#pragma mark – 7.UITextFieldDelegate

– (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
NSLog(@”ShouldBeginEditing”);

// [textField becomeFirstResponder];

return YES;

}

– (void)textFieldDidBeginEditing:(UITextField *)textField{
NSLog(@”DidBeginEditing”);

}

– (BOOL)textFieldShouldEndEditing:(UITextField *)textField{
NSLog(@”ShouldEndEditing”);

return YES;

}

– (void)textFieldDidEndEditing:(UITextField *)textField{
NSLog(@”DidEndEditing”);

}

– (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{
NSLog(@”houldChangeCharactersInRange”);

NSLog(@”::%@”,string);

return YES;

}

– (BOOL)textFieldShouldClear:(UITextField *)textField{
NSLog(@”ShouldClear”);

return YES;

}

– (BOOL)textFieldShouldReturn:(UITextField *)textField{
NSLog(@”搜索。。。”);

// 执行用户按下键盘上的return时

[textFieldendEditing:YES];

return YES;

}

#pragma mark – 6.pageControl

-(void)pageAction:(UIPageControl*)pageControl{
NSLog(@”page Control:%ld”,(long)pageControl.currentPage);

}

#pragma mark – 5.UISlider

-(void)segmentAction:(UISegmentedControl*)segment{
NSLog(@”segment value:%ld”,(long)segment.selectedSegmentIndex);

}

//定时器设置的,将会每三秒执行一次

-(void)segmentTest:(NSTimer*)timer{
UISegmentedControl *segment = [timeruserInfo];

[segment insertSegmentWithTitle:@”add”atIndex:1 animated:YES];

}

#pragma mark – 4.滑动条,一秒钟切换当前值

-(void)sliderTest:(NSTimer*)timer{
UISlider *slider = [timer userInfo];

[slidersetValue:0.8 animated:YES];

}

//值发生改变 事件

-(void)sliderAction:(UISlider*)slider{
NSLog(@”slider.value:%f”,slider.value);

}

#pragma mark – 3.定时器,三秒钟后停止activity的转动

-(void)timerTest:(NSTimer*)timer{
UIActivityIndicatorView *activityview = [timeruserInfo];

[activityviewstopAnimating];

}

#pragma mark – 1.button1:AlertView

int identifier = 1;

-(void)showAlertView{

// UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@”Alert View” message:@”message” delegate:nil cancelButtonTitle:@”sure” otherButtonTitles:@”cancel”,@”other”, nil];

// [alertView show];

//自定义alertView

AlertView *alertView = [[AlertViewalloc]initWithTitle:@”Alert View”message:@”message”delegate:selfcancelButtonTitle:@”sure”otherButtonTitles:@”cancel”,@”other”,nil];

[alertViewshow];

alertView.tag =identifier;

identifier ++;

}

#pragma mark alertView的代理方法,用于处理警告框按钮点击事件

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
NSLog(@”alert button:%ld”,(long)buttonIndex);

}

#pragma mark – 2.button2:ActionSheet

-(void)showActionView{

UIActionSheet *actionSheet = [[UIActionSheetalloc]initWithTitle:@”title”delegate:selfcancelButtonTitle:@”cancel”destructiveButtonTitle:@”destructive”otherButtonTitles:@”other button”,@”other button”,@”other button”, nil];

[actionSheetshowInView:self.view];

}

#pragma mark – actionSheet的代理方法,用于处理按钮点击事件

– (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
NSLog(@”alert button:%ld”,(long)buttonIndex);

}

– (void)didReceiveMemoryWarning {
[superdidReceiveMemoryWarning];

// Dispose of any resources that can be recreated.

}

@end

 

IOS代码实现常用控件UIButton、UISlider、UISwitch、UISegmentedControl

%title插图%num
IOS中*常用到的控件UIButton、UISlider、UISwitch、UISegmentedControl通过Xib文件拖动生成非常简单,其实用代码实现也是一样的简单,当然,用代码实现能够掌握到更多的东西。
上图中包涵提到的4种控件,UIButton按钮、UISlider滑块、UISwitch开关、UISegmentedControl分类

首先创建一个名为CodeControls的Empty Application项目

%title插图%num
AppDelegate.h和AppDelegate.m文件中和IOS代码实现Hello World中的一样

MainViewController.h

#import <UIKit/UIKit.h>

@interface MainViewController : UIViewController

@property (strong, nonatomic) UIButton *myBtn;
@property (strong, nonatomic) UISlider *mySlider;
@property (strong, nonatomic) UISwitch *mySwitch;
@property (strong, nonatomic) UISegmentedControl *mySc;

@end

MainViewController.m

#import “MainViewController.h”

@interface MainViewController ()
@end
@implementation MainViewController
@synthesize myBtn,mySlider,mySwitch,mySc;

– (void)viewDidLoad
{
// 加载UIView
UIView *mainView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
mainView.backgroundColor = [UIColor whiteColor];
self.view = mainView;
[mainView release];

// 创建一个Button按钮
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
btn.frame = CGRectMake(100, 30, 57, 57);
[btn setTitle:@”Button” forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[btn setBackgroundImage:[UIImage imageNamed:@”icon.png”] forState:UIControlStateNormal];
[btn addTarget:self action:@selector(onClick:) forControlEvents:UIControlEventTouchUpInside];
myBtn = btn;
[self.view addSubview:myBtn];

// 创建一个Slider划块按钮
UISlider *slider = [[[UISlider alloc] initWithFrame:CGRectMake(50, 180, 200, 10)] autorelease];
slider.minimumValue = 0.0f;
slider.maximumValue = 100.0f;
slider.value = 50.0f;
[slider addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventTouchUpInside];
mySlider = slider;
[self.view addSubview:mySlider];

// 创建一个UISwitch开关按钮
UISwitch *sbtn = [[[UISwitch alloc] initWithFrame:CGRectMake(50, 210, 200, 50)] autorelease];
[sbtn addTarget:self action:@selector(onSwitch:) forControlEvents:UIControlEventTouchUpInside];
mySwitch = sbtn;
[self.view addSubview:mySwitch];

// 创建一个UISegmentedControl
NSArray *btnList = [NSArray arrayWithObjects:@”left”,@”center”,@”right”, nil];
UISegmentedControl *sc = [[[UISegmentedControl alloc] initWithItems:btnList] autorelease];
sc.frame = CGRectMake(50, 250, 200, 60);
[sc addTarget:self action:@selector(onSelect:) forControlEvents:UIControlEventTouchUpInside];
mySc = sc;
[self.view addSubview:mySc];

[super viewDidLoad];
}

// 点击Button触发
– (void)onClick:(id *)sender
{

}

// 拉动Slider划块触发
– (void)onChange:(id *)sender
{

}

// 选择Switch触发
– (void)onSwitch:(id *)sender
{

}

// 选择UISegmentedControl触发
– (void)onSelect:(id *)sender
{
}
这里没有写点击每个控件的具体实现方法。

UICnotrol Class 下的所有Touch事件

UIControlEventTouchDown
UIControlEventTouchDownRepeat
UIControlEventTouchDragInside
UIControlEventTouchDragOutside
UIControlEventTouchDragEnter
UIControlEventTouchDragExit
UIControlEventTouchUpInside
UIControlEventTouchUpOutside
UIControlEventTouchCancel
UIControlEventValueChanged
UIControlEventEditingDidBegin
UIControlEventEditingChanged
UIControlEventEditingDidEnd
UIControlEventEditingDidEndOnExit
UIControlEventAllTouchEvents
UIControlEventAllEditingEvents
UIControlEventApplicationReserved
UIControlEventSystemReserved
UIControlEventAllEvents

UIButton Class下的所有按钮样式

UIButtonTypeCustom
UIButtonTypeRoundedRect
UIButtonTypeDetailDisclosure
UIButtonTypeInfoLight
UIButtonTypeInfoDark
UIButtonTypeContactAdd

 

android中MVC,MVP和MVVM三种模式详解析

我们都知道,Android本身就采用了MVC模式,model层数据源层我们就不说了,至于view层即通过xml来体现,而 controller层的角色一般是由activity来担当的。虽然我们项目用到了MVP模式,但是现在人们并没有总结出一种规范,所以MVP模式的写法并不统一,而至于MVVM模式看网上的呼声似乎也是赞同和拍砖的参半,所以对于这几种模式我也不发表意见了,适合自己的才是*好的。下面是我看到的关于这几种模式的几篇文章,整合了一下分享给大家。

 

相信大家对MVC,MVP和MVVM都不陌生,作为三个*耳熟能详的Android框架,它们的应用可以是非常广泛的,但是对于一些新手来说,可能对于区分它们三个都有困难,更别说在实际的项目中应用了,有些时候想用MVP的,代码写着写着就变成了MVC,久而久之就对它们三个的选择产生了恐惧感,如果你也是这样的人群,那么这篇文章可能会对你有很大的帮助,希望大家看完都会有收获吧!

文章重点:

(1)了解并区分MVC,MVP,MVVM。

(2)知道这三种模式在Android中如何使用。

(3)走出data binding的误区。

(4)了解MVP+data binding的开发模式。

本篇文章的demo我将会上传到我的github上。

水之积也不厚,则其负大舟也无力

正如庄子在逍遥游中说的,如果水不够深,那就没有能够担负大船的力量 。所以在真正开始涉及具体的代码之前,我们要先对MVC,MVP和MVVM做一个初步的了解。如果各位同学对此已经有所了解了,可以选择性跳过这一节。

MVC

MVC,Model View Controller,是软件架构中*常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见下图

%title插图%num

当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。

那具体到Android上是怎么样一个情况呢?

大家都知道一个Android工程有什么对吧,有Java的class文件,有res文件夹,里面是各种资源,还有类似manifest文件等等。对于原生的Android项目来说,layout.xml里面的xml文件就对应于MVC的view层,里面都是一些view的布局代码,而各种java bean,还有一些类似repository类就对应于model层,至于controller层嘛,当然就是各种activity咯。大家可以试着套用我上面说的MVC的工作原理是理解。比如你的界面有一个按钮,按下这个按钮去网络上下载一个文件,这个按钮是view层的,是使用xml来写的,而那些和网络连接相关的代码写在其他类里,比如你可以写一个专门的networkHelper类,这个就是model层,那怎么连接这两层呢?是通过button.setOnClickListener()这个函数,这个函数就写在了activity中,对应于controller层。是不是很清晰。

大家想过这样会有什么问题吗?显然是有的,不然为什么会有MVP和MVVM的诞生呢,是吧。问题就在于xml作为view层,控制能力实在太弱了,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。大家回想一下自己写的代码,如果是一个逻辑很复杂的页面,activity或者fragment是不是动辄上千行呢?这样不仅写起来麻烦,维护起来更是噩梦。(当然看过Android源码的同学其实会发现上千行的代码不算啥,一个RecyclerView.class的代码都快上万行了呢。。)

MVC还有一个重要的缺陷,大家看上面那幅图,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。

正因为MVC有这样那样的缺点,所以才演化出了MVP和MVVM这两种框架。

MVP

MVP作为MVC的演化,解决了MVC不少的缺点,对于Android来说,MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。下面还是让我们看图

%title插图%num

从图中就可以看出,*明显的差别就是view层和model层不再相互可知,完全的解耦,取而代之的presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层,整个过程中view层和model层完全没有联系。看到这里大家可能会问,虽然view层和model层解耦了,但是view层和presenter层不是耦合在一起了吗?其实不是的,对于view层和presenter层的通信,我们是可以通过接口实现的,具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的presenter中通过接口调用方法。不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。

当然,其实*好的方式是使用fragment作为view层,而activity则是用于创建view层(fragment)和presenter层(presenter)的一个控制器。

MVVM

MVVM*早是由微软提出的

%title插图%num

这里要感谢泡在网上的日子,因为前面看到的三张图我都是从它的博客中摘取的,如果有人知道不允许这样做的话请告诉我,我会从我的博客中删除的,谢谢。

从图中看出,它和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。

我们很难去说MVP和MVVM这两个MVC的变种孰优孰劣,还是要具体情况具体分析。

纸上得来终觉浅,*知此事要躬行

对于程序员来说,空谈是*没效率的一种方式,相信大家看了我上面对于三种模式的分析,或多或少都会有点云里雾里,下面让我们结合代码来看看。

让我们试想一下下面这个情景,用户点击一个按钮A,获取github上对应公司对应仓库中贡献排行*的任的名字,然后我们还会有一个按钮B,用户点击按钮B,界面上排行*的那个人的名字就会换成自己的。

MVC

MVC实现是*简单的。

首先看对应view层的xml文件

[html]
  1. <?xmlversionxmlversion=”1.0″encoding=”utf-8″?>  
  2. <LinearLayoutxmlns:androidLinearLayoutxmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     xmlns:tools=“http://schemas.android.com/tools”  
  4.     android:layout_width=“match_parent”  
  5.     android:layout_height=“match_parent”  
  6.     android:id=“@+id/container”  
  7.     android:orientation=“vertical”  
  8.     tools:context=“.ui.view.MainActivity”  
  9.     android:fitsSystemWindows=“true”>  
  10.     <Button  
  11.         android:text=“get”  
  12.         android:layout_width=“match_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“get”/>  
  15.     <Button  
  16.         android:text=“change”  
  17.         android:layout_width=“match_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“change”/>  
  20.     <TextView  
  21.         android:id=“@+id/top_contributor”  
  22.         android:layout_width=“match_parent”  
  23.         android:layout_height=“match_parent”  
  24.         android:gravity=“center”  
  25.         android:textSize=“30sp”/>  
  26. </LinearLayout>  

很简单,两个Button一个TextView

接着看对应controller层的activity

[java]
  1. public class MainActivity extends AppCompatActivity {  
  2.     private ProcessDialog dialog;  
  3.     private Contributor contributor = new Contributor();  
  4.     private TextView topContributor;  
  5.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  6.         @Override  
  7.         public void onStart() {  
  8.             showProgress();
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor contributor) {  
  18.             MainActivity.this.contributor = contributor;  
  19.             topContributor.setText(contributor.login);
  20.             dismissProgress();
  21.         }
  22.     };
  23.     @Override  
  24.     protected void onCreate(Bundle savedInstanceState) {  
  25.         super.onCreate(savedInstanceState);  
  26.         setContentView(R.layout.activity_main);
  27.         topContributor = (TextView)findViewById(R.id.top_contributor);
  28.     }
  29.     public void get(View view){  
  30.         getTopContributor(“square”, “retrofit”);  
  31.     }
  32.     public void change(View view){  
  33.         contributor.login = “zjutkz”;  
  34.         topContributor.setText(contributor.login);
  35.     }
  36.     public void getTopContributor(String owner,String repo){  
  37.         GitHubApi.getContributors(owner, repo)
  38.                 .take(1)  
  39.                 .observeOn(AndroidSchedulers.mainThread())
  40.                 .subscribeOn(Schedulers.newThread())
  41.                 .map(new Func1<List<Contributor>, Contributor>() {  
  42.                     @Override  
  43.                     public Contributor call(List<Contributor> contributors) {  
  44.                         return contributors.get(0);  
  45.                     }
  46.                 })
  47.                 .subscribe(contributorSub);
  48.     }
  49.     public void showProgress(){  
  50.         if(dialog == null){  
  51.             dialog = new ProcessDialog(this);  
  52.         }
  53.         dialog.showMessage(“正在加载…”);  
  54.     }
  55.    public void dismissProgress(){  
  56.         if(dialog == null){  
  57.             dialog = new ProcessDialog(this);  
  58.         }
  59.         dialog.dismiss();
  60.     }
  61. }

我们看一下get()方法中调用的getTopContributor方法

[java]
  1. public void getTopContributor(String owner,String repo){  
  2.     GitHubApi.getContributors(owner, repo)
  3.             .take(1)  
  4.             .observeOn(AndroidSchedulers.mainThread())
  5.             .subscribeOn(Schedulers.newThread())
  6.             .map(new Func1<List<Contributor>, Contributor>() {  
  7.                 @Override  
  8.                 public Contributor call(List<Contributor> contributors) {  
  9.                     return contributors.get(0);  
  10.                 }
  11.             })
  12.             .subscribe(contributorSub);
  13. }

 

熟悉rxjava和retrofit的同学应该都明白这是啥意思,如果对这两个开源库不熟悉也没事,可以参考给 Android 开发者的 RxJava 详解和用 Retrofit 2 简化 HTTP 请求这两篇文章。

对于这里大家只要知道这段代码的意思就是去获取github上owner公司中的repo仓库里贡献排名*的那个人。贡献者是通过Contributor这个java bean存储的。

[java]
  1. public class Contributor {  
  2.     public String login;  
  3.     public int contributions;  
  4.   @Override  
  5.     public String toString() {  
  6.         return login + “, ” + contributions;  
  7.     }
  8. }

 

很简单,login表示贡献者的名字,contributor表示贡献的次数。

然后通过rxjava的subscriber中的onNext()函数得到这个数据。

[java]
  1. private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  2.     @Override  
  3.     public void onStart() {  
  4.         showProgress();
  5.     }
  6.     @Override  
  7.     public void onCompleted() {  
  8.     }
  9.     @Override  
  10.     public void onError(Throwable e) {  
  11.     }
  12.     @Override  
  13.     public void onNext(Contributor contributor) {  
  14.         MainActivity.this.contributor = contributor;  
  15.         topContributor.setText(contributor.login);
  16.         dismissProgress();
  17.     }
  18. };

至于另外那个change按钮的工作大家应该都看得懂,这里不重复了。

好了,我们来回顾一遍整个流程。

首先在xml中写好布局代码。

其次,activity作为一个controller,里面的逻辑是监听用户点击按钮并作出相应的操作。比如针对get按钮,做的工作就是调用GithubApi的方法去获取数据。

GithubApi,Contributor等类则表示MVC中的model层,里面是数据和一些具体的逻辑操作。

说完了流程再来看看问题,还记得我们前面说的吗,MVC在Android上的应用,一个具体的问题就是activity的责任过重,既是controller又是view。这里是怎么体现的呢?看了代码大家发现其中有一个progressDialog,在加载数据的时候显示,加载完了以后取消,逻辑其实是view层的逻辑,但是这个我们没办法写到xml里面啊,包括TextView.setTextView(),这个也一样。我们只能把这些逻辑写到activity中,这就造成了activity的臃肿,这个例子可能还好,如果是一个复杂的页面呢?大家自己想象一下。

MVP

通过具体的代码大家知道了MVC在Android上是如何工作的,也知道了它的缺点,那MVP是如何修正的呢?

这里先向大家推荐github上的一个第三方库,通过这个库大家可以很轻松的实现MVP。好了,还是看代码吧。

首先还是xml

[html]
  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     xmlns:tools=“http://schemas.android.com/tools”  
  4.     android:layout_width=“match_parent”  
  5.     android:layout_height=“match_parent”  
  6.     android:id=“@+id/container”  
  7.     android:orientation=“vertical”  
  8.     tools:context=“.ui.view.MainActivity”  
  9.     android:fitsSystemWindows=“true”>  
  10.     <Button  
  11.         android:text=“get”  
  12.         android:layout_width=“match_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“get”/>  
  15.     <Button  
  16.         android:text=“change”  
  17.         android:layout_width=“match_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“change”/>  
  20.     <TextView  
  21.         android:id=“@+id/top_contributor”  
  22.         android:layout_width=“match_parent”  
  23.         android:layout_height=“match_parent”  
  24.         android:gravity=“center”  
  25.         android:textSize=“30sp”/>  
  26. </LinearLayout>  

 

这个和MVC是一样的,毕竟界面的形式是一样的嘛。

接下去,我们看一个接口。

[java]
  1. public interface ContributorView extends MvpView {  
  2.     void onLoadContributorStart();  
  3.     void onLoadContributorComplete(Contributor topContributor);  
  4.     void onChangeContributorName(String name);  
  5. }

这个接口起什么作用呢?还记得我之前说的吗?MVP模式中,view层和presenter层靠的就是接口进行连接,而具体的就是上面的这个了,里面定义的三个方法,*个是开始获取数据,第二个是获取数据成功,第三个是改名。我们的view层(activity)只要实现这个接口就可以了。

下面看activity的代码

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {  
  2.     private ProcessDialog dialog;  
  3.     private TextView topContributor;  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);
  8.         topContributor = (TextView)findViewById(R.id.top_contributor);
  9.     }
  10.     @NonNull  
  11.     @Override  
  12.     public ContributorPresenter createPresenter() {  
  13.         return new ContributorPresenter();  
  14.     }
  15.     public void get(View view){  
  16.         getPresenter().get(“square”, “retrofit”);  
  17.     }
  18.     public void change(View view){  
  19.         getPresenter().change();
  20.     }
  21.     @Override  
  22.     public void onLoadContributorStart() {  
  23.         showProgress();
  24.     }
  25.     @Override  
  26.     public void onLoadContributorComplete(Contributor contributor) {  
  27.         topContributor.setText(contributor.toString());
  28.         dismissProgress();
  29.     }
  30.     @Override  
  31.     public void onChangeContributorName(String name) {  
  32.         topContributor.setText(name);
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46. }

 

它继承自MvpActivity,实现了刚才的ContributorView接口。继承的那个MvpActivity大家这里不用太关心主要是做了一些初始化和生命周期的封装。我们只要关心这个activity作为view层,到底是怎么工作的。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public void get(View view){  
  2.     getPresenter().get(“square”, “retrofit”);  
  3. }
  4. public void change(View view){  
  5.     getPresenter().change();
  6. }

get()和change()这两个方法是我们点击按钮以后执行的,可以看到,里面完完全全没有任何和model层逻辑相关的东西,只是简单的委托给了presenter,那我们再看看presenter层做了什么

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class ContributorPresenter extends MvpBasePresenter<ContributorView> {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             ContributorView view = getView();
  6.             if(view != null){  
  7.                 view.onLoadContributorStart();
  8.             }
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor topContributor) {  
  18.             ContributorView view = getView();
  19.             if(view != null){  
  20.                 view.onLoadContributorComplete(topContributor);
  21.             }
  22.         }
  23.     };
  24.     public void get(String owner,String repo){  
  25.         GitHubApi.getContributors(owner, repo)
  26.                 .take(1)  
  27.                 .observeOn(AndroidSchedulers.mainThread())
  28.                 .subscribeOn(Schedulers.newThread())
  29.                 .map(new Func1<List<Contributor>, Contributor>() {  
  30.                     @Override  
  31.                     public Contributor call(List<Contributor> contributors) {  
  32.                         return contributors.get(0);  
  33.                     }
  34.                 })
  35.                 .subscribe(contributorSub);
  36.     }
  37.     public void change(){  
  38.         ContributorView view = getView();
  39.         if(view != null){  
  40.             view.onChangeContributorName(“zjutkz”);  
  41.         }
  42.     }
  43. }

其实就是把刚才MVC中activity的那部分和model层相关的逻辑抽取了出来,并且在相应的时机调用ContributorView接口对应的方法,而我们的activity是实现了这个接口的,自然会走到对应的方法中。好了,我们来捋一捋。

首先,和MVC*大的不同,MVP把activity作为了view层,通过代码也可以看到,整个activity没有任何和model层相关的逻辑代码,取而代之的是把代码放到了presenter层中,presenter获取了model层的数据之后,通过接口的形式将view层需要的数据返回给它就OK了。

这样的好处是什么呢?首先,activity的代码逻辑减少了,其次,view层和model层完全解耦,具体来说,如果你需要测试一个http请求是否顺利,你不需要写一个activity,只需要写一个java类,实现对应的接口,presenter获取了数据自然会调用相应的方法,相应的,你也可以自己在presenter中mock数据,分发给view层,用来测试布局是否正确。

%title插图%num

它竟然说data binding的viewmodel层是binding类,其实不止是这篇文章,其他有一些开发者写的关于data binding的文章里都犯了一样的错误。大家如果也有这样的概念,请务必纠正过来!!说完了错误的概念,那data binding中真正的viewmodel是什么呢?我们还是以之前MVC,MVP的那个例子做引导。首先是view层,这没啥好说的,和MVP一样,只不过多了数据绑定。view层就是xml和activity。

[html]
  1. <layout xmlns:android=“http://schemas.android.com/apk/res/android”>  
  2.     <data>  
  3.         <variable name=“contributor” type=”zjutkz.com.mvvm.viewmodel.Contributor”/>  
  4.     </data>  
  5.     <LinearLayout  
  6.         android:layout_width=“match_parent”  
  7.         android:layout_height=“match_parent”  
  8.         android:id=“@+id/container”  
  9.         android:orientation=“vertical”  
  10.         android:fitsSystemWindows=“true”>  
  11.         <Button  
  12.             android:id=“@+id/get”  
  13.             android:text=“get”  
  14.             android:layout_width=“match_parent”  
  15.             android:layout_height=“wrap_content”  
  16.             android:onClick=“get”/>  
  17.         <Button  
  18.             android:id=“@+id/change”  
  19.             android:text=“change”  
  20.             android:layout_width=“match_parent”  
  21.             android:layout_height=“wrap_content”  
  22.             android:onClick=“change”/>  
  23.         <TextView  
  24.             android:id=“@+id/top_contributor”  
  25.             android:layout_width=“match_parent”  
  26.             android:layout_height=“match_parent”  
  27.             android:gravity=“center”  
  28.             android:textSize=“30sp”  
  29.             android:text=“@{contributor.login}”/>  
  30.     </LinearLayout>  
  31. </layout>  

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends AppCompatActivity {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             showProgress();
  6.         }
  7.         @Override  
  8.         public void onCompleted() {  
  9.         }
  10.         @Override  
  11.         public void onError(Throwable e) {  
  12.         }
  13.         @Override  
  14.         public void onNext(Contributor contributor) {  
  15.             binding.setContributor(contributor);
  16.             dismissProgress();
  17.         }
  18.     };
  19.     private ProcessDialog dialog;  
  20.     private MvvmActivityMainBinding binding;  
  21.     @Override  
  22.     protected void onCreate(Bundle savedInstanceState) {  
  23.         super.onCreate(savedInstanceState);  
  24.         binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);  
  25.     }
  26.     public void get(View view){  
  27.         getContributors(“square”, “retrofit”);  
  28.     }
  29.     public void change(View view){  
  30.         if(binding.getContributor() != null){  
  31.             binding.getContributor().setLogin(“zjutkz”);  
  32.         }
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46.     public void getContributors(String owner,String repo){  
  47.         GitHubApi.getContributors(owner, repo)
  48.                 .take(1)  
  49.                 .observeOn(AndroidSchedulers.mainThread())
  50.                 .subscribeOn(Schedulers.newThread())
  51.                 .map(new Func1<List<Contributor>, Contributor>() {  
  52.                     @Override  
  53.                     public Contributor call(List<Contributor> contributors) {  
  54.                         return contributors.get(0);  
  55.                     }
  56.                 })
  57.                 .subscribe(contributorSub);
  58.     }
  59. }

如果你对data binding框架是有了解的,上面的代码你能轻松的看懂。

那model层又是什么呢?当然就是那些和数据相关的类,GithubApi等等。

重点来了,viewmodel层呢?好吧,viewmodel层就是是Contributor类!大家不要惊讶,我慢慢的来说。

[java]
  1. public class Contributor extends BaseObservable{  
  2.     private String login;  
  3.     private int contributions;  
  4.     @Bindable  
  5.     public String getLogin(){  
  6.         return login;  
  7.     }
  8.     @Bindable  
  9.     public int getContributions(){  
  10.         return contributions;  
  11.     }
  12.     public void setLogin(String login){  
  13.         this.login = login;  
  14.         notifyPropertyChanged(BR.login);
  15.     }
  16.     public void setContributions(int contributions){  
  17.         this.contributions = contributions;  
  18.         notifyPropertyChanged(BR.contributions);
  19.     }
  20.     @Override  
  21.     public String toString() {  
  22.         return login + “, ” + contributions;  
  23.     }
  24. }

我们可以看到,Contributor和MVP相比,继承自了BaseObservable,有基础的同学都知道这是为了当Contributor内部的variable改变的时候ui可以同步的作出响应。

我为什么说Contributor是一个viewmodel呢。大家还记得viewmodel的概念吗?view和viewmodel相互绑定在一起,viewmodel的改变会同步到view层,从而view层作出响应。这不就是Contributor和xml中那些组件元素的关系吗?所以,大家不要被binding类迷惑了,data binding框架中的viewmodel是自己定义的那些看似是model类的东西!比如这里的Contributor!

话说到这里,那binding类又是什么呢?其实具体对应到之前MVVM的那张图就很好理解了,我们想一下,binding类的工作是什么?

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. binding = DataBindingUtil.setContentView(this,R.layout.mvvm_activity_main);  
  2. binding.setContributor(contributor);

首先,binding要通过DataBindingUtil.setContentView()方法将xml,也就是view层设定。

接着,通过setXXX()方法将viewmodel层注入进去。

由于这两个工作,view层(xml的各个组件)和viewmodel层(contributor)绑定在了一起。

好了,大家知道了吗,binding类,其实就是上图中view和viewmodel中间的那根线啊!!

 

MVVM

MVVM的问题呢,其实和MVC有一点像。data binding框架解决了数据绑定的问题,但是view层还是会过重,大家可以看我上面那个MVVM模式下的activity

[java]
  1. public class MainActivity extends AppCompatActivity {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             showProgress();
  6.         }
  7.         @Override  
  8.         public void onCompleted() {  
  9.         }
  10.         @Override  
  11.         public void onError(Throwable e) {  
  12.         }
  13.         @Override  
  14.         public void onNext(Contributor contributor) {  
  15.             binding.setContributor(contributor);
  16.             dismissProgress();
  17.         }
  18.     };
  19.     private ProcessDialog dialog;  
  20.     private MvvmActivityMainBinding binding;  
  21.     @Override  
  22.     protected void onCreate(Bundle savedInstanceState) {  
  23.         super.onCreate(savedInstanceState);  
  24.         binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);  
  25.     }
  26.     public void get(View view){  
  27.         getContributors(“square”, “retrofit”);  
  28.     }
  29.     public void change(View view){  
  30.         if(binding.getContributor() != null){  
  31.             binding.getContributor().setLogin(“zjutkz”);  
  32.         }
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46.     public void getContributors(String owner,String repo){  
  47.         GitHubApi.getContributors(owner, repo)
  48.                 .take(1)  
  49.                 .observeOn(AndroidSchedulers.mainThread())
  50.                 .subscribeOn(Schedulers.newThread())
  51.                 .map(new Func1<List<Contributor>, Contributor>() {  
  52.                     @Override  
  53.                     public Contributor call(List<Contributor> contributors) {  
  54.                         return contributors.get(0);  
  55.                     }
  56.                 })
  57.                 .subscribe(contributorSub);
  58.     }
  59. }

大家有没有发现,activity在MVVM中应该是view层的,但是里面却和MVC一样写了对model的处理。有人会说你可以把对model的处理放到viewmodel层中,这样不是更符合MVVM的设计理念吗?这样确实可以,但是progressDialog的show和dismiss呢?你怎么在viewmodel层中控制?这是view层的东西啊,而且在xml中也没有,我相信会有解决的方案,但是我们有没有一种更加便捷的方式呢?

 

首先还是view层。

[html]
  1. <layout xmlns:android=“http://schemas.android.com/apk/res/android”>  
  2.     <data>  
  3.         <variable name=“contributor” type=”zjutkz.com.mvpdatabinding.viewmodel.Contributor”/>  
  4.     </data>  
  5.     <LinearLayout  
  6.         android:layout_width=“match_parent”  
  7.         android:layout_height=“match_parent”  
  8.         android:id=“@+id/container”  
  9.         android:orientation=“vertical”  
  10.         android:fitsSystemWindows=“true”>  
  11.         <Button  
  12.             android:id=“@+id/get”  
  13.             android:text=“get”  
  14.             android:layout_width=“match_parent”  
  15.             android:layout_height=“wrap_content”  
  16.             android:onClick=“get”/>  
  17.         <Button  
  18.             android:id=“@+id/change”  
  19.             android:text=“change”  
  20.             android:layout_width=“match_parent”  
  21.             android:layout_height=“wrap_content”  
  22.             android:onClick=“change”/>  
  23.         <TextView  
  24.             android:id=“@+id/top_contributor”  
  25.             android:layout_width=“match_parent”  
  26.             android:layout_height=“match_parent”  
  27.             android:gravity=“center”  
  28.             android:textSize=“30sp”  
  29.             android:text=“@{contributor.login}”/>  
  30.     </LinearLayout>  
  31. </layout>  
[java]
  1. public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {  
  2.     private ProcessDialog dialog;  
  3.     private ActivityMainBinding binding;  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         binding = DataBindingUtil.setContentView(this, R.layout.activity_main);  
  8.     }
  9.     @NonNull  
  10.     @Override  
  11.     public ContributorPresenter createPresenter() {  
  12.         return new ContributorPresenter();  
  13.     }
  14.     public void get(View view){  
  15.         getPresenter().get(“square”, “retrofit”);  
  16.     }
  17.     public void change(View view){  
  18.         if(binding.getContributor() != null){  
  19.             binding.getContributor().setLogin(“zjutkz”);  
  20.         }
  21.     }
  22.     @Override  
  23.     public void onLoadContributorStart() {  
  24.         showProgress();
  25.     }
  26.     @Override  
  27.     public void onLoadContributorComplete(Contributor contributor) {  
  28.         binding.setContributor(contributor);
  29.         dismissProgress();
  30.     }
  31.     public void showProgress(){  
  32.         if(dialog == null){  
  33.             dialog = new ProcessDialog(this);  
  34.         }
  35.         dialog.showMessage(“正在加载…”);  
  36.     }
  37.     public void dismissProgress(){  
  38.         if(dialog == null){  
  39.             dialog = new ProcessDialog(this);  
  40.         }
  41.         dialog.dismiss();
  42.     }
  43. }

然后是presenter层

[java]
  1. public class ContributorPresenter extends MvpBasePresenter<ContributorView> {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             ContributorView view = getView();
  6.             if(view != null){  
  7.                 view.onLoadContributorStart();
  8.             }
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor topContributor) {  
  18.             ContributorView view = getView();
  19.             if(view != null){  
  20.                 view.onLoadContributorComplete(topContributor);
  21.             }
  22.         }
  23.     };
  24.     public void get(String owner,String repo){  
  25.         GitHubApi.getContributors(owner, repo)
  26.                 .take(1)  
  27.                 .observeOn(AndroidSchedulers.mainThread())
  28.                 .subscribeOn(Schedulers.newThread())
  29.                 .map(new Func1<List<Contributor>, Contributor>() {  
  30.                     @Override  
  31.                     public Contributor call(List<Contributor> contributors) {  
  32.                         return contributors.get(0);  
  33.                     }
  34.                 })
  35.                 .subscribe(contributorSub);
  36.     }
  37. }

model层就是GithubApi等等。

我们使用了data binding框架去节省了类似findViewById和数据绑定的时间,又使用了presenter去将业务逻辑和view层分离。

当然这也不是固定的,你大可以在viewmodel中实现相应的接口,presenter层的数据直接发送到viewmodel中,在viewmodel里更新,因为view和viewmodel是绑定的,这样view也会相应的作出反应。

说到这里,我还是想重复刚才的那句话,*佳实践都是人想出来的,用这些框架根本的原因也是为了尽量低的耦合性和尽量高的可复用性。

Android DataBinding库(MVVM设计模式)

说到 DataBinding,就有必要先提起 MVVM设计模式

 

Model–View–ViewModel(MVVM) 是一个软件架构设计模式,相比 MVVM,大家对 MVC 或 MVP 可能会更加熟悉。

 

  • MVC:(VIew-Model-Controller)

    早期将 View、Model、Controller 代码块进行划分,使得程序大部分分离,降低耦合。

     

  • MVP:(VIew-Model-Presenter)

    由于 MVC 中 View和Model之间的依赖太强,导致 Activity 中的代码过于臃肿。为了他们可以*对独立的存在,慢慢演化出了 MVP。在 MVP 中 View 并不直接使用 Model,它们之间的通信是通过 Presenter (MVC中的Controller) 来进行的。

     

  • MVVM:(Model–View–ViewModel)

    MVVM 可以算是 MVP的升级版,将 Presenter 改名为 ViewModel。关键在于 View和Model的双向绑定,当 View 有用户输入后,ViewModel 通知 Model 更新数据,同理 Model 数据更新后,ViewModel 通知 View 更新。

 

Data Binding

 

在Google I/O 2015上,伴随着 Android M 预览版发布了Data Binding兼容函数库:

https://developer.android.com/tools/data-binding/guide.html

 

不知道要扯什么了,还是直接上代码,来看看 Data Binding 的魅力吧。

 

环境要求

 

Data Binding 对使用的环境还是有一定要求的(这货有点挑):

 

  • Android Studio 版本在 1.3以上
  • Gradle 的版本要在 1.5.0-alpha1 以上
  • 需要在 Android SDK manager 中下载 Android Support repository

 

 

然后在对应的 Module 的 build.gradle 中添加:

 

android {
    ....
    dataBinding {
        enabled =true
    }
}

 

Gradle需要升级版本的可以参考:

 

升级Gradle版本

http://www.jianshu.com/p/00beddbe3dbc

 

创建对象

 

创建一个 User类

 

%title插图%num

 

布局

 

在 activity_main.xml 中布局:

 

%title插图%num

 

这里跟平时的布局有点不同,*外层是 layout,里面分别是 data 以及 我们的布局。

 

data:声明了需要用到的 user对象type 用于指定路径。

 

可以在 TextView 中的看到 android:text=”@{user.firstName}”, 这是什么鬼,没见过这么写的!!!(不急,继续往下看)

 

绑定数据

 

看看下面的 MainActivity

 

%title插图%num

 

问我 ActivityMainBinding 哪来的?我怎么知道…

 

ActivityMainBinding 是根据布局文件的名字生成的,在后面加了 Binding

 

运行下看看效果吧:

 

%title插图%num

 

有点懵逼了,就绑定了下而已,这些数据是怎么显示到界面上的。

 

%title插图%num

 

他是怎么工作的?

 

原来 Data Binding 在程序代码正在编译的时候,找到所有它需要的信息。然后通过语法来解析这些表达式,*后生成一个类。

 

通过反编译我们可以看到,反编译可以参考这里:

http://blog.csdn.net/vipzjyno1/article/details/21039349

 

Data Binding 为我们生成了 databinding,以及 ActivityMainBinding类

 

%title插图%num

 

看看我们在 onCreate 中*后调用的 binding.setUser(user),在 ActivityMainBinding 中可以看到这个方法:

 

%title插图%num

 

我想就是这个 super.requestRebind() 对数据进行了绑定,至于里面怎么实现的,有待进一步研究。

 

更多用法

 

上面只是用一个简单的例子,展示了 Data Binding 的用法,如果想在实际项目中使用,可不是上面这例子可以搞定的。下面就来说说 Data Bindig 的更多用法。

 

消除空指针顾虑

 

自动生成的 DataBinding 代码会检查 null,避免出现NullPointerException

 

例如在表达式中 @{user.phone}如果user == null 那么会为 user.phone 设置 默认值null 而不会导致程序崩溃(基本类型将赋予默认值如 int为0,引用类型都会赋值null)。

 

自定义DataBinding名

 

如果不喜欢自动生成的 Data Binding名,我们可以自己来定义:

 

<data class="MainBinding">
    ....
</data>

 

class对应 的就是生成的 Data Binding名

 

导包

 

跟Java中的用法相似,布局文件中支持 import 的使用,原来的代码是这样:

 

<data>
    <variable name="user" type="com.example.gavin.databindingtest.User" />
</data>

 

使用 import 后可以写成这样:

 

<data>
    <import type="com.example.gavin.databindingtest.User"/>
    <variable
        name="user"
        type="User" />
</data>

 

遇到 相同的类名 的时候:

 

%title插图%num

 

使用 alias 设置别名,这样 user 对应的就是 com.example.gavin.databindingtest.UsermcUser 就对应com.example.gavin.mc.User,然后:

 

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.firstName}"/>

 

当需要用到一些包时,在Java中可以自动导包,不过在布局文件中就没有这么方便了。需要使用 import 导入这些包,才能使用。如需要用到 View 的时候:

 

<data>
    <import type="android.view.View"/></data>
    ...
    <TextView
    ...
    android:visibility="@{user.isStudent ? View.VISIBLE : View.GONE}"
/>

 

注意:只要是在Java中需要导入包的类,这边都需要导入,如:Map、ArrayList 等,不过 java.lang 包里的类是可以不用导包的。

 

表达式

 

在布局中,不仅可以使用:

 

android:text="@{user.lastName}"

 

还可以使用表达式如:

 

三元运算:

 

在 User 中添加 boolean类型  isStudent属性,用来判断是否为学生:

 

%title插图%num

 

注意:需要用到双引号的时候,外层的双引号改成单引号。

 

还可以这样用:

 

%title插图%num

 

这里用到的 View 需要在 data 中声明:

 

<data>
    <import type="android.view.View"/>
</data>

 

注意:android:visibility=”@{user.isStudent ? View.VISIBLE : View.GONE}”,可能会被标记成红色,不用管它编译会通过的。

 

??

 

除了常用的操作法,另外还提供了一个 null 的合并运算符号 ??,这是一个三目运算符的简便写法。

 

 

contact.lastName ?? contact.name

 

相当于:

 

contact.lastName != null ? contact.lastName : contact.name

 

所支持的操作符如下:

 

数学运算符 + – / * %
字符串拼接 +
逻辑运算 && ||
二进制运算 & | ^
一元运算符 + – ! ~
位运算符 >> >>> <<
比较运算符 == > < >= <=
instanceof
Grouping ()
文字 – character, String, numeric, null
类型转换 cast
方法调用 methods call
字段使用 field access
数组使用 [] Arrary access
三元运算符 ? :

 

显示图片

 

除了文字的设置,网络图片的显示也是我们常用的。来看看 Data Binding 是怎么实现图片的加载的。

 

首先要提到 BindingAdapter注解,这里创建了一个类,里面有显示图片的方法:

 

%title插图%num
(这方法必须是public static的,否则会报错)

 

这里只用了 bind 声明了一个 image 自定义属性,等下在布局中会用到。

 

这个类中只有一个静态方法 imageLoader,里面有两参数,一个是需要设置图片的 view,另一个是对应的 Url,这里使用了 ImageLoader 库加载图片。

 

看看它的布局是什么样的吧:

 

%title插图%num

 

*后在 MainActivity 中绑定下数据就可以了:

 

binding.setImageUrl("http://115.159.198.162:3000/posts/57355a92d9ca741017a28375/1467250338739.jpg");

 

哇靠!!!就这样?我都没看出来它是怎么设置这些图片的。

 

不管了,先看看效果。(其中的原理以后慢慢唠,这里就负责说明怎么使用,这篇已经够长了,不想再写了)

 

%title插图%num
看个美女压压惊

 

使用 BindingAdapter 的时候,我这还出现了这样的提示,不过不影响运行。不知道你们会不会…

 

%title插图%num

 

已解决

感谢 颜路 同学指出 @BindingAdapter({“bind:image”})  改成 @BindingAdapter({“image”}) 就不会有警告了。

 

点击事件

 

在 MainActivity 中声明方法:

 

//参数View必须有,必须是public
//参数View不能改成对应的控件,只能是View,否则编译不通过
public void onClick(View view) {
    Toast.makeText(this,"点击事件", Toast.LENGTH_LONG).show();
}

 

布局中:

 

%title插图%num

 

*后记得在 MainActivity 中调用:

 

binding.setMainActivity(this);

 

发现:布局文件中,variable 中的 name,在 binding 中都会生成一个对应的 set方法,如:setMainActivity。有 set方法,那就应该有 get方法,试试 getMainActivity,还真有。

 

运行下看看效果:

 

%title插图%num

 

当然如果你不想把点击事件写在 MainActivity 中,你把它单独写在一个类里面:

 

public class MyHandler {
    public void onClick(View view) {
        Toast.makeText(view.getContext(), "点击事件", Toast.LENGTH_LONG).show();
    }
}

 

%title插图%num

 

在 MainActivity 调用:

 

binding.setHandle(new MyHandler());

 

调用Activity中变量

 

上面看到它调用 MainActivity中的onClick方法,那么可以调用 MainActivity 中的属性吗?

 

在 MainActivity 中定义 mName

 

public static String mName = "MM";

 

布局中:

 

%title插图%num

 

注意:这个变量必须是 public static。

 

数据改变时更新UI

 

当数据发生变化时,我们可以这样更新UI:

 

%title插图%num

 

看看调用的这个 setUser 是什么:

 

%title插图%num

 

从反编译的代码中可以看出,setUser 方法中重新绑定了数据。

 

看下效果:

 

%title插图%num

 

BaseObservable

 

使用上面的代码实现了UI的更新你就满足了?其实官方为我们提供了更加简便的方式,使 User继承BaseObservable,代码如下:

 

%title插图%num

 

只要 user 发生变化,就能达到改变UI的效果。在 MainActivity 中只要调用以下代码:

 

user.setFirstName("Com");

 

有了 BaseObservable 就够了?不不不,我比较懒,不想写那么多 @Bindable 和 notifyPropertyChanged。万一里面有几十个属性,那不写哭起来?而且还有可能写丢了。

 

Data Binding的开发者贴心得为我们准备了一系列的 ObservableField,包括: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat,ObservableDouble 以及 ObservableParcelable (原文蓝字部分都是超链接,感兴趣的朋友可以通过原文查看,我这里就不贴出来了,下文若有蓝色字体视为同等情况)看看它们的用法。

 

ObservableField 的使用

 

1. 创建User2

 

%title插图%num

 

这类里面 没有Get/Set

 

2. 布局文件

 

<TextView
    ...
    android:text="@{user2.firstName}" />
<TextView
    ...
    android:text="@{user2.lastName}" />
<TextView
    ...
    android:text="@{String.valueOf(user2.age)}" />

 

3. MainActivity中:

 

mUser2 = new User2();
binding.setUser2(mUser2);
mUser2.firstName.set("Mr");
mUser2.lastName.set("Bean");
mUser2.age.set(20);
mUser2.isStudent.set(false);

 

这里 new 了一个 User2 对象后,直接就绑定了。之后只要 mUser2 中的数据发生变化,UI也会随之更新。

 

除了这几个 Map 跟 List 也是必不可少的,Data Binding为我们提供了ObservableArrayMap 和ObservableArrayList。

 

ObservableArrayMap 的使用

 

ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

 

%title插图%num

 

ObservableArrayList 的使用

 

 

ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);


%title插图%num

 

在布局中使用到 ObservableBoolean 类型时,编译无法通过:

 

android:text='@{user2.isStudent?"学生":"非学生"}'

 

【目前已知】

将中文改成英文是可以通过编译的,像下面这样:

 

android:text='@{user2.isStudent?"Student":"Not Student"}'

 

为何使用中文不可以?原因未明。(感谢指教)

 

在RecyclerView或ListView中使用

 

前面说了那么多基础的用法,可还是不能达到我们的需求。几乎在每个app中都有列表的存在,RecyclerView 或 ListView,从上面所说的似乎还看不出 Data Binding 在 RecyclerView 或 ListView 中是否也能起作用。(用屁股想也知道,Google的开发团对怎么可能会犯这么低级的错误)。下面以 RecyclerView 为例子:

 

1. 直接看 Item 的布局(user_item.xml):

 

%title插图%num

 

2. RecyclerView 的数据绑定是在 Adapter 中完成的,下面看看 Adapter,这里使用了一个 Adapter,如果你在使用的时候发现 RecyclerView 的动画没了,去这里寻找答案:

https://realm.io/cn/news/data-binding-android-boyar-mount/

 

%title插图%num

 

3. *后在布局和 MainActivity 中的使用跟平时的用法一样。

 

布局中加入 RecyclerView

 

<android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

 

MainActivity 中:

 

%title插图%num

 

这样就可以了。

 

不过,在自动生成的 ActivityMainBinding 中,我们可以看到根据 RecyclerView的id,会自动生成一个 recyclerView

 

所以在 MainActivity 中,我们可以不用 findViewById,直接使用 binding.recyclerView

 

%title插图%num

 

来看看效果吧:

 

%title插图%num

 

Tips

 

tip1

 

若需要显示int类型,需要加上””,例如:

 

user.age 为 int类型,需要这样用:

 

<TextView   
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{""+user.age}'/>

 

或者

 

<TextView   
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(user.age)}"/>

 

tip2

 

不建议新手使用,出现错误的时候根据提示,不容易找到出错位置。(是根本找不到…)
友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速