MVVM 实战之计算器
MVVM 实战之计算器
前些日子,一直在学习基于 RxAndroid + Retrofit + DataBinding
技术组合的 MVVM
解决方案。初识这些知识,深深被它们的巧妙构思和方便快捷所吸引,心中颇为激动。但是,“纸上得来终觉浅,绝知此事要躬行”,学习完以后心里还是没有谱,于是,决定自己动手做一个基于这些技术和框架的小应用。
既然是对新技术学习和掌握的练习,因此,摊子不宜铺的太大。经过思量,最终决定使用 DataBinding
技术构建一个小的 MVVM
应用。MVVM
就是 Model-View-ViewModel
的缩写,与 MVC
模式相比,把其中的 Control
更换为 ViewModel
了。MVVM
的特点:Model
与 View
之间完全没有直接的联系,但是,通过 ViewModel
,Model
的变化可以反映在 View
上,对 View
操作呢,又可以影响到 Model
。
平时在编写 Android
应用时,大家都在深受 findViewById
的折磨。DataBinding
还有个好处,就是完全不需要使用 findViewById
来获取控件(当然,需要在布局文件中给控件设置 id
属性)。有了 DataBinding
的支持,在数据变化后,也不需使用代码来改变控件的显示了。这样,我们的代码就清爽多了。
Model
在 MVVM
中,Model
的变化可以直接反映到 View
上,而不需要通过代码进行设置。这样,就不能用普通的 Java
类型的变量了。Android
专门为这种变量定义了新的变量类型:ObservableXXX
。
注意:ObservableXXX 是在 android.databinding 包下
变量定义如下:
/** 被操作数 */
public ObservableField<String> firstNum = new ObservableField<>("0");
/** 上一次结果 */
public ObservableField<String> secondNum = new ObservableField<>("");
/** 当前结果 */
public ObservableField<String> resNum = new ObservableField<>("");
变量的定义位置应该在 ViewModel
中,后方会有完整代码。
View
布局文件
DataBinding
的布局特点是把正常布局包裹在 layout
节点中,layout
布局中的第一个子直接子元素必须是 data
节点。因为,计算器布局的特点非常符合网格布局的特点,因此,我们选择 GridLayout
控件作为 layout
布局中的第二个直接子元素。布局内容如下:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="cal"
type="com.ch.wchhuangya.android.pandora.vm.CalculatorVM"/>
</data> <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"> <LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
android:layout_weight="2"
android:gravity="bottom"
android:orientation="vertical"
> <TextView
android:id="@+id/cal_top_num"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:maxLines="1"
android:paddingRight="10dp"
android:text="@{cal.secondNum}"
android:textColor="#555"
android:textSize="35sp"
tools:text="16"
/> <TextView
android:id="@+id/cal_bottom_num"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:maxLines="1"
android:paddingRight="10dp"
android:text="@{cal.firstNum}"
android:textColor="#222"
android:textSize="45sp"
tools:text="+ 3234234"
/> <TextView
android:id="@+id/cal_res"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:maxLines="1"
android:paddingRight="10dp"
android:text="@{cal.resNum}"
android:textColor="#888"
android:textSize="30sp"
tools:text="= 3234250"
/> </LinearLayout> <android.support.v7.widget.GridLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
app:columnCount="4"
app:orientation="horizontal"
app:rowCount="5"
> <Button
android:id="@+id/cal_clear"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
app:layout_rowWeight="1"
android:text="clear"
android:onClick="@{() -> cal.clear()}"
/> <Button
android:id="@+id/cal_del"
android:layout_marginRight="5dp"
app:layout_rowWeight="1"
android:text="del"
android:onClick="@{() -> cal.del()}"
/> <Button
android:id="@+id/cal_divide"
android:layout_marginRight="5dp"
app:layout_rowWeight="1"
android:text="÷"
android:onClick="@{cal::operatorClick}"
/> <Button
android:id="@+id/cal_multiply"
app:layout_rowWeight="1"
android:text="×"
android:onClick="@{cal::operatorClick}"
/> <Button
android:id="@+id/cal_7"
android:layout_marginLeft="5dp"
app:layout_rowWeight="1"
android:text="7"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_8"
app:layout_rowWeight="1"
android:text="8"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_9"
app:layout_rowWeight="1"
android:text="9"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_minus"
app:layout_rowWeight="1"
android:text="-"
android:onClick="@{cal::operatorClick}"
/> <Button
android:id="@+id/cal_4"
android:layout_marginLeft="5dp"
app:layout_rowWeight="1"
android:text="4"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_5"
app:layout_rowWeight="1"
android:text="5"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_6"
app:layout_rowWeight="1"
android:text="6"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_add"
app:layout_rowWeight="1"
android:text="+"
android:onClick="@{cal::operatorClick}"
/> <Button
android:id="@+id/cal_1"
android:layout_marginLeft="5dp"
app:layout_rowWeight="1"
android:text="1"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_2"
app:layout_rowWeight="1"
android:text="2"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_3"
app:layout_rowWeight="1"
android:text="3"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_equals"
app:layout_rowSpan="2"
app:layout_rowWeight="1"
app:layout_gravity="fill_vertical"
android:text="="
android:onClick="@{() -> cal.equalsClick()}"
/> <Button
android:id="@+id/cal_12"
android:layout_marginLeft="5dp"
app:layout_rowWeight="1"
android:text="%"
android:onClick="@{() -> cal.percentClick()}"
/> <Button
android:id="@+id/cal_zero"
app:layout_rowWeight="1"
android:text="0"
android:onClick="@{cal::numClick}"
/> <Button
android:id="@+id/cal_dot"
app:layout_rowWeight="1"
android:text="."
android:onClick="@{() -> cal.dotClick()}"
/> </android.support.v7.widget.GridLayout> </LinearLayout>
</layout>
布局文件
布局内容比较简单,下面,只说一些重点:
DataBinding
的布局中,如果需要使用tools
标签,它的声明必须放在layout
节点上。否则,布局预览中没有效果data
节点中申明的是布局文件各元素需要使用到的对象,也可以为对象定义别名布局文件中的控件如果要使用
data
中定义的对象,值的类似于:@{View.VISIBLE}
。控件的属性值中,不仅可以使用对象,还能使用对象的方法
Fragment
在 MVVM
中,Activity
或 Fragment
的作用只是用于控件的初始化,包括控件属性(如颜色)等的设置。因此,它的代码灰常简单,具体如下:
package com.ch.wchhuangya.android.pandora.view.activity.calculator; import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import com.ch.wchhuangya.android.pandora.R;
import com.ch.wchhuangya.android.pandora.databinding.CalculatorBinding;
import com.ch.wchhuangya.android.pandora.vm.CalculatorVM; /**
* Created by wchya on 2016-12-07 16:17
*/ public class CalculatorFragment extends Fragment { private CalculatorBinding mBinding;
private CalculatorVM mCalVM; @Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater, R.layout.calculator, container, false);
mCalVM = new CalculatorVM(getContext());
mBinding.setCal(mCalVM);
return mBinding.getRoot();
} @Override
public void onDestroy() {
super.onDestroy();
mCalVM.reset();
}
}
Fragment
该类中,只有两个方法。
onCreateView
方法用于返回视图,返回的方法与平时使用的 Fragment
略有不同。平时用 View.inflate
方法获取视图并返回,在 DataBinding
下,使用 DataBindingUtil.inflate
方法返回 ViewBinding
对象,然后给该对象对应的布局文件中的变量赋值。
onDestory()
方法中调用了两个释放资源的方法,这两个方法是在 ViewModel
中声明的。
ViewModel
在 MVVM
中,ViewModel
是重头,它用于处理所有非 UI
的业务逻辑。对于计算器来说,业务逻辑就是数字、符号的输入,数字运算等。具体内容如下:
package com.ch.wchhuangya.android.pandora.vm; import android.content.Context;
import android.databinding.ObservableField;
import android.view.View;
import android.widget.Button; /**
* Created by wchya on 2016-12-07 16:17
*/ public class CalculatorVM extends BaseVM { /** 用于定义操作符后的空格显示 */
public static final String EMPTY_STR = " ";
/** 用于定义结果数字前的显示 */
public static final String EQUALS_EMPTY_STR = "= "; /** 被操作数 */
public ObservableField<String> firstNum = new ObservableField<>("0");
/** 上一次结果 */
public ObservableField<String> secondNum = new ObservableField<>("");
/** 当前结果 */
public ObservableField<String> resNum = new ObservableField<>(""); /** 被操作数的数值 */
double fNum;
/** 上一次结果的数值 */
double sNum;
/** 当前结果的数值 */
double rNum;
/** 标识当前是否为初始状态 */
boolean initState = true;
/** 当前运算符 */
CalOperator mCurOperator;
/** 前一运算符 */
CalOperator mPreOperator; /** 运算符枚举 */
enum CalOperator {
ADD("+"),
MINUS("-"),
MULTIPLY("×"),
DIVIDE("÷"); private String value; CalOperator(String value) {
this.value = value;
} /** 根据运算符字符串获取运算符枚举 */
public static CalOperator getOperator(String value) {
CalOperator otor = null;
for (CalOperator operator : CalOperator.values()) {
if (operator.value.equals(value))
otor = operator;
}
return otor;
}
} public CalculatorVM(Context context) {
mContext = context;
} /**
* 数字点击处理
* 当数字变化时,先变化 firstNum,然后计算结果
*/
public void numClick(View view) {
String btnVal = ((Button) view).getText().toString(); if (btnVal.equals("0")) { // 当前点击 0 按钮
if (firstNum.get().equals("0")) // 当前显示的为 0
return;
} String originalVal = firstNum.get();
boolean firstIsDigit = Character.isDigit(originalVal.charAt(0)); if (isInitState()) { // 初始状态(既刚打开页面或点击了 Clear 之后)
handleFirstNum(btnVal, Double.parseDouble(btnVal));
handleResNum(EQUALS_EMPTY_STR + btnVal, Double.parseDouble(btnVal));
} else {
if (firstIsDigit) { // 首位是数字,直接在数字后添加
String changedVal = originalVal + btnVal;
handleFirstNum(changedVal, Double.parseDouble(changedVal));
handleResNum(EQUALS_EMPTY_STR + String.valueOf(fNum), Double.parseDouble(changedVal));
} else { // 首位是运算符,计算结果后显示 if (originalVal.length() == 3 && Double.parseDouble(originalVal.substring(2)) == 0L) // 被操作数是 运算符 + 空格 + 0
handleFirstNum(mCurOperator.value + EMPTY_STR, Double.parseDouble(btnVal));
else
handleFirstNum(originalVal + btnVal, Double.parseDouble((originalVal + btnVal).substring(2))); cal();
}
}
adjustNums();
setInitState(false);
} /** 退格键事件 */
public void del() {
String first = firstNum.get();
if (secondNum.get().length() > 0) { // 正在计算 if (first.length() <= 3) { // firstNum 是运算符,把 secondNum 的值赋值给 firstNum,secondNum 清空
handleFirstNum(sNum + "", sNum);
handleResNum(EQUALS_EMPTY_STR + secondNum.get(), sNum);
handleSecondNum("", 0L);
mCurOperator = null;
} else { // 把最后一个数字删除,重新计算
String changedVal = first.substring(0, first.length() - 1);
handleFirstNum(changedVal, Double.parseDouble(changedVal.substring(2)));
cal();
}
} else { // 没有计算 if ((first.startsWith("-") && first.length() == 2) || first.length() == 1) { // 只有一位数字
setInitState(true);
handleFirstNum("0", 0L);
handleResNum("", 0L);
} else {
String changedFirst = first.substring(0, firstNum.get().length() - 1);
handleFirstNum(changedFirst, Double.parseDouble(changedFirst));
handleResNum(EQUALS_EMPTY_STR + fNum, fNum);
}
}
adjustNums();
} /** 运算符点击处理 */
public void operatorClick(View view) {
String btnVal = ((Button) view).getText().toString(); // 如果当前有运算符,并且运算符后有数字,把当前运算符赋值给前一运算符
if (mCurOperator != null && firstNum.get().length() >= 3)
mPreOperator = mCurOperator; mCurOperator = CalOperator.getOperator(btnVal); if (secondNum.get().equals("")) { // 1. 没有 secondNum,把 firstNum 赋值给 secondNum,然后把运算符赋值给 firstNum handleSecondNum(firstNum.get(), Double.parseDouble(firstNum.get()));
handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
} else { // 2. 有 secondNum
if (firstNum.get().length() == 2) { // 2.1 只有运算符时,只改变运算符显示,其它不变 firstNum.set(mCurOperator.value + EMPTY_STR);
} else { // 2.2 既有运算符,又有 firstNum 和 secondNum 时,计算结果 if (mPreOperator != null) {
mPreOperator = null; handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
handleSecondNum(rNum + "", rNum);
} else {
cal();
handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
}
}
}
setInitState(false);
adjustNums();
} /**
* 点的事件处理
* 1. 只能有一个点
* 2. 输入点后,firstNum 的值不变,只改变显示
*/
public void dotClick() {
if (firstNum.get().contains("."))
return;
else {
setInitState(false);
String val = firstNum.get(); if (!Character.isDigit(val.charAt(0)) && val.length() == 2) {
handleFirstNum(val + "0.", fNum);
} else
handleFirstNum(val + ".", fNum);
}
} /**
* 百分号的事件处理
* 1. 初始状态或刚刚经过 clear 操作时,点击无反应
* 2. 当 firstNum 为运算符时,点击无反应
* 3. 其余情况,点击后将 firstNum 乘以 0.01
*/
public void percentClick() {
String originalVal = firstNum.get();
if (isInitState())
return;
else if (originalVal.length() == 1 && !Character.isDigit(originalVal.charAt(0)))
return;
else {
fNum = fNum * 0.01;
if (mCurOperator != null) {
handleFirstNum(mCurOperator.value + " " + fNum, fNum);
cal();
} else {
handleFirstNum(String.valueOf(fNum), fNum);
handleResNum(String.valueOf(fNum), fNum);
}
}
} /**
* 等号事件处理
* 1. 只有 firstNum,不作任何处理
* 2. 有 secondNum 时,把 secondNum 和 firstNum 的值进行运算,然后把值赋值给 firstNum,清空 secondNum,
*/
public void equalsClick() {
if (!secondNum.get().equals("")) {
cal();
handleFirstNum(String.valueOf(rNum), rNum);
handleSecondNum("", 0L);
}
adjustNums();
} /** 计算结果 */
private void cal() {
switch (mCurOperator) {
case ADD:
rNum = sNum + fNum;
handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
break;
case MINUS:
rNum = sNum - fNum;
handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
break;
case MULTIPLY:
rNum = sNum * fNum;
handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
break;
case DIVIDE:
if (fNum == 0L) {
rNum = 0L;
handleResNum("= ∞", rNum);
} else {
rNum = sNum / fNum;
handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
}
break;
}
adjustNums();
} /**
* 调整结果,主要将最后无用的 .0 去掉
*/
private void adjustNums() {
String ffNum = firstNum.get();
String ssNum = secondNum.get();
String rrNum = resNum.get();
if (ffNum.endsWith(".0")) {
firstNum.set(ffNum.substring(0, ffNum.length() - 2));
}
if (ssNum.endsWith(".0")) {
secondNum.set(ssNum.substring(0, ssNum.length() - 2));
}
if (rrNum.endsWith(".0"))
resNum.set(rrNum.substring(0, rrNum.length() - 2));
} /** 将计算器恢复到初始状态 */
public void clear() {
setInitState(true); handleFirstNum("0", 0L); handleSecondNum("", 0L); handleResNum("", 0L); mCurOperator = null;
} /** 处理被操作数的显示和值 */
private void handleFirstNum(String values, double val) {
firstNum.set(values);
fNum = val;
} /** 处理上次结果的显示和值 */
private void handleSecondNum(String values, double val) {
secondNum.set(values);
sNum = val;
} /** 处理本次结果的显示和值 */
private void handleResNum(String values, double val) {
resNum.set(values);
rNum = val;
} public boolean isInitState() {
return initState;
} public void setInitState(boolean initState) {
this.initState = initState;
} @Override
public void reset() {
// 释放其它资源
mContext = null; // 取掉观察者的注册
unsubscribe();
}
}
ViewModel
要注意的是:ObservableXXX
变量值的获取方法为—— variable.get()
,设置方法为:variable.set(xxx)
。
该类有一个父类:BaseVM
, 它用于定义一些通用的变量和子类必须实现的抽象方法。内容如下:
package com.ch.wchhuangya.android.pandora.vm; import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity; import java.util.ArrayList;
import java.util.List; import rx.Subscription; /**
* Created by wchya on 2016-11-27 20:32
*/ public abstract class BaseVM { /** VM 模式中,View 引用的持有 */
protected AppCompatActivity mActivity;
/** VM 模式中,View 引用的持有 */
protected Fragment mFragment;
/** VM 模式中,上下文引用的持有 */
protected Context mContext;
/** 所有用到的观察者 */
protected List<Subscription> mSubscriptions = new ArrayList<>(); /** 释放持有的资源引用 */
public abstract void reset(); /** 将所有注册的观察者反注册掉 */
public void unsubscribe() {
for (Subscription subscription : mSubscriptions) {
if (subscription != null && subscription.isUnsubscribed())
subscription.unsubscribe();
}
}
}
BaseVM
最终效果如下:
结束语
本文只是借助计算器这个小应用,把所学的 DataBinding
和 MVVM
的知识使用在实际当中。文中主要使用了 Google
官方 DataBinding
的一些特性,比如为控件设置属性值,为控件绑定事件等。如果读者对这一块内容还不了解,请在官网上查找相关文档进行学习,地址:https://developer.android.com/topic/libraries/data-binding/index.html 。
笔者在学习时,对官方文档进行了翻译,如果大家对英文文档比较抗拒,可以尝试看一下我的翻译。因为本人能力有限,难免出现错误,欢迎大家用评论的方式告知于我,翻译文档的地址:http://www.cnblogs.com/wchhuangya/p/6031934.html。
该应用只是实现了计算器的基本功能,功能不够完善,而且,还有一些缺陷。已知的缺陷有:1. 双精度位数的处理;2. 特别大、特别小数字的显示及处理;这些缺陷只是计算器算法处理上的缺陷,与本文的主题无关,有兴趣的朋友可以将其修改、完善。记着,改好后记得告诉我哦!
路漫漫其修远兮,吾将上下而求索。此话与诸君共勉之!
MVVM 实战之计算器的更多相关文章
- Jetpack MVVM 实战项目,附带源码+视频,收藏!
从读者的反馈来看,近期大部分安卓开发已跳出舒适圈,开始尝试认识和应用 Jetpack MVVM 到实际的项目开发中. 只可惜,关于 Jetpack MVVM,网上多是 东拼西凑.人云亦云.通篇贴代码 ...
- C# WPF MVVM 实战 – 5- 用绑定,通过 VM 设置 View 的控件焦点
本文介绍在 MVVM 中,如何用 ViewModel 控制焦点. 这焦点设置个东西嘛,有些争论.就是到底要不要用 ViewModel 来控制视图的键盘输入焦点.这里不讨论,假设你就是要通过 VM,设置 ...
- C# WPF MVVM 实战 – 4 - 善用 IValueConverter
IValueConverter,做 WPF 的都应该接触过,把值换成 Visibility .Margin 等等是最常见的例子,也有很多很好的博文解释过用法.本文只是解释一下,MVVM 中一些情景. ...
- Electron 实战桌面计算器应用
前言 Electron 是一个搭建跨平台桌面应用的框架,仅仅使用 JavaScript.HTML 以及 CSS,即可快速而容易地搭建一个原生应用.这对于想要涉及其他领域的开发者来说是一个非常大的福利. ...
- Android 开发笔记___初级控件之实战__计算器
功能简单,实现并不难,对于初学者可以总和了解初级控件的基本使用. 用到的知识点如下: 线性布局 LinearLayout:整体界面是从上往下的,因此需要垂直方向的linearlayout:下面每行四个 ...
- AvalonJS+MVVM实战部分源码
轻量级前端MVVM框架avalon,它兼容到 IE6 (其他MVVM框架,KnockoutJS(IE6), AngularJS(IE9), EmberJS(IE8), WinJS(IE9) ),它可以 ...
- python小实例——tkinter实战(计算器)
一.完美计算器实验一 import tkinter import math import tkinter.messagebox class calculator: #界面布局方法 def __init ...
- MVVM实战
1.层次依赖 - (UIViewController *)createInitialViewController { self.viewModelServices = [RWTViewModelSer ...
- Windows Phone 十一、MVVM模式
MVVM 模式介绍 模型-视图-视图模型 (MVVM) 是一种用来分离 UI 和非 UI 代码的应用设计模式 MVVM – 模型(Model) MVVM 中的 Model 与 MVC 中的一致,用于封 ...
随机推荐
- [Todo]非常好的免费IT书籍资源 & Github排名
今天看github排名,看到排在第二位的是免费书籍: https://github.com/vhf/free-programming-books/blob/master/free-programmin ...
- 使用 session_destroy() 销毁session文件时 报 Trying to destroy uninitialized session 错误解决办法
在使用 sessio_destroy() 销毁session文件的时候,必须要先使用session_start() 来开启session 后才能删除session文件
- thinkphp3.2中在模板页面使用运算符
首先要明确的是,ThinkPHP 内置模板引擎支持在模板中使用算术运算符(+.-.*./ 和 %),例子: public function index(){ $x = 1; $y = 2; $z = ...
- 给Swing的GUI组件设置前景色和背景色
JButton btn=new JButton("TEST"); btn.setForeground(Color.white);// 设置前景色(文字颜色)btn.setBackg ...
- VS提示无法连接到已配置的开发web服务器的解决方法
VS2013每次启动项目调试好好的,今天出现了提示“提示无法连接到已配置的开发web服务器“,使用环境是本地IISExpress,操作系统为windows10,之前也出现过就是重启电脑又好了,这次是刚 ...
- Wireshark Lua: 一个从RTP抓包里导出H.264 Payload,变成264裸码流文件(xxx.264)的Wireshark插件
Wireshark Lua: 一个从RTP抓包里导出H.264 Payload,变成264裸码流文件(xxx.264)的Wireshark插件 在win7-64, wireshark Version ...
- UVa145 Gondwanaland Telecom
Time limit: 3.000 seconds 限时:3.000秒 Problem 问题 Gondwanaland Telecom makes charges for calls accordin ...
- 如何为iTunes Connect准备应用
原地址:http://blog.sina.com.cn/s/blog_947c4a9f0101dded.html 如果你已经成功注册了iOS开发者,那么现在就可以登陆iTunes Connect来管理 ...
- [读后感]spring Mvc 教程框架实例以及系统演示下载
[读后感]spring Mvc 教程框架实例以及系统演示下载 太阳火神的漂亮人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致&qu ...
- angular -- $resource 用法
安装 ngResource模块是一个可选的angularjs模块,如果需要使用,我们要单独引用js <script type="text/javascript" src=&q ...