码迷,mamicode.com
首页 > 其他好文 > 详细

MVVM 实战之计算器

时间:2016-12-10 19:09:27      阅读:270      评论:0      收藏:0      [点我收藏+]

标签:状态   html   create   handle   operator   fill   script   tom   构建   

MVVM 实战之计算器

 

 

前些日子,一直在学习基于 RxAndroid + Retrofit + DataBinding 技术组合的 MVVM 解决方案。初识这些知识,深深被它们的巧妙构思和方便快捷所吸引,心中颇为激动。但是,“纸上得来终觉浅,绝知此事要躬行”,学习完以后心里还是没有谱,于是,决定自己动手做一个基于这些技术和框架的小应用。

既然是对新技术学习和掌握的练习,因此,摊子不宜铺的太大。经过思量,最终决定使用 DataBinding 技术构建一个小的 MVVM 应用。MVVM 就是 Model-View-ViewModel 的缩写,与 MVC 模式相比,把其中的 Control 更换为 ViewModel 了。MVVM 的特点:ModelView 之间完全没有直接的联系,但是,通过 ViewModelModel 的变化可以反映在 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 布局中的第二个直接子元素。布局内容如下:

技术分享
  1 <?xml version="1.0" encoding="utf-8"?>
  2 <layout xmlns:android="http://schemas.android.com/apk/res/android"
  3         xmlns:tools="http://schemas.android.com/tools"
  4         xmlns:app="http://schemas.android.com/apk/res-auto">
  5     <data>
  6         <variable
  7             name="cal"
  8             type="com.ch.wchhuangya.android.pandora.vm.CalculatorVM"/>
  9     </data>
 10 
 11     <LinearLayout
 12                   android:layout_width="match_parent"
 13                   android:layout_height="match_parent"
 14                   android:orientation="vertical">
 15 
 16         <LinearLayout
 17             android:layout_width="match_parent"
 18             android:layout_height="0dp"
 19             android:layout_marginBottom="10dp"
 20             android:layout_weight="2"
 21             android:gravity="bottom"
 22             android:orientation="vertical"
 23             >
 24 
 25             <TextView
 26                 android:id="@+id/cal_top_num"
 27                 android:layout_width="match_parent"
 28                 android:layout_height="wrap_content"
 29                 android:gravity="right"
 30                 android:maxLines="1"
 31                 android:paddingRight="10dp"
 32                 android:text="@{cal.secondNum}"
 33                 android:textColor="#555"
 34                 android:textSize="35sp"
 35                 tools:text="16"
 36                 />
 37 
 38             <TextView
 39                 android:id="@+id/cal_bottom_num"
 40                 android:layout_width="match_parent"
 41                 android:layout_height="wrap_content"
 42                 android:gravity="right"
 43                 android:maxLines="1"
 44                 android:paddingRight="10dp"
 45                 android:text="@{cal.firstNum}"
 46                 android:textColor="#222"
 47                 android:textSize="45sp"
 48                 tools:text="+ 3234234"
 49                 />
 50 
 51             <TextView
 52                 android:id="@+id/cal_res"
 53                 android:layout_width="match_parent"
 54                 android:layout_height="wrap_content"
 55                 android:gravity="right"
 56                 android:maxLines="1"
 57                 android:paddingRight="10dp"
 58                 android:text="@{cal.resNum}"
 59                 android:textColor="#888"
 60                 android:textSize="30sp"
 61                 tools:text="= 3234250"
 62                 />
 63 
 64         </LinearLayout>
 65 
 66         <android.support.v7.widget.GridLayout
 67             android:layout_width="match_parent"
 68             android:layout_height="0dp"
 69             android:layout_weight="3"
 70             app:columnCount="4"
 71             app:orientation="horizontal"
 72             app:rowCount="5"
 73             >
 74 
 75             <Button
 76                 android:id="@+id/cal_clear"
 77                 android:layout_marginLeft="5dp"
 78                 android:layout_marginRight="5dp"
 79                 app:layout_rowWeight="1"
 80                 android:text="clear"
 81                 android:onClick="@{() -> cal.clear()}"
 82                 />
 83 
 84             <Button
 85                 android:id="@+id/cal_del"
 86                 android:layout_marginRight="5dp"
 87                 app:layout_rowWeight="1"
 88                 android:text="del"
 89                 android:onClick="@{() -> cal.del()}"
 90                 />
 91 
 92             <Button
 93                 android:id="@+id/cal_divide"
 94                 android:layout_marginRight="5dp"
 95                 app:layout_rowWeight="1"
 96                 android:text="÷"
 97                 android:onClick="@{cal::operatorClick}"
 98                 />
 99 
100             <Button
101                 android:id="@+id/cal_multiply"
102                 app:layout_rowWeight="1"
103                 android:text="×"
104                 android:onClick="@{cal::operatorClick}"
105                 />
106 
107             <Button
108                 android:id="@+id/cal_7"
109                 android:layout_marginLeft="5dp"
110                 app:layout_rowWeight="1"
111                 android:text="7"
112                 android:onClick="@{cal::numClick}"
113                 />
114 
115             <Button
116                 android:id="@+id/cal_8"
117                 app:layout_rowWeight="1"
118                 android:text="8"
119                 android:onClick="@{cal::numClick}"
120                 />
121 
122             <Button
123                 android:id="@+id/cal_9"
124                 app:layout_rowWeight="1"
125                 android:text="9"
126                 android:onClick="@{cal::numClick}"
127                 />
128 
129             <Button
130                 android:id="@+id/cal_minus"
131                 app:layout_rowWeight="1"
132                 android:text="-"
133                 android:onClick="@{cal::operatorClick}"
134                 />
135 
136             <Button
137                 android:id="@+id/cal_4"
138                 android:layout_marginLeft="5dp"
139                 app:layout_rowWeight="1"
140                 android:text="4"
141                 android:onClick="@{cal::numClick}"
142                 />
143 
144             <Button
145                 android:id="@+id/cal_5"
146                 app:layout_rowWeight="1"
147                 android:text="5"
148                 android:onClick="@{cal::numClick}"
149                 />
150 
151             <Button
152                 android:id="@+id/cal_6"
153                 app:layout_rowWeight="1"
154                 android:text="6"
155                 android:onClick="@{cal::numClick}"
156                 />
157 
158             <Button
159                 android:id="@+id/cal_add"
160                 app:layout_rowWeight="1"
161                 android:text="+"
162                 android:onClick="@{cal::operatorClick}"
163                 />
164 
165             <Button
166                 android:id="@+id/cal_1"
167                 android:layout_marginLeft="5dp"
168                 app:layout_rowWeight="1"
169                 android:text="1"
170                 android:onClick="@{cal::numClick}"
171                 />
172 
173             <Button
174                 android:id="@+id/cal_2"
175                 app:layout_rowWeight="1"
176                 android:text="2"
177                 android:onClick="@{cal::numClick}"
178                 />
179 
180             <Button
181                 android:id="@+id/cal_3"
182                 app:layout_rowWeight="1"
183                 android:text="3"
184                 android:onClick="@{cal::numClick}"
185                 />
186 
187             <Button
188                 android:id="@+id/cal_equals"
189                 app:layout_rowSpan="2"
190                 app:layout_rowWeight="1"
191                 app:layout_gravity="fill_vertical"
192                 android:text="="
193                 android:onClick="@{() -> cal.equalsClick()}"
194                 />
195 
196             <Button
197                 android:id="@+id/cal_12"
198                 android:layout_marginLeft="5dp"
199                 app:layout_rowWeight="1"
200                 android:text="%"
201                 android:onClick="@{() -> cal.percentClick()}"
202                 />
203 
204             <Button
205                 android:id="@+id/cal_zero"
206                 app:layout_rowWeight="1"
207                 android:text="0"
208                 android:onClick="@{cal::numClick}"
209                 />
210 
211             <Button
212                 android:id="@+id/cal_dot"
213                 app:layout_rowWeight="1"
214                 android:text="."
215                 android:onClick="@{() -> cal.dotClick()}"
216                 />
217 
218         </android.support.v7.widget.GridLayout>
219 
220     </LinearLayout>
221 </layout>
View Code

 

布局内容比较简单,下面,只说一些重点:

  1. DataBinding 的布局中,如果需要使用 tools 标签,它的声明必须放在 layout 节点上。否则,布局预览中没有效果

  2. data 节点中申明的是布局文件各元素需要使用到的对象,也可以为对象定义别名

  3. 布局文件中的控件如果要使用 data 中定义的对象,值的类似于:@{View.VISIBLE} 。控件的属性值中,不仅可以使用对象,还能使用对象的方法

Fragment


MVVM 中,ActivityFragment 的作用只是用于控件的初始化,包括控件属性(如颜色)等的设置。因此,它的代码灰常简单,具体如下:

 

该类中,只有两个方法。

onCreateView 方法用于返回视图,返回的方法与平时使用的 Fragment 略有不同。平时用 View.inflate 方法获取视图并返回,在 DataBinding 下,使用 DataBindingUtil.inflate 方法返回 ViewBinding 对象,然后给该对象对应的布局文件中的变量赋值。

onDestory() 方法中调用了两个释放资源的方法,这两个方法是在 ViewModel 中声明的。

ViewModel


MVVM 中,ViewModel 是重头,它用于处理所有非 UI 的业务逻辑。对于计算器来说,业务逻辑就是数字、符号的输入,数字运算等。具体内容如下:

技术分享
  1 package com.ch.wchhuangya.android.pandora.vm;
  2 
  3 import android.content.Context;
  4 import android.databinding.ObservableField;
  5 import android.view.View;
  6 import android.widget.Button;
  7 
  8 /**
  9  * Created by wchya on 2016-12-07 16:17
 10  */
 11 
 12 public class CalculatorVM extends BaseVM {
 13 
 14     /** 用于定义操作符后的空格显示 */
 15     public static final String EMPTY_STR = " ";
 16     /** 用于定义结果数字前的显示 */
 17     public static final String EQUALS_EMPTY_STR = "= ";
 18 
 19     /** 被操作数 */
 20     public ObservableField<String> firstNum = new ObservableField<>("0");
 21     /** 上一次结果 */
 22     public ObservableField<String> secondNum = new ObservableField<>("");
 23     /** 当前结果 */
 24     public ObservableField<String> resNum = new ObservableField<>("");
 25 
 26     /** 被操作数的数值 */
 27     double fNum;
 28     /** 上一次结果的数值 */
 29     double sNum;
 30     /** 当前结果的数值 */
 31     double rNum;
 32     /** 标识当前是否为初始状态 */
 33     boolean initState = true;
 34     /** 当前运算符 */
 35     CalOperator mCurOperator;
 36     /** 前一运算符 */
 37     CalOperator mPreOperator;
 38 
 39     /** 运算符枚举 */
 40     enum CalOperator {
 41         ADD("+"),
 42         MINUS("-"),
 43         MULTIPLY("×"),
 44         DIVIDE("÷");
 45 
 46         private String value;
 47 
 48         CalOperator(String value) {
 49             this.value = value;
 50         }
 51 
 52         /** 根据运算符字符串获取运算符枚举 */
 53         public static CalOperator getOperator(String value) {
 54             CalOperator otor = null;
 55             for (CalOperator operator : CalOperator.values()) {
 56                 if (operator.value.equals(value))
 57                     otor = operator;
 58             }
 59             return otor;
 60         }
 61     }
 62 
 63     public CalculatorVM(Context context) {
 64         mContext = context;
 65     }
 66 
 67     /**
 68      * 数字点击处理 
 69      * 当数字变化时,先变化 firstNum,然后计算结果
 70      */
 71     public void numClick(View view) {
 72         String btnVal = ((Button) view).getText().toString();
 73 
 74         if (btnVal.equals("0")) { // 当前点击 0 按钮
 75             if (firstNum.get().equals("0")) // 当前显示的为 0
 76                 return;
 77         }
 78 
 79         String originalVal = firstNum.get();
 80         boolean firstIsDigit = Character.isDigit(originalVal.charAt(0));
 81 
 82         if (isInitState()) { // 初始状态(既刚打开页面或点击了 Clear 之后)
 83             handleFirstNum(btnVal, Double.parseDouble(btnVal));
 84             handleResNum(EQUALS_EMPTY_STR + btnVal, Double.parseDouble(btnVal));
 85         } else {
 86             if (firstIsDigit) { // 首位是数字,直接在数字后添加
 87                 String changedVal = originalVal + btnVal;
 88                 handleFirstNum(changedVal, Double.parseDouble(changedVal));
 89                 handleResNum(EQUALS_EMPTY_STR + String.valueOf(fNum), Double.parseDouble(changedVal));
 90             } else { // 首位是运算符,计算结果后显示
 91 
 92                 if (originalVal.length() == 3 && Double.parseDouble(originalVal.substring(2)) == 0L) // 被操作数是 运算符 + 空格 + 0
 93                     handleFirstNum(mCurOperator.value + EMPTY_STR, Double.parseDouble(btnVal));
 94                 else
 95                     handleFirstNum(originalVal + btnVal, Double.parseDouble((originalVal + btnVal).substring(2)));
 96 
 97                 cal();
 98             }
 99         }
100         adjustNums();
101         setInitState(false);
102     }
103 
104     /** 退格键事件 */
105     public void del() {
106         String first = firstNum.get();
107         if (secondNum.get().length() > 0) { // 正在计算
108 
109             if (first.length() <= 3) { // firstNum 是运算符,把 secondNum 的值赋值给 firstNum,secondNum 清空
110                 handleFirstNum(sNum + "", sNum);
111                 handleResNum(EQUALS_EMPTY_STR + secondNum.get(), sNum);
112                 handleSecondNum("", 0L);
113                 mCurOperator = null;
114             } else { // 把最后一个数字删除,重新计算
115                 String changedVal = first.substring(0, first.length() - 1);
116                 handleFirstNum(changedVal, Double.parseDouble(changedVal.substring(2)));
117                 cal();
118             }
119         } else { // 没有计算
120 
121             if ((first.startsWith("-") && first.length() == 2) || first.length() == 1) { // 只有一位数字
122                 setInitState(true);
123                 handleFirstNum("0", 0L);
124                 handleResNum("", 0L);
125             } else {
126                 String changedFirst = first.substring(0, firstNum.get().length() - 1);
127                 handleFirstNum(changedFirst, Double.parseDouble(changedFirst));
128                 handleResNum(EQUALS_EMPTY_STR + fNum, fNum);
129             }
130         }
131         adjustNums();
132     }
133 
134     /** 运算符点击处理 */
135     public void operatorClick(View view) {
136         String btnVal = ((Button) view).getText().toString();
137 
138         // 如果当前有运算符,并且运算符后有数字,把当前运算符赋值给前一运算符
139         if (mCurOperator != null && firstNum.get().length() >= 3)
140             mPreOperator = mCurOperator;
141 
142         mCurOperator = CalOperator.getOperator(btnVal);
143 
144         if (secondNum.get().equals("")) { // 1. 没有 secondNum,把 firstNum 赋值给 secondNum,然后把运算符赋值给 firstNum
145 
146             handleSecondNum(firstNum.get(), Double.parseDouble(firstNum.get()));
147             handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
148         } else { // 2. 有 secondNum
149             if (firstNum.get().length() == 2) { // 2.1 只有运算符时,只改变运算符显示,其它不变
150 
151                 firstNum.set(mCurOperator.value + EMPTY_STR);
152             } else { // 2.2 既有运算符,又有 firstNum 和 secondNum 时,计算结果
153 
154                 if (mPreOperator != null) {
155                     mPreOperator = null;
156 
157                     handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
158                     handleSecondNum(rNum + "", rNum);
159                 } else {
160                     cal();
161                     handleFirstNum(mCurOperator.value + EMPTY_STR, 0L);
162                 }
163             }
164         }
165         setInitState(false);
166         adjustNums();
167     }
168 
169     /**
170      * 点的事件处理
171      * 1. 只能有一个点
172      * 2. 输入点后,firstNum 的值不变,只改变显示
173      */
174     public void dotClick() {
175         if (firstNum.get().contains("."))
176             return;
177         else {
178             setInitState(false);
179             String val = firstNum.get();
180 
181             if (!Character.isDigit(val.charAt(0)) && val.length() == 2) {
182                 handleFirstNum(val + "0.", fNum);
183             } else
184                 handleFirstNum(val + ".", fNum);
185         }
186     }
187 
188     /**
189      * 百分号的事件处理
190      * 1. 初始状态或刚刚经过 clear 操作时,点击无反应
191      * 2. 当 firstNum 为运算符时,点击无反应
192      * 3. 其余情况,点击后将 firstNum 乘以 0.01
193      */
194     public void percentClick() {
195         String originalVal = firstNum.get();
196         if (isInitState())
197             return;
198         else if (originalVal.length() == 1 && !Character.isDigit(originalVal.charAt(0)))
199                 return;
200         else {
201             fNum = fNum * 0.01;
202             if (mCurOperator != null) {
203                 handleFirstNum(mCurOperator.value + " " + fNum, fNum);
204                 cal();
205             } else {
206                 handleFirstNum(String.valueOf(fNum), fNum);
207                 handleResNum(String.valueOf(fNum), fNum);
208             }
209         }
210     }
211 
212     /**
213      * 等号事件处理
214      * 1. 只有 firstNum,不作任何处理
215      * 2. 有 secondNum 时,把 secondNum 和 firstNum 的值进行运算,然后把值赋值给 firstNum,清空 secondNum,
216      */
217     public void equalsClick() {
218         if (!secondNum.get().equals("")) {
219             cal();
220             handleFirstNum(String.valueOf(rNum), rNum);
221             handleSecondNum("", 0L);
222         }
223         adjustNums();
224     }
225 
226     /** 计算结果 */
227     private void cal() {
228         switch (mCurOperator) {
229             case ADD:
230                 rNum = sNum + fNum;
231                 handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
232                 break;
233             case MINUS:
234                 rNum = sNum - fNum;
235                 handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
236                 break;
237             case MULTIPLY:
238                 rNum = sNum * fNum;
239                 handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
240                 break;
241             case DIVIDE:
242                 if (fNum == 0L) {
243                     rNum = 0L;
244                     handleResNum("= ∞", rNum);
245                 } else {
246                     rNum = sNum / fNum;
247                     handleResNum(EQUALS_EMPTY_STR + rNum, rNum);
248                 }
249                 break;
250         }
251         adjustNums();
252     }
253 
254     /**
255      * 调整结果,主要将最后无用的 .0 去掉
256      */
257     private void adjustNums() {
258         String ffNum = firstNum.get();
259         String ssNum = secondNum.get();
260         String rrNum = resNum.get();
261         if (ffNum.endsWith(".0")) {
262             firstNum.set(ffNum.substring(0, ffNum.length() - 2));
263         }
264         if (ssNum.endsWith(".0")) {
265             secondNum.set(ssNum.substring(0, ssNum.length() - 2));
266         }
267         if (rrNum.endsWith(".0"))
268             resNum.set(rrNum.substring(0, rrNum.length() - 2));
269     }
270 
271     /** 将计算器恢复到初始状态 */
272     public void clear() {
273         setInitState(true);
274 
275         handleFirstNum("0", 0L);
276 
277         handleSecondNum("", 0L);
278 
279         handleResNum("", 0L);
280 
281         mCurOperator = null;
282     }
283 
284     /** 处理被操作数的显示和值 */
285     private void handleFirstNum(String values, double val) {
286         firstNum.set(values);
287         fNum = val;
288     }
289 
290     /** 处理上次结果的显示和值 */
291     private void handleSecondNum(String values, double val) {
292         secondNum.set(values);
293         sNum = val;
294     }
295 
296     /** 处理本次结果的显示和值 */
297     private void handleResNum(String values, double val) {
298         resNum.set(values);
299         rNum = val;
300     }
301 
302     public boolean isInitState() {
303         return initState;
304     }
305 
306     public void setInitState(boolean initState) {
307         this.initState = initState;
308     }
309 
310     @Override
311     public void reset() {
312         // 释放其它资源
313         mContext = null;
314 
315         // 取掉观察者的注册
316         unsubscribe();
317     }
318 }
View Code

 

要注意的是:ObservableXXX 变量值的获取方法为—— variable.get(),设置方法为:variable.set(xxx)

该类有一个父类:BaseVM, 它用于定义一些通用的变量和子类必须实现的抽象方法。内容如下:

技术分享
 1 package com.ch.wchhuangya.android.pandora.vm;
 2 
 3 import android.content.Context;
 4 import android.support.v4.app.Fragment;
 5 import android.support.v7.app.AppCompatActivity;
 6 
 7 import java.util.ArrayList;
 8 import java.util.List;
 9 
10 import rx.Subscription;
11 
12 /**
13  * Created by wchya on 2016-11-27 20:32
14  */
15 
16 public abstract class BaseVM {
17 
18     /** VM 模式中,View 引用的持有 */
19     protected AppCompatActivity mActivity;
20     /** VM 模式中,View 引用的持有 */
21     protected Fragment mFragment;
22     /** VM 模式中,上下文引用的持有 */
23     protected Context mContext;
24     /** 所有用到的观察者 */
25     protected List<Subscription> mSubscriptions = new ArrayList<>();
26 
27     /** 释放持有的资源引用 */
28     public abstract void reset();
29 
30     /** 将所有注册的观察者反注册掉 */
31     public void unsubscribe() {
32         for (Subscription subscription : mSubscriptions) {
33             if (subscription != null && subscription.isUnsubscribed())
34                 subscription.unsubscribe();
35         }
36     }
37 }
View Code

 

最终效果如下:

 

技术分享
计算器

 

结束语


本文只是借助计算器这个小应用,把所学的 DataBindingMVVM 的知识使用在实际当中。文中主要使用了 Google 官方 DataBinding 的一些特性,比如为控件设置属性值,为控件绑定事件等。如果读者对这一块内容还不了解,请在官网上查找相关文档进行学习,地址:https://developer.android.com/topic/libraries/data-binding/index.html

笔者在学习时,对官方文档进行了翻译,如果大家对英文文档比较抗拒,可以尝试看一下我的翻译。因为本人能力有限,难免出现错误,欢迎大家用评论的方式告知于我,翻译文档的地址:http://www.cnblogs.com/wchhuangya/p/6031934.html

该应用只是实现了计算器的基本功能,功能不够完善,而且,还有一些缺陷。已知的缺陷有:1. 双精度位数的处理;2. 特别大、特别小数字的显示及处理;这些缺陷只是计算器算法处理上的缺陷,与本文的主题无关,有兴趣的朋友可以将其修改、完善。记着,改好后记得告诉我哦!

路漫漫其修远兮,吾将上下而求索。此话与诸君共勉之!

MVVM 实战之计算器

标签:状态   html   create   handle   operator   fill   script   tom   构建   

原文地址:http://www.cnblogs.com/wchhuangya/p/6156168.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!