如何使用 Firebase - 操作 Storage(kotlin)

如何使用 Firebase - 操作 Storage(kotlin)

情境

如何使用 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 範例了。