技術(shù)相對(duì)論之軟件架構(gòu)

文 | 歐陽鋒

有同學(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,008評(píng)論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,551評(píng)論 19 139
  • 2016年9月14日 陰 跟我說話的人們 睡去吧 看我恢復(fù)清醒 好好聽一席談話 日子是沙 略過了第一層選拔 被放進(jìn)...
    鮮栗子閱讀 253評(píng)論 1 0
  • Edit 我的知識(shí)管理 【法律}【知識(shí)管理】 一、首先不能把目光放得過于分散,而要盡量集中到一個(gè)適合的域。 一個(gè)合...
    唐山律師老王閱讀 198評(píng)論 0 0
  • 文/南木婉清 夜泊秦淮宿水鄉(xiāng) 桂堂西畔聽雨眠 青瓦烏蓬江如黛 長(zhǎng)亭萍州入夢(mèng)來
    南木婉清閱讀 581評(píng)論 8 7

友情鏈接更多精彩內(nèi)容