2016年10月25日 星期二

寫心經APP開發心得





寫心經是我第一款獨立開發並上架至Google Play的作品,歷經多次改版,也收到不少使用者的鼓勵與建議,今天來跟大家分享開發過程與心得!

 緣起

「寫心經」是在自我實現的期望下誕生的,當時想利用所學做出實績,經簡單的分析發現在生活美學相關的App仍不多,經過一些規劃與思考,逐步將其完成。

在決定開發這款App的當下,智慧型手機與App的生態環境已略為成熟,各界興起開發App的風氣。作為App發行重要平台的「Google Play」給了許多開發者發展機會,大幅降低了進入門檻。


從何開始

寫心經App是如何誕生的呢?或說從無到有,要創造一款App需要做什麼?
 

  1. 題材發想
    要做什麼? 在行動裝置上書寫經文
    要有什麼功能(概略)? 書寫、比對、儲存…
  2. 確認市場方向
    App的分類是什麼? 生活品味
    這個分類的現如何? (當時)不多見
    這個題材是否有人需要?
  3. 開發技術評估
    要兼顧些平台的使用者?
    要採用跨平台的開發技術嗎?
  4. 系統原型設計
    視覺化的東西有利進一步分析
    試著做出原型與視覺設計
  5. 系統開發
    實際撰寫程式,使其滿足當初規劃
    釋出前的品質確認,可以透過一些測試方法來評估
  6. 上架作業
    備妥文案與圖資,至各應用程式平台進行上架
  7. 更新維護
    定期閱覽評價與評論
    隨時追蹤平台的規章變更 (通常會發信通知)
    隨時追蹤第三方資源的安全更新 (通常會發信通知)

上述的每個步驟若再展開,仍有說不完的細節。由於資訊技術與環境的快速變動,每個時期會經歷的技術都不盡相同,在這邊不多談太多細節。

獨立開發意謂著所有事情都要自己來──不嫌累,學更多!


聆聽使用者

任何一款App都需要聆聽使用者的需求,但不只是在開發前聆聽一次,你必須不斷蒐集回饋。

寫心經有一些功能即是來自使用者的建議,有時你得到的建議正好與你自己想做的相符,但仍需考量實現的困難度。近期比較重大的改革是加入字跡記錄的功能,它的概念很簡單,即是儲存使用者書寫的字跡,並彙整輸出成手稿檔案。說來簡單,但要實踐它仍需費一番功夫,例如:如何儲存字跡?用什麼格式來存?如何縮放筆跡?如何排版為手稿?有許多問題需要開發者一一克服。



手稿輸出



無處不在的挑戰

App猶如任何一款資訊產品,都需要思考營運問題,即便沒有「營利」行為,也要為了滿足使用者努力,定期維護與發布更新版本的動作是少不了的。這些過程是充滿挑戰的,挑戰可能來自Android版本的演進,老舊的程式需要翻新、碎片化的硬體規格,讓你不易兼顧所有使用者與特定功能的實踐困難等。

面對這些挑戰,開發者必須有隨時跟上技術演進的準備,實際上這也是資訊人的必備技能,你必須不斷學習,但學習的成本是高的,無論是金錢或時間,因此你還需要培養強大的自學能力作為後盾 (關於自學的經驗,再找機會與大家分享)。

若你的APP具營利性質,挑戰則不僅功能維護,更包含如何培養、發掘、維持客源的問題。


成果

至今寫心經已累計萬次安裝,我在開發過程中學習不少,這也為我的工作打下基礎。成果可以量化,也可以是模糊的,總之這些經驗的累積多少會令你成長,無論是實質上的技術,或是思考問題的角度。

也要感謝每位使用者的迴響,您的意見、評價與評論都非常寶貴,讓我能夠針對應用進行調整與優化,期望寫心經能帶給您愉快的體驗。


後記

資訊界總是一波又一波的新趨勢,本文撰寫時App已不是時下焦點,取代而之的是VR/AR, IoT, FinTech。在競爭激烈的背景下,我想目前「App」一詞所代表的意義,更貼近資料展示平台、特殊的/靈活的/便攜的服務提供管道,開發者要思考的是如何透過它來提供服務,以及可以帶給使用者何種價值。

資訊人應該隨著時下潮流,並動手嘗試,說不定能發現各種新的可能。

2016年7月25日 星期一

Fragment畫面轉向、View.post與getActivity() 聯手演出的問題

問題

某Fragment透過View.post() 待View建立完成後執行相關動作,例如常見的取得View大小,但若post出去的Runnable物件內恰巧包含了getActivity()的相關用法,可能會遇上傳回值為null的情況。

Fragment於Activity.onCreate() 新建並替換現有畫面,在畫面轉向時隨Activity銷毀與重建,但一再新建的Fragment會取代舊有(自動重建)的Fragment,使其與Activity的連結中斷,但已post至主執行緒MessageQueue中的待執行任務仍會被執行,若其中的操作基於getActivity(),將發生Null Point Exception。

這個問題個人不常遇到,但感到挺有趣的,所以花點時間研究一下,若有謬誤麻煩告訴我,有什麼看法也歡迎討論!

還原現場

在MainActivity的onCreate()新建Fragment物件,為求簡便直接以replace方法將畫面置換。

MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    …
    Fragment fraMain = new FragmentMain();
    getFragmentManager().beginTransaction().replace(R.id.FrameLayoutContent, fraMain).commit();
    …
}

在Fragment的onActivityCreated事件中載入TextView,並以Runnable包裹待View建立完成後要執行的動作,動作內包含getActivity()的呼叫。

FragmentMain.java
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    TextView txv = (TextView)currentFragment.findViewById(R.id.txv);
    txv.post( new Runnable() {
        @Override
        public void run() {
            getActivity()……
        }
    });
}

關鍵之一「畫面轉向」

Fragment 需要附加於 Activity,其生命週期也受 Activity 影響,這點在官方指南中的附圖可以明顯看出。實測畫面轉向時,Activity 確實執行 onDestroy() 與 onCreate(),而 Fragment 亦同。

Activity 與 Fragment 生命週期
(來源:https://developer.android.com/guide/components/fragments.html)


例在本問題中,轉向後會有兩個Fragment被建立:
  1. 隨Activity重建亦自動重建的Fragment → 原有重建的
  2. 當Activity再次執行onCreate所新建Fragment → 新建的
自動重建的 Fragment 一樣會依序呼叫 onAttach()、onCreate()、onCreateView() 與onActivityCreated(),且在這些方法內呼叫 getActivity() 仍會取得物件,因此不能在一開始的onAttach() 中判斷 getActivity() 是否為 null,作為是否繼續執行的依據。當然,在真正用到getActivity() 的當下,都判斷一次是否為 null 就可避免問題,但這很累人,讓我們繼續挖掘其原理。

關鍵之二「View.post()」

View.post 傳入的 Runnable 物件將被排入主執行緒 MessegeQueue 等待執行。然而隨著Activity 的 onCreate() 執行,一再新建並且 replace 的 Fragment 物件會使舊有 Fragment 銷毀,但已 post 去出的 runnable 仍在主執行緒中等待執行,當 runnable 被執行時,裡面的getActivity() 自然會取得 null,因為該 Fragment 已onDetach(參考前面的生命週期圖)。

解決方案

經過上述分析,大致有3方案可以使用:

  1. 直接在View.post() 判斷getActivity()是否為null,這是最簡單直覺的方法。
  2. 設法重用Fragment,在replace前檢查是已有存在的相同Fragment,這可利用Activity的onSaveInstanceState()來實踐。
  3. 利用Fragment的onAttach()方法,該方法自動傳入Activity參考,可在此先準備好要使用的資料與資源,但遷就其生命週期,程式的撰寫方式與執行流程可能需要隨之更動,若需暫存預先取出的資源,亦需考量耦合問題。

當然,直接鎖定Activity使其不能轉向亦可,但總有需要支援旋轉的情況。

參考資料

  1. Android官方文件Fragment
    https://developer.android.com/guide/components/fragments.html
  2. Android Fragment中getActivity()返回null的问题
    http://www.dss886.com/android/2015/08/11/01
  3. After the rotate, onCreate() Fragment is called before onCreate() FragmentActivity
    http://stackoverflow.com/questions/14093438/after-the-rotate-oncreate-fragment-is-called-before-oncreate-fragmentactivi
  4. 通过View.post()获取View的宽高引发的两个问题:1post的Runnable何时被执行,2为何View需要layout两次;以及发现Android的一个小bug
    http://blog.csdn.net/scnuxisan225/article/details/49815269
  5. 如何使用Handler
    http://givemepass-blog.logdown.com/posts/296606-how-to-use-a-handler
  6. Android中Thread、Handler、Looper、MessageQueue的原理分析
    http://blog.csdn.net/bboyfeiyu/article/details/38555547









2016年4月17日 星期日

Raspberry Pi 3 開機自動啟動程式

前言

最近在執行的專案需要在RPi開機時自動執行Python Script,在網路上爬了不少文章,但似乎沒有一篇可以完全解決問題的,多半是設定後沒有任何反應,經過一番拼湊,終於找出可以在RPi 3上正常執行的方法,在這邊記錄一下。

本文內容是基於這幾篇文章編寫,並視實測結果進行調整


原理

將RPi開機選項設定為以GUI啟動且自動登入User Pi,配合修改對應的autostart設定檔實現開機時自動執行某程式。

開機選項設定

設定步驟

  1. 開啟autostart設定檔,位於pi使用者下的 ~/.config/lxsession/LXDE-pi/autostart。視你目前操作情況,選用適當的編輯器開啟即可(例vi, nano, lefapad)。
  2. 在設定檔最下方填入以下指令。請將demo.py替換為你的python程式所在路徑。
  3. @/usr/bin/python demo.py
    
  4. 若設定無誤,重新啟動後應會看到 demo.py 已被執行。
  5. 若不想重啟,可直接下 startx 指令。



2016年2月14日 星期日

安裝Cordova Plugin

前篇「Cordova 安裝與開發流程」講述Cordova開發環境的準備及基本的使用教學,而本篇則是要說明如何在Cordova專案加入Plugin,讓你的專案具有使用相機、GPS、麥克風等諸多行動裝置硬體設備之能力。

以下就以裝載Camera的Plugin為例,在Cordova專案中裝載該插件:


取得與安裝Plugin

  1. 連線至Cordova官方提供的Plugin Search頁面 (https://cordova.apache.org/plugins/),輸入你想查找的關鍵字即可。我們在此輸入「camera」,馬上可以看到對應的Plugin已顯示。
    Codrova Plugins 搜尋頁面



    cordova-plugin-camera


  2. 使用指令 cordova plugin add cordova-plugin-camera,將會把camera插件下載並裝到Project/platforms目錄下的所有平台。


安裝原理簡述

執行plugin add 指令後,系統已在背後悄悄的完成許多異動,例如:


  1. 將Plugin檔案下載至Project/plugins目錄。
  2. 將已下載的「cordova-plugin-camera」安裝至各平台,這邊以Android為例,即為Project\platforms\android\assets\www\plugins 目錄。
  3. 依安裝的plugin 更新 Project\platforms\android\assets\www 目錄下的「cordova_plugins.js」。
  4. Project\platforms\android中的「AndroidManifest.xml」權限異動。
  5. android\src 目錄,新增「org.apache.cordova.camera」Package與所屬java檔。

雖然這些異動有跡可尋,但仍不建議自行修正,照著正規的方法來處理比較妥當。

檢視現有的 Plugin

使用指令 cordova plugin ls 即可。

移除 Plugin

使用指令 cordova plugin rm [plugin_name],例如要移除 Camera Plugin,則下 cordova plugin rm cordova-plugin-camera。若你忘了Plugin的全名,可由前面的 ls 命令找到。

延伸閱讀

Apache Cordova Documentation - Camera, https://cordova.apache.org/docs/en/3.3.0/cordova/camera/camera.html

2016年2月11日 星期四

Java Script 跨域請求

平日寫App慣了,在App呼叫遠端PHP程式存取資料庫是件相當自然的事。但近期有個Case是以Web方式開發,透過AJAX存取另一Server上的PHP時卻罷工了,追查一下發現是跨域請求造成的問題,在這邊筆記一下。


還原現場

  • 有一個存放於遠端Server的PHP程式,它會讀取資料庫並傳回結果,使用的資料格式為JSON。
query.php
<?php
 //存取資料庫,略!
 $arr = array(
  "id" => "a01",
  "name" => "elephant"
 );    
 echo json_encode($arr);
?>


  • 本機有一JavaScript,以jQuery提供的ajax function呼叫遠端PHP取回JSON字串。

search.js
$.ajax({
 method: "POST",
 url: "http://遠端Server IP/query.php",
 dataType: "json"
}).done(function(data) {
 alert(data.name);
}).fail(function() {
 alert("fail");
});


  • 執行看看,將跳出fail的alert。


錯誤訊息

目前主流的瀏覽器皆內建了開發人員工具,這邊以Firefox為例,執行前述提到的query.js,將看到如下的錯誤訊息:




相當親民的訊息,直接告訴你這是跨域請求的問題,缺少了Access-Control-Allow-Origin檔頭。



解決跨域資源請求之問題

在此以這篇文章「js跨域访问,No ‘Access-Control-Allow-Origin‘ header is present on」提供的方法來解。文內亦有提到跨域存取之細節,有興趣可以讀讀看。欲解決此問題,必須在前述的PHP程式加入「header("Access-Control-Allow-Origin: *");」。


<?php
 header("Access-Control-Allow-Origin: *");
 //存取資料庫,略!
 $arr = array(
  "id" => "a01",
  "name" => "elephant"
 );
 echo json_encode($arr);
?>




2016年1月31日 星期日

Android存取後端Server (part 1)

前言

若您開發的App有保存資料的需求,多半希望將資料存集至遠端伺服器,比較建議的作法是透過PHP, JSP, AST.NET等技術,以Web方式實作資料傳輸的媒介,存取資料即使用POST或GET的方式來進行。

在實作前有件事情需要注意,Android 4.0規定了存取網路的操作必須執行在另一Thread(執行緒)中,為了因應此規定,目前有兩大方向可供選擇:
  1. 使用Runnable與Handler:這種方法比較複雜,但彈性高,可依自己的需求規劃,同時在Handler的使用上需注意Memory leak之情況。
  2. 使用非同步任務(AsyncTask):此為相對簡單的方法,依規定好的格式即可以輕易完成網路存取。
接下來,就開始以第一種方式實作(Runnable + Handler),簡單一點的AsyncTask預計寫在下一篇(part2)。以下以重點說明為主,完整的範例已放在GitHub上,有需要的參考一下即可。


Step 0. 理解執行架構與流程

這邊用張簡圖表示,描述自步驟1發出請求起,至將遠端資料顯示至TextView的第驟6。Handler, Thread與Activity的互動關係大略是這樣。




Step 1. 開啟網際網路存取權限

為專案加入網際網路存取權限,找到AndroidManifest.xml,加入 android.permission.INTERNET 。


Step 2. HTTP請求與回應

以下程式摘錄自SendDataRunnable.java,可以直接套用,範圍程式中已把它包成method,若您想要自己另外處理,參數對照一下即可。這段程式碼有幾個重點:
  1. 第n列,有些教學範例會使用HttpClient這個類別來操作,但其已被Android棄用,因此這邊改以HttpURLConnection實作。
  2. 第n列,URL物件需要傳入Server上某個php檔案 (或其他Web程式)的路徑。
  3. 第n列,HTTP_OK這個常數值為200,表示正常狀態,確認正常了才開始讀取Response,你也可以再該處加上else,處理一些異常狀態。
  4. 第n列,composePostString( )負責組合參數,需傳入HashMap作為數個參數的集合(HashMap有Tag與Value對應的概念),詳見範例程式。
private String sendData(String target, HashMap params){
    String responseData = "";
    try {
        URL url = new URL(target);
        //建立與目標之間的連線
        HttpURLConnection connection = (HttpURLConnection)url.openConnection(); 
        connection.setRequestMethod("POST");
        connection.setDoInput(true);
        connection.setDoOutput(true);

        OutputStream stream = connection.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"));
        String c = composePostString(params);

        writer.write(c);

        writer.flush();
        writer.close();
        stream.close();
  
        int httpResponseCode = connection.getResponseCode();
        if(httpResponseCode == HttpsURLConnection.HTTP_OK) {
            String oneLine = "";
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(connection.getInputStream()));
            while ((oneLine = reader.readLine()) != null) {
                responseData += oneLine;
            }
        }

    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

    return responseData;
}



Step 3. 實作Run()

run( )是Runnable介面中唯一的方法,當其被執行時會自動呼叫,在範例程式中拿來呼叫Step 2.所寫的sendData( )。

此段程式相當簡單,sendData( )會傳回Server回應的資料,故需檢查資料長度是否正常,並針對各情況進行處理。傳回來的資料會放在response變數中,至此所有動作仍不在Android的主執行緒上執行,若你要在這邊把資料更新到UI上是無法存取的,所以這邊有個handler.obtainMessage( ),用它把資料傳至主執行緒上的Handler。

但有一點請注意,obtainMessage傳入兩個參數,第一個是int型別的值,這是讓你識別目前的情況用的,姑且稱它為「識別碼」,你可以自己決定該如何實作,例如:1代表新增成功、2代表刪除成功、3代表取得清單資料等。為了方便管理,也可以宣告常數來處理,讓程式更易維護。


@Override
public void run() {
    String response = sendData(this.path, this.params);
    if(response.length() > 0) {
        //TODO
        handler.obtainMessage(0, response).sendToTarget();
    }else{
        //TODO
    }
}



Step 4. Handler

在上一步驟中,我們已經取得Server回應的字串資料,為了在主執行者中接收這個字串資料,需要準備Handler來承接,礙於Memory Leak的問題(有機會再談),建議以內部靜態類別來實作,完整程式請參考範例中的main.java。

這邊定義了HttpHandler類別,其繼承Handler (來自android.os.Handler,別import錯了)。

請特別注意幾個地方:
  1. 第n列,將WeakReference強迫轉型為Main,這個Main需繼承當初建立WeakReference傳入的泛型 (本例是Activity),白話的說Main是Activity,但其具有Activity沒有的成員與方法(因為繼承),因此我們要把它強迫轉型為Main,以方便進行後續處理。
  2. 第n列,這邊用if判斷了msg.what值,該值就是在Step 3所傳入的int參數(本例為0),所以這邊檢查等於0時把Server傳回的response字串顯示在TextView上。
  3. 承上,response字串也是是在Step 3傳入,但其型別為Object,所以這邊取出時要再強迫轉型為字串。


static class HttpHandler extends Handler {
    WeakReference weakReference;
 
 public HttpHandler(Activity activity) {
        weakReference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        Main main = (Main)weakReference.get();
        if(msg.what == 0) {
            //TODO 依what判斷後續處理
            main.txvResponse.setText((String)msg.obj);
        }
    }
}



Step 5. 串接一切

各細節寫好了,現在要把他們串起來!實際上該封裝的東西也都裝好了,所以真正在呼叫時動作不會太多。

首先,在呼叫前確保handler與params兩個物件有新建了,兩者的作用在前面都有提過。
handler = new HttpHandler(this);
HashMap<String, String> params = new HashMap<>();
接著填入要送至Server的參數,每個參數都會有Tag (或稱為Name)與Value的對應。
params.put("name", "tom");

最後要將整個HTTP的請求與回應動作放在另一個執行緒中執行,因此我們這麼處理。記得網址的部分自己換掉。
new Thread(
    new SendDataRunnable(handler, "http://www.example.com", params)
).start();


小結

上述的程式還可以再封裝的更細緻,但有時封過頭會綁手綁腳的,大家若要直接使用範例程式,請注意裡面標示//TODO的地方。
這邊有小建議,請盡可能的將識別碼、網址或ip之類的資源統一管理,看是要宣告成常數,還是把它放在string.xml中。
還有!安全議題也需注意,本篇沒有特針對安全部分進行討論,若你要傳輸的資料是有安全顧慮的,請以https的方式進行,亦需留意Server端的安全措施。


2016年1月10日 星期日

IoT App 開發工具 Evothings 介紹


以下內文已於 2016/4 針對正式釋出的 2.0 版進行微調。

Evothings是一套針對物聯網(IoT)相關行動App開發工具,開發技術採用Cordova。它內建了不少相關範例程式(BLE/Beacon、Arduino、LinkIt One,甚至是ESP8266等相關應用)。

Evothings主要分為2大部分:執行在PC上的Evothings Workbench與執行在行動裝置上的Evothings Viewer (小於2.0版的是使用Evothings Client)。以下內文內容以Evothings 2.0 為基準。

Evothings提供的Viewer程式已事先裝載了相關Plugin,透過Workbench可將你的開發環境(PC)與Viewer(Mobile)串接,優點是每次變更程式時,不需手動編譯與打包,更動的結果會即時反應在行動端。目前Evothings不能完成打包的動作,主要用途是用於快速開發原型(prototype)與實驗目前的想法是否可行。

接著開始看看如何使用Evothings:


下載Evothings Workbench

至官方網站下載即可,本文以Evothings Workbench 2.0 為介紹對象。
https://evothings.com/


執行Workbench

以Windows平台而言,下載後解壓縮即可,執行其中的「EvothingsWorkbench.exe」即可啟動,需無進行安裝動作。

安裝Evothings Viewer

記得這邊談的是2.0以上版本,請在行動裝置安裝Evothings Viewer。若是使用較早的Workbench搭配的是Evothings Client。在Apple App Store或Google Play搜尋Evothings即可找到相應App,將其安裝即可。




Workbench與Viewer連線

2.0版本可以透過外網連線,也因此會有KEY的產生與登入之功能,在2.0以前是Workbench與Viewer都是區網的情況下才可使用
切換至Workbench的Connect頁籤,按下「GET KEY」即可產生金鑰,並在Viewer中輸入金鑰即可連線。 (實際使用時有遇過timeout的情況,再多試幾次即可)。




範例程式的使用

前面有提到,Evothings內建了不少IoT相關應用範例,這些範例都可以在Workbench的Examples頁籤中找到。2.0版與先前版本的差異是,在使用這些範例要先按下「COPY」把它複製到你的開發目錄(依你的習慣設定即可),完成複製的專案可以在My Apps頁籤中找到。早期版本是直接修正範例檔,若沒有事先備份可能會回不去。


修改範例程式並執行

在Workbench中切換至My Apps頁籤,在想要修改的專案中按下「CODE」即會開啟放置原始碼的目錄,使用你習慣的程式碼編輯器開啟相關檔案即可。這邊用Notepad++打開index.html。在此時你可以修正這些檔案的內容,包含html、js與css等,一經修正並存檔,即會自動把結果反應至行動裝置。若你發現修正後行動裝置並沒有跟著改變,可能是Workbench與Viewer之間的連線中斷了,請檢查看看。






打包釋出

如同前述,目前Evothings沒有提供這個功能,但由於專案骨子裡是Cordova,因此可以用Cordova自己的打包流程來產出最終結果。這部分可以先參考本Blog的文章「Cordova 安裝與開發流程」會較有概念。



其他功能

若你有原本的Cordova專案,可以直接把專案中的主html檔拖拉至My App頁籤,即可透過Evothings幫你快速執行至實機。



而在Viewer右上角的選單可以打開「Info」頁面,即可查詢目前包含的Plugin,可以發現有battery、camera、geolocation等常用Plugin,以及特定用途的像是ble與beacon。