Android 개발 - Sizuha/devdog GitHub Wiki
기본 구조
- model
- ui
- 각각의 Activity
- adapter
- dialog
- fragment
- shared
- adapter
- dialog
- fragment
- 각각의 Activity
- lib
데이터에 대한 추상모델. 주로 DB나 서버 통신으로 교환되는 내용등을 담게 된다.
실질적인 View역할은 res 디렉토리에 있는 Layout XML이 담당하고 있으므로, 여기서는 View와 Model을 연결하고, User Interface 부분을 구현하는 데 중점을 둔다. 가령 Activity, Fragment, Adapter, Dialog 관련 내용이 여기에 해당한다. 각 Activity 별로 하위 디렉토리를 구성한다.
ListView 등, Adapter 개념이 들어가 있는 UI 요소들의 Adapter 부분을 정의.
Android에서 제공하는 기본 형식이 아닌, 커스텀 된 형식의 Dialog UI를 정의.
Activity 내에서 부분적으로 구현되는 Fragment들을 정의. 필요한 경우 각 Fragment 별로 하위 디렉토리를 구성할 수도 있다.
App 전반에 걸쳐 공통적으로 사용될 수 있는 UI를 정의.
UI요소와는 거의 관계없는 독립된 로직을 정의.
Android 개발환경에서 자체적으로 정의된 구조를 따라야 함으로 특별히 추가할 내용은 없다. 다만, 리소스 디렉토리 내에서 서브 디렉토리 구조를 가지지 못하므로 대체로 xml 파일이름의 접두어로 성격을 구분지어야 할 필요가 있다.
단어 사이의 구분은 낙타법이 아닌 밑줄(_)로 연결한다. 또한 전부 소문자로 쓴다.
접두어 | 설명 |
---|---|
ic_ | Icon |
dw_ | XML로 정의되는 Drawable (백터 도형 등) |
이미지 파일 등을 import한 경우, 가능한 파일명을 그대로 남겨두지만 단어는 밑줄(_)로 구분하고, 전부 소문자가 되도록 한다.
Android에서 정의하는 기본 파일명은 다음과 같다.
- colors.xml
- strings.xml
- styles.xml
가능하면 기본 파일에서 정의하되, 분리가 필요한 경우는, 기본 파일명에 밑줄(_)을 붙여서 쓴다.
예) colors_darktheme.xml
ID는 낙타법 표기를 원칙으로 한다.
접두어를 붙이고 낙타법으로 표기. 예) @+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 Package Name
final String app_id = getApplicationContext().getPackageName();
// App Version.
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
String version = pInfo.versionName;
Android UI 개발 참조.
Android 입출력 처리 참조.
Android 통신 참조.
Calendar calendar = Calendar.getInstance();
Calendar calendar = Calendar.getInstance();
calendar.clear(); // 중요: set 하기 전에 반드시 clear를 해줄 것!
calendar.set(year, month, day, hour, min, sec);
long datetime = calendar.getTimeInMillis();
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);
}
}
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
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);
The three types used by an asynchronous task are the following:
- Params, the type of the parameters sent to the task upon execution.
- Progress, the type of the progress units published during the background computation.
- 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> { ... }
When an asynchronous task is executed, the task goes through 4 steps:
- onPreExecute()
- doInBackground(Params...)
- onProgressUpdate(Progress...)
- 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.)
<dimen name="text_medium">18sp</dimen>
Set the size in code:
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.text_medium));
http://developer.android.com/guide/topics/resources/more-resources.html
// getColor(int id) is deprecated now, this must be used :
ContextCompat.getColor(context, R.color.your_color);
FCM: Firebase Cloud Messaging 참조
/* 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;
}
}
}
Android Graphics 참조.
최신 버전 확인
Pending Indent를 생성할 때, FLAG_IMMUTABLE 혹은 FLAG_MUTABLE 플래그가 반드시 지정되어 있어야 함. 일반적으로 FLAG_IMMUTABLE을 지정.
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();
}
});
}
}
}
}
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번째 인자, ... 를 의미한다.
ICS 버전에서 ConnectivityManager의 getNetworkInfo() 호출이 _null_이 되는 경우가 있다.
ConnectivityManager cm =
(ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
3G 기능이 없는 기기일 때, 위 코드에서 ni는 null 값이 되버린다.
그러니 반드시 getNetworkInfo()의 반환값이 null 인지 먼저 확인하고 진행할 것.
MapActivity에서 super.onCreate()가 먼저 호출된 다음에 MapView가 추가되어야 오류가 발생하지 않는다.
layout.xml 등으로 MapView를 추가할 때도 setContentView() 호출이 supoer.onCreate() 다음에 되도록 주의.
어느날 프로젝트를 빌드하는데 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;
}
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();
}
}
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http:// . . ."));
startActivity(browserIntent);
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;
}
}
// 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;
}
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 -> { }
}
}
})