配合分支:day14/start,Repo: 按我
鐵人賽已經快到一半囉,前面幾天提到了哪些觀念呢?
var,和不可變動兩種 val。後面會接變數可以存的資料格式是什麼,叫型態(Type),例如文字就存成 String。?
Activity 就是繼承自 ComponentActivity,建立在別人已經寫好的基礎,站在巨人的肩膀上做擴充。雖然現在每次開啟程式,ID 已經固定下來了。代表每次啟動,都能拿到相同的資料。這裡衍伸了其他的問題:有 ID 代表照片一定存在嗎?答案是否定的。
就像查看店家資訊,Google Map 就算是地址存在,也不代表現在還在營業,有可能已經倒了。也就是說,就算照片日記有了某張照片的位置,也不保證那個位置有照片存在。
今天就來強化這件事,為了避免使用者將照片刪除。可以在新增今天的日記時,就將照片複製到「App 的儲存空間」裡頭 (這也是有些 App 會越來越肥的原因呵呵)。順道複習前幾天教過的概念,像是 sealed class 之類的。
新增 OwnedFile 代表存到 App 儲存空間的檔案。
data class OwnedFile(val fileName: String) : DiaryPhoto()
先來更新寫入到本機的 JSON 資訊,一樣要做向下相容。

新增 OwnedFiles 部分,讀取的時候也需要特別注意有這個新的類別。

這就完成了寫入 JSON 的部分。
需要新增一個 class 來做以下兩件事情:
這一些動作與 Android 系統有關,所以會多花一些篇幅來解釋程式碼。首先,先新增一個 LocalphotoStore class,需要送 Context 進來(因為跟 Android 系統有關)。
class LocalPhotoStore(context: Context) {
// 👇🏼 App 儲存空間,目前是照片的資料夾啦
private val photoDirectory = context.filesDir
// 👇🏼 Android 要透過 content resolver 來取資料
private val contentResolver = context.contentResolver
}
新增一個 open 方法,這個 Method 讓其他人取得 InputStream (輸入資料串流)。會利用這個 InputStream 來轉成 bitmap 圖片。
fun open(ownedFile: DiaryPhoto.OwnedFile): InputStream {
return photoDirectory.resolve(ownedFile.fileName).inputStream()
}
import 圖片就比較麻煩了,要先取得目標圖片的 InputStream。接著,在 App 儲存空間的新檔案,要開啟 OutputStream(輸出資料串流)。將原檔案的輸入串流複製到新檔案的輸出串流即可。
想像兩間圖書館,圖書館 A 有一張照片,圖書館 B 也想要館藏。兩間圖書館有傳真機,這時圖書館 A 透過掃描器掃描圖片後,用一個線路把資料一邊掃描一邊提供出去(InputStream)。而圖書館 B 的傳真機收到訊號後,會用同一個線路作為 (OutputStream) 將照片列印出來。(現在還有人知道傳真機嗎⋯)
無論結果如何,最後都要把 stream 關閉(stream.close())。
fun import(
externalReference: DiaryPhoto.ExternalReference, // 👈🏼 他必須是一個外部檔案
diaryId: String
): DiaryPhoto.OwnedFile {
val fileName = "$diaryId.photo" // 👈🏼 用 id 作為檔案名稱
val localFile = photoDirectory.resolve(fileName) // 👈🏼 在 App 儲存空間,建立一個檔案
// 👇🏼 開啟外部檔案的 inputStream
val inputStream = contentResolver.openInputStream(Uri.parse(externalReference.uriString))
?: throw FileNotFoundException(externalReference.uriString)
// 🖨️ 開啟我們這裡接收的 outputStream
val outputStream = localFile.outputStream()
try {
inputStream.copyTo(outputStream) // 📀 把外部檔案的 stream 倒進去目標檔案的 outputStream
} finally {
inputStream.close()
outputStream.close()
}
return DiaryPhoto.OwnedFile(fileName)
}
有了這個 class,存取 App 空間的照片就方便多囉!接著,寫入新的日記時,就把檔案複製到 App 儲存空間即可。

DiaryPhotoImage 多了一個種類,這邊還沒有處理。需要新增一個 ComposeView 來預覽 App 儲存空間的照片。依照 Day 13 的做法,這邊把用遠古的 ImageView 來讀取 InputStream 比較方便。
@Composable
private fun OwnedPhotoImage(
fileName: String,
contentDescription: String,
modifier: Modifier
) {
val context = LocalContext.current
val photoStore = LocalPhotoStore(context)
AndroidView(
factory = { imageContext ->
ImageView(imageContext)
},
update = { imageView ->
imageView.contentDescription = contentDescription
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val ownedFile = DiaryPhoto.OwnedFile(fileName)
// 🖨️ 透過 LocalPhotoStore Open 方法來取得 InputStream
val inputStream = photoStore.open(ownedFile)
val bitmap = try {
BitmapFactory.decodeStream(inputStream) // 👈🏼 decode stream 成 bitmap
} finally {
inputStream.close() // 👀 記得最後都要關閉 inputStream
}
imageView.setImageBitmap(bitmap)
},
modifier = modifier.clipToBounds()
)
}
有了這個 OwnedPhotoImage 後,再來調整 DiaryPhotoImage 即可。

DiaryPhoto 類型,代表已存進 App 儲存空間的檔案,用 fileName 當自己的檔名。Uri 的入口;import 時靠它拿到來源檔。open 用它給畫面解圖,import 用它當來源。copyTo 把輸入串流倒進輸出串流;無論成功失敗都要在 finally 裡 close(),線路才不會一直開著。InputStream 解成 bitmap,OwnedPhotoImage 才能顯示 App 裡的照片。