국비교육(22-23)

77일차(1)/Android App(37) : AsyncTask(2)

서리/Seori 2023. 1. 27. 18:01

77일차(1)/Android App(37) : AsyncTask(2)

 

MainActivity

package com.example.step16asynctask;

import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
/*
    UI에 관련된 작업이 가능한 스레드는 오직 MainThread(UI Thread)에서만 가능하다.
    (아무데서나 UI를 조작할 수 없다.)
 */
public class MainActivity extends AppCompatActivity {
    EditText editText;
    TextView textView;

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

        //전송버튼
        Button sendBtn=findViewById(R.id.sendBtn);
        sendBtn.setOnClickListener(v -> {
            /*
                시간이 오래 걸리거나 혹은 실행 시간이 불확실한 작업은
                Main thread(UI thread) 에서 하면 안 된다.
             */
            //비동기 task 객체를 생성해서
            SendTask task=new SendTask();
            //execute() 메소드를 호출해서 작업을 시작한다.
            task.execute("hello", "...", "bye!");
        });
        //EditText의 참조값을 필드에 저장
        editText=findViewById(R.id.editText);
        Button testBtn=findViewById(R.id.testBtn);
        testBtn.setOnClickListener(v -> {
            //새로운 스레드에서 어떤 작업을 하고 작업이 끝나면 그 스레드 안에서 EditText에 문자열을 출력하려고 한다. 가능할까?
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //3초가 소요되는 어떤 작업이라고 가정
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //EditText에 문자열을 출력해보자
                    //editText.setText("작업이 종료되었습니다."); //ui 스레드에서 ui를 업데이트할 수 없으니

                    //handler 객체에 메시지를 보내서 ui를 업데이트하도록 한다.
                    handler.sendEmptyMessage(0);
                }
            }).start();
        });
        //TextView의 참조값을 필드에 저장하기
        textView=findViewById(R.id.textView);
        Button startBtn=findViewById(R.id.startBtn);
        startBtn.setOnClickListener(v -> {
            //비동기 Task 시작하기
            new CountTask().execute("바나나","딸기","복숭아");
        });
    }

    class CountTask extends AsyncTask<String, Integer, String>{

        @Override
        protected String doInBackground(String... strings) {
            //strings는 String[] 이다. 전달된 파라미터는 배열에 순서대로 저장되어 있다.
            String p1=strings[0]; //바나나
            String p2=strings[1]; //딸기
            String p3=strings[2]; //복숭아

            int count=0;
            //반복문을 10번 돌면서 카운트를 센다.
            for(int i=0; i<10; i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++; //카운트를 증가시키고
                publishProgress(count); //count 값을 발행한다.
            }
            //작업의 결과라고 가정
            String result="Success!";
            //리턴해주면
            return result;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //values는 Integer[] 이다. 0번 방에 카운트 값이 들어있다.
            //여기는 UI 스레드이기 때문에 UI 업데이트 가능
            //textView.setText(values[0].toString());
            textView.setText(Integer.toString(values[0])); //이렇게도 가능하다.
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            textView.setText(s);
        }
    }


    //필드에 Handler 객체를 생성해서 참조값을 넣어둔다. 단, handleMessage() 메소드를 재정의해서
    Handler handler=new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            //여기는 UI 스레드이기 때문에 UI관련 작업을 할수있다.
            editText.setText("작업이 종료되었습니다.");
        }
    };


    /*
        비동기 작업을 도와줄 클래스 설계하기
        1. AsyncTask 추상 클래스를 상속받는다.
        2. AsyncTask<파라미터 type, 진행중 type, 결과 type> 에 맞게끔
           Generic 클래스를 잘 정의한다.
        3. doInBackground() 메소드를 오버라이드한다.
        4. 추가로 필요한 메소드가 있으면 추가로 오버라이드한다.
     */
    public class SendTask extends AsyncTask<String, Void, Void>{

        //백그라운드에서 작업할 내용을 여기서 해준다(새로운 스레드에서 할 작업)
        @Override
        protected Void doInBackground(String... strings) {
            //여기는 UI 스레드가 아니다!! 즉 UI를 업데이트할 수 없다.

            //String... 은 String[] 로 간주해서 사용하면 된다.
            Messenger.sendMessage(strings[0]);
            //작업에 결과가 있다면 return 해주면 되고
            return null;
        }
        //doInBackGround() 메소드 안에서 publishProgress()
        @Override
        protected void onProgressUpdate(Void... values) {
            super.onProgressUpdate(values);
            //여기도 역시 UI 스레드이다.
        }

        // doInBackground() 메소드가 리턴하면 자동으로 호출되는 메소드
        @Override
        protected void onPostExecute(Void unused) {
            super.onPostExecute(unused);
            //여기는 UI 스레드이기 때문에 UI에 관련된 작업을 마음대로 할 수 있다.
            new AlertDialog.Builder(MainActivity.this)
                    .setMessage("작업성공")
                    .create()
                    .show();
        }
    }
}

 

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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="무언가 입력해 보세요..."
        android:id="@+id/editText"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/sendBtn"
        android:text="전송"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/testBtn"
        android:text="테스트 버튼"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/startBtn"
        android:text="카운트 시작"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00FF00"
        android:textSize="30sp"
        android:gravity="center"
        android:text="0"
        android:id="@+id/textView"/>

</LinearLayout>

 

- 이 어플이 폰에 설치되어 있다가,
  앱의 아이콘을 클릭하게 되면 활성화된다(초록)

- 앱이 활성화될때 처음 호출되는 메소드는 런처 액티비티의 onCreate()

- onCreate로 실행의 흐름이 들어온다. 이 실행의 흐름을 스레드라고 부른다.

- 안드로이드 앱을 실행하려면 최소 1개의 스레드가 필요한데,
  이것을 Main thread / UI thread 라고 한다.

 


- onCreate()에 최초에 실행순서가 들어와서 실행되고, 종료되고 나면(빨강)

  아래 버튼에 클릭 등 이벤트가 들어올 때 또 실행 흐름이 들어온다.(파랑)

 → 이것은 동일한 스레드이다.

 

- UI에 관련된 작업이 가능한 스레드는 오직 MainThread(UI Thread) 뿐이다.
- 아무데서나 UI를 조작할 수 없다. 원칙으로 정해져있다.

 

- 테스트 버튼 생성해서 다른 스레드에서도 UI에 관여할 수 있는지 테스트

 

 

** Thread 사용법

 - 새 스레드 객체를 생성해서 runnable type을 넣어주고,

 .start() 를 호출하면 이곳이 새로운 스레드가 된다.

 

 

- 빨강색은 UI thread, 파랑색은 새 스레드.

- 새 스레드에서는 원칙적으로 문자열을 출력하거나 알림을 띄울 수 없다. 동작이 제한되어 있다.

- UI를 조작하는것은 오직 MainThead에서만 가능하다.

 

- 버튼을 누르는 것까지는 UI Thread의 흐름이다.

- onCreate에서 사용했던 스레드가 다시 순서가 들어오는 것이다.

- new Thread() 로 새 스레드를 생성하는 시점에서부터 새 스레드가 시작된 것!

 

 

- 새 스레드에서 UI 관련된 코드를 넣어서 동작시키면?? 이렇게 앱이 종료되어 버린다.

- 에러 로그를 보면 오직 오리지널 스레드(MainThread)에서만 View에 관여할 수 있다고 나온다.

- 앱이 종료되어버려서 사용자의 요청에 대응할 수 없다. UI를 업데이트할 수 없다.

 

- 이 경우 대처방법은? 필드에 핸들러를 생성한다.

 

- 익명클래스에서 override 하기

 

 

- sendEmptyMessage() 메소드를  호출하면 간접적으로 이 메소드가 호출된다.

- 이 메소드가 호출되는 스레드는 메인 스레드이다.

- UI 스레드에서는 UI를 업데이트할 수 없으니, 

  필드에 Handler 메소드를 만들어놓고, 핸들러에다가 메시지를 보내는 것이다.

- 핸들 메시지를 보낸다 → 핸들러는 메인 스레드이므로 저기서는 UI 업데이트가 가능하다.

 

- EditText를 필드로만들고, handler메소드 안에서 UI 작업을 한다.

 

- editText에 출력된다. UI가 업데이트되었다!!

 


 

* 안드로이드 개발 툴에서 Deprecate 된다는것...

- 사용하다가 문제 발견 → 제거(Deprecate) → 대체할 객체 사용 → 문제 발견 → 제거 → 일단 다른 대체제라도 써봐...

- handler 에서 Asynctask 으로. 이제 Asynctask도 제거되었으면 다음은..?

 

- Handler 사용의 불편함. 핸들러 만들고 객체도 보내고 ...

- 작업이 중간중간 업데이트해야 할 수도 있고 끝났을 때 결과물을 또 보내야 할 수도 있는데

  핸들러를 사용하는 건 번거로운 작업이다. 코드가 꼬인다.

- 그래서 대체재로 나왔던 것이 AsyncTask이다.

 

- AsyncTask 객체 생성, execute() 메소드 실행 → doinBackGround 로 자동으로 순서 들어옴 → 이 안에서 새로운 스레드가 실행됨

- 시간이 오래 걸리거나 불확실한 작업은 이 새로운 스레드에서 해준다.

 

- 결과값이 있으면 리턴해준다. 지금은 void

 

- 결과값이 있으면 onPostExecute() 로 들어간다.

- 이곳은 UI Thread 이다. 여기서는 작업의 결과를 가지고 UI를 업데이트할 수 있다.

 

- 3개의 <> 제너릭 타입 : 각각 파라미터, 진행중, 결과 타입

- 비동기작업을 시작시키면서 값을 전달해야 할 때 타입을 결정

 

- 이것이 파라미터의 타입을 결정한다.

 

- 진행중 타입 : 작업을 하는 중간중간에 사용하는 타입

- 작업 중간중간에 보고하는 것이 있을 수 있다. publishProGress() 메소드로 출력한다.

 

- 이 메소드를 사용하면 작업 중간중간에도 UI를 업데이트시켜줄 수 있다.

 

- 진행중 타입을 integer로 바꿨다고 가정하면, 

 작업 중간중간 숫자 타입의 값을 onProgressUpdate() 에서 출력해줄 수 있다.

 

- AsyncTask는 자동으로 메소드가 호출되면서 그 메소드가 UI 스레드라는 점이 장점이다.

  따로 핸들러를 사용하지 않아도 되어서 편하다.

 

- 결과타입(최종적으로 리턴되는 것의 타입) : onPostExecute() 에 값을 전달할 수 있다.

 


 

- 새 textView 만들기

- 클릭하면 TextView의 숫자가 1초마다 1씩 숫자가 늘어나고 10이 되면 알림 보내기

 

- 작업 중간중간 값 발행이 필요하다. UI의 업데이트가 필요하다!

 

- CountTask 클래스 생성해서 AsyncTask 상속

- textView, 버튼 참조값 얻어오고 버튼에 리스너 지정 

- doinBackGround() 메소드를 만들어서 별도의 스레드로 진행

 

 

- 객체를 생성한 다음 excute() 메소드를 호출하면 된다.

 그러면 아래에 패러미터 값이 전달되면서 실행된다.

- strings는 String[ ] 이다. 전달된 파라미터는 배열에 순서대로 저장되어 있다.

 

- 카운트를 증가시키고 UI에 표시해주어야 하는데, 여기는 새 스레드라서 UI 수정을 못한다.

- publishProgress(count); 로 카운트 값을 발행한다. (진행 중에)

- 1초에 한번씩 count 값이 publish된다.

 

- 아래에서 onProgressUpdate 를 오버라이드

→ 여기는 MainThread여서 UI 업데이트가 가능하다.

 

- 제너릭<> 안의 타입은 클래스 타입으로 넣어주어야 한다. int 는 쓸 수 없고 Integer 라고 써야 한다.

 

 

- 이 안에 들어가는 int 인자는 resId 리소스 정수값을 가리킨다. 이것은 결국 String이다.(R.id.xxx 로 입력된다)

- int를 받는다고 되어있어도 이 안에 values[0] 등을 넣으면 오류가 발생한다.

 

textView.setText(values[0].toString());

textView.setText(Integer.toString(values[0]));

 

- 이런 식으로 Integer를 String으로! 둘중 하나로 쓰면 된다. 

 

- 값이 1초마다 한번씩 values에 전달되고 textview에 출력된다.

 

- 결과 데이터 result 는 이 onPostExecute() 메소드로 들어온다.

 

 

- 백그라운드에서 작업하면서도 UI가 바로바로 업데이트된다.

- AsyncTask의 대표적인 예제

 

 

- 코틀린의 코루틴(Coroutines)을 이용하면 이런 비동기 작업을 쉽게 할 수 있다.

- java의 새 라이브러리를 익히는 것보다 코틀린 코딩을 활용하는 게 쉬울지도..!