2024年3月11日 星期一

【Flutter】Looking up a deactivated widget's ancestor is unsafe

 簡單來說,就是要使用Router切換Widget時無法切換。

真正的原因是Router當真正要執行時呼叫程式設計給它的BuildContext參數,卻發現這個參數無法使用。


更具體來說,在很多地方(函數)會收到BuildContext傳入。

設計上會習慣在這些地方(函數)區段內將這些BuildContext傳給需要使用的功能,但偶爾會發生這些BuildContext不能使用的情況。

例如在AlertDialog裡取得的BuildContext就無法用在Router上。(因為Router會用這個BuildContext做定位、來知道Widget與Widget之間的關係,與它們在WidgetTree上的位置。而一個已經關閉的Dialog並不在WidgetTree上。)


解決這個錯誤的方法基本上是使用「GlobalKey」

簡單來說就是在Widget的建構子/Build階段傳入一個GlobalKey物件的實體到「key」這個欄位中。確認傳入後,就可以在這個Widget下使用GlobalKey的「currentContext」這個參數去取得可用的BuildContext(A),然後在宣告Router並需要傳入BuildContext的地方改傳入BuildContext(A)。

2021年8月9日 星期一

不定時開發日記:為什麼我找不到問題的解答?

網路上程式設計教學或解疑文章成千上萬,但為什麼很少有可以解決自己問題的?

假設這裡有篇解釋「如何使用某某元件」的文章,簡單看一下會發現作者對元件的認知很正確、很充分,行文也很流暢又詳盡、不會「貼個程式碼就了事」,——這樣的文章難道也沒用嗎?

「對!這種文章往往更危險!」

為什麼?怎麼可能?

一般性的原因就不重新贅述,這裡主要是要提出一個可能沒人提過的主張:因為這些文章作者使用的模組/框架有問題。


怎麼會提到模組/框架呢?


以Android來說,Activity基本的生命週期就是種模組/框架,要示範或說明如何使用元件(或任何技術)都必須要以Activity為範例的基礎。

所以模組/框架是存在的。

假定「只要順著基本的生命週期去使用元件必然不會出錯」這是個很嚴重的錯誤假設,因為想要學習怎麼使用元件(或技術)的人很多其實都有個很複雜的商業需求擺在那邊等著他們學習後去滿足,而他們的商業需求讓他們經常要忽略生命週期這類的框架。

聽起來好像沒什麼,就好像「在主線程外更新畫面就使用Handler就好了」,偏偏Android領域中有個「HandlerLeaking」的議題,而且發送Handler還有「Looper權限」.......

以上所說還在於「如果沒正確操作,Error訊息會留下足夠的指引讓人去尋找失敗的原因與解答,」但有些東西(例如MediaPlayer的IllegalState)是根本沒有解答的!(如果有IllegalState,那LegalState的標準何在......找不到啊!找不到啊!真的找不到啊!)


這時候就會發生美妙的悲劇了!

「我按照別人的教學與範本操作,最後專案碰到了一個怎樣都解不開的問題。要整個打掉重做?來不及了!但尋找解答又永遠找不到。」

2021年4月5日 星期一

【Android】onConfigurationChanged的觸發(和Configuration)

有些情況下-主要是指「配置」發生改變-,Activity會被重啟...

這些重啟發生時,經常伴隨「onConfigurationChanged的觸發」。(但官方文件說法是「不會觸發」。)

但如果只希望「onConfigurationChanged的觸發」發生、但不要有Activity重啟時,可以在AndroidManifest中,找到會被重啟的Activity下增加一個屬性「android:configChanges」,並在後面增加對應的參數來決定哪些情況要觸發、哪些情況就任由Activity重啟吧!


Configuration在官方的定義是指「決定系統要取用哪些Resource檔的方式」,像「螢幕是橫式或直式時會取用不同的Layout檔(如果有設定)」。

所以偵測螢幕橫式直式最正確的方式(除了等待onConfigurationChanged觸發)是從Activity下呼叫「getResource().getConfiguration()」去取得Configuration,然後判斷orientation的參數。


這個屬性後面可以接收的參數有:(官方網頁將它列在「activity」的Manifest說明中。)

android:configChanges=["mcc", "mnc", "locale",
                                 
"touchscreen", "keyboard", "keyboardHidden",
                                 
"navigation", "screenLayout", "fontScale",
                                 
"uiMode", "orientation", "density",
                                 
"screenSize", "smallestScreenSize"]

(官網這裡的說明有點奇妙,XML檔無法用這種方式輸入tag的參數,必須要用類似「"mcc|mnc"」這樣的格式才可以。其他欄位的說明有遵照這個模式,唯獨「configChanges」卻是錯誤的。)

比較奇妙的是:如果要針對螢幕轉向做偵測,需要使用多個參數會比較保險。

一般來說,是使用「orientation」和「screenSize」,(甚至再加上「uiMode」。)



2020年5月3日 星期日

隱私權政策

非常感謝各位使用本APP。
本APP不會收集各位的使用行為,例如閱覽習慣或GPS定位。
除了使用Google帳號進行身分認證以外,不會使用到任何使用者的個人資料。

謝謝。

2020年3月8日 星期日

【不要仰賴Activity的Android開發】什麼事情都用CustomView解決





一般來說,製作一個這樣的介面會使用一組Activity和XML。



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity"
tools:showIn="@layout/activity_main"> 

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" /> 

 <EditText
android:id="@+id/identify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="ID" />
<EditText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="Password" />
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"/>
</LinearLayout>



public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { 
 super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); 

 final EditText id = findViewById(R.id.identify);
final EditText pw = findViewById(R.id.password); findViewById(R.id.login).setOnClickListener(new View.OnClickListener() { @Override
public void onClick(View view) { 

 Log.d("Hsu", "ID:" + id.getEditableText().toString());
 Log.d("Hsu", "PW:" + pw.getEditableText().toString()); 


 }
); 
 } 
}
(因為不需要,所以OnClickListener的內容只有意思性地Log。)


接下來的做法或許會比較麻煩,但概念上來說會提供程式比較多的擴充升級彈性。

因為它完全不受限於Activity,功能和介面可以隨時彈性重組,方便調整或擴充。

首先,整個Activity會變成只剩下這樣.......


public class MainActivity extends AppCompatActivity { 
 @Override 
 protected void onCreate(Bundle savedInstanceState) {             super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main); 

 } 
}

但XML會做出比較大幅度的改變...

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"> 

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" /> 

<com.myapplication.InputText
android:id="@+id/identify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="ID" /> 

<com.myapplication.InputText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minEms="10"
android:maxLines="1"
android:hint="Password" /> 

<com.myapplication.LoginButton
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="OK"/> 

</LinearLayout>


可以看到EditText和Button從Android標準元件變成APP專案客制的元件。

「com.myapplication」是專案的設定,有興趣的人可以自己彈性的修改命名,只要是「InputText」和「LoginButton」的路徑位置就好。(這兩個View物件的命名也可以自己彈性調整。)

InputText和LoginButton的內容則在下面。

public class InputText extends EditText { 
 public static InputText ID; 
 public static InputText PW; 

 public InputText(Context context, AttributeSet attrs) { 
 super(context, attrs); 
 String hint = getHint().toString();
 switch (hint){ 

 case "ID": ID = this;
 break;
 case "Password": 

 PW = this;
 break;
 } 

 } 
}



public class LoginButton extends Button implements View.OnClickListener { 
 public LoginButton(Context context, AttributeSet attrs) { 
 super(context, attrs);
 setOnClickListener(this);
 } 


 @Override
 public void onClick(View view) { 

 Log.d("Hsu", "ID:" + InputText.ID.getEditableText().toString());
 Log.d("Hsu", "PW:" + InputText.PW.getEditableText().toString()); 

 } 
}


因為這個介面還是算簡單的,所以這樣做的好處可能無法立刻顯現,要引入一些情境做說明。

這樣做的目的在於快速方便的大量產生擁有登入功能的介面。
登入這件事情越來越複雜,會產生各種登入模式,例如多帳號登入、例如彈出視窗登入、例如重新檢查使用者帳號密碼.....
所以快速的設計跟新增登入功能或許有其必要。

InputText可以確保針對登入的輸入功能可以快速重複利用,譬如「將輸入的內容加密」「將使用者帳號自動帶入ID欄」。
而LoginButton可以延生出各種版本,例如CheckPasswordButton可以用來讓使用者再次輸入密碼做保險性登入,等物件設計好後只要修改XML,將LoginButton換成CheckPasswordButton,一樣不需要修改Activity的任何內容,就可以完成新介面新功能。

用這種方式寫程式,為的是可以同時滿足快速組合出功能並達到模組化的目的。


有些人可以從附上的程式碼中快速找到一些功能上的限制,但其實這些限制都可以藉由簡單幾行程式碼擴充來破解,但以後再寫了。

2019年8月1日 星期四

【Google】如何將SheetSpread表格內的資料轉換成JSON

其實Google自己有自己的工具,但轉換出來的資料複雜的不合理。




所以找了一下,發現有神人製作了一個工具「http://gsx2json.com/」...

可以將第一列視為「Tag」,然後將每一欄作為一筆資料,用該欄第一列的資料作為Tag進行資料分類索引。




利用工具後...「http://gsx2json.com/api?id=1bRM_dIcl67MgLinJfc_YBqifZ65vusun5XJLWHQZ_oM&sheet=1&columns=false

轉換出的資料變成「{"rows":[{"標題":"測試一","內容":"測試內容1","參數":100},{"標題":"測試二","內容":"測試內容2","參數":1002},{"標題":"測試三","內容":"測試內容3","參數":1002}]}」

這樣的規格明顯合理許多。

也有別的參數可以調整...「http://gsx2json.com/api?id=1bRM_dIcl67MgLinJfc_YBqifZ65vusun5XJLWHQZ_oM&sheet=1

{"columns":{"標題":["測試一","測試二","測試三"],"內容":["測試內容1","測試內容2","測試內容3"],"參數":[100,1002,1002]},"rows":[{"標題":"測試一","內容":"測試內容1","參數":100},{"標題":"測試二","內容":"測試內容2","參數":1002},{"標題":"測試三","內容":"測試內容3","參數":1002}]}」

2019年7月12日 星期五

【Android Html5】播放Youtube影片

目標是建立一個會佔滿整個WebView的YouTubePlayer。

必須注意到:基本上,開發時所有會遇到的問題都源於「網頁版Youtube撥放器在移動網頁上不能正常運作」。──所以Google才會另外開發一套移動式套件,但因為這套件非常不好用,所以終究還是要回來解決不能正常運作的問題。


基本建立方式在第二個回應。(測試過,可以使用。)

這個技巧播放檔案的秘訣在於監聽Player的State,所以當它Ready就開始播放,播放完可以開始再次重播、或撥下一檔。

如果要改撥放PlayList、而不是單一影片,可以使用這個方法修改範例。

範例內是使用在YT.Player內直接設定VideoId的方式,但其實有用播放指令指定ID的方法,同時也可以用清單播放。方法可以看標準官方範例。

官方範例內有player.nextVideo(),可以用來自動播放列表下一檔。


影片如果要不暫停、就退出或前進到下一層功能,這個地方有講解需要使用什麼樣的修改。簡而言之就是WebView也有自己的onPause和onResume,必須要在Fragment和Activity中相對應的地方呼叫。(同時需要使用Timer的啟動與暫停。)

另外,影片如果還沒開始撥放就被退到背景,YouTube Player這時可能會在背景被啟動。所以在執行「是否要開始撥放」前,同時要檢查WebView是否還在前景上。