当前位置: 首页 > news >正文

技术相对论之软件架构

有同学问我,你是怎样学习编程的呢?为了回答你的这个问题,今天,我们一起来做一件非常有意思的事情。我们以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 IDEAJava 1.8Spring MVCMySQLTomcat 7.0.57

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

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

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

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

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依赖

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文件中添加如下配置信息

<?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配置文件中添加如下信息

<?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自动解析

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 库。

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)指定数据源

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'
复制代码

注意:因为部分依赖包只存在于JCenter,需要在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)在类路径目录下创建db.properties文件指定MySQL数据库信息

jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/d_server
jdbc.username = root
jdbc.password = root
复制代码

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

由于控制层需要依赖模型层的代码,因此,我们按照从下往上的原则进行编码。 a)先完成数据库的访问部分(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);
}
复制代码

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

为了实现DAO的依赖注入,我们在app-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部分是控制层直接调用的接口,从抽象思维来说,也应该使用面向接口的方式编码。这里为了简化,Service部分我们直接使用一个类来实现了。

@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用于区分不同错误
        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.2Objective CAFNetworking

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

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

第一步:完成UI部分

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

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

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

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

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController


@end
复制代码

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

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

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

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

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

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

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

@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文件,并添加如下内容:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target 'IOSClient' do
pod 'AFNetworking', '~> 3.0'
end
复制代码

b)安装依赖

pod install
复制代码

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

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

//
//  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。

pod 'JSONModel'
pod 'SVProgressHUD'
复制代码

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

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

// 登录部分逻辑
- (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 StudioJava 1.8Retrofit

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

剩下步骤全部选择默认。

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

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 依赖:

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
复制代码

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

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

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:

implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
复制代码

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

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方法中加入如下代码:

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);
    }
});
复制代码

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

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文件中添加如下配置:

<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端四大组件必须在清单文件中注册。具体是什么原因,请自行思考,这不是本文研究的重点。

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

    <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"/>
复制代码

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

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客户端的开发。

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

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

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

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

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

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

附录

本篇例子完整代码:github.com/yuanhoujun/…

IntelliJ IDEA下载地址:www.jetbrains.com/idea/

Tomcat下载地址:tomcat.apache.org/

iOS开发者官网:developer.apple.com/

Android开发者官网:developer.android.com/index.html

相关文章:

  • Fragment 生命周期怎么来的?
  • Redis和Memcache和MongoDB简介及区别分析(整理)
  • ubuntu16.4安装最新版wine3.0
  • c++中局部变量初始化的问题
  • WordCount
  • 外卖也智能!美团骑手智能助手的技术与实践
  • 【协议转换和消息路由】camel-spring-boot-starter 实践
  • 坑货!阿里奇门中心
  • [ssh]如何设计ARM板上多用户key登录系统
  • 71. Simplify Path
  • numpy 数组运算
  • Java 选择排序selection sort
  • 磁盘管理
  • 利用SCVMM 2012 R2来管理Azure虚拟机
  • AlphaGo告诉我们人工智能成功的五大秘诀,AI下一个风口在哪里?
  • SegmentFault for Android 3.0 发布
  • 2019年如何成为全栈工程师?
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • Angular2开发踩坑系列-生产环境编译
  • CentOS从零开始部署Nodejs项目
  • Cumulo 的 ClojureScript 模块已经成型
  • Effective Java 笔记(一)
  • javascript 总结(常用工具类的封装)
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • PyCharm搭建GO开发环境(GO语言学习第1课)
  • ReactNative开发常用的三方模块
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 翻译 | 老司机带你秒懂内存管理 - 第一部(共三部)
  • 给新手的新浪微博 SDK 集成教程【一】
  • 后端_ThinkPHP5
  • 机器学习学习笔记一
  • 基于axios的vue插件,让http请求更简单
  • 蓝海存储开关机注意事项总结
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 实战|智能家居行业移动应用性能分析
  • 算法之不定期更新(一)(2018-04-12)
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • Nginx惊现漏洞 百万网站面临“拖库”风险
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 通过调用文摘列表API获取文摘
  • 小白应该如何快速入门阿里云服务器,新手使用ECS的方法 ...
  • ​低代码平台的核心价值与优势
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • #HarmonyOS:Web组件的使用
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • #我与Java虚拟机的故事#连载14:挑战高薪面试必看
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • (+3)1.3敏捷宣言与敏捷过程的特点
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (react踩过的坑)antd 如何同时获取一个select 的value和 label值
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • ****Linux下Mysql的安装和配置
  • .\OBJ\test1.axf: Error: L6230W: Ignoring --entry command. Cannot find argumen 'Reset_Handler'
  • .net CHARTING图表控件下载地址
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net