Android项目重构之路:实现篇

原创文章,转载请注明:转载自Keegan小钢
并标明原文链接:http://keeganlee.me/post/android/20150629
微信订阅号:keeganlee_me
写于2015-06-29


Android项目重构之路:架构篇
Android项目重构之路:界面篇
Android项目重构之路:实现篇


前两篇文章Android项目重构之路:架构篇Android项目重构之路:界面篇已经讲了我的项目开始搭建时的架构设计和界面设计,这篇就讲讲具体怎么实现的,以实现最小化可用产品(MVP)的目标,用最简单的方式来搭建架构和实现代码。
IDE采用Android Studio,Demo实现的功能为用户注册、登录和展示一个券列表,数据采用我们现有项目的测试数据,接口也是我们项目中的测试接口。

项目搭建

根据架构篇所讲的,将项目分为了四个层级:模型层、接口层、核心层、界面层。四个层级之间的关系如下图所示:

实现上,在Android Studio分为了相应的四个模块(Module):model、api、core、app
model为模型层,api为接口层,core为核心层,app为界面层。
model、api、core这三个模块的类型为library,app模块的类型为application。
四个模块之间的依赖设置为:model没有任何依赖,接口层依赖了模型层,核心层依赖了模型层和接口层,界面层依赖了核心层和模型层。
项目搭建的步骤如下:

  1. 创建新项目,项目名称为KAndroid,包名为com.keegan.kandroid。默认已创建了app模块,查看下app模块下的build.gradle,会看到第一行为:

    apply plugin: 'com.android.application'
    

    这行表明了app模块是application类型的。

  2. 分别新建模块model、api、core,Module Type都选为Android Library,在Add an activity to module页面选择Add No Activity,这三个模块做为库使用,并不需要界面。创建完之后,查看相应模块的build.gradle,会看到第一行为:

    apply plugin: 'com.android.library'
    
  3. 建立模块之间的依赖关系。有两种方法可以设置:

第一种:通过右键模块,然后Open Module Settings,选择模块的Dependencies,点击左下方的加号,选择Module dependency,最后选择要依赖的模块,下图为api模块添加了model依赖;

![](~/dependencies.jpeg)

第二种:直接在模块的build.gradle设置。打开build.gradle,在最后的dependencies一项里面添加新的一行:compile      project(':ModuleName'),比如app模块添加对model模块和core模块依赖之后的dependencies如下:
```Groovy
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile project(':model')
    compile project(':core')
}
```

通过上面两种方式的任意一种,创建了模块之间的依赖关系之后,每个模块的build.gradle的dependencies项的结果将会如下:
model:
Groovy
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
}

api:
Groovy
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
}

core:
Groovy
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
compile project(':api')
}

app:
Groovy
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.0.0'
compile project(':model')
compile project(':core')
}

创建业务对象模型

业务对象模型统一存放于model模块,是对业务数据的封装,大部分都是从接口传过来的对象,因此,其属性也与接口传回的对象属性相一致。在这个Demo里,只有一个业务对象模型,封装了券的基本信息,以下是该实体类的代码:

/**
 * 券的业务模型类,封装了券的基本信息。
 * 券分为了三种类型:现金券、抵扣券、折扣券。
 * 现金券是拥有固定面值的券,有固定的售价;
 * 抵扣券是满足一定金额后可以抵扣的券,比如满100减10元;
 * 折扣券是可以打折的券。
 *
 * @version 1.0 创建时间:15/6/21
 */
public class CouponBO implements Serializable {
    private static final long serialVersionUID = -8022957276104379230L;
    private int id;                // 券id
    private String name;           // 券名称
    private String introduce;      // 券简介
    private int modelType;         // 券类型,1为现金券,2为抵扣券,3为折扣券
    private double faceValue;      // 现金券的面值
    private double estimateAmount; // 现金券的售价
    private double debitAmount;    // 抵扣券的抵扣金额
    private double discount;       // 折扣券的折扣率(0-100)
    private double miniAmount;     // 抵扣券和折扣券的最小使用金额

    // TODO 所有属性的getter和setter
}

接口层的封装

在这个Demo里,提供了4个接口:一个发送验证码的接口、一个注册接口、一个登录接口、一个获取券列表的接口。这4个接口具体如下:

  • 发送验证码接口

    URL:http://uat.b.quancome.com/platform/api
    参数:

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method service.sendSmsCode4Register String
    phoneNum 手机号码 String

    输出样例:
    json
    { "event": "0", "msg": "success" }

  • 注册接口

    URL:http://uat.b.quancome.com/platform/api
    参数:

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method customer.registerByPhone String
    phoneNum 手机号码 String
    code 验证码 String
    password MD5加密密码 String

    输出样例:
    json
    { "event": "0", "msg": "success" }

  • 登录接口

    URL:http://uat.b.quancome.com/platform/api
    其他参数:

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method customer.loginByApp String
    loginName 登录名(手机号) String
    password MD5加密密码 String
    imei 手机imei串号 String
    loginOS 系统,android为1 int

    输出样例:
    json
    { "event": "0", "msg": "success" }

  • 券列表

    URL:http://uat.b.quancome.com/platform/api
    其他参数:

    参数名 描述 类型
    appKey ANDROID_KCOUPON String
    method issue.listNewCoupon String
    currentPage 当前页数 int
    pageSize 每页显示数量 int

    输出样例:
    json
    { "event": "0", "msg": "success", "maxCount": 125, "maxPage": 7, "currentPage": 1, "pageSize": 20, "objList":[
    {"id": 1, "name": "测试现金券", "modelType": 1, ...},
    {...},
    ...
    ]}

架构篇已经讲过,接口返回的json数据有三种固定结构:

{"event": "0", "msg": "success"}
{"event": "0", "msg": "success", "obj":{...}}
{"event": "0", "msg": "success", "objList":[{...}, {...}], "currentPage": 1, "pageSize": 20, "maxCount": 2, "maxPage": 1}

因此可以封装成实体类,代码如下:

public class ApiResponse<T> {
    private String event;    // 返回码,0为成功
    private String msg;      // 返回信息
    private T obj;           // 单个对象
    private T objList;       // 数组对象
    private int currentPage; // 当前页数
    private int pageSize;    // 每页显示数量
    private int maxCount;    // 总条数
    private int maxPage;     // 总页数
    
    // 构造函数,初始化code和msg
    public ApiResponse(String event, String msg) {
        this.event = event;
        this.msg = msg;
    }
    
    // 判断结果是否成功
    public boolean isSuccess() {
        return event.equals("0");
    }
    
    // TODO 所有属性的getter和setter
}

上面4个接口,URL和appKey都是一样的,用来区别不同接口的则是method字段,因此,URL和appKey可以统一定义,method则根据不同接口定义不同常量。而除去appKey和method,剩下的参数才是每个接口需要定义的参数。因此,对上面4个接口的定义如下:

public interface Api {
    // 发送验证码
    public final static String SEND_SMS_CODE = "service.sendSmsCode4Register";
    // 注册
    public final static String REGISTER = "customer.registerByPhone";
    // 登录
    public final static String LOGIN = "customer.loginByApp";
    // 券列表
    public final static String LIST_COUPON = "issue.listNewCoupon";
    
    /**
     * 发送验证码
     *
     * @param phoneNum 手机号码
     * @return 成功时返回:{ "event": "0", "msg":"success" }
     */
    public ApiResponse<Void> sendSmsCode4Register(String phoneNum);

    /**
     * 注册
     *
     * @param phoneNum 手机号码
     * @param code     验证码
     * @param password MD5加密的密码
     * @return 成功时返回:{ "event": "0", "msg":"success" }
     */
    public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password);

    /**
     * 登录
     *
     * @param loginName 登录名(手机号)
     * @param password  MD5加密的密码
     * @param imei      手机IMEI串号
     * @param loginOS   Android为1
     * @return 成功时返回:{ "event": "0", "msg":"success" }
     */
    public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS);

    /**
     * 券列表
     *
     * @param currentPage 当前页数
     * @param pageSize    每页显示数量
     * @return 成功时返回:{ "event": "0", "msg":"success", "objList":[...] }
     */
    public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize);
}

Api的实现类则是ApiImpl了,实现类需要封装好请求数据并向服务器发起请求,并将响应结果的数据转为ApiResonse返回。而向服务器发送请求并将响应结果返回的处理则封装到http引擎类去处理。另外,这里引用了gson将json转为对象。ApiImpl的实现代码如下:

public class ApiImpl implements Api {
    private final static String APP_KEY = "ANDROID_KCOUPON";
    private final static String TIME_OUT_EVENT = "CONNECT_TIME_OUT";
    private final static String TIME_OUT_EVENT_MSG = "连接服务器失败";
    // http引擎
    private HttpEngine httpEngine;

    public ApiImpl() {
        httpEngine = HttpEngine.getInstance();
    }

    @Override
    public ApiResponse<Void> sendSmsCode4Register(String phoneNum) {
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("appKey", APP_KEY);
        paramMap.put("method", SEND_SMS_CODE);
        paramMap.put("phoneNum", phoneNum);

        Type type = new TypeToken<ApiResponse<Void>>(){}.getType();
        try {
            return httpEngine.postHandle(paramMap, type);
        } catch (IOException e) {
            return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
        }
    }

    @Override
    public ApiResponse<Void> registerByPhone(String phoneNum, String code, String password) {
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("appKey", APP_KEY);
        paramMap.put("method", REGISTER);
        paramMap.put("phoneNum", phoneNum);
        paramMap.put("code", code);
        paramMap.put("password", EncryptUtil.makeMD5(password));

        Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
        try {
            return httpEngine.postHandle(paramMap, type);
        } catch (IOException e) {
            return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
        }
    }

    @Override
    public ApiResponse<Void> loginByApp(String loginName, String password, String imei, int loginOS) {
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("appKey", APP_KEY);
        paramMap.put("method", LOGIN);
        paramMap.put("loginName", loginName);
        paramMap.put("password", EncryptUtil.makeMD5(password));
        paramMap.put("imei", imei);
        paramMap.put("loginOS", String.valueOf(loginOS));

        Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
        try {
            return httpEngine.postHandle(paramMap, type);
        } catch (IOException e) {
            return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
        }
    }

    @Override
    public ApiResponse<List<CouponBO>> listNewCoupon(int currentPage, int pageSize) {
        Map<String, String> paramMap = new HashMap<String, String>();
        paramMap.put("appKey", APP_KEY);
        paramMap.put("method", LIST_COUPON);
        paramMap.put("currentPage", String.valueOf(currentPage));
        paramMap.put("pageSize", String.valueOf(pageSize));

        Type type = new TypeToken<ApiResponse<List<CouponBO>>>(){}.getType();
        try {
            return httpEngine.postHandle(paramMap, type);
        } catch (IOException e) {
            return new ApiResponse(TIME_OUT_EVENT, TIME_OUT_EVENT_MSG);
        }
    }

}

而http引擎类的实现如下:

public class HttpEngine {
    private final static String SERVER_URL = "http://uat.b.quancome.com/platform/api";
    private final static String REQUEST_MOTHOD = "POST";
    private final static String ENCODE_TYPE = "UTF-8";
    private final static int TIME_OUT = 15000;

    private static HttpEngine instance = null;

    private HttpEngine() {
    }

    public static HttpEngine getInstance() {
        if (instance == null) {
            instance = new HttpEngine();
        }
        return instance;
    }

    public <T> T postHandle(Map<String, String> paramsMap, Type typeOfT) throws IOException {
        String data = joinParams(paramsMap);
        HttpUrlConnection connection = getConnection();
        connection.setRequestProperty("Content-Length", String.valueOf(data.getBytes().length));
        connection.connect();
        OutputStream os = connection.getOutputStream();
        os.write(data.getBytes());
        os.flush();
        if (connection.getResponseCode() == 200) {
            // 获取响应的输入流对象
            InputStream is = connection.getInputStream();
            // 创建字节输出流对象
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 定义读取的长度
            int len = 0;
            // 定义缓冲区
            byte buffer[] = new byte[1024];
            // 按照缓冲区的大小,循环读取
            while ((len = is.read(buffer)) != -1) {
                // 根据读取的长度写入到os对象中
                baos.write(buffer, 0, len);
            }
            // 释放资源
            is.close();
            baos.close();
            connection.disconnect();
            // 返回字符串
            final String result = new String(baos.toByteArray());
            Gson gson = new Gson();
            return gson.fromJson(result, typeOfT);
        } else {
            connection.disconnect();
            return null;
        }
    }
    
    private HttpURLConnection getConnection() {
        HttpURLConnection connection = null;
        // 初始化connection
        try {
            // 根据地址创建URL对象
            URL url = new URL(SERVER_URL);
            // 根据URL对象打开链接
            connection = (HttpURLConnection) url.openConnection();
            // 设置请求的方式
            connection.setRequestMethod(REQUEST_MOTHOD);
            // 发送POST请求必须设置允许输入,默认为true
            connection.setDoInput(true);
            // 发送POST请求必须设置允许输出
            connection.setDoOutput(true);
            // 设置不使用缓存
            connection.setUseCaches(false);
            // 设置请求的超时时间
            connection.setReadTimeout(TIME_OUT);
            connection.setConnectTimeout(TIME_OUT);
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            connection.setRequestProperty("Connection", "keep-alive");
            connection.setRequestProperty("Response-Type", "json");
            connection.setChunkedStreamingMode(0);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return connection;
    }

    private String joinParams(Map<String, String> paramsMap) {
        StringBuilder stringBuilder = new StringBuilder();
        for (String key : paramsMap.keySet()) {
            stringBuilder.append(key);
            stringBuilder.append("=");
            try {
                stringBuilder.append(URLEncoder.encode(paramsMap.get(key), ENCODE_TYPE));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            stringBuilder.append("&");
        }
        return stringBuilder.substring(0, stringBuilder.length() - 1);
    }
}

至此,接口层的封装就完成了。接下来再往上看看核心层吧。

核心层的逻辑

核心层处于接口层和界面层之间,向下调用Api,向上提供Action,它的核心任务就是处理复杂的业务逻辑。先看看我对Action的定义:

public interface AppAction {
    // 发送手机验证码
    public void sendSmsCode(String phoneNum, ActionCallbackListener<Void> listener);
    // 注册
    public void register(String phoneNum, String code, String password, ActionCallbackListener<Void> listener);
    // 登录
    public void login(String loginName, String password, ActionCallbackListener<Void> listener);
    // 按分页获取券列表
    public void listCoupon(int currentPage, ActionCallbackListener<List<CouponBO>> listener);
}

首先,和Api接口对比就会发现,参数并不一致。登录并没有iemi和loginOS的参数,获取券列表的参数里也少了pageSize。这是因为,这几个参数,跟界面其实并没有直接关系。Action只要定义好跟界面相关的就可以了,其他需要的参数,在具体实现时再去获取。
另外,大部分action的处理都是异步的,因此,添加了回调监听器ActionCallbackListener,回调监听器的泛型则是返回的对象数据类型,例如获取券列表,返回的数据类型就是List,没有对象数据时则为Void。回调监听器只定义了成功和失败的方法,如下:

public interface ActionCallbackListener<T> {
    /**
     * 成功时调用
     *
     * @param data 返回的数据
     */
    public void onSuccess(T data);

    /**
     * 失败时调用
     *
     * @param errorEvemt 错误码
     * @param message    错误信息
     */
    public void onFailure(String errorEvent, String message);
}

接下来再看看Action的实现。首先,要获取imei,那就需要传入一个Context;另外,还需要loginOS和pageSize,这定义为常量就可以了;还有,要调用接口层,所以还需要Api实例。而接口的实现分为两步,第一步做参数检查,第二步用异步任务调用Api。具体实现如下:

public class AppActionImpl implements AppAction {
    private final static int LOGIN_OS = 1; // 表示Android
    private final static int PAGE_SIZE = 20; // 默认每页20条

    private Context context;
    private Api api;

    public AppActionImpl(Context context) {
        this.context = context;
        this.api = new ApiImpl();
    }

    @Override
    public void sendSmsCode(final String phoneNum, final ActionCallbackListener<Void> listener) {
        // 参数为空检查
        if (TextUtils.isEmpty(phoneNum)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "手机号为空");
            }
            return;
        }
        // 参数合法性检查
        Pattern pattern = Pattern.compile("1\\d{10}");
        Matcher matcher = pattern.matcher(phoneNum);
        if (!matcher.matches()) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "手机号不正确");
            }
            return;
        }

        // 请求Api
        new AsyncTask<Void, Void, ApiResponse<Void>>() {
            @Override
            protected ApiResponse<Void> doInBackground(Void... voids) {
                return api.sendSmsCode4Register(phoneNum);
            }

            @Override
            protected void onPostExecute(ApiResponse<Void> response) {
                if (listener != null && response != null) {
                    if (response.isSuccess()) {
                        listener.onSuccess(null);
                    } else {
                        listener.onFailure(response.getEvent(), response.getMsg());
                    }
                }
            }
        }.execute();
    }

    @Override
    public void register(final String phoneNum, final String code, final String password, final ActionCallbackListener<Void> listener) {
        // 参数为空检查
        if (TextUtils.isEmpty(phoneNum)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "手机号为空");
            }
            return;
        }
        if (TextUtils.isEmpty(code)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "验证码为空");
            }
            return;
        }
        if (TextUtils.isEmpty(password)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "密码为空");
            }
            return;
        }
        
        // 参数合法性检查
        Pattern pattern = Pattern.compile("1\\d{10}");
        Matcher matcher = pattern.matcher(phoneNum);
        if (!matcher.matches()) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "手机号不正确");
            }
            return;
        }
        
        // TODO 长度检查,密码有效性检查等

        // 请求Api
        new AsyncTask<Void, Void, ApiResponse<Void>>() {
            @Override
            protected ApiResponse<Void> doInBackground(Void... voids) {
                return api.registerByPhone(phoneNum, code, password);
            }

            @Override
            protected void onPostExecute(ApiResponse<Void> response) {
                if (listener != null && response != null) {
                    if (response.isSuccess()) {
                        listener.onSuccess(null);
                    } else {
                        listener.onFailure(response.getEvent(), response.getMsg());
                    }
                }
            }
        }.execute();
    }

    @Override
    public void login(final String loginName, final String password, final ActionCallbackListener<Void> listener) {
        // 参数为空检查
        if (TextUtils.isEmpty(loginName)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "登录名为空");
            }
            return;
        }
        if (TextUtils.isEmpty(password)) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_NULL, "密码为空");
            }
            return;
        }

        // TODO 长度检查,密码有效性检查等        
        
        // 请求Api
        new AsyncTask<Void, Void, ApiResponse<Void>>() {
            @Override
            protected ApiResponse<Void> doInBackground(Void... voids) {
                TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
                String imei = telephonyManager.getDeviceId();
                return api.loginByApp(loginName, password, imei, LOGIN_OS);
            }

            @Override
            protected void onPostExecute(ApiResponse<Void> response) {
                if (listener != null && response != null) {
                    if (response.isSuccess()) {
                        listener.onSuccess(null);
                    } else {
                        listener.onFailure(response.getEvent(), response.getMsg());
                    }
                }
            }
        }.execute();
    }

    @Override
    public void listCoupon(final int currentPage, final ActionCallbackListener<List<CouponBO>> listener) {
        // 参数检查
        if (currentPage < 0) {
            if (listener != null) {
                listener.onFailure(ErrorEvent.PARAM_ILLEGAL, "当前页数小于零");
            }
        }
        
        // TODO 添加缓存
        
        // 请求Api
        new AsyncTask<Void, Void, ApiResponse<List<CouponBO>>>() {
            @Override
            protected ApiResponse<List<CouponBO>> doInBackground(Void... voids) {
                return api.listNewCoupon(currentPage, PAGE_SIZE);
            }

            @Override
            protected void onPostExecute(ApiResponse<List<CouponBO>> response) {
                if (listener != null && response != null) {
                    if (response.isSuccess()) {
                        listener.onSuccess(response.getObjList());
                    } else {
                        listener.onFailure(response.getEvent(), response.getMsg());
                    }
                }
            }
        }.execute();
    }
}

简单的实现代码就是这样,其实,这还有很多地方可以优化,比如,将参数为空的检查、手机号有效性的检查、数字型范围的检查等等,都可以抽成独立的方法,从而减少重复代码的编写。异步任务里的代码也一样,都是可以通过重构优化的。另外,需要扩展时,比如添加缓存,那就在调用Api之前处理。
核心层的逻辑就是这样了。最后就到界面层了。

界面层

在这个Demo里,只有三个页面:登录页、注册页、券列表页。在这里,也会遵循界面篇提到的三个基本原则:规范性、单一性、简洁性。
首先,界面层需要调用核心层的Action,而这会在整个应用级别都用到,因此,Action的实例最好放在Application里。代码如下:

public class KApplication extends Application {

    private AppAction appAction;

    @Override
    public void onCreate() {
        super.onCreate();
        appAction = new AppActionImpl(this);
    }

    public AppAction getAppAction() {
        return appAction;
    }
}

另外,一个Activity的基类也是很有必要的,可以减少很多重复的工作。基类的代码如下:

public abstract class KBaseActivity extends FragmentActivity {
    // 上下文实例
    public Context context;
    // 应用全局的实例
    public KApplication application;
    // 核心层的Action实例
    public AppAction appAction;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        context = getApplicationContext();
        application = (KApplication) this.getApplication();
        appAction = application.getAppAction();
    }
}

再看看登录的Activity:

public class LoginActivity extends KBaseActivity {

    private EditText phoneEdit;
    private EditText passwordEdit;
    private Button loginBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // 初始化View
        initViews();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_login, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        // 如果是注册按钮
        if (id == R.id.action_register) {
            Intent intent = new Intent(this, RegisterActivity.class);
            startActivity(intent);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    // 初始化View
    private void initViews() {
        phoneEdit = (EditText) findViewById(R.id.edit_phone);
        passwordEdit = (EditText) findViewById(R.id.edit_password);
        loginBtn = (Button) findViewById(R.id.btn_login);
    }

    // 准备登录
    public void toLogin(View view) {
        String loginName = phoneEdit.getText().toString();
        String password = passwordEdit.getText().toString();
        loginBtn.setEnabled(false);
        this.appAction.login(loginName, password, new ActionCallbackListener<Void>() {
            @Override
            public void onSuccess(Void data) {
                Toast.makeText(context, R.string.toast_login_success, Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, CouponListActivity.class);
                startActivity(intent);
                finish();
            }

            @Override
            public void onFailure(String errorEvent, String message) {
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
                loginBtn.setEnabled(true);
            }
        });
    }
}

登录页的布局文件则如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.keegan.kandroid.activity.LoginActivity">

    <EditText
        android:id="@+id/edit_phone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/edit_vertical_margin"
        android:layout_marginBottom="@dimen/edit_vertical_margin"
        android:hint="@string/hint_phone"
        android:inputType="phone"
        android:singleLine="true" />

    <EditText
        android:id="@+id/edit_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/edit_vertical_margin"
        android:layout_marginBottom="@dimen/edit_vertical_margin"
        android:hint="@string/hint_password"
        android:inputType="textPassword"
        android:singleLine="true" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/btn_vertical_margin"
        android:layout_marginBottom="@dimen/btn_vertical_margin"
        android:onClick="toLogin"
        android:text="@string/btn_login" />

</LinearLayout>

可以看到,EditText的id命名统一以edit开头,而在Activity里的控件变量名则以Edit结尾。按钮的onClick也统一用toXXX的方式命名,明确表明这是一个将要做的动作。还有,string,dimen也都统一在相应的资源文件里按照相应的规范去定义。
注册页和登陆页差不多,这里就不展示代码了。主要再看看券列表页,因为用到了ListView,ListView需要添加适配器。实际上,适配器很多代码都是可以复用的,因此,我抽象了一个适配器的基类,代码如下:

public abstract class KBaseAdapter<T> extends BaseAdapter {

    protected Context context;
    protected LayoutInflater inflater;
    protected List<T> itemList = new ArrayList<T>();

    public KBaseAdapter(Context context) {
        this.context = context;
        inflater = LayoutInflater.from(context);
    }

    /**
     * 判断数据是否为空
     *
     * @return 为空返回true,不为空返回false
     */
    public boolean isEmpty() {
        return itemList.isEmpty();
    }

    /**
     * 在原有的数据上添加新数据
     *
     * @param itemList
     */
    public void addItems(List<T> itemList) {
        this.itemList.addAll(itemList);
        notifyDataSetChanged();
    }

    /**
     * 设置为新的数据,旧数据会被清空
     *
     * @param itemList
     */
    public void setItems(List<T> itemList) {
        this.itemList.clear();
        this.itemList = itemList;
        notifyDataSetChanged();
    }

    /**
     * 清空数据
     */
    public void clearItems() {
        itemList.clear();
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return itemList.size();
    }

    @Override
    public Object getItem(int i) {
        return itemList.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    abstract public View getView(int i, View view, ViewGroup viewGroup);
}

这个抽象基类集成了设置数据的方法,每个具体的适配器类只要再实现各自的getView方法就可以了。本Demo的券列表的适配器如下:

public class CouponListAdapter extends KBaseAdapter<CouponBO> {

    public CouponListAdapter(Context context) {
        super(context);
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ViewHolder holder;
        if (view == null) {
            view = inflater.inflate(R.layout.item_list_coupon, viewGroup, false);
            holder = new ViewHolder();
            holder.titleText = (TextView) view.findViewById(R.id.text_item_title);
            holder.infoText = (TextView) view.findViewById(R.id.text_item_info);
            holder.priceText = (TextView) view.findViewById(R.id.text_item_price);
            view.setTag(holder);
        } else {
            holder = (ViewHolder) view.getTag();
        }

        CouponBO coupon = itemList.get(i);
        holder.titleText.setText(coupon.getName());
        holder.infoText.setText(coupon.getIntroduce());
        SpannableString priceString;
        // 根据不同的券类型展示不同的价格显示方式
        switch (coupon.getModelType()) {
            default:
            case CouponBO.TYPE_CASH:
                priceString = CouponPriceUtil.getCashPrice(context, coupon.getFaceValue(), coupon.getEstimateAmount());
                break;
            case CouponBO.TYPE_DEBIT:
                priceString = CouponPriceUtil.getVoucherPrice(context, coupon.getDebitAmount(), coupon.getMiniAmount());
                break;
            case CouponBO.TYPE_DISCOUNT:
                priceString = CouponPriceUtil.getDiscountPrice(context, coupon.getDiscount(), coupon.getMiniAmount());
                break;
        }
        holder.priceText.setText(priceString);

        return view;
    }

    static class ViewHolder {
        TextView titleText;
        TextView infoText;
        TextView priceText;
    }

}

而券列表的Activity简单实现如下:

public class CouponListActivity extends KBaseActivity implements SwipeRefreshLayout.OnRefreshListener {
    private SwipeRefreshLayout swipeRefreshLayout;
    private ListView listView;
    private CouponListAdapter listAdapter;
    private int currentPage = 1;

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

        initViews();
        getData();

        // TODO 添加上拉加载更多的功能
    }

    private void initViews() {
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout);
        swipeRefreshLayout.setOnRefreshListener(this);
        listView = (ListView) findViewById(R.id.list_view);
        listAdapter = new CouponListAdapter(this);
        listView.setAdapter(listAdapter);
    }

    private void getData() {
        this.appAction.listCoupon(currentPage, new ActionCallbackListener<List<CouponBO>>() {
            @Override
            public void onSuccess(List<CouponBO> data) {
                if (!data.isEmpty()) {
                    if (currentPage == 1) { // 第一页
                        listAdapter.setItems(data);
                    } else { // 分页数据
                        listAdapter.addItems(data);
                    }
                }
                swipeRefreshLayout.setRefreshing(false);
            }

            @Override
            public void onFailure(String errorEvent, String message) {
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
                swipeRefreshLayout.setRefreshing(false);
            }
        });
    }

    @Override
    public void onRefresh() {
        // 需要重置当前页为第一页,并且清掉数据
        currentPage = 1;
        listAdapter.clearItems();
        getData();
    }
}

完结

终于写完了,代码也终于放上了github,为了让人更容易理解,因此很多都比较简单,没有再进行扩展。
github地址:https://github.com/keeganlee/kandroid


扫描以下二维码即可关注订阅号。

Comments
Write a Comment
  • loser reply

    小项目可以这么做,如果是大点的项目在业务逻辑和接口都比较多的情况下,这个AppAction的代码量会很大的;我觉得这个Action应该根据功能模块再去细分,在UI层用到的时候再去创建。

  • Keegan小钢 reply

    @loser 项目扩展后肯定是要分的,不只是AppAction,很多地方都需要扩展的。但这个Demo一开始就说了,是个最小化可用产品,用最简单的方式搭建起来。而且,项目也应该遵循渐进式设计开发的方式进行,不要一下子就构建复杂的项目。所谓,步子大了,容易扯到蛋。

  • Gao reply

    写的非常好,希望继续完善一下,集成更多通用的功能,做成一个快速开发的框架。

  • Snail reply

    写得太好了。正在为项目架构发愁,从jee转过来的,对Android框架很迷茫。这三篇系列文章很清晰很实用,给我很大的启发。我决定项目结构就按这个来设计。希望后续有更多这样实际开发中的文章用来。

  • Snail reply

    另有个问题请教:网上说:android自定义对象可序列化有两个选择:Serializable和Parcelable”,并且

    说:“在使用内存的时候Parcelable比Serializable的性能高。Parcelable不能使用在将对象存储在磁盘上这种情况,”

    我看你的Model中是实现的Serializable接口。是怎样的一种考虑?

  • Keegan小钢 reply

    @Snail 在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable。Model更多时候是将数据持久化和网络传输的,所以用Serializable。

  • Czjchn reply

    博主能给个联系方式吗 现在我用intellij做项目遇到一些问题

  • cuixbo reply

    今天看了楼主的三篇文章,觉得写的很好,也很细,结构非常清晰,赞一个

  • Keegan小钢 reply

    @null 有问题,请百度

  • huangxiaolou reply

    非常感谢博主,如果从J2EE转过来的同学,就会很容易联想到SSH的那个架构。

    博主的这一套架构其实是差不多了。

  • Nowy reply

    ( ^_^ )不错嘛,感谢博主的分享。适配器其实可以不写Viewholder.我的做法是这样的

    public class ViewHolder {

    private View[] itemView;

    public ViewHolder(View v, int[] ids) {

    itemView = new View[ids.length];

    for (int i = 0; i < ids.length; i++) {

    itemView[i] = v.findViewById(ids[i]);

    }

    v.setTag(this);

    }

    public View[] getItemView() {

    return itemView;

    }

    }

    ,然后写一个通用的适配器(layout,ids,回调等均作为参数传入),把getView的操作作为接口回调给activity。那么可以适合大部分简单的适配器。

    否则,适配器多了也是烦人的事呢。

  • Keegan小钢 reply

    @Nowy 你说得没错,ViewHolder也是可以优化的,其实还有很多地方都可以优化的。只是这一系列文章的重点不在这些优化,而在于基础的架构,因此没有展开。要讲优化的话,都可以写成一本书了。

  • liunewshine reply

    如果用一些开源项目,比如eventbus,rxjava,retrofit应该能提高很多开发效率吧

  • Keegan小钢 reply

    @liunewshine 如果只用到开源框架中的一小部分功能的话,那我不会引入;另外,如果会破坏项目结构的,我也不会引入;最后,如果太多依赖开源项目,开发人员会变懒,对开发人员的成长也不好,只知道怎么用,却不知道怎么实现,开发人员还怎么成长。

  • zcw_blue reply

    赞!对刚转入Android开发的我很有启发。

  • figtiger reply

    真心赞,觉得这个最小化实现可以用在大部分情景中,期待楼主继续,一直关注中,另,可以订阅你的bolg吗

  • Keegan小钢 reply

    @figtiger 现在我还没有搞订阅,近期内会去弄一个微信订阅号。

  • Keegan小钢 reply

    @figtiger 微信订阅号已开通,可以关注keeganlee_me

  • 吴廷玺 reply

    感谢博主分享这么好的文章,我是一个安卓新手,对我启发很大,感谢无私分享!

  • andy reply

    第一,每次请求都new一个AsyncTask对象,会不会太多?第二,如果业务很多,AppAction必然会定义很多接口方法,为什么不是直接实现,却要实现接口,难道是为了符合Java针对接口编程不针对实现编程的需要?

  • Keegan小钢 reply

    @andy 第一,这只是一个最简化的实现,这样学习成本才是最低的,AsyncTask是有优化的控件的,而且不只是AsyncTask,还有其他很多地方都是。第二,你也知道要面向接口编程,为什么还会对AppAction有疑问?

  • 我上次写一个实时公交也是想AppAction这种,每个网络请求或者数据库操作都一个 asynctask,好累。不知道怎么优化咯。

  • Keegan小钢 reply

    @hoolly 第一步优化,可以写一个继承AsyncTask的抽象类,将重复性的代码在抽象类里完成,这样,AppAction实现时就可以减少很多重复代码了。

  • river reply

    业务层core中AppAction,如果日后要扩展的话,写在KApplication中有问题,那么这个KApplication类会爆炸的,不知道楼主项目是怎么处理的?还是也放在KApplication中?

  • river reply

    测试账号密码是多少哈

  • Keegan小钢 reply

    @river KApplication只是对appAction进行了一次实例化,代码就一行,怎么会爆炸呢?

    测试账号自己去注册啊~~~

  • 418005442 reply

    真心不错,学习了,谢谢

  • 看完了!棒棒哒!!!!

  • 请问博主的评论系统用的是哪个的?本来是不想在博客里加评论的,但是看到这么棒的评论系统~~我就忍不住了!!

  • Keegan小钢 reply

    我现在整个博客都是用FarBox的,评论系统也是FarBox自带的~

  • Keegan小钢 reply

    @Mr.Fu 我现在整个博客都是用FarBox的,评论系统也是FarBox自带的~

  • tyh520life reply

    怎么把OkHttp加进去呢,坐等

  • Keegan小钢 reply

    @tyh520life 没考虑加第三方网络框架

  • sanjay reply

    楼主,还是把地址稍微改下吧。

    别拿公司的出来。

    SERVER_URL

  • Keegan小钢 reply

    @sanjay 公司的项目刚好是这个架构最适合的例子啊,不然我还得自己花时间和资源搭一个~或者你给个?

  • doney reply

    朋友,这个每个接口都有AsyncTask,而且操作很一样,感觉可以抽出来,放到HttpEngine和Api间加多一个Async类,同时处理回调,你觉得可以吗?

    另外想请教下,当AppAction 里面的接口多了,你怎么处理这情况

  • 徐牛 reply

    AsyncTask对listener是强引用,容易造成activity的泄露。

  • Keegan小钢 reply

    @doney 关于AsyncTask,的确是可以抽离出来的,我在实际项目中是有写了一个封装类的。

    另外,AppAction接口多了的话,我是将其拆分为多个Action。

  • Keegan小钢 reply

    @徐牛 为什么会造成activity的泄露?能否具体说说?

  • 分层都差不多,core 和 api 都是需要再具体细分的,不过 model 这样放的话是个争议点。另外好像没考虑每一层的测试啊,起码 core 里的代码的可测试性就不太好。

  • Keegan小钢 reply

    @Rocko 你提的问题的确存在的,测试方面也是没多考虑,这些都是待优化的地方

  • fuzhengchao reply

    楼主特别清爽,开发经验比较峰丰富。

    不过个人感觉博主的网络请求用AsyncTask不太好,Android 3.0后AsyncTask默认是线性执行的,除非博主自定义AsyncTask的线程池类型。

  • Goven reply

    楼主的功底很深,做android多久了,有用过注解框架没

  • Keegan小钢 reply

    @Goven 做android五年多吧,注解框架比较喜欢用butterknife

  • Rayboot reply

    我的项目接口就有100+ 这样ApiImpl会很庞大,不知道可以如何优化呢?

  • 我也觉得还是需要按功能模块来划分,以便快速移植。

    加功能还好说,比如说要去掉某个功能模块,以这种方式,就可能漏掉一部分。

  • Keegan小钢 reply

    @Rayboot 那可以根据模块拆分成几个Api啊

  • Keegan小钢 reply

    @petma 最好的方式是微服务架构

  • 有没有谁把代码整理好,能运行的,分享出来?

  • Keegan小钢 reply

    @no obj 文章最后不是已经给出了github地址吗~

  • doney reply

    撸主,你实际项目既然有对接口的AsyncTask抽出来,具体怎么搞的啊!我试了下,不好搞啊。

    透露个,膜拜下!

    我没太好的想法怎么抽出来这一层啊!

  • Keegan小钢 reply

    @doney 以下是我继承的类:

    public abstract class NetworkTask extends AsyncTask {

    private CallbackListener callbackListener;

    private KcuponResponse response;

    private boolean isCancel = false;

    public NetworkTask(CallbackListener callbackListener) {

    this.callbackListener = callbackListener;

    }

    public void cancel() {

    isCancel = true;

    super.cancel(true);

    }

    @Override

    protected Void doInBackground(Void... params) {

    response = run();

    return null;

    }

    @Override

    protected void onPostExecute(Void result) {

    if (callbackListener == null || response == null) {

    return;

    }

    callbackListener.onFinish();

    if (!isCancel) {

    if (response.isSuccess()) {

    T data = response.getObj();

    if (data == null) {

    data = response.getObjList();

    }

    callbackListener.onSuccess(data);

    } else {

    callbackListener.onFailure(response.getEvent(), response.getMsg());

    }

    }

    }

    public abstract KcuponResponse run();

    }

    调用时,代码就简单了,如下:

    NetworkTask task = new NetworkTask(callbackListener) {

    @Override

    public KcuponResponse run() {

    return api.loginByApp(phone, password, LOGIN_OS, imei);

    }

    };

  • Czjchn reply

    博主能给个联系方式吗 现在我用intellij做项目遇到一些问题

  • Czjchn reply

    博主能给个联系方式吗 现在我用intellij做项目遇到一些问题

  • 有态度网友06MY5V reply

    小项目可以这么做,如果是大点的项目在业务逻辑和接口都比较多的情况下,这个AppAction的代码量会很大的;我觉得这个Action应该根据功能模块再去细分,在UI层用到的时候再去创建。

  • 有态度网友06MY5V reply

    @loser 项目扩展后肯定是要分的,不只是AppAction,很多地方都需要扩展的。但这个Demo一开始就说了,是个最小化可用产品,用最简单的方式搭建起来。而且,项目也应该遵循渐进式设计开发的方式进行,不要一下子就构建复杂的项目。所谓,步子大了,容易扯到蛋。

  • 有态度网友06MY5V reply

    写的非常好,希望继续完善一下,集成更多通用的功能,做成一个快速开发的框架。

  • 有态度网友06MY5V reply

    写得太好了。正在为项目架构发愁,从jee转过来的,对Android框架很迷茫。这三篇系列文章很清晰很实用,给我很大的启发。我决定项目结构就按这个来设计。希望后续有更多这样实际开发中的文章用来。

  • 有态度网友06MY5V reply

    另有个问题请教:网上说:android自定义对象可序列化有两个选择:Serializable和Parcelable”,并且说:“在使用内存的时候Parcelable比Serializable的性能高。Parcelable不能使用在将对象存储在磁盘上这种情况,”我看你的Model中是实现的Serializable接口。是怎样的一种考虑?

  • 有态度网友06MY5V reply

    @Snail 在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable。Model更多时候是将数据持久化和网络传输的,所以用Serializable。

  • 有态度网友06MY5V reply

    博主能给个联系方式吗 现在我用intellij做项目遇到一些问题

  • 有态度网友06MY5V reply

    今天看了楼主的三篇文章,觉得写的很好,也很细,结构非常清晰,赞一个

  • 有态度网友06MY5V reply

    @null 有问题,请百度

  • 有态度网友06MY5V reply

    非常感谢博主,如果从J2EE转过来的同学,就会很容易联想到SSH的那个架构。<br>博主的这一套架构其实是差不多了。

  • 有态度网友06MY5V reply

    ( ^_^ )不错嘛,感谢博主的分享。适配器其实可以不写Viewholder.我的做法是这样的<br>public class ViewHolder {<br> private View[] itemView;<br><br> public ViewHolder(View v, int[] ids) {<br> itemView = new View[ids.length];<br> for (int i = 0; i ids.length; i ) {<br> itemView[i] = v.findViewById(ids[i]);<br> }<br> v.setTag(this);<br> }<br><br> public View[] getItemView() {<br> return itemView;<br> }<br><br>}<br>,然后写一个通用的适配器(layout,ids,回调等均作为参数传入),把getView的操作作为接口回调给activity。那么可以适合大部分简单的适配器。<br>否则,适配器多了也是烦人的事呢。

  • 有态度网友06MY5V reply

    @Nowy 你说得没错,ViewHolder也是可以优化的,其实还有很多地方都可以优化的。只是这一系列文章的重点不在这些优化,而在于基础的架构,因此没有展开。要讲优化的话,都可以写成一本书了。

  • 有态度网友06MY5V reply

    如果用一些开源项目,比如eventbus,rxjava,retrofit应该能提高很多开发效率吧

  • 有态度网友06MY5V reply

    @liunewshine 如果只用到开源框架中的一小部分功能的话,那我不会引入;另外,如果会破坏项目结构的,我也不会引入;最后,如果太多依赖开源项目,开发人员会变懒,对开发人员的成长也不好,只知道怎么用,却不知道怎么实现,开发人员还怎么成长。

  • 有态度网友06MY5V reply

    赞!对刚转入Android开发的我很有启发。

  • 有态度网友06MY5V reply

    真心赞,觉得这个最小化实现可以用在大部分情景中,期待楼主继续,一直关注中,另,可以订阅你的bolg吗

  • 有态度网友06MY5V reply

    @figtiger 现在我还没有搞订阅,近期内会去弄一个微信订阅号。

  • 有态度网友06MY5V reply

    @figtiger 微信订阅号已开通,可以关注keeganlee_me

  • 有态度网友06MY5V reply

    感谢博主分享这么好的文章,我是一个安卓新手,对我启发很大,感谢无私分享!

  • 有态度网友06MY5V reply

    第一,每次请求都new一个AsyncTask对象,会不会太多?第二,如果业务很多,AppAction必然会定义很多接口方法,为什么不是直接实现,却要实现接口,难道是为了符合Java针对接口编程不针对实现编程的需要?

  • 有态度网友06MY5V reply

    @andy 第一,这只是一个最简化的实现,这样学习成本才是最低的,AsyncTask是有优化的控件的,而且不只是AsyncTask,还有其他很多地方都是。第二,你也知道要面向接口编程,为什么还会对AppAction有疑问?

  • 有态度网友06MY5V reply

    我上次写一个实时公交也是想AppAction这种,每个网络请求或者数据库操作都一个 asynctask,好累。不知道怎么优化咯。

  • 有态度网友06MY5V reply

    @hoolly 第一步优化,可以写一个继承AsyncTask的抽象类,将重复性的代码在抽象类里完成,这样,AppAction实现时就可以减少很多重复代码了。

  • 有态度网友06MY5V reply

    业务层core中AppAction,如果日后要扩展的话,写在KApplication中有问题,那么这个KApplication类会爆炸的,不知道楼主项目是怎么处理的?还是也放在KApplication中?

  • 有态度网友06MY5V reply

    测试账号密码是多少哈

  • 有态度网友06MY5V reply

    @river KApplication只是对appAction进行了一次实例化,代码就一行,怎么会爆炸呢?<br>测试账号自己去注册啊~~~

  • 有态度网友06MY5V reply

    真心不错,学习了,谢谢

  • 有态度网友06MY5V reply

    看完了!棒棒哒!!!!

  • 有态度网友06MY5V reply

    请问博主的评论系统用的是哪个的?本来是不想在博客里加评论的,但是看到这么棒的评论系统~~我就忍不住了!!

  • 有态度网友06MY5V reply

    我现在整个博客都是用FarBox的,评论系统也是FarBox自带的~

  • 有态度网友06MY5V reply

    @Mr.Fu 我现在整个博客都是用FarBox的,评论系统也是FarBox自带的~

  • 有态度网友06MY5V reply

    怎么把OkHttp加进去呢,坐等

  • 有态度网友06MY5V reply

    @tyh520life 没考虑加第三方网络框架

  • 有态度网友06MY5V reply

    楼主,还是把地址稍微改下吧。<br>别拿公司的出来。<br> SERVER_URL

  • 有态度网友06MY5V reply

    @sanjay 公司的项目刚好是这个架构最适合的例子啊,不然我还得自己花时间和资源搭一个~或者你给个?

  • 有态度网友06MY5V reply

    朋友,这个每个接口都有AsyncTask,而且操作很一样,感觉可以抽出来,放到HttpEngine和Api间加多一个Async类,同时处理回调,你觉得可以吗?<br>另外想请教下,当AppAction 里面的接口多了,你怎么处理这情况

  • 有态度网友06MY5V reply

    AsyncTask对listener是强引用,容易造成activity的泄露。

  • 有态度网友06MY5V reply

    @doney 关于AsyncTask,的确是可以抽离出来的,我在实际项目中是有写了一个封装类的。<br>另外,AppAction接口多了的话,我是将其拆分为多个Action。

  • 有态度网友06MY5V reply

    @徐牛 为什么会造成activity的泄露?能否具体说说?

  • 有态度网友06MY5V reply

    分层都差不多,core 和 api 都是需要再具体细分的,不过 model 这样放的话是个争议点。另外好像没考虑每一层的测试啊,起码 core 里的代码的可测试性就不太好。

  • 有态度网友06MY5V reply

    @Rocko 你提的问题的确存在的,测试方面也是没多考虑,这些都是待优化的地方

  • 有态度网友06MY5V reply

    楼主特别清爽,开发经验比较峰丰富。<br>不过个人感觉博主的网络请求用AsyncTask不太好,Android 3.0后AsyncTask默认是线性执行的,除非博主自定义AsyncTask的线程池类型。

  • 有态度网友06MY5V reply

    楼主的功底很深,做android多久了,有用过注解框架没

  • 有态度网友06MY5V reply

    @Goven 做android五年多吧,注解框架比较喜欢用butterknife

  • 有态度网友06MY5V reply

    我的项目接口就有100 这样ApiImpl会很庞大,不知道可以如何优化呢?

  • 有态度网友06MY5V reply

    我也觉得还是需要按功能模块来划分,以便快速移植。<br>加功能还好说,比如说要去掉某个功能模块,以这种方式,就可能漏掉一部分。

  • 有态度网友06MY5V reply

    @Rayboot 那可以根据模块拆分成几个Api啊

  • 有态度网友06MY5V reply

    @petma 最好的方式是微服务架构

  • 有态度网友06MY5V reply

    有没有谁把代码整理好,能运行的,分享出来?

  • 有态度网友06MY5V reply

    @no obj 文章最后不是已经给出了github地址吗~

  • 有态度网友06MY5V reply

    撸主,你实际项目既然有对接口的AsyncTask抽出来,具体怎么搞的啊!我试了下,不好搞啊。<br>透露个,膜拜下!<br>我没太好的想法怎么抽出来这一层啊!

  • 有态度网友06MY5V reply

    fork起来学习,谢谢分享

  • 有态度网友06MY5Y reply

    请问楼主: CouponListAdapter getView方法 default 为什么卸载最前面?

  • 有态度网友06MY5V reply

    这个没什么区别的啊<br>

  • 有态度网友06MY5_ reply

    感谢博主写出了这么好的文章!!<br>有个问题请教一下,为什么每个Impl都要有一个Interface?在 public class AppActionImpl里面,为什么 private Api api 这样声明,而不直接 private ApiImpl api 这样声明呢?<br>谢谢~

  • 有态度网友06MY5Z reply

    基于接口和实现分离原则,不同模块之间的通信要通过接口通信,而不是通过直接实现通信

  • 有态度网友06MY5_ reply

    谢谢回复~~再请教一下,这样做有什么好处?

  • 有态度网友06MY5Z reply

    接口和实现分离原则有什么好处?去百度一下,可以详细了解

  • 有态度网友06MY5_ reply

    好的,谢谢

  • 有态度网友06MY63 reply

    大的框架感觉挺好的,细节的代码还可以重构下

  • 有态度网友06MY5Z reply

    是的,不过本系列也只是讲大的框架,属于架构方面的入门而已,所以不关注过多细节

  • 有态度网友06MY65 reply

    撸主,我反编译了那个X控app,看了下设计,学习了 <br>似乎忘了混淆加固的。还是加下吧。

  • 有态度网友06MY5Z reply

    你竟然反编译了~不过也没关系,混淆太麻烦,所以我才没加的

  • 棍哥哥 reply

    感觉自己好渣

  • 有态度网友06MY6e reply

    学习了,感谢博主分享,希望往后能出更多类似这种项目架构的文章让大伙学习借鉴一下

  • 有态度网友06MY5Z reply

    嗯嗯,我也发现架构方面的文章比较多人感兴趣,所以以后会多分享架构方面的文章

  • 有态度网友06MY5Z reply

    呵呵,多些积累就不会了

  • 有态度网友06MY6l reply

    谢谢博主分享,,对于我这种半路出家的人来说,真的受益匪浅

  • 貌似愚蠢的猪 reply

    楼主,富接口问题要怎么避免

  • 有态度网友06MY5Z reply

    按业务模块拆分

  • 有态度网友06MY6y reply

    我现在已经按你这样架构做了。但是java代码有些还是抽象的不够(比如两个界面里有一个类似功能,这个功能差不多70%类似,但是界面和逻辑都不一样,不知道该怎么抽象出来),请问是java功底问题还是?

  • 有态度网友06MY5Z reply

    这也跟经验有关,可以看看《重构——改善既有代码的设计》这本书,里面讲到很多方法

  • 有态度网友06MY5V reply

    重构这本书理论介绍很多 但是代码案例很少啊 针对代码抽象、封装等结构设计 楼主有没有值得推荐的书籍或博客

  • 有态度网友06MY5Z reply

    架构方面大部分的确都是讲理论,讲到代码层面的的确很少

  • 有态度网友06MY6O reply

    我的项目里要实现网络请求结果为未登陆状态时跳到登陆页面,应该在哪个层来实现。

  • 有态度网友06MY5Z reply

    这是贯穿整个流程的啊

  • 有态度网友06MY6N reply

    有一处还想请指点:api和 core这两层,为什么要用接口Api和实现类ApiImpl的方式写,而不直接写一个类,类里面写方法呢?

  • 有态度网友06MY5Z reply

    面向接口编程

  • 有态度网友06MY6U reply

    主楼问你一个问题 一般推送初始化代码放哪里合适?Application 还是 启动页面?[嘻嘻]

  • 有态度网友06MY5Z reply

    Application

  • 有态度网友06MY7a reply

    楼主,现在这个项目是不能运行了吗?连接服务器失败啊

  • 有态度网友06MY5Z reply

    旧公司测试的API,不是很稳定

  • 有态度网友06MY78 reply

    文章没看完,直接下载了demo运行手机自己看一遍后在看评论的,评论永远是精华,现在回去继续看Demo,安卓初学者,看着很吃力...........

  • 有态度网友06MY5V reply

    神奇,为什么工程里没有验证码错误的参数检查,却可以检测验证码的对错

  • 有态度网友06MY5Z reply

    验证码错误是服务端返回的,又不是在客户端做的

  • 秋风起-爱在深秋 reply

    请教一下 为什么core, api层不直接用static 方法 而要先定义接口 再继承?

  • 有态度网友06MY5Z reply

    面向对象设计中有个基本原则叫做依赖倒换原则,“设计要依赖于抽象而不是依赖于具体”,一个模块与外界交互的就应该是以接口的形式,就比如USB

  • 段文潇 reply

    能给个这个app的可用账号吗?人在国外不能用手机注册..谢谢

  • 有态度网友06MY5Z reply

    你可以找国内的朋友帮忙一下,反正只是收个验证码而已,我自己的手机就不方便透露了

  • 有态度网友06MY7G reply

    请教一下,在核心层中,AppActionImpl 接口的sendSmsCode方法里面,第一个listener.onFailure(ErrorEvent.PARAM_NULL, 手机号为空);调用,如果这个时间被触发的地方下面还有代码,那就会造成这个onfailed在下面的代码之前就执行了,这可能会出现问题。

  • 请输入昵称821122 reply

    楼主,对于常量constance、工具类util、系统配置Config这些,你会放在哪层?model吗?

  • 有态度网友06MY5Z reply

    每层都有自己相应的constance、util、config等吧~

  • 有态度网友06MY7V reply

    网络我用的是volley框架,该怎么来整合了,楼主能讲讲不

  • 请输入昵称821122 reply

    最近给公司的app改版借鉴了楼主这种不同层按模块(module)划分的方式,但是发现当api层想要使用全局变量(比如application.getInstance().xxx、或者SharedPreferences)的时候太不方便了。不知道楼主在实际项目中怎么解决这问题的?

  • 请输入昵称821122 reply

    比如每次api发起网络请求的时候,要把用户信息等固定参数带上。以往通过application.getInstance().xxx、或者SharedPreferences就很方便获取了。现在不知道该咋办了,总不能每次都从顶层把参数传过来吧

  • 请输入昵称821122 reply

    直接在api层调用volley就行了

  • 有态度网友06MY5Z reply

    可以向下传递一次,然后在api层将这些信息缓存起来

  • 有态度网友06MY7V reply

    那怎样像楼主一样,返回ApiResponse对象了,特别是返回信息含有items(集合)信息时,该怎么来操作了。这里有点没弄好。

  • 有态度网友06MY8k reply

    博主好,看了下您的实现篇的代码,有点东西想和你讨论下,就是,你把核心层,模型层,接口层都设置成了Library,我接触过的项目很少这么做的,如果把这些层都以不同的包名进行区分如何,都放在App里面如何?因为这些都是属于同一项目

  • 有态度网友06MY8k reply

    我也和你一样,非常希望有老司机带带我,但是这不太现实,以前我总是会为这点难受,现在想通了,开发资料这么多,我们应该多去寻找,遇到问题多百度,谷歌。

  • 有态度网友06MY5Z reply

    用包名区分也是可以的,用module区分是为了更好的分离,相互独立

  • 有态度网友06MY8k reply

    嗯嗯 我现在在认真看你的博文,希望对刚毕业的我能有帮助。之前在一家创业公司做过一个教育类的APP,当时都是照着另外一个APP做的,很多东西都似懂非懂,现在想重头学习一下。

  • 有态度网友06MY8m reply

    容易扯到蛋,确实!

  • 有态度网友06MY8n reply

    请教一个问题,楼主的意思是所以的 api 请求都写在 API 接口中,然后用一个累来实现这个接口么? 如果这样接口比较多的话这个类会不会太大了。需不需要对这个 API 类做拆解?如果需要如何拆解??<br>[互粉] 感谢楼主无私分享 顺带问下楼主的博客怎么 RSS 订阅

  • 有态度网友06MY5Z reply

    接口多了之后肯定要拆解的,一般根据不同模块来拆解。RSS订阅的话,在我的域名后添加feed就可以了:http://keeganlee.me/feed

  • 有态度网友06MY8r reply

    你好,博主,我最近在学习你的这方式,但是实际应用的时候遇到一个问题,我采取了这种架构之后,当我在app层,也就是界面层用listview的imageloader的开源框架的时候遇到了一个困惑,如果我按照http://blog.csdn.net/wwj_748/article/details/10079311这篇中listview的加载方式,那么在adapter里面就用到了本来最底层数据层的东西,也就是在界面层与网络交互异步获取了图片,后来想了想,如果把imageloader放到api层的时候那么就是界面层跨越了业务(core)层直接获取了图片类数据,这个问题博主有什么好的想法么,我是比较困惑这个问题。。

  • 有态度网友06MY8r reply

    而且如果为了不跨越业务层而在业务层中增加接口的话感觉有点繁琐,为了架构而架构的感觉,反而麻烦了。。

  • 有态度网友06MY5Z reply

    异步加载图片当然是在API层啊,ImageLoader的内部请求网络图片的实现又不用你管

  • 许春鹏 reply

    为什么总是连接服务端失败

  • 有态度网友06MY5Z reply

    你说github上的demo?

  • 有态度网友06MY8r reply

    对啊,所以不就是UI层跨越了逻辑业务层直接从数据层获取数据么,和最上面的逻辑不太符合啊,UI不是直接和逻辑业务层连接么,没有和数据层直接沟通

  • 有态度网友06MY5Z reply

    不好意思,我上面的说错了,ImageLoader应该是在UI层。<br>当你使用第三方库的时候,只要考虑你接入的地方即可,像ImageLoader,因为你用来加载图片的,就是UI层,其他不用怎么考虑。<br>如果你要自己实现图片加载,这时才需要考虑哪些放在UI层,哪些放在业务层,哪些放在数据层。

  • 有态度网友06MY8r reply

    恩,好的,谢谢~

  • 有态度网友06MY8G reply

    看完了,评论也看完了,没有胸怀怎么做技术,支持楼主

  • 有态度网友06MY8O reply

    github上给你push了一个issue,有时间看看,解答一下,谢谢

  • 有态度网友06MY5Z reply

    你指回收哪个呢?Activity?还是application和appAction?

  • 刘子吟 reply

    今天看到的很不错的一片文章 对我自己也很有启发

  • 有态度网友06MY5V reply

    败笔就是用了AsyncTask,其他算是比较标准的套路。谢谢分享经验

  • 有态度网友06MY8W reply

    onFailure 执行完后就 return 了啊

  • 张伟有内涵 reply

    看了楼主的三篇文章,受益匪浅,谢谢分享[good]

  • wcvm52020 reply

    最近,正在为命名,代码的架构,如何降低代码的耦合度,如何提取抽象而发愁。看到楼主的写的,真的有所成长,我一定认真多看几遍。好6

  • 有态度网友06MY5V reply

    看了真的受益匪浅~谢谢博主!加油!

  • 824765363 reply

    @Keegan小钢 加入要添加第三方,需要在哪里加

  • 有态度网友06MY5Z reply

    什么第三方?

  • 有态度网友06MYaf reply

    感谢楼主的分享,核心层这样处理网络请求感觉不太好,restful的API设计无法使用,不可能每个接口的url都相同,然后根据method区分,楼主后面有改进吗

  • 有态度网友06MY5Z reply

    需要了解一个背景前提:API部分是我重构之前已有的,是我无法控制的部分。

  • Find_SuperWo reply

    博主 如果是 sharedpreferences 之类的操作也可以放在App层 里不 还是说传递context 给api 层级呢

  • 有态度网友06MY5Z reply

    要看你的sharedpreferences用来保存什么信息,如果保存api数据,一般都把context传递过去

  • Czjchn reply

    博主能给个联系方式吗 现在我用intellij做项目遇到一些问题