技术相对论之软件架构

文 | 欧阳锋

有同学问我,你是怎样学习编程的呢?为了回答你的这个问题,今天,我们一起来做一件非常有意思的事情。我们以MVC架构为基,从服务端编程开始,依次类推iOS、Android,并最终完成登录、注册功能。

What is MVC ?

正文开始之前,我们先来简单了解一下MVC架构。

MVC全称是Model-View-Controller,是上个世纪80年底Xerox PARC为其编程语言SmallTalk发明的一直软件设计模式。我们可以用一张图来表示MVC架构模型:

MVC的核心思想是希望通过控制层管理视图呈现,从将逻辑层和视图层进行分离。

服务端编程其实就是MVC的最佳实践,理解了MVC架构之后,我们马上开始服务端编程。

服务端编程

服务端编程也叫后端编程,主要用于为前端提供数据源以及完成必要的业务逻辑处理。

这个部分我们使用Java语言开发,MVC框架使用最常用的 Spring MVC,完整配置请参考下方表格:

IDE 编程语言 框架 数据库 服务器
IntelliJ IDEA Java 1.8 Spring MVC MySQL Tomcat 7.0.57

为了简化数据库的访问,我们再增加一个轻量级的数据库访问框架 MyBatis

这里假设你已经正确安装了MySQL数据库和Tomcat服务器,如果你对具体的安装步骤有疑问,请在文章下方评论告诉我。

在开始编程之前,我们需要完成以下准备工作:

第一步:创建数据库d_user以及用户表t_user用于保存用户数据

1
2
3
4
5
6
7
8
9
create database d_server;
use d_server;
CREATE TABLE `t_user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`pwd` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
)

第二步:使用IntelliJ IDE创建一个Gradle依赖工程

最后一个步骤选择工作目录确定即可。

第三步:在build.gradle脚本文件中添加Spring MVC以及MyBatis依赖

1
2
compile group: 'org.springframework', name: 'spring-webmvc', version: '5.0.4.RELEASE'
compile group: 'org.mybatis', name: 'mybatis', version: '3.4.6'

第四步:关联本地Tomcat服务器

a)编辑运行设置,选择本地Tomcat服务器

b)选择以war包的方式部署到Tomcat

c)在浏览器中输入http://localhost:8080测试工作是否正常

如果看到下面这个界面,证明一切工作正常

第五步:配置Spring MVC

备注:参考官方文档 Web on Servlet Stack

a)在webapp目录下面生成WEB-INF/web.xml配置文件
选择菜单File->Project Structure进入如下界面:

在弹出的界面中设置路径为…/webapp/WEB-INF即可。

b)在web.xml文件中添加如下配置信息

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>

<servlet>
<servlet-name>/</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>/</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

</web-app>

上面这部分配置主要是使用Spring MVC的DispatcherServlet完成请求的拦截分发。配置文件中引用了另外一个配置文件app-context.xml,这个配置文件主要是完成Spring的依赖注入。

c)在app-context.xml配置文件中添加如下信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!-- 添加扫描注解的包 -->
<context:component-scan base-package="com.youngfeng.server"/>

<!-- 使用注解完成依赖注入 -->
<mvc:annotation-driven />

</beans>

d)添加jackson依赖用于Spring实现Json自动解析

1
2
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.4'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.2.3'

PS:不得不承认,Java后端开发的xml文件配置实在是一件繁琐至极的事情,尽管我们只需要配置一次。为了简化配置,Spring官方推出了一个重磅产品 Spring Boot。不过,这不是本文讨论的重点,感兴趣的同学请自行了解。

虽然我们已经完成了Spring的配置,但MyBatis的配置工作才刚刚开始。

配置MyBatis

为了简化Spring中MyBatis的配置,我们再增加一个MyBatis官方的提供的 mybatis-spring 库。

1
compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.2'

备注:参考官方文档 mybatis-spring

a)在spring配置文件app-context.xml配置文件中添加如下bean配置:

1
2
3
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>

b)指定数据源

b1) 添加Spring JDBC与MySQL Connector依赖

1
2
compile group: 'org.springframework', name: 'spring-jdbc', version: '5.0.4.RELEASE'
compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'

注意:因为部分依赖包只存在于JCenter,需要在build.gradle脚本中添加jcenter maven源

1
2
3
4
repositories {
mavenCentral()
jcenter()
}

b2)在app-context.xml文件中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<context:property-placeholder location="classpath:db.properties"/>

<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName">
<value>${jdbc.driverClassName}</value>
</property>
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="username">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
</bean>

b3)在类路径目录下创建db.properties文件指定MySQL数据库信息

1
2
3
4
jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/d_server
jdbc.username = root
jdbc.password = root

至此,所有的配置工作终于完成了,接下来进入最重要的编码阶段。

由于控制层需要依赖模型层的代码,因此,我们按照从下往上的原则进行编码。
a)先完成数据库的访问部分(DAO)

1
2
3
4
5
6
7
8
9
10
public interface UserDAO {
@Select("select * from t_user where username = #{username}")
User findByUsername(@Param("username") String username);

@Select("select * from t_user where username = #{username} and pwd = #{pwd}")
User findUser(@Param("username") String username, @Param("pwd") String pwd);

@Insert("insert into t_user(username, pwd) values(#{username}, #{pwd})")
void insert(@Param("username") String username, @Param("pwd") String pwd);
}

结合MyBatis,这个部分的工作很简单,甚至DAO的实现都不需要手动编码。

为了实现DAO的依赖注入,我们在app-context.xml配置文件中添加如下配置:

1
2
3
4
<bean id="userDAO" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.youngfeng.server.dao.UserDAO"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

b)Service层编码(也叫Domain层)
Service部分是控制层直接调用的接口,从抽象思维来说,也应该使用面向接口的方式编码。这里为了简化,Service部分我们直接使用一个类来实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component("userService")
public class UserService {
@Autowired
UserDAO userDAO;

public boolean isExist(String username) {
return null != userDAO.findByUsername(username);
}

public boolean isExist(String username, String pwd) {
return null != userDAO.findUser(username, pwd);
}

public void saveUser(String username, String pwd) {
this.userDAO.insert(username, pwd);
}
}

c)控制层编码

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
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;

@ResponseBody
@GetMapping("/login")
public Response login(@RequestParam("username") String username, @RequestParam("pwd") String pwd) {
Response response = new Response();

// 先判断用户名是否存在,给定不同Code用于区分不同错误
boolean isExist = userService.isExist(username);
if(!isExist) {
response.setCode(Response.CODE_USER_NOT_EXIST);
response.setMsg("用户不存在或密码错误");
}

// 判断用户名和密码是否匹配
isExist = userService.isExist(username, pwd);

if(!isExist) {
response.setCode(Response.CODE_USER_PWD_ERR);
response.setMsg("用户不存在或密码错误");
}

return response;
}

@ResponseBody
@GetMapping("/register")
public Response register(@RequestParam("username") String username, @RequestParam("pwd") String pwd) {
Response response = new Response();

// 注册之前,判断用户名是否已存在
boolean isExist = userService.isExist(username);
if(isExist) {
response.setCode(Response.CODE_USER_HAS_EXIST);
response.setMsg("用户名已存在");
} else {
userService.saveUser(username, pwd);
}

return response;
}

}

想必大家应该已经注意到了,控制层部分请求类型我使用了GET,这是为了方便在浏览器上面测试。测试通过后,要修改为POST请求类型。

以上代码,我已经在浏览器上测试通过。接下来,我们马上进入iOS客户端编程。

iOS客户端编程

iOS部分开发工具我们使用Xcode 9.2,其实你也可以使用AppCode,这是基于IntelliJ IDE开发的一款IDE,使用习惯完全接近IntelliJ IDE。

为了防止部分同学对Swift语言不熟悉,我们使用最常见的编程语言OC。

完整配置请参考如下表格:

IDE 编程语言 网络框架
Xcode 9.2 Objective C AFNetworking

打开Xcode,依次选择Create new Xcode Project->Single View App

下一步填入如下信息,语言选择OC

第一步:完成UI部分

这一部分参考苹果官方文档,按照苹果官方推荐,我们使用Storyboard进行布局。由于我们只是完成一个简单的Demo,所有的页面将在同一个Storyboard中完成。实际开发过程中,要根据功能划分Storyboard,方便进行小组开发。


使用约束布局我们很快完成了UI的构建,接下来进入最重要的编码阶段。约束布局的意思就是为一个控件添加N个约束,使其固定在某个位置。这个部分只要稍加尝试,就能掌握。具体的使用方法,请参考官方文档。

第二步:创建控制器,并关联UI

从服务器编程类推,iOS编程模型中应该也有一个叫Controller的东西。果不其然,在iOS新创建的工程中就有一个叫做ViewController的类,其父类是UIViewController。没错,这就是传说中的控制器。

1
2
3
4
5
6
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController


@end

完成登录、注册功能,我们至少需要三个控制器:LoginViewController、RegisterViewController、MainViewController,分别代表登录、注册、首页三个页面。

接下来,将控制器与UI进行关联。

UI关联控制器部分,如果你不知道,请先参考苹果官方文档。

事实上,Xcode的Interface Builder非常好用。按照下图操作即可:

最后,关联按钮点击事件以及输入框。

选中控件并按住鼠标右键拖拽到控制器源码中,松开,并选择相应类型即可:

以登录控制器为例,拖拽完成后的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface LoginViewController ()
@property (weak, nonatomic) IBOutlet UITextField *mUsernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *mPwdTextField;

@end

@implementation LoginViewController

- (IBAction)login:(id)sender {
}

- (IBAction)goToRegister:(id)sender {
}

接下来进入网络部分编程。

为了简化网络部分编程,我们引入AFNetworking框架。还记得服务端编程是怎么引入依赖的吗?没错,是Gradle。iOS端也有类似的依赖管理工具Cocoapods,这个部分如果不会依然请你参考官方文档。

使用如下步骤安装依赖(这里假设你已经正确安装了Cocoapod):
a)在根目录下面创建Podfile文件,并添加如下内容:

1
2
3
4
5
6
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target 'IOSClient' do
pod 'AFNetworking', '~> 3.0'
end

b)安装依赖

1
pod install

PS:可能有人会问,为什么服务端编程没有安装依赖的步骤。其实,很简单,intelliJ IDE非常智能,它自动检测了build.gradle文件的修改。一旦发现修改,自动安装依赖。因此,看起来就像没有依赖安装这个步骤一样。事实上,Cocoapod并非苹果官方的产品,如果产品来自苹果官方,恐怕Xcode也会支持自动安装依赖。

依赖安装完成后,为了更好地服务我们的业务。我们对网络请求做一点简单封装,增加HttpClient类,仅提供一个POST请求接口即可。

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
//
// HttpClient.m
// IOSClient
//
// Created by 欧阳锋 on 17/03/2018.
// Copyright © 2018 xbdx. All rights reserved.
//

#import "HttpClient.h"
#import <AFNetworking.h>
#import "Response.h"

@implementation HttpClient

static const NSString *BASE_URL = @"http://192.168.31.146:8080";

- (instancetype)init {
self = [super init];
if (self) {
self.baseUrl = BASE_URL;
}
return self;
}

+ (HttpClient *)initWithBaseUrl:(NSString *)baseUrl {
HttpClient *client = [[HttpClient alloc] init];
client.baseUrl = baseUrl;

return client;
}

+ (HttpClient *)sharedInstance {
static HttpClient *client = nil;
static dispatch_once_t once;
dispatch_once(&once, ^{
client = [[self alloc] init];
});
return client;
}

- (void)POST:(NSString *)url params:(NSDictionary *)params success:(void (^)(NSString *, id))success error:(void (^)(NSString *, NSInteger, NSInteger, NSString *))error {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[[AFHTTPSessionManager manager] POST: [_baseUrl stringByAppendingString:url]
parameters: params
progress: nil
success: ^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if(nil != success) {
if(nil != responseObject) {
if([responseObject isKindOfClass: [NSDictionary class]]) {
NSInteger code = ((NSDictionary *)responseObject)[@"code"];

if(SUCCESS == code) {
success(url, responseObject);
} else {
if(nil != error) {
NSString *msg = ((NSDictionary *)responseObject)[@"msg"];
error(url, SC_OK, code, msg);
}
}
}
}
}
}
failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull nsError) {
if(nil != nsError) {
error(url, nsError.code, nil, nsError.description);
}
}];

}

@end

为了简化JSON解析,我们增加一个最常见的Json解析库 jsonmodel 库。等待对话框也使用最常见的第三方库 SVProgressHUD

1
2
pod 'JSONModel'
pod 'SVProgressHUD'

安装依赖使用同样的命令pod install即可。

接下来,我们添加登录注册逻辑,完成最后部分编码:

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
// 登录部分逻辑
- (IBAction)login:(id)sender {
[SVProgressHUD show];
HttpClient *client = [HttpClient sharedInstance];
[client POST: @"/user/login"
params: @{@"username" : _mUsernameTextField.text, @"pwd" : _mPwdTextField.text}
success:^(NSString *url, id data) {
[SVProgressHUD dismiss];

if([data isKindOfClass: [NSDictionary class]]) {
// 例子代码,这里不做严格判断了
User *user = [[User alloc] initWithDictionary: data[@"data"] error: nil];
[self pushToMainViewController: user];
}
} error:^(NSString *url, NSInteger httpCode, NSInteger bizCode, NSString *error) {
[SVProgressHUD dismiss];

[self promptError: error];
}];
}

- (void)pushToMainViewController: (User *) user {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName: @"Main" bundle: [NSBundle mainBundle]];
MainViewController *mainViewController = [storyboard instantiateViewControllerWithIdentifier: @"mainViewController"];
mainViewController.user = user;
[self.navigationController presentViewController: mainViewController animated: YES completion: nil];
}

// 注册部分逻辑
- (IBAction)register:(id)sender {
NSString *username = _mUsernameTextField.text;
NSString *pwd = _mPwdTextField.text;
NSString *confrimPwd = _mConfirmTextField.text;

if([StringUtil isBlankString: username]) {
[self promptError: @"请输入用户名"];
return;
}

if([StringUtil isBlankString: pwd]) {
[self promptError: @"请输入用户密码"];
return;
}

if([StringUtil isBlankString: confrimPwd]) {
[self promptError: @"请输入确认密码"];
return;
}

if(![pwd isEqualToString: confrimPwd]) {
[self promptError: @"两次密码输入不一致,请重新输入"];
return;
}

HttpClient *client = [HttpClient sharedInstance];
[client POST: @"/user/register" params: @{@"username" : username, @"pwd" : pwd} success:^(NSString *url, id data) {
[self promptError: @"注册成功" handler:^(UIAlertAction *action) {
[self.navigationController popViewControllerAnimated: YES];
}];
} error:^(NSString *url, NSInteger httpCode, NSInteger bizCode, NSString *error) {
[self promptError: error];
}];
}

通过上面的步骤,我们已经完成了iOS客户端的开发。苹果官方默认支持的就是经典的MVC模式。因此,我们完全参考服务端开发模式完成了iOS客户端的开发。你唯一需要克服的是对新语言的恐惧,以及适应UI开发的节奏。事实上,大部分服务端程序员都害怕UI编程。

最后,我们进入Android客户端编程。

Android客户端编程

Android部分开发工具,我们使用Android Studio,网络框架使用Retrofit,完整配置参考下方表格:

IDE 编程语言 网络框架
Android Studio Java 1.8 Retrofit

打开Android Studio,选择Start a new Android Studio Project,在打开的页面中填入以下信息:

剩下步骤全部选择默认。

按照iOS编码部分类推,Android端应该也有一个类似UIViewController的控制器。果不其然,在模板工程中就有一个MainActivity,其父类是AppCompatActivity,这就是Android的控制器。

1
2
3
4
5
6
7
8
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}

PS:事实上Android早期版本的控制器就叫Activity,由于系统设计不断变更,最终诞生了兼容性子类AppCompatActivity。这都是早期设计不够严谨,导致的问题。相对而言,iOS端的设计就靠谱了许多。

同样地,在开始编码之前,我们加入所需的第三方依赖。那么,问题来了。Android端如何添加依赖呢?

碰巧,Android端主要的开发语言就是Java。因此,我们依然可以使用Gradle进行依赖管理。碰巧,Android Studio默认支持的就是使用Gradle进行依赖管理。

首先,在app模块目录的build.gradle添加 Retrofit 依赖:

1
implementation 'com.squareup.retrofit2:retrofit:2.4.0'

添加完成后,点击文件右上方Sync now下载依赖:

相对于AFNetworking,Retrofit设计的更加精妙。参考Retrofit官方文档,我们开始加入登录注册逻辑:

1
2
3
4
5
6
7
8
9
10
public interface UserService {

@FormUrlEncoded
@POST("user/login")
Call<User> login(@Field("username") String username, @Field("pwd") String pwd);

@FormUrlEncoded
@POST("user/register")
Call<User> register(@Field("username") String username, @Field("pwd") String pwd);
}

Retrofit设计的其中一个巧妙之处在于:你只需要定义好接口,具体的实现交给Retrofit。你可以看到,上面的代码中我们仅仅定义了请求的类型,以及请求所需要的参数就已经完成了网络部分的所有工作。

不过,操作这个接口实现,需要使用Retrofit实例。接下来,我们参考官方文档生成一个我们需要的Retrofit实例。

在生成Retrofit实例之前,还需要注意一个事情。还记得iOS端我们是怎么完成JSON解析的吗?是的,我们使用了第三方库jsonmodel。

在Json解析的设计上,Retrofit也相当巧妙。Retrofit提供了一个转换适配器用于实现Json数据的自动转换。使用它,你可以自定义自己的Json转换适配器;也可以使用官方已经实现好的适配器。一旦添加了这个适配器,所有的Json解析工作Retrofit就会自动帮忙完成。不再需要像AFNetworking一样在回调里面反复进行Json解析操作。

因此,我们增加一个官方版本的Json转换适配器依赖 converter-json:

1
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

加入Json适配器之后,我们使用一个新的Retrofit管理类RetrofitManager用于生成项目所需要的Retrofit实例。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RetrofitManager {
private static final String BASE_URL = "http://192.168.31.146:8080";

public static Retrofit create(String baseUrl) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
}

public static Retrofit createDefault() {
return create(BASE_URL);
}
}

接下来,我们尝试在MainActivity中测试登录接口,确定是否编写正确。我们在MainActivity的onCreate方法中加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Retrofit retrofit = RetrofitManager.createDefault();

UserService userService = retrofit.create(UserService.class);
Call < User > call = userService.login("1", "1");
call.enqueue(new Callback < User > () {

@Override
public void onResponse(Call < User > call, Response < User > response) {
Log.e("MainActivity", call + "" + response);
}

@Override
public void onFailure(Call < User > call, Throwable t) {
Log.e("MainActivity", call + "" + t);
}
});

打开模拟器,运行,你将看到以下错误:

1
2
3
4
5
6
7
8
9
10
03-18 04:03:24.546 7277-7277/com.youngfeng.androidclient D/NetworkSecurityConfig: No Network Security Config specified, using platform default
03-18 04:03:24.574 7277-7277/com.youngfeng.androidclient W/System.err: java.net.SocketException: Permission denied
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at java.net.Socket.createImpl(Socket.java:454)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at java.net.Socket.getImpl(Socket.java:517)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at java.net.Socket.setSoTimeout(Socket.java:1108)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:238)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:160)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:257)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135)
03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err: at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114)

提示我们权限被拒绝,这是和iOS平台不一样的地方。如果你的应用需要使用网络,你需要在清单文件中手动指定使用网络权限。为此,我们在AndroidManifest.xml文件中添加如下配置:

1
<uses-permission android:name="android.permission.INTERNET" />

再次运行,一切正常。

注意:这里的service部分和服务端的service不一样,它只是Retrofit用于将网络接口分模块处理的一种手段,不要混淆。

上面说到,Android里面的AppCompatActivity就是MVC中的控制器,接下来我们就完成最重要的控制器以及UI部分编码。

a)创建LoginActivity以及布局文件activity_login.xml,在其onCreate方法中使用setContentView接口进行关联。

b)UI编程
你相信吗?一旦你学会了一门新的技术,你的技能就会Double。

iOS UI部分我们使用了约束布局的方式完成了整体布局,Android是否也可以使用约束布局呢?答案是:当然可以。

事实上,Android官方也推荐使用这种布局方式进行页面布局。

切换到可视化布局模式,我们依然使用拖拽UI的方式完成整个布局,完整代码请参考文章最后的附录部分:

PS:目前,Android端的约束布局相对iOS逊色不少,希望后面官方能够提供更多功能支持。

按照同样的方式完成注册页面和首页布局,UI部分开发完成后,尝试跳转到指定控制器。你会发现,出错了。这也是和iOS不一样的地方,Android端四大组件必须在清单文件中注册。具体是什么原因,请自行思考,这不是本文研究的重点。

因此,我们首先在清单文件中对所有控制器进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<activity android:name=".login.LoginActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name=".MainActivity"
android:screenOrientation="portrait"/>

<activity android:name=".register.RegisterActivity"
android:screenOrientation="portrait"/>

然后,以登录为例,我们在控制器中完善登录逻辑:

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
public class LoginActivity extends BaseActivity {
private EditText mUsernameEdit;
private EditText mPwdEdit;
private Button mLoginBtn;
private Button mRegisterBtn;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);

mUsernameEdit = findViewById(R.id.edit_username);
mPwdEdit = findViewById(R.id.edit_pwd);
mLoginBtn = findViewById(R.id.btn_login);
mRegisterBtn = findViewById(R.id.btn_register);

mLoginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
login(mUsernameEdit.getText().toString(), mPwdEdit.getText().toString());
}
});

mRegisterBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(LoginActivity.this, RegisterActivity.class);
startActivity(intent);
}
});
}

private void login(String username, String pwd) {
Retrofit retrofit = RetrofitManager.createDefault();
UserService userService = retrofit.create(UserService.class);
Call<HttpResponse<User>> call = userService.login(username, pwd);

showLoading(true);
call.enqueue(new Callback<HttpResponse<User>>() {
@Override
public void onResponse(Call<HttpResponse<User>> call, Response<HttpResponse<User>> response) {
showLoading(false);

// 例子代码,暂时忽略空值判断
if(HttpResponse.CODE_SUCCESS != response.body().getCode()) {
promptError(response.body().getMsg() + "");
} else {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
intent.putExtra(MainActivity.KEY_USER, response.body().getData());
startActivity(intent);
finish();
}
}

@Override
public void onFailure(Call<HttpResponse<User>> call, Throwable t) {
showLoading(false);

promptError(t.getMessage() + "");
}
});
}
}

至此,按照iOS的开发模式,我们完成了Android客户端的开发。与iOS不同的地方是,Android端控制器必须在清单文件中注册。程序员不能主动创建Activity,只能间接使用intent进行通信。而对于布局,两者都可以使用约束管理的方式完成。从这个角度来说,Android端和iOS端开发切换的难度还是比较低的。

距离全栈还差最后一步

至此,我们已经完成了文章开头定下的目标。以MVC架构为基础,完成了服务端、iOS客户端、Android客户端编码。

然而,很多同学希望成为一个全栈工程师。按照现在的主流开发分支来说,成为一个全栈工程师,你还需要掌握Web前端开发。那么,问题来了,Web前端开发是否也是使用MVC架构呢?

事实上,如果你使用 Angular,你应该早就习惯了MVC。而如果你偏爱React,你恐怕会搭配Redux,使用这种响应式的数据流框架编码。如果你使用Vue,你恐怕也会选择MVC或者MVVM架构。

如果你选择使用MVC,你依然可以按照类推的方式来学习。由于文章篇幅的原因,这部分就不予展示了。

编后说

这篇文章我们以MVC为架构,从服务端编程开始,使用类推的方式依次完成了iOS客户端、Android客户端的开发。

有人可能会说,文章中的例子太简单,没有实际意义。事实上,在学习一门新技术的时候,就要从最基础的部分出发,建立对这门技术的最初印象。很多同学容易一开始就陷入细节当中无法自拔,产生的最直观的结果就是对新技术产生恐惧。因此,你常常可以看到一个程序员面对新东西骂娘,无怪乎。

其实,如果你慢慢进入到细节编程中,你会发现技术之间越来越多的相似性。这个时候你的积极性就会越来越高,编码也会更加得心应手。

我在学习一门新技术的时候,都是先从相似性开始。然后,再去攻克不同的部分。从不同的部分中去提炼相同的思想,这样在面对不同问题的时候,我始终可以使用同样的思想去解决。

当然,我想,你应该会说。虽然克服了框架问题,可是不同的编程语言千差万别。我们无法从一门语言快速过渡到另外一门语言,这在学习新技术的时候才是最大的拦路虎。

你说的很对,这恰好是下一个我想和你分享的问题。

扫描下方二维码,关注欧阳锋工作室,回复“相对论”抢先阅读相对论系列下一篇文章《技术相对论之编程语言》。

附录

本篇例子完整代码:https://github.com/yuanhoujun/it-theory-of-relativity
IntelliJ IDEA下载地址:https://www.jetbrains.com/idea/
Tomcat下载地址:http://tomcat.apache.org/
iOS开发者官网:https://developer.apple.com/
Android开发者官网:https://developer.android.com/index.html

欧阳锋工作室 wechat
扫描二维码,关注欧阳锋工作室