[Android] AsyncTask와 카운트다운 타이머 앱

Android/Android · 2020. 7. 30. 03:50
반응형

https://survivalcoding.com/p/android_basic

 

될 때까지 안드로이드

될 때까지 안드로이드에 수록된 예제의 라이브 코딩 해설

survivalcoding.com

위 서적을 참고하였습니다.

 

 

 

 저번 포스트에선 스레드와 핸들러에 대해 알아봤습니다. 이번 시간에는 AsyncTask에 대해 알아보겠습니다.

 

 스레드와 핸들러를 많이 사용하면 코드가 복잡해지고, 가독성이 떨어지는 경우가 생깁니다. 이를 대비해 안드로이드는 스레드와 핸들러를 하나의 작업 형태로 추상화한 AsyncTask 클래스가 존재합니다. 복잡한 스레드와 핸들러를 많이 사용하기보단, AsyncTask만 제대로 구현할 줄 안다면, 스레드 구현에 있어서 문제 될 일이 없다고 봐도 됩니다.

 

 AsyncTask는 메인 스레드에서 쉽게 시작 가능하며, 백그라운드에서 오래 걸리는 작업을 하고, 스레드와 핸들러 없이 UI를 갱신할 수 있도록 도와주는 일종의 헬퍼(Helper) 클래스입니다. 

 

 

 먼저, AsyncTaskGeneric type입니다. AsyncTask는 상속받는 클래스를 작성하고, 세 가지 제네릭 타입을 정의해야 합니다. 각각의 제네릭에는 다음과 같은 의미가 있습니다.

 

class DownLoadTask extends AsyncTask<Params, Progress, Result>

 

 1. Params : task가 실행되며, 함께 전달해야 하는 파라미터.(ex. 다운로드 주소)

 2. Progress : 백그라운드 작업이 수행되는 동안 발생되는 프로그래스 타입.(ex. 진행률 %)

 3. Result : 백그라운드 작업이 끝나고 그 결과의 타입.(ex. 최종 전송 용량)

 - 사용하지 않는 제네릭 타입은 Void로 설정하면 됩니다.

 

 

 다음은 AsyncTask를 작성할 때 필요한 콜백 메서드입니다.

 

 1. onPreExecute() : 메인 스레드에서 동작. task가 실행된 직후에 호출(백그라운드 작업 전, doInBackground(Params...) 호출 전)

 

 2. doInBackground(Params...) : 작업 스레드에서 동작. onPreExecute()의 수행 종료 시 호출. 오래 걸리는 작업을 이 메서드에서 백그라운드 작업으로 수행. 도중에 UI를 변경하려면 publishProgress(Progress...) 메서드를 호출하면 onProgressUpdate(Progress...) 메서드 호출됨. 반환 값은 onPostExecute(Result)로 전달.

 

 3. onProgressUpdate(Progress...) : 메인 스레드에서 동작. doInBackground(Paramas)에서 publishProgress(Progress...) 메서드 호출 시 이 메서드가 호출됨. 백그라운드 작업 간에 UI를 변경할 때 사용.

 

 4. onPostExecute(Result) : 메인 스레드에서 동작. doInBackground(Params...) 메서드가 종료된 후 호출. 작업에 대한 종료 시 처리할 것이 있다면 여기서 처리.

 

AsyncTask 흐름도

 

 

 이제 이전에 스레드와 핸들러로 작성했던 앱을 이제 AsyncTask로 변경하고, 취소 기능도 추가해보겠습니다.

 

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    private ProgressBar progressBar;
    private DownLoadTask downLoadTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView)findViewById(R.id.tv);
        progressBar = (ProgressBar)findViewById(R.id.pb);
        // 취소 버튼
        findViewById(R.id.cbtn).setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(downLoadTask != null && !downLoadTask.isCancelled()) {
                    downLoadTask.cancel(true);
                }
            }
        });
    }

    public void startDownload(View view) {
        downLoadTask = new DownLoadTask();
        downLoadTask.execute();
    }

    class DownLoadTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... voids) {
            for(int i=0; i<=100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                final int percent = i;
                final int percent1 = i+1;
                // 여러개의 값 전달 가능
                publishProgress(percent, percent1);
                if(isCancelled()) {
                    break;
                }
            }
            // onPostExecute() 메서드로 전달
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            // ... 타입은 여러개의 값을 전달받을 수 있음.
            // values는 배열로 저장.
            textView.setText(values[0] +"%\n"+ values[1] +"%");
            progressBar.setProgress(values[0]);
        }

        @Override
        protected void onCancelled() {
            Toast.makeText(getApplicationContext(), "취소 됨", Toast.LENGTH_SHORT).show();
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            Toast.makeText(getApplicationContext(), "완료 됨", Toast.LENGTH_SHORT).show();
        }
    }
}

 

 

 별 특별히 어려운 부분은 보이지 않습니다. 스레드의 취소 버튼을 추가했습니다. 여기서 ... 타입은 0개 이상의 값을 받을 수 있는 타입입니다. 이 값들은 배열에 담기게 되며, values[0], values[1], ... 이렇게 구분할 수 있습니다.

 

 이렇게 AsyncTask에 대해 알아봤습니다. AsyncTask에도 몇 가지 규칙이 존재합니다.

 

 1. AsyncTask의 인스턴스와 execute() 메서드는 반드시 메인 스레드에서 생성 및 실행돼야 한다.

 

 2. onPreExecute(), onPostExecute(Result), doInBackground(Params...), onProgressUpdate(Progress...) 콜백 메서드들은 직접 호출하면 안 된다.

 

 3. AsyncTask의 인스턴스는 한 번만 실행할 수 있다.

 

 이러한 규칙들이 있습니다. 3번의 경우, 

 

DownloadTask task = new DownloadTask();
task.execute();
task.execute();

 

 이렇게 호출하면 오류가 생길 수 있습니다. 그래서

 

new DownloadTask().execute();
new DownloadTask().execute();

 

 이렇게 실행해야 오류가 발생하지 않습니다. 그런데 여기서 문제가 하나 더 있습니다. 여러 개의 AsyncTask동시에 호출하게 하려면 어떻게 해야 할까요? AsyncTask순서대로 실행됩니다. 그렇기에 위 코드에서 첫 번째 줄의 onPostExecute(Result...) 메서드가 호출되고 두 번째 줄의 AsyncTask가 실행됩니다. 그래서 동시에 사용하기 위해 다음과 같은 코드로 작성합니다.

 

new AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0);
new AsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, 0);

 

 이렇게 하면 여러 개의 AsyncTask병렬로 실행할 수 있습니다.

 

 

 이제 어느 정도 AsyncTask에 익히셨으니 간단한 카운트다운 타이머 앱을 만들어보겠습니다.

 

 

 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="10"
        android:textSize="100sp"
        android:layout_margin="3dp"
        android:gravity="center"/>

    <Button
        android:id="@+id/start_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="3dp"
        android:text="시작"
        android:textSize="20sp"/>

    <Button
        android:id="@+id/cancel_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="3dp"
        android:text="취소"
        android:textSize="20sp"/>
</LinearLayout>

 

 

 MainActivity.java

public class MainActivity extends AppCompatActivity {

    private Button startBtn, cancelBtn;
    private TextView textView;
    private StartCountdown startCountdown;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        startBtn = (Button)findViewById(R.id.start_btn);
        cancelBtn = (Button)findViewById(R.id.cancel_btn);
        textView = (TextView)findViewById(R.id.tv);

        startBtn.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "카운트다운 시작", Toast.LENGTH_SHORT).show();
                startCountdown = new StartCountdown();
                startCountdown.execute();
            }
        });

        cancelBtn.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(startCountdown != null && !startCountdown.isCancelled()) {
                    startCountdown.cancel(true);
                }
            }
        });
    }

    class StartCountdown extends AsyncTask<Void, Integer, Void> {

        @Override
        protected void onPreExecute() {
            String time = "10";
            textView.setText(time);
        }

        @Override
        protected Void doInBackground(Void... voids) {
            for(int i=10; i>=0; i--) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                final int time = i;
                publishProgress(time);
                if(isCancelled()) {
                    break;
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            textView.setText(String.valueOf(values[0]));
        }

        @Override
        protected void onCancelled() {
            Toast.makeText(getApplicationContext(), "취소", Toast.LENGTH_SHORT).show();
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            Toast.makeText(getApplicationContext(), "완료", Toast.LENGTH_SHORT).show();
        }
    }
}

 

 

결과

 

 감사합니다.

반응형