
有同學(xué)問我,你是怎樣學(xué)習(xí)編程的呢?為了回答你的這個(gè)問題,今天,我們一起來做一件非常有意思的事情。我們以MVC架構(gòu)為基,從服務(wù)端編程開始,依次類推iOS、Android,并最終完成登錄、注冊(cè)功能。
What is MVC ?
正文開始之前,我們先來簡(jiǎn)單了解一下MVC架構(gòu)。
MVC全稱是Model-View-Controller,是上個(gè)世紀(jì)80年底Xerox PARC為其編程語言SmallTalk發(fā)明的一直軟件設(shè)計(jì)模式。我們可以用一張圖來表示MVC架構(gòu)模型:

MVC的核心思想是希望通過控制層管理視圖呈現(xiàn),從將邏輯層和視圖層進(jìn)行分離。
服務(wù)端編程其實(shí)就是MVC的最佳實(shí)踐,理解了MVC架構(gòu)之后,我們馬上開始服務(wù)端編程。
服務(wù)端編程
服務(wù)端編程也叫后端編程,主要用于為前端提供數(shù)據(jù)源以及完成必要的業(yè)務(wù)邏輯處理。
這個(gè)部分我們使用Java語言開發(fā),MVC框架使用最常用的 Spring MVC,完整配置請(qǐng)參考下方表格:
| IDE | 編程語言 | 框架 | 數(shù)據(jù)庫 | 服務(wù)器 |
|---|---|---|---|---|
| IntelliJ IDEA | Java 1.8 | Spring MVC | MySQL | Tomcat 7.0.57 |
為了簡(jiǎn)化數(shù)據(jù)庫的訪問,我們?cè)僭黾右粋€(gè)輕量級(jí)的數(shù)據(jù)庫訪問框架 MyBatis。
這里假設(shè)你已經(jīng)正確安裝了MySQL數(shù)據(jù)庫和Tomcat服務(wù)器,如果你對(duì)具體的安裝步驟有疑問,請(qǐng)?jiān)谖恼孪路皆u(píng)論告訴我。
在開始編程之前,我們需要完成以下準(zhǔn)備工作:
第一步:創(chuàng)建數(shù)據(jù)庫d_user以及用戶表t_user用于保存用戶數(shù)據(jù)
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創(chuàng)建一個(gè)Gradle依賴工程



最后一個(gè)步驟選擇工作目錄確定即可。
第三步:在build.gradle腳本文件中添加Spring MVC以及MyBatis依賴
compile group: 'org.springframework', name: 'spring-webmvc', version: '5.0.4.RELEASE'
compile group: 'org.mybatis', name: 'mybatis', version: '3.4.6'
第四步:關(guān)聯(lián)本地Tomcat服務(wù)器
a)編輯運(yùn)行設(shè)置,選擇本地Tomcat服務(wù)器


b)選擇以war包的方式部署到Tomcat

c)在瀏覽器中輸入http://localhost:8080測(cè)試工作是否正常

如果看到下面這個(gè)界面,證明一切工作正常

第五步:配置Spring MVC
備注:參考官方文檔 Web on Servlet Stack
a)在webapp目錄下面生成WEB-INF/web.xml配置文件
選擇菜單File->Project Structure進(jìn)入如下界面:

在彈出的界面中設(shè)置路徑為.../webapp/WEB-INF即可。
b)在web.xml文件中添加如下配置信息
<?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完成請(qǐng)求的攔截分發(fā)。配置文件中引用了另外一個(gè)配置文件app-context.xml,這個(gè)配置文件主要是完成Spring的依賴注入。
c)在app-context.xml配置文件中添加如下信息
<?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實(shí)現(xiàn)Json自動(dòng)解析
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:不得不承認(rèn),Java后端開發(fā)的xml文件配置實(shí)在是一件繁瑣至極的事情,盡管我們只需要配置一次。為了簡(jiǎn)化配置,Spring官方推出了一個(gè)重磅產(chǎn)品 Spring Boot。不過,這不是本文討論的重點(diǎn),感興趣的同學(xué)請(qǐng)自行了解。
雖然我們已經(jīng)完成了Spring的配置,但MyBatis的配置工作才剛剛開始。
配置MyBatis
為了簡(jiǎn)化Spring中MyBatis的配置,我們?cè)僭黾右粋€(gè)MyBatis官方的提供的 mybatis-spring 庫。
compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.2'
備注:參考官方文檔 mybatis-spring
a)在spring配置文件app-context.xml配置文件中添加如下bean配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
b)指定數(shù)據(jù)源
b1) 添加Spring JDBC與MySQL Connector依賴
compile group: 'org.springframework', name: 'spring-jdbc', version: '5.0.4.RELEASE'
compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'
注意:因?yàn)椴糠忠蕾嚢淮嬖谟贘Center,需要在build.gradle腳本中添加jcenter maven源
repositories {
mavenCentral()
jcenter()
}
b2)在app-context.xml文件中添加如下配置:
<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)在類路徑目錄下創(chuàng)建db.properties文件指定MySQL數(shù)據(jù)庫信息
jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/d_server
jdbc.username = root
jdbc.password = root
至此,所有的配置工作終于完成了,接下來進(jìn)入最重要的編碼階段。
由于控制層需要依賴模型層的代碼,因此,我們按照從下往上的原則進(jìn)行編碼。
a)先完成數(shù)據(jù)庫的訪問部分(DAO)
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);
}
結(jié)合MyBatis,這個(gè)部分的工作很簡(jiǎn)單,甚至DAO的實(shí)現(xiàn)都不需要手動(dòng)編碼。
為了實(shí)現(xiàn)DAO的依賴注入,我們?cè)赼pp-context.xml配置文件中添加如下配置:
<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部分是控制層直接調(diào)用的接口,從抽象思維來說,也應(yīng)該使用面向接口的方式編碼。這里為了簡(jiǎn)化,Service部分我們直接使用一個(gè)類來實(shí)現(xiàn)了。
@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)控制層編碼
@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用于區(qū)分不同錯(cuò)誤
boolean isExist = userService.isExist(username);
if(!isExist) {
response.setCode(Response.CODE_USER_NOT_EXIST);
response.setMsg("用戶不存在或密碼錯(cuò)誤");
}
// 判斷用戶名和密碼是否匹配
isExist = userService.isExist(username, pwd);
if(!isExist) {
response.setCode(Response.CODE_USER_PWD_ERR);
response.setMsg("用戶不存在或密碼錯(cuò)誤");
}
return response;
}
@ResponseBody
@GetMapping("/register")
public Response register(@RequestParam("username") String username, @RequestParam("pwd") String pwd) {
Response response = new Response();
// 注冊(cè)之前,判斷用戶名是否已存在
boolean isExist = userService.isExist(username);
if(isExist) {
response.setCode(Response.CODE_USER_HAS_EXIST);
response.setMsg("用戶名已存在");
} else {
userService.saveUser(username, pwd);
}
return response;
}
}
想必大家應(yīng)該已經(jīng)注意到了,控制層部分請(qǐng)求類型我使用了GET,這是為了方便在瀏覽器上面測(cè)試。測(cè)試通過后,要修改為POST請(qǐng)求類型。
以上代碼,我已經(jīng)在瀏覽器上測(cè)試通過。接下來,我們馬上進(jìn)入iOS客戶端編程。
iOS客戶端編程
iOS部分開發(fā)工具我們使用Xcode 9.2,其實(shí)你也可以使用AppCode,這是基于IntelliJ IDE開發(fā)的一款I(lǐng)DE,使用習(xí)慣完全接近IntelliJ IDE。
為了防止部分同學(xué)對(duì)Swift語言不熟悉,我們使用最常見的編程語言O(shè)C。
完整配置請(qǐng)參考如下表格:
| IDE | 編程語言 | 網(wǎng)絡(luò)框架 |
|---|---|---|
| Xcode 9.2 | Objective C | AFNetworking |
打開Xcode,依次選擇Create new Xcode Project->Single View App
下一步填入如下信息,語言選擇OC

第一步:完成UI部分
這一部分參考蘋果官方文檔,按照蘋果官方推薦,我們使用Storyboard進(jìn)行布局。由于我們只是完成一個(gè)簡(jiǎn)單的Demo,所有的頁面將在同一個(gè)Storyboard中完成。實(shí)際開發(fā)過程中,要根據(jù)功能劃分Storyboard,方便進(jìn)行小組開發(fā)。

使用約束布局我們很快完成了UI的構(gòu)建,接下來進(jìn)入最重要的編碼階段。約束布局的意思就是為一個(gè)控件添加N個(gè)約束,使其固定在某個(gè)位置。這個(gè)部分只要稍加嘗試,就能掌握。具體的使用方法,請(qǐng)參考官方文檔。
第二步:創(chuàng)建控制器,并關(guān)聯(lián)UI
從服務(wù)器編程類推,iOS編程模型中應(yīng)該也有一個(gè)叫Controller的東西。果不其然,在iOS新創(chuàng)建的工程中就有一個(gè)叫做ViewController的類,其父類是UIViewController。沒錯(cuò),這就是傳說中的控制器。
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
完成登錄、注冊(cè)功能,我們至少需要三個(gè)控制器:LoginViewController、RegisterViewController、MainViewController,分別代表登錄、注冊(cè)、首頁三個(gè)頁面。
接下來,將控制器與UI進(jìn)行關(guān)聯(lián)。
UI關(guān)聯(lián)控制器部分,如果你不知道,請(qǐng)先參考蘋果官方文檔。
事實(shí)上,Xcode的Interface Builder非常好用。按照下圖操作即可:

最后,關(guān)聯(lián)按鈕點(diǎn)擊事件以及輸入框。
選中控件并按住鼠標(biāo)右鍵拖拽到控制器源碼中,松開,并選擇相應(yīng)類型即可:

以登錄控制器為例,拖拽完成后的源碼如下:
@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 {
}
接下來進(jìn)入網(wǎng)絡(luò)部分編程。
為了簡(jiǎn)化網(wǎng)絡(luò)部分編程,我們引入AFNetworking框架。還記得服務(wù)端編程是怎么引入依賴的嗎?沒錯(cuò),是Gradle。iOS端也有類似的依賴管理工具Cocoapods,這個(gè)部分如果不會(huì)依然請(qǐng)你參考官方文檔。
使用如下步驟安裝依賴(這里假設(shè)你已經(jīng)正確安裝了Cocoapod):
a)在根目錄下面創(chuàng)建Podfile文件,并添加如下內(nèi)容:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
target 'IOSClient' do
pod 'AFNetworking', '~> 3.0'
end
b)安裝依賴
pod install
PS:可能有人會(huì)問,為什么服務(wù)端編程沒有安裝依賴的步驟。其實(shí),很簡(jiǎn)單,intelliJ IDE非常智能,它自動(dòng)檢測(cè)了build.gradle文件的修改。一旦發(fā)現(xiàn)修改,自動(dòng)安裝依賴。因此,看起來就像沒有依賴安裝這個(gè)步驟一樣。事實(shí)上,Cocoapod并非蘋果官方的產(chǎn)品,如果產(chǎn)品來自蘋果官方,恐怕Xcode也會(huì)支持自動(dòng)安裝依賴。
依賴安裝完成后,為了更好地服務(wù)我們的業(yè)務(wù)。我們對(duì)網(wǎng)絡(luò)請(qǐng)求做一點(diǎn)簡(jiǎn)單封裝,增加HttpClient類,僅提供一個(gè)POST請(qǐng)求接口即可。
//
// 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
為了簡(jiǎn)化JSON解析,我們?cè)黾右粋€(gè)最常見的Json解析庫 jsonmodel 庫。等待對(duì)話框也使用最常見的第三方庫 SVProgressHUD。
pod 'JSONModel'
pod 'SVProgressHUD'
安裝依賴使用同樣的命令pod install即可。
接下來,我們添加登錄注冊(cè)邏輯,完成最后部分編碼:
// 登錄部分邏輯
- (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]]) {
// 例子代碼,這里不做嚴(yán)格判斷了
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];
}
// 注冊(cè)部分邏輯
- (IBAction)register:(id)sender {
NSString *username = _mUsernameTextField.text;
NSString *pwd = _mPwdTextField.text;
NSString *confrimPwd = _mConfirmTextField.text;
if([StringUtil isBlankString: username]) {
[self promptError: @"請(qǐng)輸入用戶名"];
return;
}
if([StringUtil isBlankString: pwd]) {
[self promptError: @"請(qǐng)輸入用戶密碼"];
return;
}
if([StringUtil isBlankString: confrimPwd]) {
[self promptError: @"請(qǐng)輸入確認(rèn)密碼"];
return;
}
if(![pwd isEqualToString: confrimPwd]) {
[self promptError: @"兩次密碼輸入不一致,請(qǐng)重新輸入"];
return;
}
HttpClient *client = [HttpClient sharedInstance];
[client POST: @"/user/register" params: @{@"username" : username, @"pwd" : pwd} success:^(NSString *url, id data) {
[self promptError: @"注冊(cè)成功" handler:^(UIAlertAction *action) {
[self.navigationController popViewControllerAnimated: YES];
}];
} error:^(NSString *url, NSInteger httpCode, NSInteger bizCode, NSString *error) {
[self promptError: error];
}];
}
通過上面的步驟,我們已經(jīng)完成了iOS客戶端的開發(fā)。蘋果官方默認(rèn)支持的就是經(jīng)典的MVC模式。因此,我們完全參考服務(wù)端開發(fā)模式完成了iOS客戶端的開發(fā)。你唯一需要克服的是對(duì)新語言的恐懼,以及適應(yīng)UI開發(fā)的節(jié)奏。事實(shí)上,大部分服務(wù)端程序員都害怕UI編程。
最后,我們進(jìn)入Android客戶端編程。
Android客戶端編程
Android部分開發(fā)工具,我們使用Android Studio,網(wǎng)絡(luò)框架使用Retrofit,完整配置參考下方表格:
| IDE | 編程語言 | 網(wǎng)絡(luò)框架 |
|---|---|---|
| Android Studio | Java 1.8 | Retrofit |
打開Android Studio,選擇Start a new Android Studio Project,在打開的頁面中填入以下信息:

剩下步驟全部選擇默認(rèn)。
按照iOS編碼部分類推,Android端應(yīng)該也有一個(gè)類似UIViewController的控制器。果不其然,在模板工程中就有一個(gè)MainActivity,其父類是AppCompatActivity,這就是Android的控制器。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
PS:事實(shí)上Android早期版本的控制器就叫Activity,由于系統(tǒng)設(shè)計(jì)不斷變更,最終誕生了兼容性子類AppCompatActivity。這都是早期設(shè)計(jì)不夠嚴(yán)謹(jǐn),導(dǎo)致的問題。相對(duì)而言,iOS端的設(shè)計(jì)就靠譜了許多。
同樣地,在開始編碼之前,我們加入所需的第三方依賴。那么,問題來了。Android端如何添加依賴呢?
碰巧,Android端主要的開發(fā)語言就是Java。因此,我們依然可以使用Gradle進(jìn)行依賴管理。碰巧,Android Studio默認(rèn)支持的就是使用Gradle進(jìn)行依賴管理。
首先,在app模塊目錄的build.gradle添加 Retrofit 依賴:
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
添加完成后,點(diǎn)擊文件右上方Sync now下載依賴:

相對(duì)于AFNetworking,Retrofit設(shè)計(jì)的更加精妙。參考Retrofit官方文檔,我們開始加入登錄注冊(cè)邏輯:
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設(shè)計(jì)的其中一個(gè)巧妙之處在于:你只需要定義好接口,具體的實(shí)現(xiàn)交給Retrofit。你可以看到,上面的代碼中我們僅僅定義了請(qǐng)求的類型,以及請(qǐng)求所需要的參數(shù)就已經(jīng)完成了網(wǎng)絡(luò)部分的所有工作。
不過,操作這個(gè)接口實(shí)現(xiàn),需要使用Retrofit實(shí)例。接下來,我們參考官方文檔生成一個(gè)我們需要的Retrofit實(shí)例。
在生成Retrofit實(shí)例之前,還需要注意一個(gè)事情。還記得iOS端我們是怎么完成JSON解析的嗎?是的,我們使用了第三方庫jsonmodel。
在Json解析的設(shè)計(jì)上,Retrofit也相當(dāng)巧妙。Retrofit提供了一個(gè)轉(zhuǎn)換適配器用于實(shí)現(xiàn)Json數(shù)據(jù)的自動(dòng)轉(zhuǎn)換。使用它,你可以自定義自己的Json轉(zhuǎn)換適配器;也可以使用官方已經(jīng)實(shí)現(xiàn)好的適配器。一旦添加了這個(gè)適配器,所有的Json解析工作Retrofit就會(huì)自動(dòng)幫忙完成。不再需要像AFNetworking一樣在回調(diào)里面反復(fù)進(jìn)行Json解析操作。
因此,我們?cè)黾右粋€(gè)官方版本的Json轉(zhuǎn)換適配器依賴 converter-json:
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
加入Json適配器之后,我們使用一個(gè)新的Retrofit管理類RetrofitManager用于生成項(xiàng)目所需要的Retrofit實(shí)例。完整代碼如下:
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中測(cè)試登錄接口,確定是否編寫正確。我們?cè)贛ainActivity的onCreate方法中加入如下代碼:
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);
}
});
打開模擬器,運(yùn)行,你將看到以下錯(cuò)誤:
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)
提示我們權(quán)限被拒絕,這是和iOS平臺(tái)不一樣的地方。如果你的應(yīng)用需要使用網(wǎng)絡(luò),你需要在清單文件中手動(dòng)指定使用網(wǎng)絡(luò)權(quán)限。為此,我們?cè)贏ndroidManifest.xml文件中添加如下配置:
<uses-permission android:name="android.permission.INTERNET" />
再次運(yùn)行,一切正常。
注意:這里的service部分和服務(wù)端的service不一樣,它只是Retrofit用于將網(wǎng)絡(luò)接口分模塊處理的一種手段,不要混淆。
上面說到,Android里面的AppCompatActivity就是MVC中的控制器,接下來我們就完成最重要的控制器以及UI部分編碼。
a)創(chuàng)建LoginActivity以及布局文件activity_login.xml,在其onCreate方法中使用setContentView接口進(jìn)行關(guān)聯(lián)。
b)UI編程
你相信嗎?一旦你學(xué)會(huì)了一門新的技術(shù),你的技能就會(huì)Double。
iOS UI部分我們使用了約束布局的方式完成了整體布局,Android是否也可以使用約束布局呢?答案是:當(dāng)然可以。
事實(shí)上,Android官方也推薦使用這種布局方式進(jìn)行頁面布局。
切換到可視化布局模式,我們依然使用拖拽UI的方式完成整個(gè)布局,完整代碼請(qǐng)參考文章最后的附錄部分:

PS:目前,Android端的約束布局相對(duì)iOS遜色不少,希望后面官方能夠提供更多功能支持。
按照同樣的方式完成注冊(cè)頁面和首頁布局,UI部分開發(fā)完成后,嘗試跳轉(zhuǎn)到指定控制器。你會(huì)發(fā)現(xiàn),出錯(cuò)了。這也是和iOS不一樣的地方,Android端四大組件必須在清單文件中注冊(cè)。具體是什么原因,請(qǐng)自行思考,這不是本文研究的重點(diǎn)。
因此,我們首先在清單文件中對(duì)所有控制器進(jìn)行注冊(cè):
<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"/>
然后,以登錄為例,我們?cè)诳刂破髦型晟频卿涍壿嫞?/p>
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);
// 例子代碼,暫時(shí)忽略空值判斷
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的開發(fā)模式,我們完成了Android客戶端的開發(fā)。與iOS不同的地方是,Android端控制器必須在清單文件中注冊(cè)。程序員不能主動(dòng)創(chuàng)建Activity,只能間接使用intent進(jìn)行通信。而對(duì)于布局,兩者都可以使用約束管理的方式完成。從這個(gè)角度來說,Android端和iOS端開發(fā)切換的難度還是比較低的。
距離全棧還差最后一步
至此,我們已經(jīng)完成了文章開頭定下的目標(biāo)。以MVC架構(gòu)為基礎(chǔ),完成了服務(wù)端、iOS客戶端、Android客戶端編碼。
然而,很多同學(xué)希望成為一個(gè)全棧工程師。按照現(xiàn)在的主流開發(fā)分支來說,成為一個(gè)全棧工程師,你還需要掌握Web前端開發(fā)。那么,問題來了,Web前端開發(fā)是否也是使用MVC架構(gòu)呢?
事實(shí)上,如果你使用 Angular,你應(yīng)該早就習(xí)慣了MVC。而如果你偏愛React,你恐怕會(huì)搭配Redux,使用這種響應(yīng)式的數(shù)據(jù)流框架編碼。如果你使用Vue,你恐怕也會(huì)選擇MVC或者M(jìn)VVM架構(gòu)。
如果你選擇使用MVC,你依然可以按照類推的方式來學(xué)習(xí)。由于文章篇幅的原因,這部分就不予展示了。
編后說
這篇文章我們以MVC為架構(gòu),從服務(wù)端編程開始,使用類推的方式依次完成了iOS客戶端、Android客戶端的開發(fā)。
有人可能會(huì)說,文章中的例子太簡(jiǎn)單,沒有實(shí)際意義。事實(shí)上,在學(xué)習(xí)一門新技術(shù)的時(shí)候,就要從最基礎(chǔ)的部分出發(fā),建立對(duì)這門技術(shù)的最初印象。很多同學(xué)容易一開始就陷入細(xì)節(jié)當(dāng)中無法自拔,產(chǎn)生的最直觀的結(jié)果就是對(duì)新技術(shù)產(chǎn)生恐懼。因此,你常常可以看到一個(gè)程序員面對(duì)新東西罵娘,無怪乎。
其實(shí),如果你慢慢進(jìn)入到細(xì)節(jié)編程中,你會(huì)發(fā)現(xiàn)技術(shù)之間越來越多的相似性。這個(gè)時(shí)候你的積極性就會(huì)越來越高,編碼也會(huì)更加得心應(yīng)手。
我在學(xué)習(xí)一門新技術(shù)的時(shí)候,都是先從相似性開始。然后,再去攻克不同的部分。從不同的部分中去提煉相同的思想,這樣在面對(duì)不同問題的時(shí)候,我始終可以使用同樣的思想去解決。
當(dāng)然,我想,你應(yīng)該會(huì)說。雖然克服了框架問題,可是不同的編程語言千差萬別。我們無法從一門語言快速過渡到另外一門語言,這在學(xué)習(xí)新技術(shù)的時(shí)候才是最大的攔路虎。
你說的很對(duì),這恰好是下一個(gè)我想和你分享的問題。關(guān)注我的簡(jiǎn)書,下一篇我們一起探討《技術(shù)相對(duì)論之編程語言》
附錄
本篇例子完整代碼:https://github.com/yuanhoujun/it-theory-of-relativity
IntelliJ IDEA下載地址:https://www.jetbrains.com/idea/
Tomcat下載地址:http://tomcat.apache.org/
iOS開發(fā)者官網(wǎng):https://developer.apple.com/
Android開發(fā)者官網(wǎng):https://developer.android.com/index.html