情境
在 如何使用 Firebase - 用 Android Studio 建立帳戶篇 簡單介紹如何建立一個 Firebase 的專案,所以我們來玩一下 Firebase 的檔案空間系統,Firebase 提供良好的 Storage 讓你能夠輕鬆操作各項檔案,無論是上傳,下載或者刪除檔案,都提供非常簡便的介面讓開發者使用,因此我們只需要幾行程式碼,就可以完成檔案存取的複雜功能。
完整程式碼
可以至 GitHub 上觀看完整程式碼
操作流程 + 程式碼說明
在這邊我們會示範怎麼使用 Firebase 的檔案新增、上傳、下載以及刪除,也會簡單的介紹一下 Storage 的操作。
透過 Android Studio 來建立 Storage 連結
在 如何使用 Firebase - 用 Android Studio 建立帳戶篇 當中,我們只操作到連結 Firebase 這個步驟,而 Firebase 每一個功能的操作,都會依據使用的功能有不同的參考。
第三個步驟是教你如何取得 Firebase 的連結。
mStorageRef = FirebaseStorage.getInstance().getReference()
所以我們就直接在程式內加入這段程式。
private fun initData() {
mStorageRef = FirebaseStorage.getInstance().getReference()
}
接下來兩個步驟分別是上傳檔案跟下載檔案,這部分我們直接看官網文件會比較詳細,因此在後面進行補充。
Storage 套件
因為我們用到 Storage 記得到 Gradle 加入以下程式(版本會隨著時間的推移而調整)。
implementation 'com.google.firebase:firebase-storage:x.y.z'
權限
要操作檔案系統必須開啟兩種權限,一種是檔案讀取的權限,另外一種是使用者所授予的權限,因此我們要在 AndroidManifest.xml 的 <Application />
加上以下的程式碼。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
接著要在程式內加入 Run Time Permission,根據如何使用 Runtime Permission這篇文章,我們加入了 Runtime Permission 的機制,在 MainActivity.java 中我們寫入了一個 checkPermission 的方法,在裡面判斷使用者是否有授予權限給 App 使用,如果沒有就開啟了對話框請求權限。
private fun checkPermission() {
val permission = ActivityCompat.checkSelfPermission(this@MainActivity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
//未取得權限,向使用者要求允許權限
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_EXTERNAL_STORAGE)
} else {
getLocalImg()
}
}
而當使用者勾選的結果會跳入至 onRequestPermissionsResult 這個方法,來判斷使用者是否允許,如果允許則可以進行選取圖片的操作,如果不允許則跳出 Toast 來告知 App 無法繼續操作。
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
REQUEST_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getLocalImg()
} else {
Toast.makeText(this@MainActivity, R.string.do_nothing, Toast.LENGTH_SHORT).show()
}
}
}
}
建構 Firebase 參照
前面有稍微提到怎麼建立一個 Firebase 的 Storage 參照,從這邊的官方文件也可以看到豐富的資訊。
// Create a storage reference from our app
val storageRef = storage.getReference()
一開始可以建立一個參照,這個參照可以讓你自由存取檔案資料,以下程式碼指向 images 這個參照路徑。
imagesRef = storageRef.child("images")
如果你直接使用 getName 這個方法,那麼就會得到 images 這串文字。
imagesRef = storageRef.child("images")
val name = umagesRef.getName()
接著指派一個 space.jpg 的檔案,參照路徑就會變成 images/space.jpg
。
val fileName = "space.jpg"
spaceRef = imagesRef.child(fileName)
此時你就可以透過 getPath 這個方法取得完整路徑,以下程式碼取得的路徑為 images/space.jpg
。
val path = spaceRef.getPath()
你也可以取得檔案名稱,以下程式碼就會取得 space.jpg
。
val name = spaceRef.getName()
你也可以取得 Parent 的路徑,以下程式碼會取得 /images
。
imagesRef = spaceRef.getParent()
val path = imagesRef.getPath()
如果你使用了 getBucket 這個方法,那麼就會得到一串 Bucket 的名稱,什麼是 Bucket 在這邊要視為 Google 雲端儲存的一個基本單位,它包含了 Storage 的所有資料,因此,取得呼叫 getBucket 這個方法,就可以得到 Bucket 的名稱。
val bucket = imagesRef.getBucket()
除了這些以外 ,還可以看到 StorageReference 有許多的方法可以操作。
選取手機照片並且上傳檔案
在這邊我們要調整一下 Layout,增加了一個上傳的 Button,當使用者按下 Button 的時候就上傳檔案到 Firebase。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<Button
android:text="@string/get_local_img"
android:id="@+id/pick_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_toRightOf="@id/pick_button"
android:id="@+id/upload_button"
android:text="@string/upload_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:layout_below="@id/upload_button"
android:id="@+id/pick_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
我們要借助一個處理圖片的第三方 Glide,操作方法可以參考 如何使用Glide 以及如何使用Glide-2。
- Gradle import
因為要用到 Glide 幫我們處理圖片,所以在 Gradle 上面就必須 import 這個第三方。
compile 'com.github.bumptech.glide:glide:x.y.z'
接下來我們知道怎麼操作基本的 StorageReference 物件以後,就來寫一個選取照片並且上傳到 Firebase 的功能,
透過 ImagePicker 這個第三方可以從 Android 的手機端取出照片,因為讓使用者自己取出想要的照片再透過 Firebase 進行一些操作會是一個比較有彈性的作法,所以用這個第三方來改寫成選取照片後上傳到 Firebase。
首先加入第三方的導入。
implementation 'com.github.dhaval2404:imagepicker-support:x.y'
透過以下程式碼,可以看到當我們喚醒內建的相簿選取器。
private fun getLocalImg() {
ImagePicker.with(this)
.crop() //Crop image(Optional), Check Customization for more option
.compress(1024) //Final image size will be less than 1 MB(Optional)
.maxResultSize(1080, 1080) //Final image resolution will be less than 1080 x 1080(Optional)
.start()
}
而當你選完照片以後,透過覆寫 onActivityResult 來判斷從選取器回來的相簿資料,接著使用 Glide 去呈現選取完的相簿資料。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (resultCode) {
Activity.RESULT_OK -> {
val filePath: String = ImagePicker.getFilePath(data) ?: ""
if (filePath.isNotEmpty()) {
imgPath = filePath
Toast.makeText(this@MainActivity, imgPath, Toast.LENGTH_SHORT).show()
Glide.with(this@MainActivity).load(filePath).into(pick_img)
} else {
Toast.makeText(this@MainActivity, R.string.load_img_fail, Toast.LENGTH_SHORT).show()
}
}
ImagePicker.RESULT_ERROR -> Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show()
}
}
Firebase 提供了一個簡易的工具,可以讓你輕鬆的操作檔案上傳,透過以下的程式碼,就可以完成上傳檔案的 callback。
val file = Uri.fromFile(File(path))
val metadata = StorageMetadata.Builder()
.setContentDisposition("universe")
.setContentType("image/jpg")
.build()
riversRef = mStorageRef?.child(file.lastPathSegment ?: "")
val uploadTask = riversRef?.putFile(file, metadata)
uploadTask?.addOnFailureListener { exception ->
upload_info_text.text = exception.message
}?.addOnSuccessListener {
upload_info_text.setText(R.string.upload_success)
}?.addOnProgressListener { taskSnapshot ->
val progress = (100.0 * taskSnapshot.bytesTransferred / taskSnapshot.totalByteCount).toInt()
upload_progress.progress = progress
if (progress >= 100) {
upload_progress.visibility = View.GONE
}
}
為了上傳所以我們再加入了一個 Button 來處理。
如果還沒選照片就點選上傳照片會出現提示訊息。
所以當我們串接好 Firebase,整個 onActivityResult 方法內的程式碼就會下面所呈現的。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (resultCode) {
Activity.RESULT_OK -> {
val filePath: String = ImagePicker.getFilePath(data) ?: ""
if (filePath.isNotEmpty()) {
imgPath = filePath
Toast.makeText(this@MainActivity, imgPath, Toast.LENGTH_SHORT).show()
Glide.with(this@MainActivity).load(filePath).into(pick_img)
} else {
Toast.makeText(this@MainActivity, R.string.load_img_fail, Toast.LENGTH_SHORT).show()
}
}
ImagePicker.RESULT_ERROR -> Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show()
}
}
因此當我們一執行程式找到對應的照片,按下確定要上傳,卻跳出以下的訊息。
user does not have permission to access
原來是 Firebase 內的 Storage 規格其實是有限制,一定要經過認證的帳戶才可以隨意存取,所以必須到 Firebase 的後台部分,點選你的專案,旁邊有一個規則。
你會看到。
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
因此我們要把規則部分拿掉。
改成以下的程式碼。
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
它會顯示一些警告的訊息。
此時只需要忽略這些警告訊息,再回到我們的程式進行一次的上傳動作,你就會發現程式可以正常的上傳了。
接著我們到 Firebase 的主控台上面去看,記得切到 storage,你會看到我們選擇的照片已經被上傳到 Firebase 了。
上傳進度條
如果只是單純選照片,那麼其實可以更花俏,Firebase 提供了上傳的進度條讓你使用,
在前面我的 ref putFile以後會回傳一個 UploadTask 的物件,
透過這個物件我們可以使用它的 Callback method,前面已經使用了 OnSuccessListener 以及 OnFailureListener 來處理我們的上傳結果,
這邊就可以使用 OnProgressListener 來監控我們的上傳進度。
private fun uploadImg(path: String) {
val file = Uri.fromFile(File(path))
val metadata = StorageMetadata.Builder()
.setContentDisposition("universe")
.setContentType("image/jpg")
.build()
riversRef = mStorageRef?.child(file.lastPathSegment ?: "")
val uploadTask = riversRef?.putFile(file, metadata)
uploadTask?.addOnFailureListener { exception ->
upload_info_text.text = exception.message
}?.addOnSuccessListener {
upload_info_text.setText(R.string.upload_success)
}?.addOnProgressListener { taskSnapshot ->
val progress = (100.0 * taskSnapshot.bytesTransferred / taskSnapshot.totalByteCount).toInt()
upload_progress.progress = progress
if (progress >= 100) {
upload_progress.visibility = View.GONE
}
}
}
透過 OnProgressListener 的 Callback method 我們可以得知上傳的進度,再由剛剛所宣告的 ProgressBar 來顯示我們的進度條。
上傳完成就顯示上傳成功的字串。
暫停、繼續以及取消上傳圖片
上傳照片有時候會想做暫停、繼續或者取消的動作,就可以透過以下三個方法來處理。
uploadTask.pause()
uploadTask.resume()
uploadTask.cancel()
上傳圖片的 Metadata
- Metadata
檔案其實可以再附加一些資訊,
你可以透過 Metadata 的方式來把這些資訊加入至檔案
Uri file = Uri.fromFile(new File(path));
StorageMetadata metadata = new StorageMetadata.Builder()
.setContentDisposition("universe")
.setContentType("image/jpg")
.build();
StorageReference riversRef = mStorageRef.child(file.getLastPathSegment());
UploadTask uploadTask = riversRef.putFile(file, metadata);
- Custom Metadata
你也可以自行定義 Metadata
metadata = new StorageMetadata.Builder()
.setCustomMetadata("location", "Yosemite, CA, USA")
.setCustomMetadata("activity", "Hiking")
.build();
- Metadata 其他參數
其他 Metadata 參數的呼叫方法
https://firebase.google.com/docs/storage/android/file-metadata#file_metadata_properties
下載圖片並且呈現
讓我們再調整一下畫面,多一個上傳的按鈕。
上傳可以如此簡單,同理下載也是相同的道理,如果想要下載剛剛上傳的圖片,可以透過 StorageReference 的參考來進行下載,如果你剛剛有上傳圖片,那麼 StorageReference 就會存在一個 Firebase 的參照,另外 Firebase 有結合 Glide,
因此,其實可以直接把 StorageReference 物件傳給 Glide 就可以呈現圖片,
所以我們可以透過 Firebase 工具包直接引用 Glide。
compile 'com.firebaseui:firebase-ui-storage:x.y.z'
建立了以下的方法,就可以透過這個方法進行上傳。
private fun downloadImg(ref: StorageReference?) {
if (ref == null) {
Toast.makeText(this@MainActivity, R.string.plz_upload_img, Toast.LENGTH_SHORT).show()
return
}
ref.downloadUrl.addOnSuccessListener {
Glide.with(this@MainActivity)
.load(imgPath)
.into(download_img)
download_info_text.setText(R.string.download_success)
}.addOnFailureListener {
exception -> download_info_text.text = exception.message
}
}
如果下載的時候還沒有上傳過照片,那麼 StorageReference 的物件就會是空的,因此,會顯示以下的訊息。
如果已經上傳過照片,那麼在下載完成就會呈現在上傳照片的下方,並且顯示下載成功。
刪除檔案
當然如果你擁有權限也可以對 Firebase 進行刪除,目前我們把權限全部打開,因此,所有 App 的使用者都可以對檔案刪除 (之後會透過 Auth 的方式來進行權限的控管),在 UI 上增加一個刪除按鈕,當按下按鈕則會呼叫 deleteImg 這個方法,做法跟上傳還有下載大同小異。
private fun deleteImg(ref: StorageReference?) {
if (ref == null) {
Toast.makeText(this@MainActivity, R.string.plz_upload_img, Toast.LENGTH_SHORT).show()
return
}
ref.delete().addOnSuccessListener {
Toast.makeText(this@MainActivity, R.string.delete_success, Toast.LENGTH_SHORT).show() }.addOnFailureListener { exception ->
Toast.makeText(this@MainActivity, exception.message, Toast.LENGTH_SHORT).show() }
}
如果刪除成功則會顯示訊息。
如果 Firebase 上面已經沒有這個檔案了,則會顯示提示訊息。
到 Firebase 上面看會發現圖片已經被刪除。
這樣就是一個簡單的 Firebase Storage 範例了。