JSON 的 Structure 可以像一個樹一樣去表達資料,但是傳統的 Relational Database 只可以使用一張張的 Table 來儲存資料,如果要把數張 Table 的內容合併為一個 JSON 來使用的話,可以通過使用 mapping 的方法把關聯的 ID 值串合起來。 ### 例子 考慮到有以下的資料: Table: users |id|name|age| |---| |1|peter|11| |2|tom|12| |3|mary|13| Table: user_fruits |id|user_id|fruit| |---| |1|1|apple| |2|1|orange| |3|2|apple| |3|2|banana| |3|3|orange| |3|3|banana| |3|3|apple| 考慮到要抽出以下的 JSON 格式 : ```json [ {"name":"peter", "age": 11, fruits:["apple","orange"]}, {"name":"tom", "age": 12, fruits:["apple","banana"]}, {"name":"mary", "age": 13, fruits:["orange","banana","apple"]} ] ``` 應該要如何轉換呢? ### 解決方法 使用 Loop 是必需要的,因為而一個個 Data Object 互相 Mapping。不過在語法上可以使用 JS 一句內完成的。 ```js users.forEach(user => user.fruits = userFruits.filter(userFruit => user.id === userFruit.user_id).map(userFruit => userFruit.fruit)); ``` 以下是一個完整的示範 : ```js // get users var users = await db.query(`select * from users`); // fetch user id as array var userIds = users.map(user => user.id); // check users is empty if( users.length > 0 ) { // fetch user fruits by user id var userFruits = await db.query(`select * from user_fruits where user_id in (?)`, [userIds]); // map user fruit data to users users.forEach(user => user.fruits = userFruits.filter(userFruit => user.id === userFruit.user_id).map(userFruit => userFruit.fruit)); } ```
ExtJS 的 Combobox 方便好用,配上 Proxy 後可以自動載入 Ajax 的 JSON 內容。不過 API 內就好像沒有一個方法設定自動選擇第一選項。那應該要如何達成呢? ### 使用 Store 的 load 事件 我們可以通過使用 Store 的 load 事件,把 load 入 Store 的記錄抽取出來,然後使用 Combobox 的 `select(r)` 來設定選擇項目。 ```js /** * on load event listener callback */ function load(this, records, successful, operation, eOpts) {} ``` 可以透過 `load` 事件來取得 `records` 資料。 不過我們無法從 Store 取得 Combobox 元件,因為一個 Store 可以 Attach 到很多不同的元件,所以我們需要在第三方的元件上設定好關聯事件。這最好是 Combobox 的 top parent 元件。 ### 設定方法 以下時通過使用 `load` 事件來自動選擇第一選項目的例子 : ```js combobox.getStore().on('load', function(ele, records) { if( records.length > 0 ) { combobox.select(records[0]); } }}; ``` 而這段代碼運行的最佳時機,可以是 combobox 元件的 top parent 元件的 `initComponent()` 方法內。
 要達到可以在 Grid 內即時修改 Cell 的內容,我們要為 Grid 加入 Plugin 設定才可以。 ### 為 Grid Panel 加入 Plugin 設定 我們需要為 Grid Panel 加入以下的設定 : ```js { selModel: 'cellmodel', plugins: { ptype: 'cellediting', clicksToEdit: 1 } } ``` ### 參數解說 - selModel : 選擇 Grid 內容時所以使用的 Model - ptype : Plugin 的種類 - clicksToEdit : 按多少下滑鼠可以進入修改模式 ### Column 設定 除了要設定好 Grid 外,還需要為你目標相修改的 column 進行設定 : ```js { columns: [ {header: 'name', dataIndex: 'name', editor: 'textfield'}, {header: 'name', dataIndex: 'name', editor: { completeOnEnter: false, field: { xtype: 'textfield', allowBlank: false } }, ] } ``` ### 參數解說 - editor (String) - 可以直接使用 xtype 來指定修改器的種類 - editor (Object) - 可以通過使用 field 方法來指定修改器的種類,更適合像 Combobox 一類的 Component。 ### 例子 以下例子是從 ExtJS 的官方說明文件中節錄出來 : https://docs.sencha.com/extjs/6.0.2/classic/Ext.grid.plugin.CellEditing.html ```js Ext.create('Ext.data.Store', { storeId: 'simpsonsStore', fields:[ 'name', 'email', 'phone'], data: [ { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' }, { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' }, { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' }, { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' } ] }); Ext.create('Ext.grid.Panel', { title: 'Simpsons', store: Ext.data.StoreManager.lookup('simpsonsStore'), columns: [ {header: 'Name', dataIndex: 'name', editor: 'textfield'}, {header: 'Email', dataIndex: 'email', flex:1, editor: { completeOnEnter: false, // If the editor config contains a field property, then // the editor config is used to create the Ext.grid.CellEditor // and the field property is used to create the editing input field. field: { xtype: 'textfield', allowBlank: false } } }, {header: 'Phone', dataIndex: 'phone'} ], selModel: 'cellmodel', plugins: { ptype: 'cellediting', clicksToEdit: 1 }, height: 200, width: 400, renderTo: Ext.getBody() }); ```
 ExtJS 的 Form 很方便,有時我們需要從伺服器上載入 JSON 內容,然後把 JSON 的內容放回入 Form 內供使用者修改,然後再儲存回到伺服器上。 ### 伺服器 AJAX 回傳的 JSON 假設呼叫伺服器一個 `users.php` 會回傳以下的 JSON 資料: ```json { "success":true, "user":{ "name":"19site", "age":20 } } ``` ### Ext.form.Panel 元件 另外有一個 `Ext.form.Panel` 元件 : ```json { xtype: 'form', layout: { type: 'vbox' }, items:[{ name: 'name', fieldLabel: '名稱' }, { name: 'age', fieldLabel: '年齡' }] } ``` ### 實作方法 我們使用 `Ext.Ajax.request` 呼叫 `users.php` 後,可以使用以下方法把 JSON 直接載入到 `Ext.form.Panel` 內 : ```js form.getForm.setValues(data.data); ``` 我們是可以使用 `form.getForm.setValues(data)` 來把 JSON 直接定到到 Form Panel 內,JSON 的 KEY 會對應 Form Panel 內 Text Field 的 name 來配對,設定相應的 value。 ```js var form = view.down('selector of form'); Ext.Ajax.request({ url: 'users.php', success: function(res) { var data = JSON.parse(res.responseText); form.getForm.setValues(data.data); } }); ```
當 Android 由 Action Bar 到變成使用 Toolbar 後,雖然兩者大體上是差不多的,不過都有少少地方是有不同,例如如果要在 Toolbar 上顯示 Back Arrow 應該要怎樣做呢?  ### 把 Toolbar 設定為 ActionBar 我們可以把 Toolbar 設定為 ActionBar,這樣就可以使用以前的方法來把返回的按鈕設定出來。 把 Toolbar 設為 support action bar : ```java Toolbar toolbar = (Toolbar) findViewById(R.id.my_toolbar); setSupportActionBar(toolbar); ``` 然後就可以用以前的方法來設定顯示返回按鈕 : ```java etSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); ``` 記住要 override 入下面的方法才可以處理按下的事件 : ```java @Override public boolean onSupportNavigateUp() { onBackPressed(); return true; } ```
### Component 大小 使用 XML 可以為 Layout 內的 Component 設定高度大小等,用的單位可以自行選擇,只要鍵入在數字後便可以。 ```xml <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center|center" android:text="text" android:textSize="20sp" /> ``` ### 使用 Programming 方法設定 我們也可以使用 Programming 的方法來生成一個 TextView。 ```java // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` 上面的代碼應該也能生成一個和上面 XML 相同的 TextView。我們再修改一下上面的 XML : ```xml <TextView android:layout_width="match_parent" android:layout_height="100dp" android:gravity="center|center" android:text="text" android:textSize="20sp" /> ``` 這樣可以生成一個高度 100dp 的 TextView,於是我們又改一下 Java Program : ```java // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` 這樣能生成一個 100dp 的 TextView 嗎? 答案是不行的,因為 LayoutParams 沒有像 setTextSize() 一樣能接受一個 Unit (單位) 的值傳入去,所以上面傳入去的 100 只是一個不知道什麽單位的數字,要達到使用 dp 單位的效果,需要加多一點計算才可以啊 ! ### 算出 dp 值 通過下面的方法,我們可以計算出不同單位的數值 : ```java // get resources Resources r = getResources(); // target height in dp int height = 100; // calculate number float heightInDp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, r.getDisplayMetrics()); ``` 然後就可以把計算出來的值放入 LayoutParams 內,就可以設定出相應的單位。 ```java // get resources Resources r = getResources(); // target height in dp int height = 100; // calculate number float heightInDp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, height, r.getDisplayMetrics()); // layout params ViewGroup.LayoutParams mLayoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightInDp); // create text view TextView mTextView = new TextView(context); mTextView.setLayoutParams(mLayoutParam); mTextView.setGravity(Gravity.CENTER|Gravity.CENTER); mTextView.setText("text"); mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); ``` ### TypedValue 的種類 TypedValue 有以下的類型 (不同的單位) : |常數|說明| |---| |TypedValue.COMPLEX_UNIT_PX|Pixels| |TypedValue.COMPLEX_UNIT_SP|Scaled Pixels| |TypedValue.COMPLEX_UNIT_DIP|Device Independent Pixels|
### new vs newInstance() 我們有一個 Class 叫 MyFragment.java,內容如下 : ```java /** * my fragment */ public class MyFragment extends Fragment { public static MyFragment newInstance() { return new MyFragment(); } } ``` 我們可以通過下面的代碼來生成 MyFragment 的 Object : ```java // create fragment using new Fragment f1 = new MyFragment(); // create fragment using static method Fragment f2 = MyFragment.newInstance(); ``` 接觸過 Android Programming 的朋友都會見過上面兩種方法,那一種才是正確的方法呢 ? ### Android 的生命週期 上面的答案和 Android 的生命週期有極大的關係,因為 Android 會把不在面前 (顯示中) 的 Fragment Recycle 掉,然後在 Fragment 再出現 (例如使用者按了 Back 制) 時重新建立。這時,Android 是會使用 new Fragment() 的方法建立新的 Fragment 重新顯示。 ### 產生問題 如果 Fragment 是沒有參數的話,是沒有任何問題的,像上面的 MyFragment 一樣。我們改寫一下 MyFragment 的內容,讓它可以接收參數 : ```java /** * my fragment */ public class MyFragment extends Fragment { private int id; public static MyFragment newInstance() { return new MyFragment(); } public MyFragment() { super(); } public MyFragment(int id) { super(); this.id = id; } } ``` 有了新的 Constructor 後,我們可以在 `new MyFragment()` 時傳入 id 參數了 ! ```java // create fragment with arguments Fragment f1 = new MyFragment(1); ``` 這樣很完美 ! 但是我們忘記了 Android 可是會因為節省記憶體而把 Fragment 斬掉,然後需要再顯示時以 new Fragment() 重新建立,那我們傳入的 id 不就會一同被斬掉了嗎 ? 答案是對的,id 會隨著 Fragment 斬掉而一同遠去 ... ### 解決方法 如果有看過另一篇文章講解過 [Android 為 Fragment 傳入參數](https://cdn.19site.net/posts/118)的朋友,會知道我們可以使用 Bundle 來為 Fragment 傳入參數 ! Bundle 不會因為 Fragment 的斬掉而清走,當 Android 重新建立 Fragment 時,先前的 Bundle 會重新連接上,故可以使用 `getArguments()` 取得 Bundle 物件進行設定動作。而 newInstance 的作用就是為了把傳入到 Fragment 的參數規範起來。 我們再把上面的 `MyFragment.java` 改寫一下 : ```java /** * my fragment */ public class MyFragment extends Fragment { private int id; public static MyFragment newInstance(int id) { Bundle bundle = new Bundle(); bundle.putInt("id", id); MyFragment fragment = new MyFragment() fragment.setArguments(bundle); return fragment ; } } ``` 使用這樣的方法來傳入參數,就可以確保 Fragment 重新建立時不會有缺失了 ! ```java // create fragment with arguments Fragment f1 = MyFragment.newInstance(1); ``` 官方說明文件 https://developer.android.com/reference/android/app/Fragment.html#Fragment()
在使用 Fragment Transaction 時,我們可以為 Fragment 傳入一些參數,來改變 Fragment 的起始設定。 ### 使用 Bundle 在 Android 中,Bundle 就像一個 JSONObject 物件,可以使用 Key-Value 的方式來放入 / 取出一些常用的資料型別。我們可以通過 Bundle 來把資料傳入到 Fragment 內。 ### 例子 以下代碼會建立一個 Bundle Object,然後設定 id 及 name 值,再把 Bundle Object 傳入到 MyFragment 內 : ```java // create bundle object Bundle bundle = new Bundle(); bundle.putInt("id", 1); bundle.putString("name", "19Site"); // create fragment object Fragment fragment = new MyFragment(); // set bundle as fragment arguments fragment.setArguments(bundle); // prepare fragment transaction FragmentTransaction mFragmentTransaction = getSupportFragmentManager().beginTransaction(); mFragmentTransaction.addToBackStack(null); mFragmentTransaction.replace(R.id.frame_layout, fragment); mFragmentTransaction.commit(); ``` 在檔案 `MyFragment.java` 內,我們可以使用 `getArguments()` 來取得傳入的 Bundle Object : ```java // get arguments Bundle bundle = getArguments(); // get values from bundle int id = bundle.getInt("id", 0); String name = bundle.getString("name", null); ```
### FileProvider 先前有一篇文章提及到為什麼會有 FileProvider 的出現,內容可以參考那篇文章。 [Android Intent 透過 FileProvider 分享檔案的使用權限](https://cdn.19site.net/posts/113) 主要也是為了加強對檔案的安全性管理及加強對內容檔案的操作強化。 ### 如何設定 FileProvider 我們要先在檔案 `AndroidManifest.xml` 一段對 Provider 的定義 : ```xml <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:enabled="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> ``` 然後在 `res/xml/file_paths.xml` 加入以下內容,沒有檔案不存在就建立一個新檔案 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-files-path name="external_files" path="." /> </paths> ``` 依照官方的說明文件,這這檔案可以設定更多的內容,使這個 APK 能支援更多的檔案輸出路徑。 https://developer.android.com/reference/androidx/core/content/FileProvider 以下是 XML 設定對應的 Java 方法表格 : |XML 設定|Java 方法| |---| |files-path|Context.getFilesDir()| |cache-path|Context.getCacheDir()| |external-path|Environment.getExternalStorageDirectory()| |external-files-path|Context#getExternalFilesDir(String), Context.getExternalFilesDir(null)| |external-cache-path|Context.getExternalCacheDir()| |external-media-path|Context.getExternalMediaDirs()| 只要設定好對的內容,就可以讓 Intent 的目標讀取得到 URI 的內容。 ### 實例 以下是把一個儲存在 APP 自己 DATA 內的檔案想透過 FileProvider 讓外部可以讀取。 檔案 `AndroidManifest.xml` 有以下設定 : ```xml <provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:enabled="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> ``` 檔案 `res/xml/file_paths.xml` 有以下設定 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <files-path name="my_videos" path="videos/" /> <files-path name="my_images" path="images/" /> </paths> ``` 然後在 Java 程式內使用 FileProvider 取得檔案的 Uri : ```java // video file path File mFilePath = new File(context.getFilesDir(), "videos"); // file from application directory File mFile = new File(mFilePath, "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/my_videos/myvideo.mp4 ``` 我們會發現 FileProvider 還可以改寫來自不同 Directory 的名稱,甚至可以用來限制只輸出某一個 sub directory 的檔案,達到進一步的安全效果。 再看看下一個例子 : ```java // video file path File mFilePath = new File(context.getFilesDir(), "images"); // file from application directory File mFile = new File(mFilePath, "myimage.jpg"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/my_images/myimage.jpg ``` 從輸出的效果我們可以推算出 `res/xml/file_paths.xml` 設定對 FileProvider 輸出的影響。 ### 秘技 功能愈多設定也會愈複雜,要一個個路徑設定到 `file_paths.xml` 實在是費時又易出錯,所以在普通的情況下,大家可以使用以下的設定來減輕功夫。 ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external" path="." /> <external-files-path name="external_files" path="." /> <cache-path name="cache" path="." /> <external-cache-path name="external_cache" path="." /> <files-path name="files" path="." /> </paths> ``` 要注意,以上的設定可以把你 App 內的所有檔案透過 FileProvider 分享出去。不過也不必太擔心,因為只能透過 FileProvider 主動分享出去,對算對方 App 知道 content uri 也好,也是無法讀取到檔案的。
### Android FileProvider Android 使用 FileProvided 的時候,可能會遇到這個問題。 ```text FATAL EXCEPTION: main Process: com.example.your.andrroidapp java.lang.IllegalArgumentException: Failed to find configured root that contains /path/to/your.file ... error stacks ``` ### 問題源因 遇到這個問題的原因,是因為沒有設定好 FileProvider 要存取的路徑。 假設在 `res/xml/file_paths.xml` 有以下內容 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <external-files-path name="external_files" path="." /> </paths> ``` 然後用以下的代碼運行 : ```java // file from application directory File mFile = new File(context.getFilesDir(), "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); ``` 如果我們使用上面的代碼來取得檔案 URI 的話,就會出現錯誤。因為 `context.getFilesDir()` 對應的 XML 是 `files-path`,但是在 `res/xml/file_paths.xml` 內沒有相關的設定。 有關 file_paths 的設定可以參考這篇文章 : [Android FileProvider (API 24+)](https://cdn.19site.net/posts/117) ### 解決方法 修改一下上面的 XML 設定 : ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <files-path name="files" path="." /> <external-files-path name="external_files" path="." /> </paths> ``` 再運行一次這段代碼 : ```java // file from application directory File mFile = new File(context.getFilesDir(), "myvideo.mp4"); // get uri via file provider Uri mUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", mFile); mUri.toString(); >>> content://com.example.fileprovider/files/myvideo.mp4 ``` 這樣就應該能成功運作了。
RecyclerView 是 Android 一個不可以缺少的 Component,雖然初學時用起上來非常之複雜,但是學會了後是一個絕對強勁的東東。 ### 設定 Adapter 在使用 RecyclerView 時也是需要設定 Adapter 才能夠使用,像從前的 ListView 一樣 : ```java // check adapter has initialized if( adapter == null ) { // new adapter adapter = new YourAdapter(); // set adapter to recycler view mRecyclerView.setAdapter(adapter); } ``` ### 問題產生 上面的 Code 應該是日常用來設定 RecyclerView Adapter 時用到。但是原來以上的 Code 會出現一個問題 !! 就是當 RecyclerView 重新建立過後,但上一個 adapter 沒有變成 null 時,新的 RecyclerView 就永遠不會設定 adapter 了 !! 這個情況會當使用者離開了這個 Fragment 後,在下一個 Fragment 使用 Back press 退行時發生,因為在後排的 View 已經 destroy 了,所以在載入時會運行一次 `onCreateView()` 重新畫出 UI,但是先前的 Adapter 卻沒有清空變成 null 的,所以 if 入面的條件是永遠不能進入。 ### 解決方法 我們應該雖要檢查 RecyclerView 的 Adapter 是否有設定。 ``` // check adapter has initialized if( adapter == null ) { // new adapter adapter = new YourAdapter(); } // check recycler view has set adapter if( mRecyclerView.getAdapter() == null ) { // set adapter to recycler view mRecyclerView.setAdapter(adapter); } ``` 簡單加入檢查,就可以確保萬無一失了 !
在 Java 上,我們會很常使用到 `instanceof` 來檢查某一個 Object 是否由特定的 Class 生成出來。除此之後我們還可能會比對一下兩個 Object 是否由同一個 Class 生成出來。可是應該要怎樣做呢? ### 比對兩個 Object 是否由同一個 Class 生成 我們可以通過使用 `.getClass()` 方法從 Object 中取得 Class 值。 ```java String a = "foo"; a.getClass(); ``` 既然能夠取得 Class 值,我們只要對比一下 Class 是否相同就可以判定是否由同一個 Class 生成。 ```java String a = "foo"; String b = "bar"; boolean result = a.getClass().equals(b.getClass()); ```
 自從 Android 上左 API 24 後,所有使用 Intent 進出的檔案路徑都要使用 FileProvider 的封裝才可以讓第三方的 APK 讀取得到,政策實行已久但是常常會也記不起語法,所以記錄一下。 ### 以前的方法 API 24- 下面是一個例子示範,以先前 ( API 24 之前 ) 的方法去把影片傳給第三方 APK 來播放 : ```java Uri.fromFile(new File("path/to/your/file")); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, getContentResolver().getType(uri)); startActivity(intent); ``` ### 現在的方法 API 24+ 現在需要使用 FileProvider 來封裝 URI,讓第三方的 APK 不知道真實檔案的位置 : ```js Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", new File("path/to/your/file")); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, getContentResolver().getType(uri)); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); ``` 有一點要記住記住記住 (重要的事要說三次),必定要加以下這句才可以成功運行的,不然目標的 Intent Receiver 會讀不到檔案。 ```java intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); ``` 有關 FileProvider 的設定,我們在第二篇文章講解。
### Mime-Types Mime-Types 也就是 Content-Types,是指網際網路媒體類型。說白一點就是傳送過來的資料是什麼類型,好讓 Browser 去正確解讀檔案。以下會列出幾個最常見的 mime-type。 |副檔名|對應 Mime-Types| |---| |html|text/html| |txt|text/plain| |js|application/javascript |jpg|image/jpeg| |png|image/png| |mp4|video/mp4| ### 實際應用 處理 HTTP 上載檔案時,難免要處理一些檔案的 mime-types 事情。現實中是很難記得每一種檔案的 mime-types,由其是 AWS S3 API 在 `upload` 時是必需要指定上載檔案的 mime-types,所以我們必需要找個好幫手去代勞 !! NPM 上已經有大神寫好了程式庫,只要學會用就可以輕鬆取得檔案對應的 mime-types 了。 ```sh $ npm i mime-types ``` 程式試調 : ```js // import mime-types var mime = require('mime-types'); // loopup mime.lookup('json'); // application/json mime.lookup('.md'); // text/markdown mime.lookup('file.html'); // text/html mime.lookup('folder/file.js'); // application/javascript mime.lookup('folder/.htaccess'); // false mime.lookup('cats'); // false ``` 還有一個強勁功能,就是能把 mime-types 取得對應的 file extension。 ```js mime.extension('image/jpeg'); // jpeg mime.extension('text/html'); // html ``` 這樣就可以輕鬆處理好 mime-types 的轉換工作。
### 清空 ImageView 為什麼要清空 ImageView 呢? 在 Inflate XML 時 ImageView 不是已經空白的嗎? 對的,在最初 Inflate XML 時 ImageView 的確空白的,但是如果在程式中動態載入外部的資料更新到 ImageView 顯示,然後又要使用相同的 ImageView 顯示第二筆的資料,那麼就需要先把 ImageView 先清空,才能安心把第二筆資料的圖片顯示到 ImageView 內。 最好的例子是 RecyclerView ,因為 RecyclerView 會把用過的 ViewHolder 重新使用,就會有機會殘留著上一筆資料的 "殘留物"。必需要清空才能確保資料正確。 ### 實作 其實也不是一個很複雜的事情,只需要利用到 Android 原生的 color 設定就可以。 ```java // get image view from view holder viewHolder.mImageView // set image resources to transparent color .setImageResources(android.R.color.transparent); ``` 另外,坊間有些人會提及第二個方法,就是把 `android.R.color.transparent` 取代為 0 值。但是有一部份人嘗試過是不成功的,大家最好還是使用上面的方法較好。
在使用 EditText 時,預設是可以使用 Enter 鍵來換行的。換行的高度會反映在 UI 上。如果想限制 EditText 只可以得一行的話,我們需要在 XML 內加一點設定。 ### 設定 EditText 我們需要設定以下兩參數 : - 設定最高行高 `android:maxLines="1"` - 設定輸入的種類 `android:inputType="text"` ```xml <EditText android:id="@+id/et" android:layout_width="match_parent" android:layout_height="wrap_content" android:maxLines="1" android:inputType="text" /> ``` 這樣就可以把 EditText 鎖定為 1 行高了。
### Picasso Picasso 是一個用來載入圖片的程式庫,可以用來處理從 HTTP 的檔案,能夠輕鬆處理 RecyclerView 的 Async 圖片載入行為。 > Picasso 網址 : https://square.github.io/picasso ### 問題 在使用 Picasso 載入圖片時,可能會遇到以下這個問題 : ```java Picasso.get() .load("https://example.com/image.jpg") .centerCrop() .into(mImageView); ``` 在 Runtime 時會出以下的 Exception : ```text Center inside requires calling resize with positive width and height ``` 原因是因為 Picasso 要把圖片載入到 Image View 時,要先把 Bitmap resize 為一個正數的數值,才能夠使用 `center inside` 或 `center crop`。但是我們應該要 resize 做什麼數值才好呢? ### 解決方法 通過使用 `.resize()` 方法,可以把 Bitmap resize 為一個正數值的大小。 ```java Picasso.get() .load("https://example.com/image.jpg") .resize(800, 800) .centerCrop() .into(mImageView); ``` 以上的代碼是能夠運行,但是好像欠缺彈性。我們可以使用 `.fit()` 方法來讓 Picasso 幫我們自動 resize 為 Image View 的大小,這讓我們就不用 hard code 數值到 `.resize()` 了。 ```java Picasso.get() .load("https://example.com/image.jpg") .fit() .centerCrop() .into(mImageView); ```
在疫情下學校好多時都會使用 ZOOM 進行直播教學,教學完結時可以把影片留在 ZOOM 上翻看,但是如果想要下載影片到本的話,就好像沒有辧法。 ### 使用 Chrome Developer Tool 把 Video 連結抽出 我們可以通過使用 Chrome Developer Tool 把 Video 的路徑抽出來,然後下載影片。因為 ZOOM 有使用 cookies 保護 URL 的來源,如果把 Video 的路徑直接貼到 Browser 上的話,會得到 HTTP 403。 ```js (function(window) { var video = document.getElementsByTagName('video')[0]; var src = video.src; var a = document.createElement('a'); a.setAttribute('href', src); a.innerText = 'right click and save target'; a.style.position = 'absolute'; a.style.border = 'solid 1px #AAAAAA'; a.style.top = '20%'; a.style.left = '20%'; a.style.right = '20%'; a.style.textAlign = 'center'; a.style.display = 'block'; a.style.lineHeight = '200px'; a.style.fontSize = '30px'; a.style.backgroundColor = '#FFFFFF'; document.body.append(a); })(window); ``` 我們可以把上面的代碼貼到 Chrome Developer Tool Console 上,然後按 ENTER。  畫面上就會出現 'right click and save target' 的方塊,我可以使用滑鼠 Right Click 然後按另存連結,就可以把影片儲存到本機了。 
由 19Site 開發的項目 FBucket 上線啦 ! 這個項目主要是發開一個檔案的網站服務器,並會提供 Rest API 及 NodeJS API 以提供檔案的上傳及公開。 GitHub: https://github.com/19Site/FBucket NPM: https://www.npmjs.com/package/fbucket
在處理伺服器的工作時,為了能自動化完成一些常常會進行的工作,最常用到就是 Linux 的 Shell Script。 ### Linux Shell Linux Shell 就好像的 Window CMD (用 DOS 更為貼切),不同的是 Window CMD 比較像一個軟體,運行在 Window 之上,而 Shell 本身就是的外殼 (所以叫 Shell),它的底下就是 OS 本身了 ! Shell 有好多種,最常見到可能會是 `sh`、`bash`。`bash` 是 `sh` 的子集,並基於 `sh` 上加入了好多功能。 ### 使用 Shell Script 讀取使用者 Input String 我們可以通過使用 `read` 來讀取使用者在 shell 鍵入的字串。我們使用以下的指令建立 `test_command.sh`。 ```sh $ vi test_command.sh ``` 然後輸入以下內容 : ```sh #! /bin/sh printf "Enter you name: " read VARIABLE_NANE printf "\n\nYour name: ${VARIABLE_NANE}" ``` 完成後儲存,並試試運行 : ```sh $ chmod 755 ./test_command.sh $ ./test_command.sh Enter your name: 19Site Your name: 19Site ``` ### 收集 Password 有時我們可能要輸入 Password 來進行動作,但是不又想在畫面上顯示出 Password 的內容,要如何做呢? 我們可以透過修改 Shell 的設定,來把使用者鍵入的字符設為不顯示。我休修改一下上面的 Script。 ```sh #! /bin/sh stty -echo printf "Enter password: " read VARIABLE_NANE stty echo printf "\n\nYour password: ${VARIABLE_NANE}" ``` 我們可以透過使用 `stty` 指令來變更 Shell 的 `echo` 設定,從而達成把輸入隱藏目的。 ```sh $ chmod 755 ./test_command.sh $ ./test_command.sh Enter your password: Your password: 19Site ```
### 網站上的自動填入 一般的情況下,瀏覽器會記住用戶通過網站上的 `<input>` 所提交的信息。 這使瀏覽器能夠提供自動完成功能 (建議用戶已開始鍵入文字時的可能字眼) 或自動填充 (加載時預填充某些字段)。 這些功能通常預設情況下處於啟用狀態,但對於用戶來說可能是隱私問題,因此瀏覽器可以讓用戶禁用它們。 但是,以表格形式提交的某些數據將來可能不再有用 (例如一次性密碼),或者包含敏感信息 (例如信用卡安全碼)。作為網站的作者,即使啟用了瀏覽器的自動完成功能,您也可能希望瀏覽器不記住這些字段的值。 ### 實行方法 通過加入 `autocomplete='off'`,可以把瀏覽器的自動完成功能不套用到該 Element。 ```html <!-- disable form auto complete --> <form autocomplete='off'> [...] </form> <!-- disable input:text auto complete --> <input type='text' autocomplete='off' /> ```
### Regular Expression 我們常常會使用到 Regular Expression 來驗證字串的格式,由日期、電話號碼到特定的格式都可以用 Regular Expression 表達出來。而在 Javascript 中使用者可以使用 `new RegExp()` 來建立 Regular Expression 物件。 ```js // create regexp object var re1 = /^\d{4}-\d{2}-\d{2}$/i; // create regexp object var re2 = new RegExp('^\d{4}-\d{2}-\d{2}$', 'i'); console.log(re1.test('2019-11-22')); // true console.log(re2.test('2019-11-22')); // true ``` 它們的運作是一樣功能的。由上面的代碼推算出,我們可以透過使用 `new RegExp()` 來動態建立一個 Regular Expression Object。 ```js // create regex by providing pattern string const createRegExp = string => new RegExp('/^' + string + '$/'); ``` ### 產生問題 上面代碼雖然很方便,但是如果有使用者輸入了和 Regular Expression 相同的保留字,就會有可能把你的程式弄壞了 !! ```js // create regex var re1 = createRegExp('*^*^*$*'); ``` 這樣就會把程式弄壞了。要防止這個情況,我們需要為傳入的參數 Escape 走 Regular Expression 的保留字。例如以下字符 : ```txt \ ^ $ * + ? . ( ) | { } [ ] ``` 我們可以寫一個 Function 把上面的字符 Escape,然後回傳出去。 ```js /** * escape regexp */ const escapeRegExp = string => { // $& means the whole matched string return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // example escapeRegExp("All of these should be escaped: \ ^ $ * + ? . ( ) | { } [ ]"); // result >>> "All of these should be escaped: \\ \^ \$ \* \+ \? \. \( \) \| \{ \} \[ \]" ``` 經過 Escape 後的字串就可以放心放到 `new RegExp()` 中使用,以產生 Regular Expression Object。 > 參考網址 : https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex ### NPM Package 另外已經有大神把 Function 打包,放到 NPM 上去了,只要使用 NPM 下載就可以直接使用。 ```sh # npm install escape-string-regexp ```
### TL;DR 使用以下的 CSS 來把 Focus 時的邊框取消。 ```css input:focus, select:focus, textarea:focus, button:focus { outline: none; } ``` ### Focus Outline Focus Outline 大概是 Chrome 最先有的東西 (筆者自己經驗),就是當使用者的 Cursor 放到 Text Input Field 上,該 DOM Element 的邊框就會有一個藍色的 Highlight 出現,像亮了燈一樣。  當 Search Box 被 Focus 時,就會有一個藍色的邊框包著,像亮了燈一樣。 ### 把 Focus 的 Border 消除 我們可以透過使用以的 CSS 來把 Focus 的 Border 消除。 ```css input:focus, select:focus, textarea:focus, button:focus { outline: none; } ```
使用 Laravel 時,我們都會用到 `url()` 產生超連結 (hyper link),使用 `url()`所產生的 URL 會是完整的 URL,即包括有 Protocol、Domain、Port、Path 及 Query String 所有資料。 ### 完整的 URL 以下是一個完整的 URL ```text https://19site.com:80/posts?page=2 ``` - https - Protocol - 19site.com - Domain - :80 - Port number - /posts - Path - ?page=2 - Query String 使用 Laravel 的 `url('/posts')` 產生的 URL 會是以下的樣子。 ```php // laravel url() helper function echo url('/posts'); // https://19site.com/posts ``` ### 相對的 URL 如果我們要用 `url()` 來產生相對 (relative) 的 URL,最直接是不使用 `url()` 就可以解決了 ! 但如果我們硬是要使用 `url()` 的方式來產生 URL 的話,就需要自己再外建 helper function 了。 ```php /** * create relative url */ function relativeUrl($value='') { return str_replace(url(''), '', url($value)); } // use new helper function echo relativeUrl('/posts'); // /posts ```
在使用 DB 的時候,有時需要先從 DB 內抽出記錄,然後經過一些運算後,才會寫入到 DB 。但是如果在運算中的時間,有其他的 Thread 來 DB 修改記錄的話,就可能會有互相覆寫的情況出現。 ### 實例 想像以下的情況 : Table : fruits |id|name|quantity| |---|---|---| |1|apple|10| |2|orange|5| 使用以下的代碼進行運算,目的是把水果的數量減去一定數量,減去後水果數量如果少於 0 ,就要拒絕。代碼如下 : ```js // generate a random number var random = Math.round(Math.random() * 8); // fetch data from db var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`); // get first record data var {id, quantity} = rows[0]; // check quantity is enough if( quantity >= random ) { // new quantity var newQuantity = quantity - random; // update db record await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]); } else { // not enough quantity throw new Error('not enough quantity'); } ``` 以上的代碼在單一的 Thread 上運行是沒有問題的,但是如果是在 Multi Thread 的情況下,就可能會有機會出現覆寫舊值的情況。 ### 覆寫舊值 當 Thread 1 和 Thread 2 同時啟動這段代碼時,就會有機同時在 DB 內抽取到相同的值 (因為互相還未寫入新值到 DB)。 然後分別寫入各自減去 `Random` 的數值入 DB 內,這樣 DB 內的數量就會不正確。 ### 解決方法 要解決就需要進行阻塞 (Blocking),要後執行的 Thread 等待先執行的執行完成,才會開始進入執行階段,像是 Java 內的 Synchronized Method 一樣。 在 MySQL 內我們可以通過使用 `lock table` 方法來達成。 以下這句可以防止其他 Connection 寫入表格 fruits : ```sql lock tables fruits read; ``` 以下這句可以防止其他 Connection 讀取及寫入表格 fruits : ```sql lock tables fruits write; ``` 記住 `lock table` 後一定要 `unlock table` 才行,不然一直 lock 住就會變成為 Dead Lock 了。 ```sql unlock tables; ``` ### 改寫代碼 我們把上面的代碼改寫一下 : ```js // generate a random number var random = Math.round(Math.random() * 8); // lock tables await db.query(`lock tables fruits write`); // fetch data from db var rows = await db.query(`select id, name, quantity from fruits where name = 'apple'`); // get first record data var {id, quantity} = rows[0]; // check quantity is enough if( quantity >= random ) { // new quantity var newQuantity = quantity - random; // update db record await db.query(`update fruits set quantity = ? where id = ?`, [newQuantity, id]); // unlock tables await db.query(`unlock tables`); } else { // unlock tables await db.query(`unlock tables`); // not enough quantity throw new Error('not enough quantity'); } ``` 只需要使用 Lock Table 功能把需要讀出運算及寫入的邏輯包裝起來,就可以防止其他的 DB Connection 在中間讀取未更新的參數。 *** 但是 Lock Table 在 Transaction 中會引發無法 Rollback 的情況,需要使用第二種方法來處理,在第二篇文章再介紹。