34岁!100天!学会Java编程(Day81-Day98)—Android与Web应用一站式开发
2018-02-01 22:14
387 查看
快到春节了,三年多没回过老家,今年打算回家看看,跟老妈在老家过个年,带点小酒给老爸扫扫墓。一个月前西城高铁开通,北京-成都坐火车只需要9个多小时,天知道这列火车是如何穿越大秦岭的。打算体验一次回家的高速列车,但是票一如既往的抢不着,只好买到初一的票,留在北京过除夕吧。
百日总结
这一篇主要是对之前学习Java的一点总结,Android应用与Web应用是企业应用常见的两种形式。对于当前的创业性公司来说,多是把重心放在移动端App的开发上。但是个人觉得一开始设计时将两者统一规划,将核心的业务都放在服务器端,增加一些Web前端设计的工作,就可以实现一站式开发,适应更广的业务场景,这样做还可以适当减轻Android端业务逻辑处理的压力。本案从Java零基础开始,在工作之余用一百天的业余时间,对一个“娱乐社区”(代号CE)习作的不断迭代升级开发过程中,完成了Android客户端、Web前端、服务器后端完整框架的开发,掌握了基本的全栈开发能力。
Web网站浏览和安卓apk下载地址:娱乐社区习作
(一)整体框架
这个框架很容易理解,只是在之前Web应用的框架的基础上,在服务器前端控制器增加了JSON数据的交互接口,用于与Android端进行远程交互。
(二)统一设计风格
Android与Web应用一站式开发要求将两者作为一个产品考虑,会要求统一的设计风格。由于Web前端设计与Android前端设计的方法相差较大,通常不会是由一个设计师开展设计,甚至对于稍大的应用,光是其一就不只一个设计师进行设计。不同的设计师的设计语言与理念通常也会有所区别,而过多的设计理念对于产品呈现会是一场灾难。此时,设计规范可以很好地解决这个问题。
适用于Web前端与Android前端的通用设计规范通常有:色彩规范、文字规范等。
对于Android应用来说还需考虑布局规范、控件规范、图标规范等。对于适用于手机浏览器的动态响应Web前端,也应尽量遵守Android设计规范。
以下是某应用的部分设计规范(示例):
(三)android客户端的工作
(1)CE(v7.0)APP客户端架构在设计过程中,考虑下面几个数据层尽量使用外观模式,简化接口。
(2)开发文档结构
开发工具使用AS,此部分大体上是上述APP架构的具体实现,由于时间有限,对功能进行了部分阉割,所以实际的开发项目会比这个复杂。
其中,资源res部分还有不少东西,限于篇幅就不展开了。在实现过程中发现各层和模块之间交叉太多,想要实现外观模式并不容易,下次再想想办法。
(3)AndroidManifest.xml
这部分重点是应用权限的获取,应用名称和图标的修改,四大天王的注册等基本设置。我曾经因为没有设置权限,访问不了网络;曾经编写一个新的Activity后没有注册,导致跳转失败还花了些时间找原因。
(4)build.gradle(Module:app)
这个文件很重要,其中包含了SDK版本的配置,app应用版本的管理,MVVP数据绑定设置,以及最重要的中央仓库管理。仅列举我这个应用所用到的框架和库:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //Support implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' //Material Design implementation 'com.android.support:design:26.1.0' //Vectro-Drawable implementation 'com.android.support:support-vector-drawable:26.1.0' //RecyclerView implementation 'com.android.support:recyclerview-v7:26.1.0' //CardView implementation 'com.android.support:cardview-v7:26.1.0' //ButterKnife implementation 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' //BottomTabBar implementation 'com.hjm:BottomTabBar:1.1.1' //Retrofit,OkHttp,RxJava implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.squareup.retrofit2:adapter-rxjava:2.3.0' implementation 'com.google.code.gson:gson:2.6.1' implementation 'com.squareup.okhttp3:okhttp:3.9.1' implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1' implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.9.1' implementation 'com.squareup.okio:okio:1.13.0' implementation 'com.jakewharton.rxbinding:rxbinding:0.4.0' implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxjava:1.1.6' //Test testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' //media, animation implementation 'com.github.bumptech.glide:glide:4.0.0' implementation 'com.nineoldandroids:library:2.4.0' 4000 //servlet implementation 'javax.servlet:javax.servlet-api:4.0.0' }
(5)主要功能开发要点
1.底部Tab主菜单的实现
实现底部Tab主菜单,有很多选择,我比较了一下material design库中的BottomNavigationView和第三方支持库BottomTabBar。
BottomTabBar的突出优点就是实现简单,只需要短短几行代码就可以搞定:
mBottomTabBar.init(getSupportFragmentManager()) .addTabItem("首页", R.drawable.icon1, HomeFragment.class) .addTabItem("发现", R.drawable.icon2, DiscoverFragment.class) .addTabItem("发起", R.drawable.icon3, PublishFragment.class) .addTabItem("圈子", R.drawable.icon4, CircleFragment.class) .addTabItem("我的", R.drawable.icon5, MineFragment.class);
而BottomNavigationView实现起来代码则要多很多:
private void initFragments() { fragment1 = new HomeFragment(); fragment2 = new DiscoverFragment(); fragment3 = new PublishFragment(); fragment4 = new CircleFragment(); fragment5 = new MineFragment(); } public boolean switchFragment(int fragmentid){ transaction=getSupportFragmentManager().beginTransaction(); lastShowFragment=fragmentid; boolean flag; switch (fragmentid) { case R.id.navigation_home: transaction.replace(R.id.fragment_container,fragment1).commit(); return true; case R.id.navigation_discover: transaction.replace(R.id.fragment_container,fragment2).commit(); return true; case R.id.navigation_publish: flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_publish); if(!flag)transaction.replace(R.id.fragment_container,fragment3).commit(); return true; case R.id.navigation_circle: flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_circle); if(!flag)transaction.replace(R.id.fragment_container,fragment4).commit(); return true; case R.id.navigation_mine: flag=LoginInterceptor.ContinueOrLogin(this,R.id.navigation_mine); if(!flag)transaction.replace(R.id.fragment_container,fragment5).commit(); return true; default: lastShowFragment=-1; return false; } } private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener= new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { return switchFragment(item.getItemId()); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); navigation = (BottomNavigationView) findViewById(R.id.navigation); navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener); BottomNavigationViewHelper.disableShiftMode(navigation); initFragments(); if(savedInstanceState==null){ switchFragment(R.id.navigation_home); }else{ switchFragment(savedInstanceState.getInt("index",R.id.navigation_home)); } } public class BottomNavigationViewHelper { @SuppressLint("RestrictedApi") public static void disableShiftMode(BottomNavigationView view) { BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0); try { Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode"); shiftingMode.setAccessible(true); shiftingMode.setBoolean(menuView, false); shiftingMode.setAccessible(false); for (int i = 0; i < menuView.getChildCount(); i++) { BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i); //noinspection RestrictedApi item.setShiftingMode(false); // set once again checked value, so view will be updated //noinspection RestrictedApi item.setChecked(item.getItemData().isChecked()); } } catch (NoSuchFieldException e) { Log.e("BNVHelper", "Unable to get shift mode field", e); } catch (IllegalAccessException e) { Log.e("BNVHelper", "Unable to change value of shift mode", e); } }
但是,最终我仍然选择了BottomNavigationView,因为两个原因:一是BottomTabBar不能使用AS自带的大量矢量图标,这是一大浪费啊;二是因为,我都花了那么多时间把BottomNavigationView搞明白了,舍不得呀!!!
2.首页RecyclerView复合布局的实现
所有实用app的首页基本都采用的是复合布局,主框架采用RecyclerView,然后将主框架分成若干层,每一层采用不同的布局或者嵌套其他的View组件,比如ViewPager。
复合布局麻烦的地方在于,不同层的Item-Layout不同,需要使用不同的ViewHolder。具体的实现代码太多就不贴了,只贴一个示意图。
在此基础上可以进一步衍生嵌套其他的View组件。
3.矢量图形资源的使用
先上图
这是一大宝库呀,几乎大部分常用图标都能在里面找到,牛人还可以尝试自建矢量图。具体步骤为:右键点击Drawable–>New–>Vector Asset。
4.TabLayout与ViewPager的联动
这个也是安卓应用中常用的一种方式,实现起来相对简单:
private void initFragments() { fragment1 = new LoginFragment(); fragment2 = new RegisterFragment(); fragments=new Fragment[]{fragment1,fragment2}; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_log_reg); mViewPager=(ViewPager)findViewById(R.id.viewpager_log_reg); mTabLayout= (TabLayout) findViewById(R.id.tab_log_reg); //初始化 initFragments(); //设置viewpager Adapter FragmentManager fragmentManager=getSupportFragmentManager(); mViewPager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) { @Override public Fragment getItem(int position) { return fragments[position]; } @Override public int getCount() { return fragments.length; } //解决TabLayout与ViewPager联动后无标题问题!!! @Override public CharSequence getPageTitle(int position) { CharSequence[] list_title=new String[]{"登 录","注 册"}; return list_title[position]; } }); //重点来了,实现联动就靠这句 mTabLayout.setupWithViewPager(mViewPager); }
5.使用SharedPreferences管理Cookie
SharedPreferences是安卓系统提供的一种数据持久化机制,使用较为简洁,常用来存储Cookie中的用户登录信息、应用版本信息等。
public class CookieUtils { protected static final String TAG="CE7"; private final static String PREF_COOKIE_STRINGS="CE7_cookie_strings"; private final static String LOGIN_STATUS="is_login"; public static class AddCookiesInterceptor implements Interceptor { private Context context; public AddCookiesInterceptor(Context context) { this.context = context; } @Override public Response intercept(Chain chain) throws IOException { Request.Builder builder = chain.request().newBuilder(); //读取SharedPreferences HashSet<String> preferences = (HashSet) PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).getStringSet(PREF_COOKIE_STRINGS, new HashSet<String>()); for (String cookie : preferences) { builder.addHeader("Cookie", cookie); Log.d(TAG, "Adding Header: " + cookie); // This is done so I know which headers are being added; this interceptor is used after the normal logging of OkHttp } return chain.proceed(builder.build()); } } public static class ReceivedCookiesInterceptor implements Interceptor { private Context context; public ReceivedCookiesInterceptor(Context context) { this.context = context; } @Override public 117da Response intercept(Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); if (!originalResponse.headers("Set-Cookie").isEmpty()) { HashSet<String> cookies = new HashSet<>(); for (String header : originalResponse.headers("Set-Cookie")) { cookies.add(header); } //写入SharedPreferences PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).edit() .putStringSet(PREF_COOKIE_STRINGS, cookies) .apply(); } return originalResponse; } } //从SharedPreferences获取 public static HashSet<String> getPreferences(Context context){ HashSet<String> preferences =(HashSet) PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).getStringSet(PREF_COOKIE_STRINGS, new HashSet<String>()); return preferences; } //从SharedPreferences获取List<String> public static List<String> getCookieStringsList(Context context){ List<String> mCookieStringsList=new ArrayList<>(); for(String str:getPreferences(context)){ mCookieStringsList.add(str); } return mCookieStringsList; } //从SharedPreferences或者response获取List<Cookie> public static List<Cookie> getCookieList(Context context, List<String> mCookieStringsList){ List<Cookie> mCookieList=new ArrayList<>(); for(int i=0;i<mCookieStringsList.size();i++){ String[] content=SplitCookieString(mCookieStringsList.get(i)); Cookie cookie=new Cookie(content[0],content[1]); mCookieList.add(cookie); } return mCookieList; } //从SharedPreferences获取CookiesKVMap public static Map<String,String> getCookieKvMap(Context context){ Map<String,String> mCookieKvMap=new HashMap<>(); List<Cookie> mCookieList=getCookieList(context,getCookieStringsList(context)); for(int i=0;i<mCookieList.size();i++) { mCookieKvMap.put(mCookieList.get(i).getName(),mCookieList.get(i).getValue()); } return mCookieKvMap; } public static String[] SplitCookieString(String cookie_string){ String[] firstsplit=cookie_string.split(";",2); String[] secondsplit=firstsplit[0].split("=",2); return secondsplit; } }
先开发出一个基于SharedPreferences的CookieUtils类,然后基于此类在业务层可以开发出Cookie的拦截器、登录令牌管理、退出登录等业务逻辑。
(6)开发过程中的那些坑
简直太多了,这个过程中我倒是熟练掌握了AS的调试方法。这里仅列举我印象比较深刻的几个事。
1.使用MVVP绑定ViewModel和Layout中图片资源问题
问题描述:MVVP绑定传统数据不成问题,但是绑定ImageView:src属性时无法显示。
解决办法:在ActiveViewModel中添加一个适配器搞定:
@BindingAdapter("android:src") public static void setSrc(ImageView view, int resId) { view.setImageResource(resId); }
2.在TableLayout中使用EditText:inputType=”textMultiLine”控件无法自动换行问题。
问题描述:我有一个最多输入100个字的EditText,放到TableLayout中无法自动换行了。
解决办法:在EditText的属性中加一行搞定:
android:layout_weight="0"
3.TabLayout与ViewPager联动时,会自动删除Item-Tab问题。
问题描述:这两者关联起来后,会莫名其妙删除TabLayout的Item-Tab标签。
解决办法:在TabLayout中删掉Item-Tab,然后在ViewPager的Adapter中加一段搞定:
@Override public CharSequence getPageTitle(int position) { CharSequence[] list_title=new String[]{"登 录","注 册"}; return list_title[position]; }
还有太多槽点,就不一一列举了。
(四)服务器端的工作
详见完整Web应用开发与升级这里补充JSON数据交互接口的编写。
@RestController public class AndroidCon { @Autowired private BLLServer bllserver; @ResponseBody @RequestMapping(value="androidlogin", produces = "text/json;charset=UTF-8") public String androidlogin(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException { //解析请求表单数据 String username=request.getParameter("username"); String password1=request.getParameter("password1"); String password2=request.getParameter("password2"); //获取cookies Cookie[] cookies=request.getCookies(); //请求业务层 int result=-1; if(username!=null&&password!=null) { result=bllserver.Login(username, password); } //添加cookie String mTime=String.valueOf(new SimpleDateFormat("yy-MM-dd HH:mm").format(new Date())); String str= java.net.URLEncoder.encode(mTime,"UTF-8"); if(result==2) { Cookie mCookie_time=new Cookie("last_visit_time",str); Cookie mCookie_islogin=new Cookie("is_login", "true"); mCookie_islogin.setMaxAge(60*60*24*10); str = java.net.URLEncoder.encode(username,"UTF-8"); Cookie mCookie_username=new Cookie("user_name", str); response.addCookie(mCookie_time); response.addCookie(mCookie_islogin); response.addCookie(mCookie_username); } //返回响应数据 HttpBean.Result r=new HttpBean().InitiateResult(); r.setResult(String.valueOf(result)); ObjectMapper mapper = new ObjectMapper(); String jsonString = mapper.writeValueAsString(r); System.out.println("访问成功,result="+r.getResult()); return jsonString; } @RequestMapping(value="androidregister", produces = "text/plain;charset=UTF-8") public @ResponseBody String androidregister(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException { //解析请求数据 //请求业务层 //添加cookie //返回响应数 } @ResponseBody @RequestMapping(value="androidpublish", produces = "text/plain;charset=UTF-8") public String androidpublish(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException { //解析请求数据 //请求业务层 //添加cookie //返回响应数据 } }
为了在网站提供android App下载,在控制层增加了下载功能模块:
@Controller public class DownloadService { @Autowired private BLLServer bllserver; private final static String FILENAME="app.apk"; @RequestMapping("apkdownload") public ResponseEntity<byte[]> download(HttpServletRequest request) throws IOException { String filePath=request.getServletContext().getRealPath("/WEB-INF/file/download/"); String fileName=FILENAME; File file = new File(filePath+fileName); byte[] body = null; InputStream is = new FileInputStream(file); body = new byte[is.available()]; is.read(body); HttpHeaders headers = new HttpHeaders(); headers.add("Content-Disposition", "attchement;filename=" + file.getName()); HttpStatus statusCode = HttpStatus.OK; ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(body, headers, statusCode); is.close(); return entity; }
为了在提供用户头像和活动宣传照片的上传,在控制层增加了上传功能模块:
@Controller public class UploadService { @RequestMapping("upload") public String upload(HttpServletRequest request, @RequestParam(value="file") MultipartFile originalfile) throws Exception { //如果原始文件不为空,写入目标文件 if(!originalfile.isEmpty()) { //上传文件路径 String path =request.getServletContext().getRealPath("/WEB-INF/file/upload/"); //目标文件名 String filename = originalfile.getOriginalFilename(); //创建目标文件 File targetfile = new File(path,filename); //判断目标文件路径是否存在,如果不存在就创建一个 if (!targetfile.getParentFile().exists()) { targetfile.getParentFile().mkdirs(); } //将上传文件保存到一个目标文件当中 originalfile.transferTo(targetfile); return "index"; } else { return "hello"; } } public static String uploadimg(HttpServletRequest request,MultipartFile originalfile) throws Exception { String visit_path=null; //如果原始文件不为空,写入目标文件 if(!originalfile.isEmpty()) { //目标文件路径 String path=request.getServletContext().getRealPath("/WEB-INF/file/upload/"); //目标文件名 String filename = originalfile.getOriginalFilename(); String[] departname=filename.split("\\.",2); UUID fileid=UUID.randomUUID(); String targetfilename=fileid.toString()+"."+departname[1]; //创建目标文件 File targetfile = new File(path,targetfilename); //判断目标文件路径是否存在,如果不存在就创建一个 if (!targetfile.getParentFile().exists()) { targetfile.getParentFile().mkdirs(); } //将上传文件保存到一个目标文件当中 originalfile.transferTo(targetfile); //创建访问路径 visit_path="/CommunityEntertain6/file/upload/"+targetfilename; return visit_path; } else { return "error"; } } }
其中,第一个方法供客户端直接调用,第二个方法供服务器内部调用,为每一张上传的图片生成唯一的文件名存储在文件夹中,并且将文件路径保存在用户和活动数据库中。
(五)web前端的工作
详见Web前端编程本部分补充了用户头像和活动照片上传功能,并且在图片上传前提供图片预览功能。图片预览的Html和JavaScript代码如下:
<form method="post" action="postregister" enctype="multipart/form-data" class="form-group"> <h3>请您注册</h3> <br> <p class="form-inline"> <label class="input-group">用户名 </label> <input type="text" name="username" class="form-control" placeholder="username"> </p> <p class="form-inline"> <label class="input-group">密 码 </label> <input type="password" name="password1" class="form-control" placeholder="password"> </p> <p class="form-inline"> <label class="input-group">密 码 </label> <input type="password" name="password2" class="form-control" placeholder="confirm password"> </p> <p class="form-inline" > <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tbody> <tr> <td align="center" style="padding-top:10px;"> <label class="form-control btn-primary" for="xFile" style="display: block; width: 100px;">上传头像</label> </td> <td height="101" align="center"> <div id="localImag"> <img id="preview" src="" alt="portrait" style="display: block; width: 180px; height: 150px;"> </div> </td> </tr> </tbody> </table> <div id="InfoDiv"></div> <input type="file" id="xFile" name="originalfile" accept="image/*" onchange="PreviewImg(this)" style="position:absolute;clip:rect(0 0 0 0);"> </p> <p> <input type="submit" name="submit" class="form-control btn-primary" value="注 册"> </p> </form> <script type="text/javascript"> //判断浏览器是否支持FileReader接口 if (typeof FileReader == 'undefined') { document.getElementById("InfoDiv").InnerHTML = "<h1>当前浏览器不支持FileReader接口</h1>"; //使选择控件不可操作 document.getElementById("xFile").setAttribute("disabled", "disabled"); } //选择图片,马上预览 function PreviewImg(obj) { var file = obj.files[0]; var reader = new FileReader(); reader.onload = function (e) { var img = document.getElementById("preview"); img.src = e.target.result; //或者 img.src = this.result; //e.target == this } reader.readAsDataURL(file); } </script>
(六)毕业感言
学习Java是我设定的第一个百日计划,目的是通过上班之外的业余时间自学,基本掌握Android和Web应用全栈编程的能力。目前看来基本能力已经具备,尚缺的是更加底层的知识、不同行业应用场景的应对、以及用户界面的设计。这些都还需要花大量时间,不过基于对自己的定位,下一阶段我的目标是向行业专业领域进军,将自己对于生活的想象力释放出来。
眼下最紧要的,是……休息……休息,好好……补觉。
上两张图纪念我的第一个百日计划。
我的服务器
我的战果
相关文章推荐
- 34岁!100天!学会Java编程(Day68-Day80)—Android开发基础
- Android应用开发之Android平台向web应用提交信息
- Android应用开发之(WebView中loadData与loadDataWithBaseURL的使用上的区别)[转]
- Android 的WebView开发及应用
- Android应用开发之(WebView中loadData与loadDataWithBaseURL的使用上的区别)
- Android 上的 10 款 Web 开发应用工具
- Android开发之web应用提交信息
- android开发对Webview的应用
- 总结使人进步,可视化界面GUI应用开发总结:Android、iOS、Web、Swing、Windows开发等
- Android应用开发之(WebView中loadData与loadDataWithBaseURL的使用上的区别)
- 总结使人进步,可视化界面GUI应用开发总结:Android、iOS、Web、Swing、Windows开发等
- 索爱针对Android平台推出WebSDK, 主要是为其Android手机开发应用。
- Android应用开发之(WebView中loadData与loadDataWithBaseURL的使用上的区别)
- Android开发之WebView应用
- Android 上的 10 款 Web 开发应用工具
- Android开发之WebView应用
- android-使用webview来开发混合应用
- Android应用开发之获取web服务器xml数据
- Android应用开发中webview上传文件的几种思路
- Android应用开发之Android平台向web应用提交信息