[Module] 03 - Software Design and Architecture
本篇涉及内容:
- ORM框架(无需再用contentprovider或者sqlitedatebasehelper之类的古董工具了)
- 规划各种业务Bean文件(配合ORM框架)
- 设计一个好的请求基类(BaseRequest、BaseResponse)
- BaseActivity和BaseFragment(把公用的代码写在里面,比如检测网络、弹出alertdialog等等)
- 定制一个Application类代替默认的(很多第三方框架需要把一些代码写到定制的Application类里面)
3 Http请求框架(volley,推荐使用okHttp,RxJava+Rxandroid+retrofit等) 4 JSON解析和构建框架(Gson,fastJson,不要用jackson因为比较大,除非需要用嵌套的需求) 6 JWT 9 消息推送(比如友盟) 10 用户反馈(比如友盟) 11 数据统计(比如友盟) 12 更新(比如友盟) 13 数据备份和恢复 14 点赞、评论、收藏模块 15 About界面(版权申明+常用软件设置+版本更新+国际化等) 16 在线crash log上报(比如腾讯的bugly) 17 快速开发框架(这里推荐使用butterknife和eventbus) 18 内存泄漏检测工具(leakcanary) 19 图片加载库(Glide) 20 加密解密(RSA,MD5,DES) 23 listview/recyclerview的基础adapter 24 定制搜索框 25 工具类(比如sharepreference,File,ScreenDesity,Sql,字符串处理,dpsp互转等等) 31 各种新式的Material design兼容控件 32 界面滑动冲突的问题(横竖冲突、同向冲突) 33 离线登录功能 34 bitmap缓存策略 35 最后项目要发布了,那么久需要混淆和打包了,前者关于混淆网上有很多相关的文章,这里需要注意的是很多你所使用的第三方库都需要在混淆的时候给剔除,因为他们是基于反射机制的。这点需要你在使用每个第三方库的时候多看他们的说明书多加小心。其次,混淆后一定要打个包重新回归测试一下,以免出现因混淆而导致的问题。
如何设计开发一款Android App【各个模块需要的技术】
局部设计
设置选项
Setting功能设置策略
PreferenceActivity & PreferenceFragment
Ref: Android 首选项框架及PreferenceScreen, PreferenceActivity, PreferenceFragment的用法与分析
在Android1.0的时候,官方就有了PreferenceActivity,这个类继承自ListActivity,是一个抽象类,其持有PreferenceManager对象的引用。
在Android3.0时,由于Fragment的出现,官方同步支持了PreferenceFragment。
同时,PreferenceActivity中的一些API也被相应标记为过时,官方给出的解释是:
因为PreferenceActivity这个类仅仅只被允许展示一个单独的偏好设置,而这些现在都能在PreferenceFragment中找到了,
其实从中可以看出google官方的思想是用碎片化来达到解藕
【貌似没什么可写的,就是个简单的类似json的东西】
主题风格选择
ref: https://github.com/dersoncheng/MultipleTheme【有代码,有讲解】
1. attr.xml中定义:哪些属性是可以随着主题而改变的。
2. style.xml中为上述这些属性 根据主题的不同而设置不同的值。
3. layout中的控件相应属性设置为变量:android:textColor = "?attr/main_textcolor"
4. 在基类的onCreate方法里添加切换主题模式的逻辑代码(相当于默认主题设置)。
public class BaseActivity extends Activity{ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(SharedPreferencesMgr.getInt("theme", 0) == 1) { setTheme(R.style.theme_2); } else { setTheme(R.style.theme_1); } } }
5. 设置按键触发事件切换主题。
btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(SharedPreferencesMgr.getInt("theme", 0) == 1) { SharedPreferencesMgr.setInt("theme", 0); setTheme(R.style.theme_1); } else { SharedPreferencesMgr.setInt("theme", 1); setTheme(R.style.theme_2); }
});
6. 针对切换主题模式时需要对页面ui立即更新,需要使用框架里的封装控件。
这里是动态更新主题,还需要进一步理解原理。
或者,不妨使用一些框架,例如:Android 主题动态切换框架:Prism ----> viewpager的大翻身经验效果demo
多语言选择
传统方案:Android多语言项目的实现
进入到res目录下,创建对应语言的values目录,命名规则为values-语言的缩写-语言的简称,例如“values-zh-rCN”,表达的意思为:“中文-简体中文”。
import java.util.Locale;
public class BaseActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 1.初始化PreferenceUtil PreferenceUtil.init(this); // 2.根据上次的语言设置,重新设置语言 switchLanguage(PreferenceUtil.getString("language", "zh")); // --> } protected void switchLanguage(String language) { //设置应用语言类型 Resources resources = getResources(); Configuration config = resources.getConfiguration(); DisplayMetrics dm = resources.getDisplayMetrics();
if (language.equals("en")) { config.locale = Locale.ENGLISH; } else { config.locale = Locale.SIMPLIFIED_CHINESE; } resources.updateConfiguration(config, dm); //保存设置语言的类型 PreferenceUtil.commitString("language", language); } }
ORM框架
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
为什么要用ORM?
因为JDBC不好用!
一般情况下,当预料到数据库会复杂到某个程度,就有必要引入数据库的ORM框架,这样可以大大降低开发和维护的成本。
用JDBC的API编程访问数据库,代码量较大,特别是访问字段较多的表的时候,代码显得繁琐、累赘,容易出错,例如:
public void addAccount(final Account account) throws DAOException { final Connection conn=getConnection(); PreparedStatement pstmt=con.prepareStatment("insert into account value(?,?,?,?,?,?,?,?,?)"); pstmt.setString(1,account.getUserName()); pstmt.setInt(2,account.getPassWord()); pstmt.setString(3,account.getSex()); pstmt.setString(4,account.getQq()); ...... pstmt.execute(); conn.Close(); }
ORM实现的效果如下:
public Double calcAmount(String customerid, double amount) { // 根据客户ID获得客户记录 Customer customer = CustomerManager.getCustomer(custmerid); // 根据客户等级获得打折规则 Promotion promotion = PromotionManager.getPromotion(customer.getLevel()); // 累积客户总消费额,并保存累计结果 customer.setSumAmount(customer.getSumAmount().add(amount); CustomerManager.save(customer); // 返回打折后的金额 return amount.multiply(protomtion.getRatio()); }
选一个ORM试试!
ref: 最好的5个Android ORM框架
ref: Android ORM框架选择的一些总结
OrmLite 不是 Android 平台专用的ORM框架,它是Java ORM。支持JDBC连接,Spring以及Android平台。语法中广泛使用了注解(Annotation)。
SugarORM 是 Android 平台专用ORM。提供简单易学的APIs。可以很容易的处理1对1和1对多的关系型数据,并通过3个函数save(), delete() 和 find() (或者 findById()) 来简化CRUD基本操作。
GreenDao是一个很快的解决方案,它能够支持数千条记录的CRUD每秒,和OrmLite相比,GreenDAO要快几乎4.5倍。
Active Record(活动目录)是Yii、Rails等框架中对ORM实现的典型命名方式。Active Android 帮助你以面向对象的方式来操作SQLite。
Realm 是一个将可以使用的Android ORM,基于C++编写,直接运行在你的设备硬件上(不需要被解释),因此运行很快。它同时是开源跨平台的,iOS的代码可以在GitHub找到,你还可以找到Objective-C以及Swift编写的Realm使用实例。
ObjectBox 速度方面碾压SQLite.
BaseActivity
需求:
1, 标题栏:项目中基本每个acctivity都会有相同的标题栏,返回事件相同,标题文字都居中,标题栏侧边按钮有些界面有,有些界面没有
2, 加载界面统一
3, 联网获取失败界面显示
4, 再按一次返回键退出
5, activity管理
Goto: https://www.jianshu.com/p/b2458fc96a65【页尾有代码】
Goto: BaseActivity 里到底应该写哪些内容?【更为详细】
自定制Application类
Application,与Activity、Service一样是Android框架的一个系统组件,当Android程序启动时系统会创建一个Application对象,用来存储系统的一些信息。
Android系统自动会为每个程序运行时创建一个Application类的对象且只创建一个,所以Application可以说是单例(singleton)模式的一个类。
启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。
那么我们在Application创建的时候初始化全局变量,同一个应用的所有Activity都可以取到这些全局变量的值,换句话说,我们在某一个Activity中改变了这些全局变量的值,那么在同一个应用的其他Activity中值就会改变。
Application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。
因为它是全局的单例的,所以在不同的Activity、Service中获得的对象都是同一个对象。
1. 想要在系统启动时,首先初始化一些东西,可自定制Application类。
2. 有时候我们获取Context并不太容易,但是context又是必须的,定义自己的Application,让你任何时候都可以获取到想要的全局Context。
3. 可以通过Application来进行一些,如:数据传递、数据共享和数据缓存等操作。这种全局变量方法相对静态类更有保障,直到应用的所有Activity全部被destory掉之后才会被释放掉。
定义:
import android.app.Application; import android.content.Context;
public class MyApplication extends Application { private static Context context;
@Override public void onCreate() { super.onCreate(); context = getApplicationContext(); } /** * 获取全局的Context * @return */ public static Context getContext(){ return context; } }
配置:告知系统加载我们自定义的Application类在AndroidManifest.xml
中
<application android:name="com.cml.example.MyApplication" ...> </application>
接下来在项目的任何地方你只需要调用MyApplication.getContext()就可以得到你想要的context了。
实战:
第一个activity设置一个值,让另外一个activity也能get到。
public class FirstActivity extends Activity { private CustomApplication app; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); app = (CustomApplication) getApplication(); // 获得CustomApplication对象 Log.i("FirstActivity", "初始值=====" + app.getValue()); // 获取进程中的全局变量值,看是否是初始化值 app.setValue("Harvey Ren"); // 重新设置值 Log.i("FirstActivity", "修改后=====" + app.getValue()); // 再次获取进程中的全局变量值,看是否被修改 Intent intent = new Intent(); intent.setClass(this, SecondActivity.class); startActivity(intent); } }
第二个activity获得该变化。
public class SecondActivity extends Activity { private CustomApplication app; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); app = (CustomApplication) getApplication(); // 获取应用程序 Log.i("SecondActivity", "当前值=====" + app.getValue()); // 获取全局值 } }
AndroidManifest.xml
中做一点改变。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name" android:name="CustomApplication"> <!-- 将我们以前一直用的默认Application设置成自定义的CustomApplication --> <activity android:name=".FirstActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SecondActivity" android:label="@string/app_name"> </activity> </application> </manifest>
Ref: getApplication()和getApplicationContext()区别
相同:
两个方法获取的是同一个对象。
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main);
Application application = getApplication(); Log.i("WY", "打印getApplication:" + application);
Context pContext = getApplicationContext(); Log.i("WY", "打印getApplicationContext:" + pContext); } }
区别:
getApplication()是用来获取Application实例的,但是该方法只在Activity和Service中才能调用;
在一些其他的地方,比如说当我们在BroadcastReceiver中也想获取Application实例,这时就需要使用getApplicationContext()方法
整体设计
资源:Android Architecture Blueprints【Google官方推荐代码框架,适合coding from scratch】
From: Android项目开发如何设计整体架构?【思维的逐渐深入,值得进一步研究】
框架代码:https://github.com/ShonLin/QuickDevFramework【个人的一个框架总结】
FLUX是Facebook提出的一种架构,有点小复杂,此处作为开篇,停留在简单的了解层面。
本篇重点先放在传统的架构的理解和使用。
Flux
推荐用最新的Android Flux来架构你的Android程序,Facebook提出的架构,文档比较全,数据流总是单向的。用过MVC, MVP后,我还是是比较认同Flux的,而且之前公司用的架构模式跟Flux也比较像。
参考文章:
-
数据流总是单向的
一个单向的数据流 是 Flux 架构的核心,也是它简单易学的原因。就如下面讨论的,在进行应用测试的时候,它提供了非常大的帮助。
-
应用被分成三个主要部分:
-
View: 应用的界面。这里创建响应用户操作的action。
-
Dispatcher: 中心枢纽,传递所有的action,负责把它们运达每个Store。
-
Store: 维护一个特定application domain的状态。它们根据当前状态响应action,执行业务逻辑,同时在完成的时候发出一个change事件。这个事件用于view更新其界面。
- MVP的思路是“挪”,在MVC的基础上把业务逻辑从View中挪走;
- Flux的思路是“单向流”,用严格的单向数据流去实现比较容易跟踪检测的逻辑结构;
- RxAndroid的思路则是“链式逻辑”,用函数反应式编程的思想把逻辑、代码和线程统统简化为一条链式调用。
让我们从最简单的架构开始。
了解其演化历史,才能了解其本质。
MVC
MVP
注意这里的IPresenter还有IView。
Presenter层中的业务逻辑,也比较容易做单元测试,做代码复用,做进一步的抽象和分离。
抛出一个高级例子:
Ref: https://github.com/BaronZ88/MinimalistWeather
MVP+RxJava在实际项目中的应用,MVP中RxJava生命周期的管理
但我们还是从入门例子开始:
Ref: MVP模式在Android项目中的使用【阅读笔记】
View与Model并不直接交互,而是使用Presenter作为View与Model之间的桥梁。
其中Presenter中同时持有View层以及Model层的Interface的引用,而View层持有Presenter层Interface的引用。
这就是MVP模式的整个核心过程:
- 当View层某个界面需要展示某些数据的时候,
- 首先会调用Presenter层的某个接口,
- 然后Presenter层会调用Model层请求数据,
- 当Model层数据加载成功之后会调用Presenter层的回调方法通知Presenter层数据加载完毕,
- 最后Presenter层再调用View层的接口将加载后的数据展示给用户。
好处:
减少了Model与View层之间的耦合度。
- 一方面可以使得View层和Model层单独开发与测试,互不依赖。
- 另一方面Model层可以封装复用,可以极大的减少代码量。
---------------------------------------------------- view ----------------------------------------------------
需求定义
新闻列表模块主要是展示从网络获取的新闻列表信息,View层的接口大概需要如下方法:
(1)加载数据的过程中需要提示“正在加载”的反馈信息给用户。
(2)加载成功后,将加载得到的数据填充到RecyclerView展示给用户。
(3)加载成功后,需要将“正在加载”反馈信息取消掉。
(4)若加载数据失败,如无网络连接,则需要给用户提示信息。
View层的接口定义
先是定义套路中的四个接口。
public interface NewsView { void showProgress(); void addNews(List<NewsBean> newsList); void hideProgress(); void showLoadFailMsg(); }
actvity / fragment中的接口实现
public class NewsListFragment extends Fragment implements NewsView, SwipeRefreshLayout.OnRefreshListener {
public static NewsListFragment newInstance(int type) {...}
@Override public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNewsPresenter = new NewsPresenterImpl(this); // ---->
mType = getArguments().getInt("type");
}
@Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {...}
private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {...}; private NewsAdapter.OnItemClickListener mOnItemClickListener = new NewsAdapter.OnItemClickListener() {...}
@Override public void showProgress() { mSwipeRefreshWidget.setRefreshing(true); }
@Override public void addNews(List<NewsBean> newsList) { // 加载成功后,将数据展示给用户 mAdapter.isShowFooter(true); if(mData == null) { mData = new ArrayList<NewsBean>(); } mData.addAll(newsList); if(pageIndex == 0) { mAdapter.setmDate(mData); // <-------- } else { //如果没有更多数据了,则隐藏footer布局 if(newsList == null || newsList.size() == 0) { mAdapter.isShowFooter(false); } mAdapter.notifyDataSetChanged(); } pageIndex += Urls.PAZE_SIZE; }
@Override public void hideProgress() { mSwipeRefreshWidget.setRefreshing(false); }
@Override public void showLoadFailMsg() { if(pageIndex == 0) { mAdapter.isShowFooter(false); mAdapter.notifyDataSetChanged(); } Snackbar.make(getActivity().findViewById(R.id.drawer_layout), getString(R.string.load_fail), Snackbar.LENGTH_SHORT).show(); }
---------------------------------------------------------------------
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mAdapter.getItemCount() && mAdapter.isShowFooter())
{ LogUtils.d(TAG, "loading more data"); mNewsPresenter.loadNews(mType, pageIndex + Urls.PAZE_SIZE); // 加载更多 } }
@Override public void onRefresh() { pageIndex = 0; if(mData != null) { mData.clear(); } mNewsPresenter.loadNews(mType, pageIndex); }
}
加入一级大目录:
public class NewsFragment extends Fragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {...} private void setupViewPager(ViewPager mViewPager) { //Fragment中嵌套使用Fragment一定要使用getChildFragmentManager(),否则会有问题 MyPagerAdapter adapter = new MyPagerAdapter(getChildFragmentManager()); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_TOP), getString(R.string.top)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_NBA), getString(R.string.nba)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_CARS), getString(R.string.cars)); adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_JOKES), getString(R.string.jokes)); mViewPager.setAdapter(adapter); } public static class MyPagerAdapter extends FragmentPagerAdapter {...} }
---------------------------------------------------- model ----------------------------------------------------
public interface NewsModel { void loadNews(String url, int type, OnLoadNewsListListener listener); // 内部使用的是回调的方法 voidloadNewsDetail(String docid, OnLoadNewsDetailListener listener); }
利用OkHttp实现了网络通信。
将网络请求进行封装class OkHttpUtils,可以减少很多的代码量,并且后期如果我不想用okhttp了,想换成其它的库,修改起来也方便。
【注意,这里使用了回调的方法】
public class NewsModelImpl implements NewsModel { @Override public void loadNews(String url, final int type, final OnLoadNewsListListener listener) { OkHttpUtils.ResultCallback<String> loadNewsCallback = new OkHttpUtils.ResultCallback<String>() { @Override public void onSuccess(String response) { <---- 回调 List<NewsBean> newsBeanList = NewsJsonUtils.readJsonNewsBeans(response, getID(type)); listener.onSuccess(newsBeanList); } @Override public void onFailure(Exception e) { <---- 回调 listener.onFailure("load news list failure.", e); } };
OkHttpUtils.get(url, loadNewsCallback); } @Override public void loadNewsDetail(final String docid, final OnLoadNewsDetailListener listener) { String url = getDetailUrl(docid); OkHttpUtils.ResultCallback<String> loadNewsCallback = new OkHttpUtils.ResultCallback<String>() { @Override public void onSuccess(String response) { <---- 回调 NewsDetailBean newsDetailBean = NewsJsonUtils.readJsonNewsDetailBeans(response, docid); listener.onSuccess(newsDetailBean); } @Override public void onFailure(Exception e) { <---- 回调 listener.onFailure("load news detail info failure.", e); } };
OkHttpUtils.get(url, loadNewsCallback); } private String getID(int type) {...} private String getDetailUrl(String docId) {...} }
public interface NewsPresenter { void loadNews(int type, int page); }
NewsPresenterImpl的构造函数中需要传入View层的接口对象NewView,并且需要创建一个NewsModel对象。Presenter的具体实现:
public class NewsPresenterImpl implements NewsPresenter, OnLoadNewsListListener { private static final String TAG = "NewsPresenterImpl"; private NewsView mNewsView; private NewsModel mNewsModel; public NewsPresenterImpl(NewsView newsView) { this.mNewsView = newsView; this.mNewsModel = new NewsModelImpl(); // 相当于造了一个工具,提供网络通信服务 } @Override public void loadNews(final int type, final int pageIndex) { String url = getUrl(type, pageIndex); LogUtils.d(TAG, url); //只有第一页的或者刷新的时候才显示刷新进度条 if(pageIndex == 0) { mNewsView.showProgress(); } mNewsModel.loadNews(url, type, this); // Jeff: 可见,只有presenter会使用这个包裹回调方法的loadNews,view是不知道的,但view会调用presenter的loadNews } /** * 根据类别和页面索引创建url * @param type * @param pageIndex * @return */ private String getUrl(int type, int pageIndex) { StringBuffer sb = new StringBuffer(); switch (type) { case NewsFragment.NEWS_TYPE_TOP: sb.append(Urls.TOP_URL).append(Urls.TOP_ID); break; case NewsFragment.NEWS_TYPE_NBA: sb.append(Urls.COMMON_URL).append(Urls.NBA_ID); break; case NewsFragment.NEWS_TYPE_CARS: sb.append(Urls.COMMON_URL).append(Urls.CAR_ID); break; case NewsFragment.NEWS_TYPE_JOKES: sb.append(Urls.COMMON_URL).append(Urls.JOKE_ID); break; default: sb.append(Urls.TOP_URL).append(Urls.TOP_ID); break; } sb.append("/").append(pageIndex).append(Urls.END_URL); return sb.toString(); } @Override public void onSuccess(List<NewsBean> list) { mNewsView.hideProgress(); mNewsView.addNews(list); } @Override public void onFailure(String msg, Exception e) { mNewsView.hideProgress(); mNewsView.showLoadFailMsg(); } }
再回首view中的使用体验:
public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mAdapter.getItemCount() && mAdapter.isShowFooter())
{ LogUtils.d(TAG, "loading more data"); mNewsPresenter.loadNews(mType, pageIndex + Urls.PAZE_SIZE); // 加载更多 } } public void onRefresh() { pageIndex = 0; if(mData != null) { mData.clear(); } mNewsPresenter.loadNews(mType, pageIndex); }
以上就是MVP的整个过程。
REST API
Ref: 理解RESTful架构
Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是"表现层状态转化"。
每种资源对应一个特定的URI。所谓"上网",就是与互联网上一系列的"资源"互动,调用它的URI。
状态转化(State Transfer):GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。
Ref: RESTful API 设计指南
RESTful API是目前比较成熟的一套互联网应用程序的API设计理论。
Ref: 怎样用通俗的语言解释REST,以及RESTful?
就是用URL定位资源,用HTTP描述操作。
看Url就知道要什么
看http method就知道干什么
看http status code就知道结果如何
样例示范:Github developer REST API v3