Building Your First App
Supporting Different Devices
Building a Dynamic UI with Fragments
為了適配不同的屏幕尺寸(大屏幕可以比小屏幕多顯示幾個 Fragment),這一節(jié)主要說明如何通過 Fragments 創(chuàng)造動態(tài)化的用戶體驗,使你的 App 在不同的屏幕尺寸上都可獲得最優(yōu)的用戶體驗,設備最低支持到 Android 1.6.
Creating a Fragment
可以認為Fragment是Activity組合的一部分,有自己獨立的生命周期,自己的輸入事件,當其所依附的Activity還在運行時,可以自由添加或刪除Fragment。
在創(chuàng)建Fragment之前,需要讓App使用 Support Library。
Create a Fragment Class
- extend
Fragment - override key lifecycle methods
必須使用 onCreateView() 的callback來定義layout組件
Add a Fragment to an Activity using XML
FragmentActivity 是用來支持 API level 11 以下的版本,如果版本在 11 及以上,則可以使用普通的Activity.
可以通過在 xml 文件中指定Fragment的name屬性,從而指定特定的Fragment class.
Building a Flexible UI
FragmentManager 類提供添加、移除、替換fragment的方法,給用戶動態(tài)的適配體驗。
Add a Fragment to an Activity at Runtime
使用FragmentManager 創(chuàng)建一個 FragmentTransaction。
在Activity運行時添加Fragment有一點需要注意:Activity必須包含一個你可以插入 fragment 的 View.
- Get a
FragmentManager:getSupportFragmentManager() - Create a
FragmentTransaction:beginTransaction() - Add a
Fragment:add()
Replace One Fragment with Another
使用 replace() 代替 add().
Best Practice:在進行Fragment替換時,最好允許用戶返回或者取消操作:addToBackStack()(在 FragmentTransaction.commit() 之前,F(xiàn)ragment 將不會被銷毀,只會被remove掉).
Communicating with Other Fragments
所有Fragment之間的信息交換都是通過與其相關的Activity來完成,任何Fragment不應該直接交流。
Define an Interface
在 Fragment 中定義一個 interface,在 Activity 中實現(xiàn)這個 interface。Fragment 將會在 onAttach() 中通過得到 Activity 對象捕獲這個實現(xiàn),從而通過 Activity 來進行信息交流。
Implement the Interface
Activity 需要實現(xiàn) Fragment 中聲明的 interface.
Deliver a Message to a Fragment
Activity 可以通過 findFragmentById() 獲取到 Fragment 實例,直接調用 Fragment 的方法。
場景:AFragment 有一堆文章列表,點擊某個文章,進入 BFragment,閱讀這篇文章。
- AFragment: callback.click(title)
- Activity: click(title) {title -> content -> replaceToBFragment(content) -> BFragment.updateArticleView(content)}
- BFragment: updateArticleView(content)
Saving Data
在 Android 中,有三種數(shù)據(jù)存儲方式:
- shared preferences 文件:key-value
- 文件系統(tǒng):任何文件
- SQLite: 數(shù)據(jù)庫
Saving Key-Value Sets
一個 SharedPreferences 對象指向一個包含key-value的文件,提供簡單的方法進行讀寫。
Get a Handle to a SharedPreferences
創(chuàng)建或獲取一個 shared preference 文件:
-
getSharedPreferences():擁有多個shared preference文件,通過傳入文件名獲取對象,可以從任意的context中獲取。 -
getPreferences():如果一個activity只有一個shared preference文件,通過這個方法可以獲取activity對應的SP文件
Write to Shared Preferences
- 創(chuàng)建
SharedPreferences.Editor:調用 SharedPreferences 對象的edit()方法 - 寫入鍵值對:
putInt(),putString, .etc - 保存更改:
commit()
Read from Shared Preferences
getInt(), getString, .etc.
Saving Files
使用 File API 來操作 Android 中的文件系統(tǒng)。
一個File對象適合讀寫大數(shù)據(jù)文件,從頭到尾沒有中斷的順序讀取。
Choose Internal or External Storage
所有的Android設備擁有兩個文件存儲域:“internal” 和 “external”:
- Internal Storage
- 永遠可用
- 文件只能被 App 訪問
- 當 App 被卸載時,所有存儲的 internal 的文件都會被刪除
- External Storage
- 不一定可用
- 可被全局訪問
- 當 App 被卸載時,系統(tǒng)只會刪除特定的文件夾(
getExternalFilesDir())
App 會被默認載入到internal中,在代碼中如何設置下載位置?
在AndroidManifest中:更改
android:installLocation
Obtain Permissions for External Storage
在external寫文件:需要權限 android.permission.WRITE_EXTERNAL_STORAGE
在external寫文件(in future):需要權限:android.permission.READ_EXTERNAL_STORAGE
Save a File on Internal Storage
-
getFilesDir():返回app在internal中的位置 -
getCacheDir(): 返回app在internal中保存cache的位置,一定要在不需要的時候及時刪掉 - 寫文件:
FileOutputStream fos = openFileOutput(filename, file_mode); - 創(chuàng)建cache文件:
File file = File.createTempFile(filename, null, context.getCacheDir());
Save a File on External Storage
由于external文件有很多不在場的不確定因素,所以在訪問文件前最好驗證其可用性:
getExternalStorageState() 獲取external storage狀態(tài):如果返回MEDIA_MOUNTED,則可用。
- Public Files: 需要留存 -> create from ->
getExternalStoragePublicDirectory() - Private Files: 需要刪除 -> create from ->
getExternalFilesDir() - 文件類型:
DIRECTORY_PICTURES,DIRECTORY_MUSIC,DIRECTORY_RINGTONES, .etc
Query Free Space
getFreeSpace()getTotalSpace()
Delete a File
file.delete()context.deleteFile(filename)
Saving Data in SQL Databases
Define a Schema and Contract
在 Contract 類中通過實現(xiàn)BaseColumns內部類,可以獲得內部key_ID。
Create a Database Using a SQL Helper
SQLiteOpenHelper 用來提供僅在的需要時候可長時間運行的操作(添加/更新數(shù)據(jù)庫),避免在項目運行時就實例化數(shù)據(jù)庫操作類。
-
getWritableDatabase():獲取可寫的database -
getReadableDatabase():獲取可讀的database
只可在非UI線程調用以上兩種方法,例如AsyncTask和IntentService.
繼承SQLiteOpenHelper,需要重寫onCreate(), onUpgrade(), onOpen(), (可選)onDowngrade().
Put Information into a Database
Insert: Database -> ContentValues -> db.insert(table_name, action_if_content_values_empty, content_values)
Read Information from a Database
Read: Database -> db.query(table_name, columns_to_return, column_where, column_where_value, group, filter, sort_order)
Return: Cursor -> cursor starts at position -1.
-
moveToNext(): position+1 -
getXXX(): 獲取列值 -
getColumnIndex()/getColumnIndexOrThrow(): 獲取當前 position -
close(): 關閉游標
Delete Information from Database
Delete: Database -> db.delete(table_name, selection, selection_args)
Update a Database
Update: combine insert() & delete() -> db.update(table_name, content_values, selection, selection_args)
Persisting Database Connection
一般在Activity被摧毀時關閉DBHelper -> dbHelper.close()
Interacting with Other Apps
Sending the User to Another App
在與其它的App進行交互時,只能使用implicit intent。
Build an Implicit Intent
定義Action去具體化啟動事件。
- 使用
Uri定義啟動事件:-
打開撥號頁面
Uri number = Uri.parse("tel:5551234"); Intent callIntent = new Intent(Intent.ACTION_DIAL, number); -
打開地圖頁面
Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California"); Intent mapIntent = new Intent(Intent.ACTION_VIEW, location); -
打開網(wǎng)頁
Uri webpage = Uri.parse("http://www.android.com"); Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage); -
使用 extra data 具體化啟動事件:
setType(): 指定MIME(Multipurpose Internet Mail Extensions) Type-
發(fā)送 email
Intent emailIntent = new Intent(Intent.ACTION_SEND); emailIntent.setType(HTTP.PLAIN_TEXT_TYPE); emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"jon@example.com"}); emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email subject"); emailIntent.putExtra(Intent.EXTAR_TEXT, "Email message text"); emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://path/to/email/attachment")); -
發(fā)送 calendar 事件
Intent calendarIntent = new Intent(Intent.ACTION_INSERT, Events.CONTENT_URI); Calendar beginTime = Calendar.getInstance().set(2012, 0, 19, 7, 30); Calendar endTime = Calendar.getInstance().set(2012, 0, 19, 10, 30); calendarIntent.putExrea(CalendarContract.EXTRA_EVENT_BEGIN_TIME, beginTime.getTimeInMillis()); calendarIntent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endTime.getTimeInMillis()); calendarIntent.putExtra(Events.TITLE, "Ninja class"); calendarIntent.putExtra(Events.EVENT_LOCATION, "Secret dojo);
-
-
Verify There is an App to Receive the Intent
如果intent聲明的喚起事件并不存在,app將會crash。
-
quertIntentActivities(): 查看可用事件PackageManager packageManager = getPackageManager(); List activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); boolean isIntentSafe = activities.size() > 0;
Start an Activity with the Intent
startActivity(intent)
Show an App Chooser
如果有多個喚起事件存在,需要用戶自行選擇具體的喚起事件,調用createChooser()來調起具體的被選事件。
Intent intent = new Intent(Intent.ACTION_SEND);
String title = getResources().getString(R.string.choose_title);
Intent chooser = Intent.createChooser(intent, title);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(chooser);
}
Getting a Result from an Activity
使用startActivityForResult()來啟動一個activity并接收返回數(shù)據(jù)。
使用onActivityResult()來處理返回的數(shù)據(jù)
Start the Activity
static final int PICK_CONTACT_REQUEST = 1;
private void pickContact() {
Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
pickContactIntent.setType(Phone.CONTENT_TYPE);
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
}
Receive the Result
通過 resultCode 來判斷返回類型:
RESULT_OK: 操作成功
-
RESULT_CANCELED: 操作取消
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PICK_CONTACT_REQUEST) {
if (resultCode == RESULT_OK) {
//do something...
}
}
}
Allowing Other Apps to Start Your Activity
通過定義支持ACTION_SEND的intent:<intent-filter>.
Add an Intent Filter
在intent-filter中定義以下幾種criteria:
Action : action 名稱,一般定義為
ACTION_XXX格式Data : 與 intent 相關的數(shù)據(jù)描述,可以多重定義:MIME Type / URI prefix / URI scheme / combination.
-
Category : 提供額外的方式描述處理intent的activity,通常與用戶行為或地址相關。一般定義為
CATEGORY_DEFAULT.<activity android:name="ShareActivity"
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
必須定義 CATEGORY_DEFAULT 否則implicit intent 無法處理跳轉事件.
Handle the Intent in Your Activity
調用getIntent().
在activity的生命周期的任何時間段都可以調用,但是最好在onCreate() / onStart()中處理。
Return a Result
Intent result = new Intent("com.example.RESULT_ACTION", Uri.parse("content://result_uri"));
setResult(Activity.RESULT_OK, result);
finish();
Working with System Permissions
為了保證App的數(shù)據(jù)安全,Android 在每一個有權限控制的沙箱中運行App。
Declaring Permissions
Determine What Permissions Your App Needs
Android 5.1 以下,用戶會在安裝App的時候賦予權限,在Android 6.0 以上,用戶會在App運行時動態(tài)賦予權限。
Add Permissions to the Manifest
在manifest屬性下,申請permission使用uses-permission標簽。
Requesting Permissions at Run Time
系統(tǒng)權限分為兩種:normal 和 dangerous:
- 系統(tǒng)會自動賦予 normal 權限
- dangerous 權限需要用戶手動授予
在Android 6.0以上,由于權限是動態(tài)授予的,所以需要保證在某些權限不可用時,App依然可以正常運行。
Check for Permissions
ContextCompat.checkSelfPermissions()
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);
-
PackageManager.PERMISSION_GRANTED= permissionCheck: 權限被授予 -
PackageManager.PERMISSION_DENED= permissionCheck: 權限被拒絕
Request Permissions
最佳實踐:在用戶已經關閉權限時,App運行到需要使用權限才能正常運行的功能時,可以為用戶提供權限解釋。
Explain why the app needs permissions
shouldShowRequestPermissionRationale(): 如果App曾經請求過permission,用戶拒絕了請求,該方法會返回true;
如果App曾經請求過permission,用戶拒絕了請求,且選擇Don't ask again,該方法會返回false;
如果設備安全等級拒絕授予該permission請求,該方法會返回false.
Request the permissions you need
requestPermissions() 用來請求權限。
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) {
//Show an explanation
} else {
ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS);
}
}
Handle the permissions request response
onRequestPermissionsResult() override 該方法用來查詢permission是否成功申請。
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTE) {
//Do what you want
} else {
//Do what when permission was denied
}
return;
}
}
}
Permissions Usage Notes
權限控制準則
- Consider Using an Intent
- Only Ask for Permissions You Need
- Don't Overwhelm the User
- Explain Why You Need Permissions
- Test for Both Permissions Models
使用 adb 工具管理權限:
-
分組列出權限和狀態(tài)
adb shell pm list permissions -d -g
-
賦予/禁止一或多個權限
adb shell pm [grant|revoke] <permission-name> ...