Android 개발 - Sizuha/devdog GitHub Wiki

프로젝트 디렉토리 구조 및 명명 규칙 (예시)

Source (java)

기본 구조

  • model
  • ui
    • 각각의 Activity
      • adapter
      • dialog
      • fragment
    • shared
      • adapter
      • dialog
      • fragment
  • lib

model

데이터에 대한 추상모델. 주로 DB나 서버 통신으로 교환되는 내용등을 담게 된다.

ui

실질적인 View역할은 res 디렉토리에 있는 Layout XML이 담당하고 있으므로, 여기서는 View와 Model을 연결하고, User Interface 부분을 구현하는 데 중점을 둔다. 가령 Activity, Fragment, Adapter, Dialog 관련 내용이 여기에 해당한다. 각 Activity 별로 하위 디렉토리를 구성한다.

adapter

ListView 등, Adapter 개념이 들어가 있는 UI 요소들의 Adapter 부분을 정의.

dialog

Android에서 제공하는 기본 형식이 아닌, 커스텀 된 형식의 Dialog UI를 정의.

fragment

Activity 내에서 부분적으로 구현되는 Fragment들을 정의. 필요한 경우 각 Fragment 별로 하위 디렉토리를 구성할 수도 있다.

shared

App 전반에 걸쳐 공통적으로 사용될 수 있는 UI를 정의.

lib

UI요소와는 거의 관계없는 독립된 로직을 정의.

Resource (res)

Android 개발환경에서 자체적으로 정의된 구조를 따라야 함으로 특별히 추가할 내용은 없다. 다만, 리소스 디렉토리 내에서 서브 디렉토리 구조를 가지지 못하므로 대체로 xml 파일이름의 접두어로 성격을 구분지어야 할 필요가 있다.

리소스 파일명 명명 규칙

단어 사이의 구분은 낙타법이 아닌 밑줄(_)로 연결한다. 또한 전부 소문자로 쓴다.

drawable / mipmap

접두어 설명
ic_ Icon
dw_ XML로 정의되는 Drawable (백터 도형 등)

이미지 파일 등을 import한 경우, 가능한 파일명을 그대로 남겨두지만 단어는 밑줄(_)로 구분하고, 전부 소문자가 되도록 한다.

values

Android에서 정의하는 기본 파일명은 다음과 같다.

  • colors.xml
  • strings.xml
  • styles.xml

가능하면 기본 파일에서 정의하되, 분리가 필요한 경우는, 기본 파일명에 밑줄(_)을 붙여서 쓴다.
예) colors_darktheme.xml

리소스 ID 명명 규칙

ID는 낙타법 표기를 원칙으로 한다.

UI 컨트롤

접두어를 붙이고 낙타법으로 표기. 예) @+id/tvUserName

접두어 UI 컨트롤
ll LinearLayout
fl FrameLayout
sv ScrollView
lv ListView
rv RecyclerView
tv TextView
et EditText
btn Button, ImageButton
cb CheckBox, CheckedTextView
rb RadioButton
rg RadioGroup
swi Switch
spi Spinner
pv ProgressView
wb WebView
iv ImageView
cal CalendarView
dp DatePicker
tp TimePicker
v View

일부 예외를 제외하고, 기본적으로 각 단어의 앞글자로 조합. 한단어의 경우는 앞3글자.

그외 UI 요소들은 클래스 이름의 앞 단어(중복되는 경우 그 다음 단어까지)부분을 소문자로 해서 접두어로 쓴다.

소스 파일 내 명명 규칙

대원칙

  • 영어 표기가 원칙이다. (한국어, 일본어 발음을 그대로 옮기지 않는다)
  • 타입명(Class, Enum 등)은 대문자로 시작한다.
  • 그외는 전부 소문자로 시작한다.
  • 낙타법 표기를 원칙으로 한다.

Class

  • 기본적으로 클래스와 소스 파일은 1:1로 대응한다. 따라서 파일명도 클래스명과 동일해야 한다.

Method

  • 동사로 시작한다.

시스템 정보

StringBuffer buf = new StringBuffer();

buf.append("VERSION.RELEASE {"+Build.VERSION.RELEASE+"}");
buf.append("\\nVERSION.INCREMENTAL {"+Build.VERSION.INCREMENTAL+"}");
buf.append("\\nVERSION.SDK {"+Build.VERSION.SDK+"}");
buf.append("\\nBOARD {"+Build.BOARD+"}");
buf.append("\\nBRAND {"+Build.BRAND+"}");
buf.append("\\nDEVICE {"+Build.DEVICE+"}");
buf.append("\\nFINGERPRINT {"+Build.FINGERPRINT+"}");
buf.append("\\nHOST {"+Build.HOST+"}");
buf.append("\\nID {"+Build.ID+"}");

Log.d("build",buf.toString()); 

App 정보

// App Package Name
final String app_id = getApplicationContext().getPackageName();

// App Version.
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
String version = pInfo.versionName;

UI

Android UI 개발 참조.

IO

Android 입출력 처리 참조.

통신

Android 통신 참조.

위치정보

Google Play Location Service

Date/Time

현재 날짜/시간 가져오기

Calendar calendar = Calendar.getInstance();

날짜/시간 셋팅

Calendar calendar = Calendar.getInstance();
calendar.clear(); // 중요: set 하기 전에 반드시 clear를 해줄 것!
calendar.set(year, month, day, hour, min, sec);
long datetime = calendar.getTimeInMillis();

Date/String 변한

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DataUtil {
    private static SimpleDateFormat api_date_format = new SimpleDateFormat("yyyy-MM-DD");

    public static Date toDate(String date_str) throws ParseException {
        return api_date_format.parse(date_str);
    }

    public static String toString(Date date) {
        return api_date_format.format(date);
    }
}

SimpleDateFormat 패턴정의 예제

Date and Time Pattern Result
"yyyy.MM.dd G 'at' HH:mm:ss z" 2001.07.04 AD at 12:08:56 PDT
"EEE, MMM d, ''yy" Wed, Jul 4, '01
"h:mm a" 12:08 PM
"hh 'o''clock' a, zzzz" 12 o'clock PM, Pacific Daylight Time
"K:mm a, z" 0:08 PM, PDT
"yyyyy.MMMMM.dd GGG hh:mm aaa" 02001.July.04 AD 12:08 PM
"EEE, d MMM yyyy HH:mm:ss Z" Wed, 4 Jul 2001 12:08:56 -0700
"yyMMddHHmmssZ" 010704120856-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSZ" 2001-07-04T12:08:56.235-0700
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX" 2001-07-04T12:08:56.235-07:00
"YYYY-'W'ww-u" 2001-W27-3

TimeZone Format

  • z: General time zone
  • Z: RFC 822 time zone
  • X: ISO 8601 time zone

Threading

AsyncTask

http://developer.android.com/reference/android/os/AsyncTask.html

 private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }

     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }

     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }
new DownloadFilesTask().execute(url1, url2, url3);

AsyncTask's generic types

The three types used by an asynchronous task are the following:

  1. Params, the type of the parameters sent to the task upon execution.
  2. Progress, the type of the progress units published during the background computation.
  3. Result, the type of the result of the background computation.

Not all types are always used by an asynchronous task. To mark a type as unused, simply use the type Void:

 private class MyTask extends AsyncTask<Void, Void, Void> { ... }

Events

When an asynchronous task is executed, the task goes through 4 steps:

  1. onPreExecute()
  2. doInBackground(Params...)
  3. onProgressUpdate(Progress...)
  4. onPostExecute(Result)

주의할 점

  • The AsyncTask class must be loaded on the UI thread. This is done automatically as of JELLY_BEAN.
  • The task instance must be created on the UI thread.
  • execute(Params...) must be invoked on the UI thread.
  • Do not call onPreExecute(), onPostExecute(Result), doInBackground(Params...), onProgressUpdate(Progress...) manually.
  • The task can be executed only once (an exception will be thrown if a second execution is attempted.)

Reource

리소스를 참조해 코드에서 텍스트 크기 설정

<dimen name="text_medium">18sp</dimen>

Set the size in code:

textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.text_medium));

More Resource Types

http://developer.android.com/guide/topics/resources/more-resources.html

Color

// getColor(int id) is deprecated now, this must be used :
ContextCompat.getColor(context, R.color.your_color);

Cloud Messaging (Push)

FCM: Firebase Cloud Messaging 참조

Alarm 구현

/* MainActivity class */
    public void setAlram(String title, String message, int year, int month, int day, int hour, int min, int sec) {
        Intent alarmIntent = new Intent(this, AlarmReceiver.class);
        alarmIntent.putExtra("title", title);
        alarmIntent.putExtra("message", message);

        if (Config.DEBUG_MODE) {
            Log.i("PROJECT_D", "alarm: " + title + " | "+ message);
        }

        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

        Calendar startTime = Calendar.getInstance();
        startTime.set(Calendar.YEAR, year);
        startTime.set(Calendar.MONTH, month-1);
        startTime.set(Calendar.DAY_OF_MONTH, day);
        startTime.set(Calendar.HOUR_OF_DAY, hour);
        startTime.set(Calendar.MINUTE, min);
        startTime.set(Calendar.SECOND, sec);

        long alarmStartTime = startTime.getTimeInMillis();
        alarmManager.set(AlarmManager.RTC_WAKEUP, alarmStartTime , pendingIntent);
    }
public class AlarmReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        String title = intent.getStringExtra("title");
        String message = intent.getStringExtra("message");

        UILib.showNoti(context, title, message, message);
    }
}

public class UILib {
    static final int REQUEST_CODE_MAIN_ACTIVITY = 1000;
    public static int noti_id_start = 0;

    public static void showNoti(Context context, String title, String message, String ticker) {
        Intent intent = new Intent(context, MainActivity.class);

        // Intent の作成
        PendingIntent contentIntent = PendingIntent.getActivity(
                context, REQUEST_CODE_MAIN_ACTIVITY, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        //// LargeIcon の Bitmap を生成
        //Bitmap largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.app_icon);

        // NotificationBuilderを作成
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context.getApplicationContext());
        builder.setContentIntent(contentIntent);
        // ステータスバーに表示されるテキスト
        builder.setTicker(ticker);
        // Notificationを開いたときに表示されるタイトル
        builder.setContentTitle(title);
        // Notificationを開いたときに表示されるサブタイトル
        builder.setContentText(message);

        // アイコン
        builder.setSmallIcon(android.R.drawable.ic_dialog_info);
        // Notificationを開いたときに表示されるアイコン
        //builder.setLargeIcon(largeIcon);

        // 通知するタイミング
        builder.setWhen(System.currentTimeMillis());
        // 通知時の音・バイブ・ライト
        builder.setDefaults(Notification.DEFAULT_SOUND
                | Notification.DEFAULT_VIBRATE
                | Notification.DEFAULT_LIGHTS);
        // タップするとキャンセル(消える)
        builder.setAutoCancel(true);

        // NotificationManagerを取得
        NotificationManager manager = (NotificationManager) context.getSystemService(Service.NOTIFICATION_SERVICE);
        // Notificationを作成して通知


        manager.notify(noti_id_start++, builder.build());

        if (noti_id_start >= Integer.MAX_VALUE) {
            noti_id_start = 0;
        }
    }
}

Graphics

Android Graphics 참조.

Google Play Service

최신 버전 확인

문제 및 해결

Pending Indent (Android 12, SDK 31 이상)

Pending Indent를 생성할 때, FLAG_IMMUTABLE 혹은 FLAG_MUTABLE 플래그가 반드시 지정되어 있어야 함. 일반적으로 FLAG_IMMUTABLE을 지정.

Runtime Permissions

Android 6.0 (Marshmallow, SDK 23) 이상에서는 AndroidManifest.xml 에서 권한을 지정하는 것만으로는 안된다.

일부 권한은 Runtime Permission 요청을 통해서 유저로부터 직접 확인을 받아야만 한다.

public static final class Permissions {
    public static final int REQUEST_PERMISSIONS = 777;
    private static String[] PERMISSIONS = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
    };

    public static void verifyPermissions(Activity activity) {
        if (android.os.Build.VERSION.SDK_INT < 23) return;

        boolean is_ok = true;
        for (String p : PERMISSIONS) {
            int check_self = ActivityCompat.checkSelfPermission(activity, p);
            if (check_self != PackageManager.PERMISSION_GRANTED) {
                is_ok = false;
            }
        }

        if (!is_ok) {
            // We don't have permission so prompt the user
            ActivityCompat.requestPermissions(
                    activity,
                    PERMISSIONS,
                    REQUEST_PERMISSIONS
            );
        }
    }
}

권한 요청에 대한 결과는 Activity의 onRequestPermissionsResult 이벤트로 확인 할 수 있다.

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == Permissions.REQUEST_PERMISSIONS) {
            for (int r : grantResults) {
                if (r != PackageManager.PERMISSION_GRANTED) {
                    Alert.showError(getContext(), getString(R.string.err_app_permissions), new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            finish();
                        }
                    });
                }
            }
        }
    }

formatted string을 strings.xml 리소스로 뺄 때의 문제

http://androidgamepark.blogspot.jp/2013/05/multiple-substitutions-specified-in-non.html

String.format() 등에서 사용하는 형식 문자열(formatted string)을 그대로 xml 리소스로 빼내면 다음과 같은 에러가 발생. Multiple substitutions specified in non-positional format; did you mean to add the formatted="false" attribute? 이유는 %문자가 xml에서 사용되는 특수 기호이기 때문. 다음과 같이 처리하자.

<!-- 이것은 에러 -->
<string name="msg_format_date">%4d年%02d月%02d日</string>

<!-- 이렇게 한다 -->
<string name="msg_format_date">%1$4d年%2$02d月%3$02d日</string>

1$, 2$ 등의 의미는 1번째 인자, 2번째 인자, ... 를 의미한다.

ConnectivityManager.getNetworkInfo() 문제

ICS 버전에서 ConnectivityManager의 getNetworkInfo() 호출이 _null_이 되는 경우가 있다.

ConnectivityManager cm = 
  (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);

3G 기능이 없는 기기일 때, 위 코드에서 ni는 null 값이 되버린다.
그러니 반드시 getNetworkInfo()의 반환값이 null 인지 먼저 확인하고 진행할 것.

MapView 추가할 때

MapActivity에서 super.onCreate()가 먼저 호출된 다음에 MapView가 추가되어야 오류가 발생하지 않는다.

layout.xml 등으로 MapView를 추가할 때도 setContentView() 호출이 supoer.onCreate() 다음에 되도록 주의.

Debug Certificate expired

어느날 프로젝트를 빌드하는데 Debug Certificate expired 오류가 뜬다.

  • 해결: clean project 후 rebuild를 시도. 그래도 저 오류가 뜬다면 debug.keystore 인증기간이 지난 것이다. (인증기간이 1년으로 설정되어 있다) 윈도우라면 Windows '사용자(Users)' 폴더 밑에 .android 라는 폴더에서 debug.keystore 파일을 지워보고 다시 빌드하면 된다.

네트워크 스레드 문제

Android 3.0 이상에서 android.os.NetworkOnMainThreadException 오류를 만났다면, 이것은 메인 스레드에서 네트워크 작업을 수행했기 때문이다.

3.0부터 안드로이드는 이같은 동작을 오류로 판단하고 강제 종료시킨다.

앱 우선순위 높이기

시스템 자원 부족으로 프로세스 킬이 진행될 때, 조금이라도 오래 살고 싶으면 Service(비록 아무일도 안하더라도)를 이용해서 앱의 우선순위(중요도)를 높여야 한다.

남용하지는 말자. 리소스를 많이 사용하는 게임이나 네비게이션, 영상 처리 등의 앱에서 사용하는게 좋을 듯.

서비스 작성(예시)

public class NokillService extends Service {
  @Override
  public IBinder onBind(Intent intent) {
    return null;
  }
  
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    setForegroundService();
    return Service.START_STICKY;
  }
  
  void setForegroundService() {
    final Intent it = new Intent(this, MainActivity.class);
    final PendingIntent pi = PendingIntent.getActivity(this, 0, it, 0);
    
    Notification noti = new NotificationCompat.Builder(this, "channel_id")
      .setContentTitle("AppTitle")
      .setContentText("running...")
      .setTicker("running...")
      .setSmallIcon(R.drawable.ic_launcher)
      .setContentIntent(pi)
      .build();
    
    startForeground(1, noti);
  }
}

중요한건, 서비스 내에서 startForeground()를 호출해서 foreground 서비스로 활성화되어야 한다는 것.

foreground 서비스로 등록하기 위해서는 Notification이 필수로 제공되어야 한다.

AndroidManifest.xml 등록

<service android:name="com.xxx.service.NokillService"/>

서비스 호출 및 종료(예시)

public void startNokillService() {
  nokillServ = new Intent(this, NokillService.class);
  startService(nokillServ);
}

public void stopNokillService() {
  if (nokillServ != null)
    stopService(nokillServ);
  
  nokillServ = null;
}

Tips

Email 형식 검사

    public final static boolean isValidEmail(CharSequence target) {
        if (target == null || target.length() < 3) {
            return false;
        } else {
            return android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches();
        }
    }

open URL

Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http:// . . ."));
startActivity(browserIntent);

Locale 설정 가져오기

Locale systemLocale = getResources().getConfiguration().locale;
String strDisplayCountry = systemLocale.getDisplayCountry();
String strCountry = systemLocale.getCountry();
String strLanguage = systemLocale.getLanguage();

네트워크 사용 가능 여부

// ネットワーク接続確認
public static boolean networkCheck(Context context){
    ConnectivityManager cm =  (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
    if (cm == null) return false;

    NetworkInfo info = cm.getActiveNetworkInfo();
    if( info != null ){
        return info.isConnected();
    } 
    else {
        return false;
    }
}

Mac Address 가져오기

// need permission: "android.permission.ACCESS_WIFI_STATE"
public static String getMacAddr(Context context) {
    WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    WifiInfo wInfo = wifiManager.getConnectionInfo();
    String mac = wInfo.getMacAddress();

    return mac;
}

화면 꺼짐(절전 모드) 방지

// Activity에서
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

카메라 혹은 갤러리에서 사진 가져오기

    public static void openImageBrowser(Activity from, int request_code) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("image/*");
        from.startActivityForResult(intent, request_code);
    }

    public static void openCamera(Activity from, int request_code, String img_filename) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(from.getPackageManager()) != null) {
            from.startActivityForResult(takePictureIntent, request_code);
        }
    }

    public static void openCameraOrSelect(final Activity from, final int request_code_camera, final int request_code_select) {
        final String title = "Open Photo";
        final CharSequence[] itemlist = {
                "Take a Photo",
                "Pick from Gallery"};

        AlertDialog.Builder builder = new AlertDialog.Builder(from)
            .setTitle(title)
            .setItems(itemlist, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    switch (which) {
                        case 0:// Take Photo
                            // Do Take Photo task here
                            openCamera(from, request_code_camera, "temp_pic.jpg");
                            break;
                        case 1:// Choose Existing Photo
                            // Do Pick Photo task here
                            openImageBrowser(from, request_code_select);
                            break;
                        default:
                            break;
                    }
                }
            });
        AlertDialog alert = builder.create();
        alert.setCancelable(true);
        alert.show();
    }

    public static String getFilepathFromMediaStore(Context context, Intent data) {
        final Uri uri = data.getData();

        String[] filepath = { MediaStore.Images.Media.DATA };
        Cursor cursor = context.getContentResolver().query(uri, filepath, null, null, null);
        cursor.moveToFirst();

        int columnIndex = cursor.getColumnIndex(filepath[0]);
        String file_path = cursor.getString(columnIndex);
        cursor.close();

        Logger.i("data: " + data.getData().toString());
        Logger.i("image file path: " + file_path);

        return file_path;
    }

    public static String getRealPathFromURI_API19(Context context, Uri uri){
        if (android.os.Build.VERSION.SDK_INT < 19) return null;

        if ("com.android.providers.downloads.documents".equals(uri.getAuthority())) {
            // ダウンロードからの場合
            String id = DocumentsContract.getDocumentId(uri);
            Uri docUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
            Cursor cursor = context.getContentResolver().query(docUri, new String[]{MediaStore.MediaColumns.DATA}, null, null, null);
            if (cursor.moveToFirst()) {
                File file = new File(cursor.getString(0));
                return file.getAbsolutePath();
            }
        }

        String filePath = "";
        String wholeID;
        String id;

        try {
            wholeID = DocumentsContract.getDocumentId(uri);
            Logger.d("wholeID: " + wholeID);

            final String[] result = wholeID.split(":");
            id = result[1];
        }
        catch (Exception e) {
            Logger.d(e.toString());
            return null;
        }

        String[] column = { MediaStore.Images.Media.DATA };

        // where id is equal to
        String sel = MediaStore.Images.Media._ID + "=?";

        Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                column, sel, new String[]{ id }, null);

        int columnIndex = cursor.getColumnIndex(column[0]);
        if (cursor.moveToFirst()) {
            filePath = cursor.getString(columnIndex);
        }
        cursor.close();
        return filePath;
    }


    public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) {
        if (android.os.Build.VERSION.SDK_INT < 11 || android.os.Build.VERSION.SDK_INT > 18)
            return null;

        String[] proj = { MediaStore.Images.Media.DATA };
        String result = null;

        CursorLoader cursorLoader = new CursorLoader(
                context,
                contentUri, proj, null, null, null);
        Cursor cursor = cursorLoader.loadInBackground();

        if(cursor != null){
            int column_index =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            cursor.moveToFirst();
            result = cursor.getString(column_index);
        }
        return result;
    }

라이프 사이클 감시

https://stackoverflow.com/questions/52369540/what-is-lifecycle-observer-and-how-to-use-it-correctly/70808707#70808707

imports

implementation 'androidx.lifecycle:lifecycle-process:X.Y.Z'

Application의 라이프 사이클 획득.

ProcessLifecycleOwner.get().getLifecycle()

감시방법1:

lifecycle.addObserver(object: DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        TODO()
    }
    override fun onPause(owner: LifecycleOwner) {
        TODO()
        super.onPause(owner)
    }
})

감시방법2:

lifecycle.addObserver(object: LifecycleEventObserver {
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        when (event) {
            Lifecycle.Event.ON_RESUME -> TODO()
            Lifecycle.Event.ON_PAUSE -> TODO()
            else -> { }
        }
    }
})
⚠️ **GitHub.com Fallback** ⚠️