對Android的應用開發,如果熟悉Java,那么Android studio或Eclipse將是不錯的選擇,而對熟悉.net平臺開發人員,在強大的Visual Studio幫助下,開發Android應用不再是難題,本文基于Visual Studio 2017及以上的版本討論,如果低于2017的版本,因為xamarin并未集成,需要單獨安裝,所以在搭建開發環境上會有些麻煩,
本文假設你有一定的開發經驗,對Android的有基礎的了解,假如你還不熟悉,建議先從MSDN上的Hello, Android開始,將是不錯的入門,
- 開發環境搭建
- 第一個應用程式 - 企業級應用程式場景
- 要點決議
1. 開發環境搭建
在Windows 10,僅需要做下面兩個就足夠了:
a. 在Visual studio上開發,需要Mobile development with .NET組件,詳細的程序可參考Installing Xamarin in Visual Studio,也可以通過Visual studio installer,修改已有的安裝,在最小安裝的情況下,對components的選擇,需要注意開發不同Android版本的應用,其API Level也不一樣,Android API levels詳見MSDN,如果需要原生支持,那么NDK也需要一并安裝,

b. Android Emulator:在模擬器的選擇上,這里推薦Genymotion,對個人是免費的,資源占用下,啟動迅速,對除錯、可操作性都非常便利,雖然在visual studio的Mobile development with .NET默認安裝情況下,會有一個hardware accelerated emulator,但,這里非常不推薦,MSDN上Android Emulator Setup這篇文章提到的模擬器,在硬體不是特別強大的情況下,都不建議去嘗試,
Notes:如果硬體不夠強大,vs自帶的hardware accelerated emulator啟動會非常慢,每次編譯除錯會很費時,在T480筆記本上(i5-7300U+16G+SSD),默認的模擬器j僅成功了幾次,后來修改了程式,旋轉了一次模擬器,再啟動就卡在應用加載上,模擬器無法回應或者無法加載應用,因為這個,曾一度懷疑是不是程式那里修改錯了或者開發環境哪里少了步驟而沒有搭建完成,折騰了近一下午的時間,第二天,安裝了Genymotion模擬器,一切都清爽了,
Gemymotion模擬器的安裝步驟:
- 從官網下載后(對首次下載,建議選擇帶有Virtualbox的版本),注冊賬號,因為在安裝完成,啟動該軟體,仍然需要登錄賬號,才可以創建模擬器,在安裝完成后,可以看到:

- 啟動Genymotion, 創建模擬器,如下圖所示,可根據需要創建不同Android版本的模擬器:
上面兩步完成后,開發環境就搭建成功了,
啟動新建的Genymotion虛擬設備,打開Android project后,在visual studio的除錯設備串列中,默認就是該模擬器,否則將是hardware accelerated emulator,

2. 應用程式
這里會有些不同于MSDN上的Hello, Android,稍微有些復雜,將從Activity,View(axml),Intent相關點介紹,
2.1 程式開發 - 應用程式結構及代碼結構:


- Logon activity & logon view:登錄相關,應用程式啟動后,此為主activity啟動 一個main activity,其對應的view放在axml檔案中
- Main activity & view:登錄后的相關操作,此處呈現簡單的click計數器,并提供導航到history activity和回傳logon的操作,其對應的view放在axml檔案中
- History list activity:此activity繼承自Built-in Control ListView, 不單獨創建xml結構的view
初步介紹程式結構后,接下來從創建該程式開始:
A. 在visual studio中,新建一個Xamarin project

B. 在接下來的向導中,選擇空白模板,對最小Android版本,其字面直譯,表示該應用程式運行所需的最低版本,根據開發環境搭建步驟a中所選擇安裝的API Level不同,該串列呈現的可選版本也不同,

C. 完成后,可見到程式默認結構,在Resource/Layout目錄下,Activity_main.axml為默認的啟動的activity設圖,這里,將其作為main activity的視圖(非程式啟動后的第一個頁面),為了保持一致,可將其重命名為其它試圖的activity,為了簡化,這里不做改名,
默認的布局結構為RelativeLayout, 這里將其修改為LinearLayout,并設定屬性android:orientation="vertical"縱向線性布局結構,本axml使用嵌套LinearLayout布局,

完整的代碼如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_margin="5dip"> <TextView android:id="@+id/form_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/logon_title_tip" /> <LinearLayout android:id="@+id/layout_login_name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_margin="5.0dip" android:layout_marginTop="10.0dip" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/logon_usr" /> <EditText android:id="@+id/txt_login_name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="15.0sp" /> </LinearLayout> <LinearLayout android:id="@+id/login_pwd_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@id/layout_login_name" android:layout_centerHorizontal="true" android:layout_margin="5.0dip" android:orientation="horizontal"> <TextView android:id="@+id/login_pass_edit" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/logon_pwd" android:textSize="15.0sp" /> <EditText android:id="@+id/txt_login_pwd" android:layout_width="fill_parent" android:layout_height="wrap_content" android:password="true" android:textSize="15.0sp" /> </LinearLayout> <Button android:id="@+id/btn_login" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" android:text="@string/logon_logonBtnText" /> </LinearLayout>View Code
D. Logon對應的Activity, 其默認繼承自AppCompatActivity,且其被ActivityAttribute修飾為MainLauncher = true,在Android程式中,并沒有主程式的入口點,理論上,任何一個activity都可以被作為主入口,在xaml開發中,做了更為易于理解的標注( AppCompatActivity, MainLauncher = true),完整的代碼:
[Activity(Label = "LogonActivity", MainLauncher = true)] public class LogonActivity : AppCompatActivity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); // Create your application here SetContentView(Resource.Layout.activity_logon); EditText usr = FindViewById<EditText>(Resource.Id.txt_login_name); usr.KeyPress += Usr_KeyPress; var logonBtn = FindViewById<Button>(Resource.Id.btn_login); logonBtn.Click += LogonBtn_Click; CreateNotificationChannel(); } private void Usr_KeyPress(object sender, View.KeyEventArgs e) { e.Handled = false; if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter) { var msg = FindViewById<EditText>(Resource.Id.txt_login_name).Text; Toast.MakeText(this, msg, ToastLength.Short).Show(); EditText pwdTxt = FindViewById<EditText>(Resource.Id.txt_login_pwd); pwdTxt.Text = msg; e.Handled = true; #region notification var builder = new NotificationCompat.Builder(this, "location_notification") .SetAutoCancel(true) // Dismiss the notification from the notification area when the user clicks on it //.SetContentIntent(resultPendingIntent) // Start up this activity when the user clicks the intent. .SetContentTitle("Button Clicked") // Set the title //.SetNumber(count) // Display the count in the Content Info .SetSmallIcon(Resource.Drawable.abc_tab_indicator_mtrl_alpha) // This is the icon to display .SetContentText("只有圖示、標題、內容:" + FindViewById<EditText>(Resource.Id.txt_login_name).Text); // the message to display. // Finally, publish the notification: var notificationManager = NotificationManagerCompat.From(this); notificationManager.Notify(1000, builder.Build()); #endregion } } private void LogonBtn_Click(object sender, EventArgs e) { var intent = new Intent(this, typeof(MainActivity)); intent.PutExtra("username", FindViewById<EditText>(Resource.Id.txt_login_name).Text); StartActivity(intent); } void CreateNotificationChannel() { //in case API 26 or above if (Build.VERSION.SdkInt < BuildVersionCodes.O) return; var channel = new NotificationChannel("location_notification", "Noti_name", NotificationImportance.Default) { Description = "Hello description" }; var notificationManager = (NotificationManager)GetSystemService(NotificationService); notificationManager.CreateNotificationChannel(channel); } }LogonActivity
這里,User name的輸入框中,增加了按鍵press down事件,用回車鍵按下后,觸發Toast及通知欄展示(此處僅為演示用),對通知欄,在API 26以后,需要首先注冊Channel,
var channel = new NotificationChannel("location_notification", "Noti_name", NotificationImportance.Default) { Description = "Hello description" }; var notificationManager =(NotificationManager)GetSystemService(NotificationService); notificationManager.CreateNotificationChannel(channel);Channel Registration - API 26
E. 輸入user name后,點擊Logon,跳轉到Main activity頁面,此頁面,Enter code默認呈現user name,在Click me按鈕點擊后,內部計數器增加,訊息呈現在Enter code并記錄到Intent中,

axml完整代碼:
<?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:text="Enter code" android:textAppearance="?android:attr/textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="25px" android:minHeight="25px" android:id="@+id/textView1" /> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/editText1" /> <Button android:text="Click ME" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/button1" /> <Button android:text="@string/callhistory" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/callhistoryBtn" android:enabled="false" /> <Button android:text="Logout" android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/switchBtn" /> </LinearLayout>Main_View axml
F. Main Activity, 視圖對應的代碼實作
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme")] public class MainActivity:Activity { static readonly List<string> phoneNumbers = new List<string>(); protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); // Set our view from the "main" layout resource SetContentView(Resource.Layout.activity_main); Button btn = FindViewById<Button>(Resource.Id.button1); btn.Click += Btn_Click; Button callhis = FindViewById<Button>(Resource.Id.callhistoryBtn); callhis.Click += Callhis_Click; FindViewById<Button>(Resource.Id.switchBtn).Click+= (obj, e)=> { //SetContentView(Resource.Layout.activity_logon); StartActivity(typeof(LogonActivity)); }; //set user name EditText usr = FindViewById<EditText>(Resource.Id.editText1); usr.Text = Intent.Extras?.Get("username")?.ToString(); } private void Callhis_Click(object sender, System.EventArgs e) { var intent = new Intent(this, typeof(CallHistoryActivity)); intent.PutStringArrayListExtra("phone_numbers", phoneNumbers); StartActivity(intent); } private int counter = 1; private void Btn_Click(object sender, System.EventArgs e) { var cl = FindViewById<EditText>(Resource.Id.editText1); cl.Text = $"your counter is {counter++}"; phoneNumbers.Add(cl.Text); FindViewById<Button>(Resource.Id.callhistoryBtn).Enabled = true; } }Main Actitity
對該頁面,當點擊"Click ME"按鈕后,計數器自增,Call History的按鈕可用,當點擊Call History,頁面跳轉到view list頁面,呈現計數器Counter的變化歷史,

G. 在Call History,該activity繼承自ListView,資料源為計數器Counter的變化歷史記錄,詳細的代碼為:
[Activity(Label = "@string/callhistory")] public class CallHistoryActivity : ListActivity { protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); // Create your application here var phoneNumbers = Intent.Extras.GetStringArrayList("phone_numbers") ?? new string[0]; this.ListAdapter = new ArrayAdapter<string>(this, Android.Resource.Layout.SimpleListItem1, phoneNumbers); } }Call History Activity
除了這里演示的ListView, 還有LinearLayout,RelativeLayout , TableLayout , RecyclerView, GridView, GridLayout, Tabbed Layouts多種布局構建頁面,
2.2 程式部署
和傳統的windows程式有些不太一樣(DEBUG/RELEASE模式下,直接編譯后得到的為dll而非.apk檔案),在程式需要發布的時候,在project右鍵或者Tools -> Arhive Manager,可以看到已經創建的Archive或者新的Archive,
NOTE:右鍵選單中的Deploy按鈕,對沒有多少經驗的開發者有些不太友好,在模擬器環境中,通常會報不支持CPU型號的錯誤,這是由于deploy會依賴Simulation串列設備的選擇,如果是Gemymotion模擬器,基本會失敗,如果連接的硬體(usb除錯模式下的硬體),會直接部署到對應的設備上,

在Archive Manger中,選擇相應的Archive,將其分發到本地或者應用市場:

對Ad Hoc選項,可以創建/選擇已有的簽名,對所要發布的程式進行簽名,
3. 所涉及的要點
3.1 Activity & axml
這里更多的是從設計的角度考慮,Activity和axml以一對一的形式構建,單從程式實作角度,一個activity可使用多個axml檔案以構建不同業務場景的試圖(同一個時刻,content view只會有一個),這種情況下多個axml的事件或業務,將只能在對應的那個Activity中實作(呼叫SetContentView的地方),在設計上,這種很難理解維護,即使以partial這種投機的方式達到可維護性,對OO的設計模式也是一種破壞(或美其名曰反設計模式),
3.2 Activity lifecycle
在Android應用程式中(不像傳統的桌面/web程式,有指定的程式入口點Main),任何activity都可以成為入口點,在vs中,Xamarin.Android很好的照顧了剛入門的開發人員,將activity及對應的axml檔案直接以main關鍵字命名,借用MSDN上的這幅圖,形象生動說明整個actity的生命周期,

對各個關鍵點,提供了相應的重寫方法,如默認的OnCreate, 執行activity啟動以初始化,需要注意,該方法是在OnStart之后執行,
3.3 Activity之間的資料傳遞
對于不同Activity之間的資料傳遞,Intent類提供了多種方式,對簡單資料型別,呼叫內置的PutExtra不會有任何問題,對實體物件或復雜物件,需要將其序列化,在取的時候,反序列化即可,
而對于同一個Activity不同的活動期間,則無需這么復雜,通過Bundle即可,如OnCreate, OnPause等可重寫的方法,通過引數Bundle即可完成生命周期內的資料傳遞,在實際應用中,OnSaveInstanceState在activity被銷毀時保存相應資料或試圖狀態,在恢復的時候,OnRestoreInstanceState是一種選擇,但更多的時候, 通過OnCreate已經足夠,
protected override void OnSaveInstanceState (Bundle outState) { outState.PutString("UsrCfg", MyStringData); base.OnSaveInstanceState (outState); }OnSaveInstanceState
3.4 Localization
如演示程式所示,如果應用需要多語言支持,對本地化策略:
android:text="@string/callhistory"
以@string或者類似值,將以字面直譯的方式處理,涉及的resource在Resources/Values/xx.axml檔案中,比如上述代碼所演示的,具體的resource資源在Resources/Values/string.axml中,
對熟悉.NET平臺開發,又想開發Android應用的朋友,希望這篇文章對你有所幫助,
另外,在寫這篇文章2天前,我也沒有相關的Android開發經驗,因為基于專案要求,需要在PDA設備開發相應的程式,于是便有了此文,對于想要了解更詳細的知識點,可詳見Application Fundamentals,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/204762.html
標籤:Android
上一篇:談談OKHttp的幾道面試題
