https://recipes4dev.tistory.com/114

1. 안드로이드에서 텍스트(text) 파일 읽고 쓰기.

안드로이드 앱에서, 사용자로부터 텍스트를 입력받아 화면에 표시하는 것은 매우 흔한 일입니다. 입력받은 텍스트는 앱 내부에서 가공되어 변형된 형태로 표시되기도 하고, 또는 다른 여러 화면에서 재사용되기도 하죠. 그런데 화면에 표시되는 텍스트 데이터를 메모리 변수에만 저장하고 있으면, 앱이 종료될 때 그 데이터가 모두 사라지게 됩니다.


그래서 앱 종료 후에도 계속적으로 유지되어야 할 데이터는 반드시 어딘가에 저장해둔 다음, 앱이 다시 실행될 때, 저장된 데이터를 로드하여 사용할 수 있도록 만들어야 합니다. 이 때 데이터를 저장하기 위해 개발자가 선택할 수 있는 가장 손 쉬운 방법은 바로 파일을 사용하는 것입니다. 안드로이드 기기 내부 저장소(Internal Storage)에서 앱이 접근할 수 있는 디렉토리 하위에 파일을 생성한 다음, 파일 입출력 API 함수를 통해 데이터를 저장하거나 로드할 수 있죠.


바이너리(binary) 데이터를 파일에 읽고 쓰는 방법에 대해서는 [안드로이드 바이너리(binary) 파일 입출력 1]에서 그 개념과 절차를 설명하고, [안드로이드 바이너리(binary) 파일 입출력 2]에서 실질적인 예제 코드를 살펴보았습니다. 그리고 텍스트 파일에 대한 입출력 방법은 [안드로이드 텍스트(text) 파일 입출력 1]에서 설명하였습니다.


그럼 이제, 간단한 앱 예제 작성을 통해 안드로이드에서 텍스트 데이터를 파일에 읽고 쓰는 방법에 대해 알아보도록 하겠습니다.

2. 안드로이드 파일 입출력 하기에 앞서 알아두어야 할 사항.

2.1 안드로이드 앱에서 파일을 일고 쓸 수 있는 디렉토리 경로.

안드로이드 앱에서 파일을 읽고 쓰기 위해서는, 해당 디렉토리에 대한 읽기 권한 또는 쓰기 권한이 허용되어야 합니다. 만약 권한이 없어도 마음대로 파일을 읽고 쓸 수 있다면, 앱이 가진 고유한 데이터가 유출되거나 시스템 파일을 마음대로 바꿀 수 있게 되어 제대로 된 시스템 운영을 할 수 없게 되죠.


[안드로이드 바이너리(binary) 파일 입출력 2]에서 설명했듯이, 안드로이드 앱이 기본적으로 읽고 쓸 수 있는 디렉토리 경로는 "/data/user/[USER-NO]/[PACKAGE]/files/"(또는 "/data/data/[PACKAGE-NAME]/files/") 입니다. 하지만 직접 전체 경로 이름을 지정하여 파일에 접근하는 것은 바람직하지 않고, 대신 Context(android.content.Context)의 getFilesDir() 함수를 사용하여 해당 경로를 얻어와야 합니다.

    File file = new File(getFilesDir(), FILENAME) ;

2.2 줄(line) 단위로 읽기, 줄 바꿈 쓰기.

바이트(byte) 단위로 처리되는 바이너리 데이터는, 각 바이트 값이 사용되는데 있어 딱히 용도가 구분되어 있지 않기 때문에, "XX 값이 나오기 전까지의 데이터를 읽는 함수"가 존재하지 않습니다. 그래서 데이터를 읽는 read() 함수에는 반드시 읽어들일 데이터의 크기를 명시하게 되어 있죠.

    int read() ;                           // 1 바이트 데이터 읽기.
    int read(byte[] b, int off, int len) ; // off 위치에서 len 만큼 b 버퍼에 읽기.
    int read(byte[] b) ;                   // b.length만큼 b 버퍼에 읽기.

하지만 문자(char) 단위로 처리되는 텍스트 데이터는, 화면에 문자로 표시되는 값 외에 "줄 바꿈" 이라는 조금은 특수한(?) 용도의 값이 구분되어 있어서, "'줄 바꿈'문자가 나올 때까지 데이터를 읽는 함수"가 존재합니다. 즉, 고정된 버퍼 크기가 아닌, 가변 길이의 줄 단위로 데이터를 읽는 것이 가능하다는 것이며, 이는 BufferedReader 클래스의 readLine() 함수가 그 역할을 수행합니다.

    String readLine() ;     // 한 줄 텍스트 데이터를 읽어 String 타입으로 리턴.

또한 "줄 바꿈" 문자는 텍스트 데이터를 쓰는 경우에도 유사하게 적용될 수 있습니다. 텍스트 데이터를 파일에 쓴 다음 임의의 줄을 바꿀 때, BufferedWriter 클래스의 newLine() 함수를 사용하면 됩니다.

    void newLine() ;        // 줄 바꿈 문자 쓰기.

3. 안드로이드 텍스트(text) 파일 입출력 예제.

이제 예제 앱 작성을 통해, 안드로이드에서 텍스트 파일을 읽고 쓰는 방법을 살펴보겠습니다.


예제의 화면은 아래 그림과 같이 구성됩니다.

파일 입출력 예제 화면 구성도


예제 화면의 EditText에 문자열을 입력하고, "ADD" 버튼을 누르면 입력된 문자열은 리스트뷰에 추가됩니다. 그리고 동시에 리스트뷰의 모든 항목은 파일에 저장됩니다.


ListView의 아이템을 하나 선택하고 "Del" 버튼을 선택하는 경우도 마찬가지입니다. 선택된 아이템은 ListView에서 삭제되고, 동시에 리스트뷰의 모든 항목은 파일에 저장됩니다.


마지막으로 앱을 종료한 다음 재실행하면, 파일에 저장된 텍스트 데이터를 읽어들여 리스트뷰에 표시합니다.

3.1 MainActivity의 Layout 구성.

예제 앱을 작성하기 위해 첫 번째로 해야 할 일은 MainActivity의 Layout을 작성하는 것입니다. 앞서 설계한 예제 화면의 구성도에 따라 아래와 같이 XML 코드를 작성합니다.

[STEP-1] "activity_main.xml" - MainActivity의 Layout 구성.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/content_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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.recipes4dev.examples.filetextioexample1.MainActivity"
    tools:showIn="@layout/activity_main">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ListView
            android:id="@+id/listview1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:choiceMode="singleChoice" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <EditText
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:id="@+id/editTextNew"/>

            <Button
                android:id="@+id/buttonAdd"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="10dp"
                android:text="Add" />

            <Button
                android:id="@+id/buttonDel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Delete" />

        </LinearLayout>

    </LinearLayout>
</RelativeLayout>

3.2 파일 이름과 리스트뷰를 위한 클래스 변수 선언.

다음, 리스트뷰를 사용하기 위한 변수들과, 파일이름을 위한 상수를 선언합니다.

[STEP-2] "MainActivity.java" - MainActivity에 변수 추가.
public class MainActivity extends AppCompatActivity {
    private final String fileName = "items.list" ;

    private ListView listview ;
    private ArrayAdapter adapter ;
    private ArrayList<String> items = new ArrayList<String>() ;

    // ... 코드 계속

}

3.3 리스트뷰와 어댑터 초기화.

이제 MainActivity의 onCreate() 함수에서 리스트뷰와 어댑터를 초기화 합니다.

[STEP-3] "MainActivity.java" - onCreate() 함수에서 리스트뷰 초기화.
public class MainActivity extends AppCompatActivity {

    // 코드 계속 ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
        // 코드 계속 ...
        listview = (ListView) findViewById(R.id.listview1) ;
        adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_single_choice, items) ;

        listview.setAdapter(adapter) ;

        // ... 코드 계속
    }
}

3.4 리스트뷰 아이템을 파일에 저장하는 함수 작성.

리스트뷰의 아이템이 추가 또는 삭제되면, 리스트뷰의 아이템을 파일에 저장하도록 만듭니다. 이는 saveItemsToFile() 이라는 함수로 만들어놓고, 각 버튼의 클릭 이벤트 핸들러에서 호출해서 사용합니다. 특히 리스트뷰의 아이템에는 "줄 바꿈" 문자가 포함되어 있지 않기 때문에, 아이템을 줄 단위로 저장하기 위해 문자열 저장 후 newLine() 함수를 호출한 것을 확인하시기 바랍니다.

[STEP-4] "MainActivity.java" - 리스트뷰 아이템을 파일에 저장하는 saveItemsToFile() 추가 및 작성.
public class MainActivity extends AppCompatActivity {

    // 코드 계속 ...

    private void saveItemsToFile() {
        File file = new File(getFilesDir(), fileName) ;
        FileWriter fw = null ;
        BufferedWriter bufwr = null ;

        try {
            // open file.
            fw = new FileWriter(file) ;
            bufwr = new BufferedWriter(fw) ;
            
            for (String str : items) {
                bufwr.write(str) ;
                bufwr.newLine() ;
            }

            // write data to the file.
            bufwr.flush() ;

        } catch (Exception e) {
            e.printStackTrace() ;
        }

        try {
            // close file.
            if (bufwr != null) {
                bufwr.close();
            }

            if (fw != null) {
                fw.close();
            }
        } catch (Exception e) {
            e.printStackTrace() ;
        }
    }
}

3.5 리스트뷰 아이템을 파일로부터 읽어들이는 함수 작성.

앱 시작 시, 파일로부터 줄 단위의 텍스트 데이터를 읽어들여, 리스트뷰에 표시하는 기능을 수행하는 함수를 작성합니다. 아이템을 저장할 때 줄 단위로 아이템을 구분하였으므로, 한 줄의 텍스트를 읽기 위해 readLine() 함수를 사용한 것에 주의하세요.

[STEP-5] "MainActivity.java" - 파일로부터 리스트뷰 아이템을 읽어들이는 loadItemsFromFile() 추가 및 작성.
public class MainActivity extends AppCompatActivity {

    // 코드 계속 ...

    private void loadItemsFromFile() {
        File file = new File(getFilesDir(), fileName) ;
        FileReader fr = null ;
        BufferedReader bufrd = null ;
        String str ;
        
        if (file.exists()) {
            try {
                // open file.
                fr = new FileReader(file) ;
                bufrd = new BufferedReader(fr) ;

                while ((str = bufrd.readLine()) != null) {
                    items.add(str) ;
                }

                bufrd.close() ;
                fr.close() ;
            } catch (Exception e) {
                e.printStackTrace() ;
            }
        }
    }

    // ... 코드 계속
}

3.6 앱 시작 시, 파일에서 데이터 로딩하여 리스트뷰에 표시하기.

마지막으로, 앱 시작 시, 최종적으로 파일에 저장된 데이터를 읽어들여 리스트뷰에 표시되도록 만듭니다.

[STEP-6] "MainActivity.java" - 앱 시작 시, onCreate() 함수에서 파일 내용 읽어들여 리스트뷰에 표시.
public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
        // 코드 계속 ...

        // 파일에서 데이터를 읽어들여 리스트뷰에 표시.
        loadItemsFromFile() ;
        adapter.notifyDataSetChanged();

        // ... 코드 계속
    }
}

3.7 아이템 추가(Add) 버튼 클릭 이벤트 처리

앱을 실행하면 최초에 아이템 추가(Add) 버튼은 비활성(disabled) 상태로 표시됩니다. 그리고 앱의 에디트텍스트에 텍스트가 입력되면, 아이템 추가(Add) 버튼은 활성(enabled) 상태로 변경됩니다.
활성 상태로 변경된 아이템 추가(Add) 버튼이 눌려지면, 에디트텍스트에 입력된 문자열을 리스트뷰에 추가하고 파일에 그 내용을 갱신합니다.

[STEP-7] "MainActivity.java" - onCreate() 함수에서 아이템 추가(Add) 버튼 이벤트 처리.
public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
        // 코드 계속 ...

        Button buttonAdd = (Button) findViewById(R.id.buttonAdd) ;
        buttonAdd.setEnabled(false) ; // 초기 버튼 상태 비활성 상태로 지정.
        buttonAdd.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                EditText editTextNew = (EditText) findViewById(R.id.editTextNew) ;
                String strNew = (String) editTextNew.getText().toString() ;

                if (strNew.length() > 0) {
                    // 리스트에 문자열 추가.
                    items.add(strNew);

                    // 에디트텍스트 내용 초기화.
                    editTextNew.setText("") ;

                    // 리스트뷰 갱신
                    adapter.notifyDataSetChanged();

                    // 리스트뷰 아이템들을 파일에 저장.
                    saveItemsToFile() ;
                }
            }
        });

        // ... 코드 계속
    }
}

3.8 아이템 삭제(Del) 버튼 클릭 이벤트 처리

리스트뷰에서 아이템을 선택하고 아이템 삭제(Del) 버튼을 누르면, 선택된 아이템을 리스트뷰에서 삭제하고 파일에 반영하도록 구현합니다.

[STEP-8] "MainActivity.java" - onCreate() 함수에서 아이템 삭제(Del) 버튼 이벤트 처리.
public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
        // 코드 계속 ...

        Button buttonDel = (Button) findViewById(R.id.buttonDel) ;
        buttonDel.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                int count ;
                int checkedIndex ;

                count = adapter.getCount() ;

                if (count > 0) {
                    // 리스트뷰에서 선택된 아이템 인덱스 얻어오기.
                    checkedIndex = listview.getCheckedItemPosition();
                    if (checkedIndex > -1 && checkedIndex < count) {
                        // 아이템 삭제
                        items.remove(checkedIndex) ;

                        // 리스트뷰 선택 초기화.
                        listview.clearChoices();

                        // 리스트뷰 갱신
                        adapter.notifyDataSetChanged();

                        // 리스트뷰 아이템들을 파일에 저장.
                        saveItemsToFile() ;
                    }
                }
            }
        });

        // ... 코드 계속
    }
}

3.9 입력 필드 내용 변경 시, 아이템 추가(Add) 버튼 상태 변경.

화면에 배치된 입력 필드인 에디트텍스트의 내용이 변경되면, 아이템 추가(Add) 버튼의 상태를 변경하는 코드를 작성합니다. 이 때, 에디트텍스트에 텍스트가 존재하면 버튼의 상태를 활성(enabled) 상태로 만들고, 텍스트가 비어 있으면 비활성(disabled) 상태로 만듭니다.

[STEP-9] "MainActivity.java" - onCreate() 함수에서 에디트텍스트 내용 변경 이벤트 처리.
public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        
        // 코드 계속 ...

        EditText editTextNew = (EditText) findViewById(R.id.editTextNew) ;
        editTextNew.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable edit) {
                Button buttonAdd = (Button) findViewById(R.id.buttonAdd) ;
                if (edit.toString().length() > 0) {
                    // 버튼 상태 활성화.
                    buttonAdd.setEnabled(true) ;
                } else {
                    // 버튼 상태 비활성화.
                    buttonAdd.setEnabled(false) ;
                }
            }

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }
        }) ;

        // ... 코드 계속
    }
}

4. 예제 실행 화면.

예제 코드를 작성한 다음 앱을 실행하면, 아래와 같은 화면이 표시됩니다.

파일 입출력 예제 실행 화면 1


화면의 에디트텍스트에 추가할 텍스트를 입력하고 "Add" 버튼을 누르면 입력한 텍스트가 리스트뷰에 표시됩니다.

파일 입출력 예제 실행 화면 2


여러 개의 아이템을 추가한 다음, 앱을 종료하고 다시 실행해도 리스트뷰에는 마지막 아이템들이 그대로 표시됩니다. 이는 삭제의 경우에도 마찬가지입니다.

파일 입출력 예제 실행 화면 3


5. 참고.

.END.