Building Your First App
Supporting Different Devices
Building a Dynamic UI with Fragments
為了適配不同的屏幕尺寸(大屏幕可以比小屏幕多顯示幾個(gè) Fragment),這一節(jié)主要說(shuō)明如何通過(guò) Fragments 創(chuàng)造動(dòng)態(tài)化的用戶(hù)體驗(yàn)救拉,使你的 App 在不同的屏幕尺寸上都可獲得最優(yōu)的用戶(hù)體驗(yàn),設(shè)備最低支持到 Android 1.6.
Creating a Fragment
可以認(rèn)為Fragment是Activity組合的一部分客扎,有自己獨(dú)立的生命周期昼钻,自己的輸入事件燎潮,當(dāng)其所依附的Activity還在運(yùn)行時(shí),可以自由添加或刪除Fragment。
在創(chuàng)建Fragment之前合敦,需要讓App使用 Support Library桩皿。
Create a Fragment Class
- extend
Fragment
- override key lifecycle methods
必須使用 onCreateView()
的callback來(lái)定義layout組件
Add a Fragment to an Activity using XML
FragmentActivity
是用來(lái)支持 API level 11 以下的版本豌汇,如果版本在 11 及以上,則可以使用普通的Activity.
可以通過(guò)在 xml 文件中指定Fragment
的name
屬性业簿,從而指定特定的Fragment class.
Building a Flexible UI
FragmentManager
類(lèi)提供添加瘤礁、移除、替換fragment的方法梅尤,給用戶(hù)動(dòng)態(tài)的適配體驗(yàn)柜思。
Add a Fragment to an Activity at Runtime
使用FragmentManager
創(chuàng)建一個(gè) FragmentTransaction
。
在A(yíng)ctivity運(yùn)行時(shí)添加Fragment有一點(diǎn)需要注意:Activity必須包含一個(gè)你可以插入 fragment 的 View.
- Get a
FragmentManager
:getSupportFragmentManager()
- Create a
FragmentTransaction
:beginTransaction()
- Add a
Fragment
:add()
Replace One Fragment with Another
使用 replace()
代替 add()
.
Best Practice:在進(jìn)行Fragment替換時(shí)巷燥,最好允許用戶(hù)返回或者取消操作:addToBackStack()
(在 FragmentTransaction.commit()
之前赡盘,F(xiàn)ragment 將不會(huì)被銷(xiāo)毀,只會(huì)被remove掉).
Communicating with Other Fragments
所有Fragment之間的信息交換都是通過(guò)與其相關(guān)的Activity來(lái)完成缰揪,任何Fragment不應(yīng)該直接交流陨享。
Define an Interface
在 Fragment 中定義一個(gè) interface葱淳,在 Activity 中實(shí)現(xiàn)這個(gè) interface。Fragment 將會(huì)在 onAttach()
中通過(guò)得到 Activity 對(duì)象捕獲這個(gè)實(shí)現(xiàn)抛姑,從而通過(guò) Activity 來(lái)進(jìn)行信息交流赞厕。
Implement the Interface
Activity 需要實(shí)現(xiàn) Fragment 中聲明的 interface.
Deliver a Message to a Fragment
Activity 可以通過(guò) findFragmentById()
獲取到 Fragment 實(shí)例,直接調(diào)用 Fragment 的方法定硝。
場(chǎng)景:AFragment 有一堆文章列表皿桑,點(diǎn)擊某個(gè)文章,進(jìn)入 BFragment蔬啡,閱讀這篇文章诲侮。
- AFragment: callback.click(title)
- Activity: click(title) {title -> content -> replaceToBFragment(content) -> BFragment.updateArticleView(content)}
- BFragment: updateArticleView(content)
Saving Data
在 Android 中,有三種數(shù)據(jù)存儲(chǔ)方式:
- shared preferences 文件:key-value
- 文件系統(tǒng):任何文件
- SQLite: 數(shù)據(jù)庫(kù)
Saving Key-Value Sets
一個(gè) SharedPreferences
對(duì)象指向一個(gè)包含key-value
的文件箱蟆,提供簡(jiǎn)單的方法進(jìn)行讀寫(xiě)沟绪。
Get a Handle to a SharedPreferences
創(chuàng)建或獲取一個(gè) shared preference 文件:
-
getSharedPreferences()
:擁有多個(gè)shared preference文件,通過(guò)傳入文件名獲取對(duì)象空猜,可以從任意的context
中獲取绽慈。 -
getPreferences()
:如果一個(gè)activity只有一個(gè)shared preference文件,通過(guò)這個(gè)方法可以獲取activity對(duì)應(yīng)的SP文件
Write to Shared Preferences
- 創(chuàng)建
SharedPreferences.Editor
:調(diào)用 SharedPreferences 對(duì)象的edit()
方法 - 寫(xiě)入鍵值對(duì):
putInt()
,putString
, .etc - 保存更改:
commit()
Read from Shared Preferences
getInt()
, getString
, .etc.
Saving Files
使用 File
API 來(lái)操作 Android 中的文件系統(tǒng)抄肖。
一個(gè)File
對(duì)象適合讀寫(xiě)大數(shù)據(jù)文件久信,從頭到尾沒(méi)有中斷的順序讀取。
Choose Internal or External Storage
所有的Android設(shè)備擁有兩個(gè)文件存儲(chǔ)域:“internal” 和 “external”:
- Internal Storage
- 永遠(yuǎn)可用
- 文件只能被 App 訪(fǎng)問(wèn)
- 當(dāng) App 被卸載時(shí)漓摩,所有存儲(chǔ)的 internal 的文件都會(huì)被刪除
- External Storage
- 不一定可用
- 可被全局訪(fǎng)問(wèn)
- 當(dāng) App 被卸載時(shí)裙士,系統(tǒng)只會(huì)刪除特定的文件夾(
getExternalFilesDir()
)
App 會(huì)被默認(rèn)載入到internal中,在代碼中如何設(shè)置下載位置管毙?
在A(yíng)ndroidManifest中:更改
android:installLocation
Obtain Permissions for External Storage
在external寫(xiě)文件:需要權(quán)限 android.permission.WRITE_EXTERNAL_STORAGE
在external寫(xiě)文件(in future):需要權(quán)限:android.permission.READ_EXTERNAL_STORAGE
Save a File on Internal Storage
-
getFilesDir()
:返回app在internal中的位置 -
getCacheDir()
: 返回app在internal中保存cache的位置腿椎,一定要在不需要的時(shí)候及時(shí)刪掉 - 寫(xiě)文件:
FileOutputStream fos = openFileOutput(filename, file_mode);
- 創(chuàng)建cache文件:
File file = File.createTempFile(filename, null, context.getCacheDir());
Save a File on External Storage
由于external文件有很多不在場(chǎng)的不確定因素,所以在訪(fǎng)問(wèn)文件前最好驗(yàn)證其可用性:
getExternalStorageState()
獲取external storage狀態(tài):如果返回MEDIA_MOUNTED
夭咬,則可用啃炸。
- Public Files: 需要留存 -> create from ->
getExternalStoragePublicDirectory()
- Private Files: 需要?jiǎng)h除 -> create from ->
getExternalFilesDir()
- 文件類(lèi)型:
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 類(lèi)中通過(guò)實(shí)現(xiàn)BaseColumns
內(nèi)部類(lèi),可以獲得內(nèi)部key_ID
卓舵。
Create a Database Using a SQL Helper
SQLiteOpenHelper
用來(lái)提供僅在的需要時(shí)候可長(zhǎng)時(shí)間運(yùn)行的操作(添加/更新數(shù)據(jù)庫(kù))南用,避免在項(xiàng)目運(yùn)行時(shí)就實(shí)例化數(shù)據(jù)庫(kù)操作類(lèi)。
-
getWritableDatabase()
:獲取可寫(xiě)的database -
getReadableDatabase()
:獲取可讀的database
只可在非UI線(xiàn)程調(diào)用以上兩種方法掏湾,例如AsyncTask
和IntentService
.
繼承SQLiteOpenHelper
裹虫,需要重寫(xiě)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()
: 獲取當(dāng)前 position -
close()
: 關(guān)閉游標(biāo)
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
被摧毀時(shí)關(guān)閉DBHelper
-> dbHelper.close()
Interacting with Other Apps
Sending the User to Another App
在與其它的App進(jìn)行交互時(shí),只能使用implicit intent融击。
Build an Implicit Intent
定義Action
去具體化啟動(dòng)事件筑公。
- 使用
Uri
定義啟動(dòng)事件:-
打開(kāi)撥號(hào)頁(yè)面
Uri number = Uri.parse("tel:5551234"); Intent callIntent = new Intent(Intent.ACTION_DIAL, number);
-
打開(kāi)地圖頁(yè)面
Uri location = Uri.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California"); Intent mapIntent = new Intent(Intent.ACTION_VIEW, location);
-
打開(kāi)網(wǎng)頁(yè)
Uri webpage = Uri.parse("http://www.android.com"); Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);
-
使用 extra data 具體化啟動(dòng)事件:
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將會(huì)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
如果有多個(gè)喚起事件存在匣屡,需要用戶(hù)自行選擇具體的喚起事件封救,調(diào)用createChooser()
來(lái)調(diào)起具體的被選事件。
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()
來(lái)啟動(dòng)一個(gè)activity并接收返回?cái)?shù)據(jù)捣作。
使用onActivityResult()
來(lái)處理返回的數(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
通過(guò) resultCode
來(lái)判斷返回類(lèi)型:
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
通過(guò)定義支持ACTION_SEND
的intent:<intent-filter>
.
Add an Intent Filter
在intent-filter
中定義以下幾種criteria:
Action : action 名稱(chēng)誉结,一般定義為
ACTION_XXX
格式Data : 與 intent 相關(guān)的數(shù)據(jù)描述,可以多重定義:MIME Type / URI prefix / URI scheme / combination.
-
Category : 提供額外的方式描述處理intent的activity虾宇,通常與用戶(hù)行為或地址相關(guān)搓彻。一般定義為
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 無(wú)法處理跳轉(zhuǎn)事件.
Handle the Intent in Your Activity
調(diào)用getIntent()
.
在activity的生命周期的任何時(shí)間段都可以調(diào)用如绸,但是最好在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 在每一個(gè)有權(quán)限控制的沙箱中運(yùn)行App怔接。
Declaring Permissions
Determine What Permissions Your App Needs
Android 5.1 以下搪泳,用戶(hù)會(huì)在安裝App的時(shí)候賦予權(quán)限,在A(yíng)ndroid 6.0 以上扼脐,用戶(hù)會(huì)在A(yíng)pp運(yùn)行時(shí)動(dòng)態(tài)賦予權(quán)限岸军。
Add Permissions to the Manifest
在manifest
屬性下,申請(qǐng)permission使用uses-permission
標(biāo)簽瓦侮。
Requesting Permissions at Run Time
系統(tǒng)權(quán)限分為兩種:normal 和 dangerous:
- 系統(tǒng)會(huì)自動(dòng)賦予 normal 權(quán)限
- dangerous 權(quán)限需要用戶(hù)手動(dòng)授予
在A(yíng)ndroid 6.0以上艰赞,由于權(quán)限是動(dòng)態(tài)授予的,所以需要保證在某些權(quán)限不可用時(shí)肚吏,App依然可以正常運(yùn)行方妖。
Check for Permissions
ContextCompat.checkSelfPermissions()
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);
-
PackageManager.PERMISSION_GRANTED
= permissionCheck: 權(quán)限被授予 -
PackageManager.PERMISSION_DENED
= permissionCheck: 權(quán)限被拒絕
Request Permissions
最佳實(shí)踐:在用戶(hù)已經(jīng)關(guān)閉權(quán)限時(shí),App運(yùn)行到需要使用權(quán)限才能正常運(yùn)行的功能時(shí)罚攀,可以為用戶(hù)提供權(quán)限解釋党觅。
Explain why the app needs permissions
shouldShowRequestPermissionRationale()
: 如果App曾經(jīng)請(qǐng)求過(guò)permission,用戶(hù)拒絕了請(qǐng)求斋泄,該方法會(huì)返回true
;
如果App曾經(jīng)請(qǐng)求過(guò)permission杯瞻,用戶(hù)拒絕了請(qǐng)求,且選擇Don't ask again
炫掐,該方法會(huì)返回false
;
如果設(shè)備安全等級(jí)拒絕授予該permission請(qǐng)求魁莉,該方法會(huì)返回false
.
Request the permissions you need
requestPermissions()
用來(lái)請(qǐng)求權(quán)限。
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 該方法用來(lái)查詢(xún)permission是否成功申請(qǐng)募胃。
@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
權(quán)限控制準(zhǔn)則
- 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 工具管理權(quán)限:
-
分組列出權(quán)限和狀態(tài)
adb shell pm list permissions -d -g
-
賦予/禁止一或多個(gè)權(quán)限
adb shell pm [grant|revoke] <permission-name> ...