Giới thiệu
Android cung cấp **Internal Storage** (bộ nhớ trong) riêng biệt cho mỗi app tại context.filesDir (/data/data/package.name/files/). File ở đây **private** - app khác không truy cập được, và bị xóa khi uninstall app. Bài viết hướng dẫn ghi/đọc file text cơ bản và best practices.
Các bước thực hiện
Bước 1: Tạo dự án mới
Android Studio → File → New Project → Empty View Activity.
Bước 2: Thiết kế giao diện (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ghi nội dung vào file text"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="120dp"
android:gravity="top|start"
android:hint="Nhập nội dung..."
android:inputType="textMultiLine"
android:padding="12dp"
android:background="@drawable/edittext_rounded" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/btnWrite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="Ghi file" />
<Button
android:id="@+id/btnRead"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Đọc file" />
</LinearLayout>
<TextView
android:id="@+id/tvFileContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="24dp"
android:padding="12dp"
android:background="@android:color/darker_gray"
android:textSize="14sp"
tools:text="Nội dung file sẽ hiển thị ở đây" />
</LinearLayout>
Bước 3: Code ghi/đọc file (MainActivity.kt)
package com.example.internalstorage
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.internalstorage.databinding.ActivityMainBinding
import java.io.File
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val FILE_NAME = "my_text_file.txt"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnWrite.setOnClickListener { writeToFile() }
binding.btnRead.setOnClickListener { readFromFile() }
}
private fun writeToFile() {
val content = binding.etInput.text.toString()
if (content.isBlank()) {
Toast.makeText(this, "Vui lòng nhập nội dung", Toast.LENGTH_SHORT).show()
return
}
// Cách 1: openFileOutput (API cũ, vẫn hoạt động)
try {
openFileOutput(FILE_NAME, MODE_PRIVATE).use { outputStream ->
outputStream.write(content.toByteArray())
}
Toast.makeText(this, "Đã ghi file thành công", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this, "Lỗi ghi file: ${e.message}", Toast.LENGTH_SHORT).show()
e.printStackTrace()
}
/*
// Cách 2: File API hiện đại (khuyến nghị)
val file = File(filesDir, FILE_NAME)
file.writeText(content) // Kotlin extension - tự handle encoding UTF-8
*/
}
private fun readFromFile() {
// Cách 1: openFileInput
try {
val content = openFileInput(FILE_NAME).bufferedReader().use { it.readText() }
binding.tvFileContent.text = content
} catch (e: Exception) {
binding.tvFileContent.text = "Chưa có file hoặc lỗi đọc: ${e.message}"
}
/*
// Cách 2: File API
val file = File(filesDir, FILE_NAME)
if (file.exists()) {
binding.tvFileContent.text = file.readText()
} else {
binding.tvFileContent.text = "File không tồn tại"
}
*/
}
}
Các chế độ mở file (openFileOutput)
| Mode | Mô tả |
|---|---|
MODE_PRIVATE (0) | Mặc định. File private cho app. Ghi đè nếu tồn tại. |
MODE_APPEND (32768) | Mở file để append (ghi tiếp cuối file). |
MODE_WORLD_READABLE | Deprecated API 17, không dùng. Dùng FileProvider/share Intent. |
MODE_WORLD_WRITEABLE | Deprecated API 17, không dùng. |
Đường dẫn file thực tế
- Internal Storage:
/data/data/com.example.internalstorage/files/my_text_file.txt - Lấy đường dẫn:
File(filesDir, FILE_NAME).absolutePath - Liệt kê file:
fileList()hoặcfilesDir.listFiles() - Xóa file:
deleteFile(FILE_NAME)hoặcfile.delete()
Internal vs External vs Cache Storage
| Loại | Đường dẫn | Quyền | Xóa khi | Dùng cho |
|---|---|---|---|---|
| Internal Storage | filesDir | Private | Uninstall app | Data nhạy cảm, config, DB |
| Cache Dir | cacheDir | Private | Hệ thống cần dung lượng / Uninstall | Temp file, image cache |
| External Storage (Public) | Environment.getExternalStoragePublicDirectory() | Cần permission (API < 29) | User xóa thủ công | Media, document chia sẻ |
| External Storage (App-specific) | getExternalFilesDir() | Không cần permission (API 19+) | Uninstall app | File lớn, backup |
Best Practices
- Luôn dùng
use { }(Kotlin) hoặctry-with-resources(Java) để tự đóng stream. - Ghi file lớn/nhiều lần: dùng background thread (coroutine IO dispatcher).
- Encoding: Luôn dùng **UTF-8** (mặc định của Kotlin
writeText/readText). - File nhạy cảm (token, password):考虑加密 (EncryptedFile từ Security library).
- Kiểm tra dung lượng trước khi ghi file lớn:
StatFshoặc catchIOException(No space left).
Mã hóa file nhạy cảm (Security Library)
// build.gradle.kts
implementation("androidx.security:security-crypto:1.1.0-alpha06")
val masterKey = MasterKey.Builder(this)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedFile = EncryptedFile.Builder(this,
File(filesDir, "secret.txt"),
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
// Ghi
encryptedFile.openFileOutput().use { it.write("Secret data".toByteArray()) }
// Đọc
val secret = encryptedFile.openFileInput().bufferedReader().use { it.readText() }