
快到春節(jié)了,三年多沒回過老家,今年打算回家看看,跟老媽在老家過個年,帶點小酒給老爸掃掃墓。一個月前西城高鐵開通,北京-成都坐火車只需要9個多小時,天知道這列火車是如何穿越大秦嶺的。打算體驗一次回家的高速列車,但是票一如既往的搶不著,只好買到初一的票,留在北京過除夕吧。
百日總結
這一篇主要是對之前學習Java的一點總結,Android應用與Web應用是企業(yè)應用常見的兩種形式。對于當前的創(chuàng)業(yè)性公司來說,多是把重心放在移動端App的開發(fā)上。但是個人覺得一開始設計時將兩者統(tǒng)一規(guī)劃,將核心的業(yè)務都放在服務器端,增加一些Web前端設計的工作,就可以實現(xiàn)一站式開發(fā),適應更廣的業(yè)務場景,這樣做還可以適當減輕Android端業(yè)務邏輯處理的壓力。
本案從Java零基礎開始,在工作之余用一百天的業(yè)余時間,對一個“娛樂社區(qū)”(代號CE)習作的不斷迭代升級開發(fā)過程中,完成了Android客戶端、Web前端、服務器后端完整框架的開發(fā),掌握了基本的全棧開發(fā)能力。
Web網(wǎng)站瀏覽和安卓apk下載地址:娛樂社區(qū)習作
(一)整體框架

這個框架很容易理解,只是在之前Web應用的框架的基礎上,在服務器前端控制器增加了JSON數(shù)據(jù)的交互接口,用于與Android端進行遠程交互。
(二)統(tǒng)一設計風格
Android與Web應用一站式開發(fā)要求將兩者作為一個產(chǎn)品考慮,會要求統(tǒng)一的設計風格。
由于Web前端設計與Android前端設計的方法相差較大,通常不會是由一個設計師開展設計,甚至對于稍大的應用,光是其一就不只一個設計師進行設計。不同的設計師的設計語言與理念通常也會有所區(qū)別,而過多的設計理念對于產(chǎn)品呈現(xiàn)會是一場災難。此時,設計規(guī)范可以很好地解決這個問題。
適用于Web前端與Android前端的通用設計規(guī)范通常有:色彩規(guī)范、文字規(guī)范等。
對于Android應用來說還需考慮布局規(guī)范、控件規(guī)范、圖標規(guī)范等。對于適用于手機瀏覽器的動態(tài)響應Web前端,也應盡量遵守Android設計規(guī)范。
以下是某應用的部分設計規(guī)范(示例):

(三)android客戶端的工作
(1)CE(v7.0)APP客戶端架構

在設計過程中,考慮下面幾個數(shù)據(jù)層盡量使用外觀模式,簡化接口。
(2)開發(fā)文檔結構
開發(fā)工具使用AS,此部分大體上是上述APP架構的具體實現(xiàn),由于時間有限,對功能進行了部分閹割,所以實際的開發(fā)項目會比這個復雜。



其中,資源res部分還有不少東西,限于篇幅就不展開了。在實現(xiàn)過程中發(fā)現(xiàn)各層和模塊之間交叉太多,想要實現(xiàn)外觀模式并不容易,下次再想想辦法。
(3)AndroidManifest.xml
這部分重點是應用權限的獲取,應用名稱和圖標的修改,四大天王的注冊等基本設置。我曾經(jīng)因為沒有設置權限,訪問不了網(wǎng)絡;曾經(jīng)編寫一個新的Activity后沒有注冊,導致跳轉失敗還花了些時間找原因。
(4)build.gradle(Module:app)
這個文件很重要,其中包含了SDK版本的配置,app應用版本的管理,MVVP數(shù)據(jù)綁定設置,以及最重要的中央倉庫管理。僅列舉我這個應用所用到的框架和庫:
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'
//servlet
implementation 'javax.servlet:javax.servlet-api:4.0.0'
}
(5)主要功能開發(fā)要點
1.底部Tab主菜單的實現(xiàn)
實現(xiàn)底部Tab主菜單,有很多選擇,我比較了一下material design庫中的BottomNavigationView和第三方支持庫BottomTabBar。
BottomTabBar的突出優(yōu)點就是實現(xiàn)簡單,只需要短短幾行代碼就可以搞定:
mBottomTabBar.init(getSupportFragmentManager())
.addTabItem("首頁", R.drawable.icon1, HomeFragment.class)
.addTabItem("發(fā)現(xiàn)", R.drawable.icon2, DiscoverFragment.class)
.addTabItem("發(fā)起", R.drawable.icon3, PublishFragment.class)
.addTabItem("圈子", R.drawable.icon4, CircleFragment.class)
.addTabItem("我的", R.drawable.icon5, MineFragment.class);
而BottomNavigationView實現(xiàn)起來代碼則要多很多:
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搞明白了,舍不得呀!?。?/p>
2.首頁RecyclerView復合布局的實現(xiàn)
所有實用app的首頁基本都采用的是復合布局,主框架采用RecyclerView,然后將主框架分成若干層,每一層采用不同的布局或者嵌套其他的View組件,比如ViewPager。
復合布局麻煩的地方在于,不同層的Item-Layout不同,需要使用不同的ViewHolder。具體的實現(xiàn)代碼太多就不貼了,只貼一個示意圖。

在此基礎上可以進一步衍生嵌套其他的View組件。
3.矢量圖形資源的使用
先上圖

這是一大寶庫呀,幾乎大部分常用圖標都能在里面找到,牛人還可以嘗試自建矢量圖。具體步驟為:右鍵點擊Drawable-->New-->Vector Asset。
4.TabLayout與ViewPager的聯(lián)動
這個也是安卓應用中常用的一種方式,實現(xiàn)起來相對簡單:
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聯(lián)動后無標題問題?。。? @Override
public CharSequence getPageTitle(int position) {
CharSequence[] list_title=new String[]{"登 錄","注 冊"};
return list_title[position];
}
});
//重點來了,實現(xiàn)聯(lián)動就靠這句
mTabLayout.setupWithViewPager(mViewPager);
}
5.使用SharedPreferences管理Cookie
SharedPreferences是安卓系統(tǒng)提供的一種數(shù)據(jù)持久化機制,使用較為簡潔,常用來存儲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 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;
}
}
先開發(fā)出一個基于SharedPreferences的CookieUtils類,然后基于此類在業(yè)務層可以開發(fā)出Cookie的攔截器、登錄令牌管理、退出登錄等業(yè)務邏輯。
(6)開發(fā)過程中的那些坑
簡直太多了,這個過程中我倒是熟練掌握了AS的調(diào)試方法。這里僅列舉我印象比較深刻的幾個事。
1.使用MVVP綁定ViewModel和Layout中圖片資源問題
問題描述:MVVP綁定傳統(tǒng)數(shù)據(jù)不成問題,但是綁定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聯(lián)動時,會自動刪除Item-Tab問題。
問題描述:這兩者關聯(lián)起來后,會莫名其妙刪除TabLayout的Item-Tab標簽。
解決辦法:在TabLayout中刪掉Item-Tab,然后在ViewPager的Adapter中加一段搞定:
@Override
public CharSequence getPageTitle(int position) {
CharSequence[] list_title=new String[]{"登 錄","注 冊"};
return list_title[position];
}
還有太多槽點,就不一一列舉了。
(四)服務器端的工作
詳見完整Web應用開發(fā)與升級
這里補充JSON數(shù)據(jù)交互接口的編寫。
@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 {
//解析請求表單數(shù)據(jù)
String username=request.getParameter("username");
String password1=request.getParameter("password1");
String password2=request.getParameter("password2");
//獲取cookies
Cookie[] cookies=request.getCookies();
//請求業(yè)務層
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);
}
//返回響應數(shù)據(jù)
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 {
//解析請求數(shù)據(jù)
//請求業(yè)務層
//添加cookie
//返回響應數(shù)
}
@ResponseBody
@RequestMapping(value="androidpublish", produces = "text/plain;charset=UTF-8")
public String androidpublish(HttpServletRequest request,HttpServletResponse response) throws JsonGenerationException, JsonMappingException, IOException {
//解析請求數(shù)據(jù)
//請求業(yè)務層
//添加cookie
//返回響應數(shù)據(jù)
}
}
為了在網(wǎng)站提供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();
//創(chuàng)建目標文件
File targetfile = new File(path,filename);
//判斷目標文件路徑是否存在,如果不存在就創(chuàng)建一個
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];
//創(chuàng)建目標文件
File targetfile = new File(path,targetfilename);
//判斷目標文件路徑是否存在,如果不存在就創(chuàng)建一個
if (!targetfile.getParentFile().exists()) {
targetfile.getParentFile().mkdirs();
}
//將上傳文件保存到一個目標文件當中
originalfile.transferTo(targetfile);
//創(chuàng)建訪問路徑
visit_path="/CommunityEntertain6/file/upload/"+targetfilename;
return visit_path;
} else {
return "error";
}
}
}
其中,第一個方法供客戶端直接調(diào)用,第二個方法供服務器內(nèi)部調(diào)用,為每一張上傳的圖片生成唯一的文件名存儲在文件夾中,并且將文件路徑保存在用戶和活動數(shù)據(jù)庫中。
(五)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>
(六)畢業(yè)感言
學習Java是我設定的第一個百日計劃,目的是通過上班之外的業(yè)余時間自學,基本掌握Android和Web應用全棧編程的能力。目前看來基本能力已經(jīng)具備,尚缺的是更加底層的知識、不同行業(yè)應用場景的應對、以及用戶界面的設計。
這些都還需要花大量時間,不過基于對自己的定位,下一階段我的目標是向行業(yè)專業(yè)領域進軍,將自己對于生活的想象力釋放出來。
眼下最緊要的,是……休息……休息,好好……補覺。
上兩張圖紀念我的第一個百日計劃。

