먼저 Material Design 사이트에서 제공하는 예제 파일을 열어봤더니 아래와 같은 예제가 나온다.
자식 View는 반드시 2개만 가진다는 점을 기억하자.
순서는 content 자식 View가 먼저 나오고 drawer View는 나중에 나온다.
그러면 drawer View가 나중에 나오기만 하면 되는 것인가? 아니다.
<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <!-- As the main content view, the view below consumes the entire space available using match_parent in both dimensions. Note that this child does not specify android:layout_gravity attribute. --> <FrameLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" />
<!-- android:layout_gravity="start" tells DrawerLayout to treat this as a sliding drawer on the starting side, which is left for left-to-right locales. The navigation view extends the full height of the container. A solid background is used for contrast with the content view. android:fitsSystemWindows="true" tells the system to have DrawerLayout span the full height of the screen, including the system status bar on Lollipop+ versions of the plaform. --> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:background="#333" android:fitsSystemWindows="true" app:menu="@menu/navigation_view_content" app:itemIconTint="@color/emerald_translucent" app:itemTextColor="@color/emerald_text" app:itemBackground="@color/sand_default" app:itemTextAppearance="@style/TextMediumStyle" />
<!-- 1. 콘텐츠 영역--> <include layout="@layout/content_main" android:layout_width="match_parent" android:layout_height="match_parent" />
<!-- 2. 왼쪽 사이드 메뉴--> <include layout="@layout/nav_header_main" />
</androidx.drawerlayout.widget.DrawerLayout>
아래 nav_header_main.xml 파일을 보면 com.google.android.material.navigation.NavigationView 대신에 LinearLayout 으로 되어 있다. 꼭 com.google.android.material.navigation.NavigationView 를 사용하지 않아도 된다는 의미다.
android:layout_gravity="start" 를 추가해서 왼쪽 슬라이드 메뉴로 사용하겠다는 것만 포함되어 있다.
// com.google.android.material.navigation.NavigationView 를 구현했을 때 처리 (구현 안해도 됨) //navigationView = findViewById(R.id.nav_view); //navigationView.setItemIconTintList(null); //navigationView.setNavigationItemSelectedListener(this);
// content_main.xml 에서 지정한 버튼을 클릭하면 drawer 가 오픈되도록 함. FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { drawer_layout.openDrawer(GravityCompat.START); } });
}
// drawer 오픈된 상태에서 실행할 때 동작 이벤트 처리 public void NaviItemSelected(View view) { switch (view.getId()) { case R.id.btn_close: drawer_layout.closeDrawer(Gravity.LEFT, true); break; case R.id.btn_logout: backPressHandler.appShutdown(); break; } } }
구현을 위한 핵심사항만 살펴본 것이므로 실제 구현 예제는 다른 블로그를 찾아보면 도움될 것이다.
예전 예제들은 android.support.v4.widget.DrawerLayout 로 되어 있는데 androidx.drawerlayout.widget.DrawerLayout 로 변경되었다는 것만 알자.
public class Fragment1 extends Fragment { private View view;
private String parameter;
public static Fragment1 newInstance(String parameter){ Fragment1 fragment1 = new Fragment1(); Bundle args = new Bundle(); //Use bundle to pass data args.putString("parameter", parameter); instance.setArguments(args); //Finally set argument bundle to fragment return instance; }
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { parameter = getArguments().getString("parameter"); Log.e("FragParameter", parameter); } }
class ViewPagerAdapter extends FragmentPagerAdapter { private final List<Fragment> mFragmentList = new ArrayList<>(); private final List<String> mFragTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) { super(manager); }
@Override public Fragment getItem(int position) { return mFragmentList.get(position); }
@Override public int getCount() { return mFragmentList.size(); }
public void addFrag(Fragment fragment, String title) { mFragmentList.add(fragment); mFragTitleList.add(title); }
@Override public CharSequence getPageTitle(int position) { return mFragTitleList.get(position); } }
public class FirstFragment extends Fragment { // Store instance variables private String title; private int page;
// newInstance constructor for creating fragment with arguments public static FirstFragment newInstance(int page, String title) { FirstFragment fragmentFirst = new FirstFragment(); Bundle args = new Bundle(); args.putInt("someInt", page); args.putString("someTitle", title); fragmentFirst.setArguments(args); return fragmentFirst; }
// Store instance variables based on arguments passed @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); page = getArguments().getInt("someInt", 0); title = getArguments().getString("someTitle"); }
// Inflate the view for the fragment based on layout XML @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_first, container, false); TextView tvLabel = (TextView) view.findViewById(R.id.tvLabel); tvLabel.setText(page + " -- " + title); return view; } }
이 부분에는 GET/POST/PUT/DELETE 등 필요한 모든 함수를 선언하는 부분이다.
Call <> 사이에는 서버의 Json 형태에 따라 클래스 파일을 만들어서 넣어주면 된다.
/** * GET 방식, URL/getphoto.php/{code} 호출. * Data Type의 여러 개의 JSON을 통신을 통해 받음. * 주소값이 "http://www.abc.com/getphoto.php?code=1" 이 됨. * @param code 요청에 필요한 code * @return 다수의 Data 객체를 JSON 형태로 반환. */ @GET("/getphoto.php") Call<Data> getData(@Query("code") String code);
public class LoginActivity extends AppCompatActivity implements View.OnClickListener { Context context; EditText etId; EditText etPw; String userID; String userPW; String uID;// 스마트 기기의 대체 고유값
LoginAPI mloginService;
PermissionListener permissionlistener = new PermissionListener() { @Override public void onPermissionGranted() { initView(); }
@Override public void onPermissionDenied(ArrayList<String> deniedPermissions) { Toast.makeText(LoginActivity.this, "권한 허용을 하지 않으면 서비스를 이용할 수 없습니다.", Toast.LENGTH_SHORT).show(); } };
private void checkPermissions() { if (Build.VERSION.SDK_INT >= 23) {// 마시멜로(안드로이드 6.0) 이상 권한 체크 TedPermission.with(context) .setPermissionListener(permissionlistener) .setRationaleMessage("앱을 이용하기 위해서는 접근 권한이 필요합니다") .setDeniedMessage("앱에서 요구하는 권한설정이 필요합니다...\n [설정] > [권한] 에서 사용으로 활성화해주세요.") .setPermissions(new String[]{ android.Manifest.permission.READ_PHONE_STATE, android.Manifest.permission.READ_CALL_LOG, // 안드로이드 9.0 에서는 이것도 추가하라고 되어 있음. android.Manifest.permission.CALL_PHONE, // 전화걸기 및 관리 android.Manifest.permission.ACCESS_FINE_LOCATION }) .check();
Retrofit retrofit = new Retrofit.Builder() .baseUrl(Value.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client(createOkHttpClient()) .build();
mloginService= retrofit.create(LoginAPI.class); }
@Override public void onClick(View v) { userID = etId.getText().toString().trim(); userPW = etPw.getText().toString().trim(); uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID); mloginService.Login(userID, userPW, uID) .enqueue(new Callback<LoginResult>() { @Override public void onResponse(Call<LoginResult> call, Response<LoginResult> response) { // 네트워크 통신 성공 LoginResult result = response.body(); if(result.getResult().contains("success")){ Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); finish(); } else { showAlert(result.getResult(),result.getMessage()); } }
@Override public void onFailure(Call<LoginResult> call, Throwable t) { // 네트워크 통신 실패 } }); }
private OkHttpClient createOkHttpClient() { // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기 OkHttpClient.Builder builder = new OkHttpClient.Builder(); HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); builder.addInterceptor(interceptor); return builder.build(); }
public void showAlert(String title, String message) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(title); builder.setMessage(message) .setCancelable(false) .setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); } }); AlertDialog alert = builder.create(); alert.show(); } }
public interface LoginAPI {// 서버에 호출할 메소드를 선언하는 인터페이스 // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다. @FormUrlEncoded @POST(Value.URL_LOGIN) Call<LoginResult> Login( @Field("userId") String userId, @Field("userPw") String userPw, @Field("uuid") String uuid ); }
public classLoginResult { // 결과를 받을 모델 private String result; private String message;
public String getResult() { return result; }
public String getMessage() { return message; } }
public class Value extends AppCompatActivity { public static final String BASE_URL = "http://www.abc.com/mobile/"; public static final StringURL_LOGIN = "login.php"; }
예제는 결과를 받아서 RecyclerView 에 보여준다거나 후속 action 에 대한 건 기술하지 않았다.
이 예제는 그냥 복사해서 붙여넣기를 어떻게 하는지에 대한 설명이라고 보면 된다.
PHP 파일에 대한 사항은 기술하지 않는다. 이미 본 블로그 다른 예제를 보면 충분히 이해할 수 있으리라.
public class RetrofitAPI { private static Retrofit retrofit = null;
public static Retrofit getClient() { retrofit = new Retrofit.Builder() .baseUrl(Value.IPADDRESS) .addConverterFactory(GsonConverterFactory.create()) .client(createOkHttpClient()) .build();
return retrofit; }
private static OkHttpClient createOkHttpClient() { // 네트워크 통신 로그(서버로 보내는 파라미터 및 받는 파라미터) 보기 OkHttpClient.Builder builder = new OkHttpClient.Builder(); HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); builder.addInterceptor(interceptor); return builder.build(); } }
사용 예제
public interface RemoteService {// 서버에 호출할 메소드를 선언하는 인터페이스 // POST 방식으로 데이터를 주고 받을 때 넘기는 변수는 Field 라고 해야 한다. @FormUrlEncoded @POST(RetrofitUrl.URL_Verson) Call<VersionResult> lastVersion( @Field("os") String os );
if (Integer.parseInt(version) < Integer.parseInt(Response)) { // 서버 버전이 더 높으면 UpgradeProcess(); if(Build.VERSION.SDK_INT >= 26) { NonSecretApp_Setting(); // 출처를 알 수 없는 앱 설정 화면 띄우기 } else { startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)); } } else { AutoLoginProgress(); } }
@Override public void onFailure(Call<VersionResult> call, Throwable t) {
In android 10, couldn't get device id using permission "READ_PHONE_STATE".
getDeviceId() has been deprecated since API level 26. You can use an instance id from firebase e.g FirebaseInstanceId.getInstance().getId();. String deviceId = android.provider.Settings.Secure.getString( context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
삼성갤럭시 S10(Android 10) 에서 테스트한 결과 코드를 적어둔다.
기존 안드로이드 폰은 IMEI 값이 15자리 숫자로만 되어 있었는데
아래 코드에서 제공하는 deviceID값은 IMEI 값이 아닌 다른 값을 제공하는 거 같다.
개발자모드에서 수집한 deviceID 와 release 모드로 앱을 만든 것과 값이 다르더라.
cc64a5b80853bc8d
36f7660af1dc22f2
public static String getDeviceId(Context context) {
// 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요 String deviceId;
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } else { final TelephonyManager mTelephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { //권한 없을 경우 if (ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, Manifest.permission.READ_PHONE_STATE)) { // 사용자에게 해당 권한이 필요한 이유에 대해 설명 Toast.makeText(context, "앱 실행을 위해서는 전화 관리 권한을 설정해야 합니다.", Toast.LENGTH_SHORT).show(); } // 최초로 권한을 요청하는 경우 ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_PHONE_STATE} , 2); return null; } else { Log.v("TAG","Permission is granted"); if (mTelephony.getDeviceId() != null) { deviceId = mTelephony.getDeviceId(); } else { deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); } } } return deviceId; }
QR Code는 흑백 격자 무늬 패턴으로 정보를 나타내는 매트릭스 형식의 이차원 바코드이다.
QR코드는 1994년 일본 도요타 자동차 자회사인 덴소 웨이브가 도요타 자동차 전용 부품을 구별하기 위해 개발하면서 시작됐다. 기존 바코드 방식이 1차원적인 가로 선만으로는 담을 수 있는 정보의 양이 제한되기 때문에, 일정 면적에 정보를 담을 수 있는 2차원 코드를 개발한 것이다.
기존의 바코드가 20자 내외의 숫자 정보만 저장할 수 있지만, QR코드는 숫자 최대 7,089자, 문자 최대 4,296자를 저장할 수 있다.
QR코드는 정사각형 안 흑백 격자선 위로 다양한 무늬가 입혀진 것이다. QR코드는 크게 3가지 패턴으로 구성된다. 위치 찾기 패턴과 얼라인먼트 패턴, 셀 패턴 이다. 위치 찾기 패턴은 모든 QR코드의 세 모서리에 크게 자리 잡고 있는 사각형이다. 해당 기능은 QR코드를 인식기가 360도 어느 방향에서 감지하더라도 QR코드의 위치를 정확하게 파악, 빠른 정보 탐색이 가능하도록 만드는 일종의 나침반 기능을 한다. 이를 통해 데이터 인식 및 분석 속도가 빨라져 QR코드란 이름도 ‘빠른 응답’(Quick Response)에서 나왔다.
QR코드가 널리 쓰이면서 새로운 문제도 등장하고 있다. QR코드는 바코드에 비해 많은 정보를 담을 수 있어 QR코드에 악성코드나 유해 웹사이트 주소를 담아 유포하는 사례가 많아지고 있다. 검증된 기관이나 기업이 아닌 곳에서 제공하는 QR코드의 경우 접속할 때 신중해야 한다.
스마트폰의 사진촬영 기능으로 QR코드를 찍으면 QR코드에 담긴 정보에 따라 다양한 기능을 할 수 있다.
가장 많이 쓰는 기능은 특정 홈페이지로 보내는 것이다. 가령 제품 상자에 인쇄된 QR코드를 찍는 순간 제품을 자세히 소개하는 홈페이지를 보여주는 식이다. 인쇄매체를 가진 언론사는 기사 옆에 QR코드를 찍어 관련된 동영상을 쉽게 보여줄 수 있다.
인터넷에서 찾아서 테스트한 코드가 매우 오래된 코드라서 보안이 강화된 폰에서는 읽어내질 못하더라.
주황색으로 표시된 코드를 다른 코드로 찾아서 대체하면 해결될 것이다.
최신폰에서 동작되도록 하는 것은 개발자 각자의 몫으로....
미리 보기를 위한 팝업창 코드에는 전혀 문제가 없다는 것도 확인했다.
에러 때문에 좀 삽질을 하기는 했지만.
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "MainActivity"; Context mContext;
intent = new Intent(Intent.ACTION_GET_CONTENT); // or ACTION_OPEN_DOCUMENT intent.setType("*/*"); intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); }
try { startActivityForResult(Intent.createChooser(intent, "Select a File"), READ_REQUEST_CODE); } catch (android.content.ActivityNotFoundException ex) { Toast.makeText(this, "Please install a File Manager.",Toast.LENGTH_SHORT).show(); } }
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case READ_REQUEST_CODE: if (resultCode == RESULT_OK) { // Get the Uri of the selected file Uri uri = data.getData(); select_filePath = getRealPathFromURI(uri); // 이 함수를 다른 걸로 대체하는게 공부 Log.e(TAG, "file_path = " + select_filePath); if(select_filePath != null){ mPopupWindowShow(readFile(select_filePath)); } } break; } super.onActivityResult(requestCode, resultCode, data); }
//파일의 이름을 통해 where 조건식을 만든다. String sel = MediaStore.Files.FileColumns.DATA + " LIKE '%" + id + "%'";
//External storage에 있는 파일의 DB를 접근하는 방법이다. Cursor cursor = getContentResolver().query(MediaStore.Files.getContentUri("external"), column, sel, null, null);
int columnIndex = cursor.getColumnIndex(column[0]);
pScrollView.setVerticalScrollBarEnabled(true); // 수직방향 스크롤바 사용 가능하도록 설정 popupContent.setMovementMethod(new ScrollingMovementMethod()); popupContent.setTextSize(12);
// create the popup window boolean focusable = true; // lets taps outside the popup also dismiss it final PopupWindow popupWindow = new PopupWindow(mPopupView, width, height, focusable);
popupWindow.showAtLocation(popupView, Gravity.BOTTOM, 0, 0); // Parent View 로부터의 위치
<application android:allowBackup="false" android:icon="@drawable/icon" android:label="@string/app_name" android:supportsRtl="true" android:usesCleartextTraffic="true" <!-- http:// 통신일 경우에도 통신 가능하도록 -->
Android 7.0(Nougat / API 24)에서 Intent로 URI 파일 경로 전송시 "file://" 노출되어 있으면 FileUriExposedException 오류가 발생하게 되고 앱이 종료된다. 앱간 파일을 공유하려면 "file://" 대신 "content://"로 URI를 보내야 한다. URI로 데이터를 보내기 위해선 임시 액세스 권한을 부여해야 하고 FileProvider를 이용해야 한다.
객체 지향 언어에서는 객체를 생성하기 위해서는 먼저 클래스를 선언하고, 해당 클래스로부터 객체를 생성한다.
하지만, 코틀린에서는 이런 과정을 거치지 않고도 객체를 생성할 수 있다.
object 키워드를 사용하면, 클래스가 아니라 객체 그 자체, 인스턴스가 된다.
코틀린에는 static keyword가 없고 object 키워드를 통해 - 싱글턴을 정의하는 방법 - 동반객체 companion object를 이용한 팩토리 메서드 구현 - 무명클래스(익명 클래스)의 선언 를 한다.
예제1
fun main(){ // Person은 그 자체로 하나의 객체이기 때문에, 바로 .을 찍어 프로퍼티나 메서드에 접근할 수 있다. Person.name = "홍길동" Person.age = 30 println("이름 : ${Person.name}, 나이 : ${Person.age}") }
// object 는 subclass 의 명시적인 선언 없이 객체 생성 object Person { var name: String = "" var age: Int = 0 }
예제2
- object 키워드는 어떤 객체가 반드시 하나만 존재해야 할 때, 즉 싱글톤 패턴에 활용할 수 있다.
// 예제 2.1
fun main(args: Array<String>) { Singleton.printVarName()
println("\nSingleton variableName Change") Singleton.variableName = "강감찬" var a = A() }
object Singleton{ init { println("Singleton class invoked.") } var variableName = "홍길동" fun printVarName(){ println("변수명 : $variableName ") } }
class A { init { println("Class init method. Singleton variableName property : ${Singleton.variableName}") Singleton.printVarName() } }
// 예제 2.2 fun main(args: Array<String>) { var a = A() a.printVarName()
println() Singleton.printVarName() }
open class A { open fun printVarName() { println("I am in class printVarName") }
class CarSales(val name: String){ // 코틀린에는 정적(static) 변수 혹은 메소드가 없고, // 대신 패키지 내에 함수를 선언하여 사용할 수 있다. companion object { var total = 0 }
var count = 0 fun sales(){ total++ count++ } }
예제4
fun main(args: Array<String>){ var first = MyHtml.generateHtmlObject("first") first.setColumnHeaders("1열","2열","3열") //println("first.generateTable() => ${first.generateTable()}")
val second = MyHtml.generateHtmlObject("second", 5,5) second.setColumnHeaders("월","화","수","목","금")
val third = MyHtml.generateHtmlObject("third",2,7) third.setColumnHeaders("적색","녹색","노랑","파랑","보라","검정","주황")
object MyRecord { private var _count = 0 private val _tables = hashMapOf<String,MyHtml>()
fun gen(table: MyHtml){ _tables.put(table.name, table) _count++ } fun showTableCount() = _count fun showTable(): Map<String,MyHtml> = _tables }
class MyHtml { val name: String val row: Int val col: Int private val HEAD = "<table border='1' cell-spacing='10' cell-padding='10'" private val TAIL = "</table>" private lateinit var comment: String private val tableColumns = ArrayList<String>()
private constructor(_name: String, row: Int, col: Int ){ this.name = _name this.row = row this.col = col }
fun setColumnHeaders(vararg colNames: String){ this.tableColumns.addAll(colNames) }
companion object _GenTable { fun generateHtmlObject(_name: String, row: Int = 2, col: Int = 3): MyHtml { val mh = MyHtml(_name, row, col) mh.comment = "<!-- auto-generator table by MyHtml v1.0 -->" // 테이블 객체 기록 MyRecord.gen(mh) return mh } }
private inner class Tr { val rowHtml = """ |<tr> | ${"<td>-</td>".repeat(col)} |</tr>${"\n"} """.trimMargin() }
private inner class Th(val wpx: Int = 40) { val headers = tableColumns.map { "<th width='$wpx'> $it </th>" } val rowHtml = """ |<tr> | ${headers.joinToString("")} |</tr> """.trimMargin() }
// 테이블 생성 실행 fun generateTable() = """ |$comment |$HEAD |${Th().rowHtml} |${Tr().rowHtml.repeat(row)} |$TAIL """.trimMargin() }
코틀린에서는 접근자와 설정자를 따로 만들어줄 필요가 없다. 자바에서는 private 접근제한자로 은닉화된 멤버 변수에 getter/setter 메소드를 사용해서 접근한다. 코틀린은 property가 getter/setter를 대체한다.
코틀린에서 속성(property)란 최상위 변수(함수나 클래스 외부에 정의된 변수)나 클래스의 멤버 변수로 선언하면 속성으로 간주된다. 클래스의 멤버 변수는 모두 private 제한자로 지정된다. 따라서 해당 클래스의 내부의 getter와 setter를 통해서만 속성을 참조할 수 있다.
package com.myapp.funcargs
fun main(){ val a = Person("홍길동", 30) println(a) //println(a.toString())
val b = a.copy("이순신") println(b)
val c = b.copy(age=40) println(c)
val(name, age) = Person("강감찬",35) println("성명 : $name, 나이 : $age")
val obj = Pair<String, Int>("김좌진", 50) println(obj) }
// 데이터는 보유하지만 아무것도 안하는 클래스 data class Person(val name: String, val age: Int) // 파라미터 없는 기본 생성자가 필요한 경우에는 // data class Person(val name: String = "", val age: Int = 0) // 모든 프로퍼티에 기본 값을 설정해 주면 된다. // 기본 생성자에서 선언된 속성을 통해, 컴파일러가 자동으로 toString(), hashCode(), equals() 등을 생성한다. // 명시적으로 정의해준 경우에는 컴파일러가 자동으로 생성하지 않는다.
실행결과
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Memo_AddEdit (
var idx: String,
var memo: String,
var userID: String,
var userNM: String,
): Parcelable
class MainActivity : AppCompatActivity() {
private lateinit var mContext: Context
private lateinit var mMemo_AddEdit: Memo_AddEdit
override fun onCreate(savedInstanceState: Bundle?) {
고차함수(High-order Function)란 다른 함수를 인자로 사용하거나, 함수를 결과값으로 반환하는 함수를 말한다.
package com.myapp.funcargs
fun main(){ val a = sum(4,5) // 일반 인자 println("a result : $a") val b = mul(sum(1,2), 3) // 인자에 함수 사용 println("b result : $b")
println("ResultFun : ${resultFun()}") }
fun sum(a: Int, b: Int) = a + b fun mul(a: Int, b: Int) = a * b fun resultFun(): Int { return mul(3,5) // 함수의 반환 값으로 함수 사용 }
람다 표현식
람다식, 또는 람다 함수는 프로그래밍 언어에서 사용되는 개념으로 익명 함수 (Anonymous Functions)를 지칭하는 용어이다.
val lambdaName: Type = {argument -> codeBody}
람다 함수는 { } 안에 매개변수와 함수 내용을 선언하는 함수로 다음 규칙에 따라 정의한다. - 람다 함수는 항상 { }으로 감싸서 표현해야 한다. - { } 안에 -> 표시가 있으며 -> 왼쪽은 매개변수, 오른쪽은 함수 내용이다. - 매개변수 타입을 선언해야 하며, 추론할 수 있을 때는 생략할 수 있다. - 함수의 반환값은 함수 내용의 마지막 표현식이다.
fun sum(a: Int, b: Int) = a + b 를 람다 함수로 정의하면 val sum ={a: Int, b: Int -> a + b}
또는
val sum: (Int, Int) -> Int ={a, b -> a + b}
package com.myapp.funcargs
fun main(){ val a = sum(4,5) // 일반 인자 println("a result : $a") val b = mul(sum(1,2), 3) // 인자에 함수 사용 println("b result : $b")
println("ResultFun : ${resultFun()}")
val c:(String)->Unit ={str -> println("$str 람다함수")} b(c) }
val sum ={a: Int, b: Int -> a + b} // val sum: (Int, Int) -> Int ={a, b -> a + b} // val mul ={a: Int, b: Int -> a * b} val mul: (Int, Int) -> Int ={a, b -> a * b} fun resultFun(): Int { return mul(3,5) // 함수의 반환 값으로 함수 사용 } fun b (function: (String)->Unit){ function("b가 호출한") }
확장함수(Extension Function) Extension functions(확장 함수)는 기존에 정의된 클래스에 함수를 추가하는 기능이다. 자신이 만든 클래스는 새로운 함수가 필요할 때 쉽게 추가할 수 있다. 하지만 Standard Library 또는 다른 사람이 만든 라이브러리를 사용할 때 함수를 추가하기가 매우 어렵다.
확장 함수는 fun 클래스이름.함수이름(매개변수): 리턴타입 { 구현부 }으로 정의할 수 있다.
또는 fun 확장하려는대상.함수명(매개변수):Unit { }
fun 확장하려는대상.함수명(매개변수):리턴타입 { return 값 }
확장함수 예제1
class Car { fun getPrice() : Int { return 8000 } }
fun Car.getBrandName() : String { return "BMW" }
fun Car.getBrandName2() : Car { println("KIA") return this }
fun main(args: Array<String>) { val car = Car() println(car.getBrandName()) println(car.getBrandName2().getPrice()) }
확장함수 예제2
fun String.getLonggerLength(x: Int) : Int { return if(this.length > x) this.length else x }
fun main(args: Array<String>) { println("Hello".getLonggerLength(3)) }
fun main(args: Array<String>) { println("Hello".getLonggerStringLength("Have a nice day!")) }
fun String.getLonggerStringLength(x: String) : Int { return if(this.length > x.length) this.length else x.length }
확장함수 예제3
class Car { fun getPrice(): Int { return 10000 } }
fun Car.getPrice(): Int { return 20000 }
fun Car.getPrice(price: Int): Int { return 30000 }
fun main(args: Array<String>) { val car = Car() println(car.getPrice()) println(car.getPrice(0)) }
확장함수 예제4
fun List<Int>.getHigherThan(num: Int): List<Int> { val result = arrayListOf<Int>() for (item in this) { if (item > num) { result.add(item) } } return result }
fun main() { val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6, 7) val filtered = numbers.getHigherThan(3).toString() System.out.println(filtered) }
자바에서는 접근 제한자(Access Modifier)라는 용어를 사용하고 코틀린에서는 Visibility Modifier로 사용하는데, 우리말로는 접근 제한자 또는 가시성 제한자라고 할 수 있다. 코틀린에서 접근 제한자를 가질 수 있는 요소로는 class, object, interface, constructor, function, property 등이 있다.
자바에서는 접근 제한자를 붙이지 않으면 default(package-private) 이다. 그러나 코틀린에서는 아무것도 붙이지 않으면 public 이다.
코틀린에서 말하는 모듈은 자바와는 약간의 차이가 있는데, 자바에서의 모듈은 기능이 비슷한 클래스들의 집합체를, 코틀린에서는 동일한 컴파일의 집단을 의미한다.
패키지 레벨의 접근제한자
프로퍼티와 함수를 최상위(Top) 레벨에 작성한다는 것은 클래스에 멤버로 포함하지 않고 코틀린 파일에 정의하는 것을 의미한다.
package foo
public val myData: Int = 10 public fun myFun() {} public class myClass() {}
- 접근 제한자를 표기하지 않으면 public이 기본으로 설정되며, 누구나 접근이 가능하다.
- private : 해당 .kt 파일 내부에서만 사용 가능하다. - internal : 프로젝트의 모듈 안에서 누구나 접근이 가능하다. - protected : 최상위(top-level)에서는 사용할 수 없다.
클래스와 인터페이스의 접근제한자 클래스 또는 인터페이스 안에 선언되는 멤버들에 사용되는 접근 제한자는 다음과 같은 의미를 갖는다. - public : 누구나 접근이 가능하다. - private : 클래스 내부에서만 접근이 가능하다. - protected : 클래스 내부와 상속받는 객체에서 접근이 가능하다. - internal : 프로젝트의 모듈 안의 누구나 접근이 가능하다.
private, protected, public 과 Java와 코틀린에서 같은 의미로 사용된다. 하지만 internal은 자바에서는 default(같은 패키지내에서만 접근가능) 코틀린에서는 같은 모듈안에서만 사용 가능하다.
생성자의 접근제한자 생성자에도 접근제한자를 붙일 수 있다. 기본은 public 생성자로 누구나 접근이 가능하다. internal을 붙이면 모듈 내에서만 접근 가능하다. private을 붙이면 클래스 내에서만 접근이 가능하여 외부에서 생성할 수 없다.
// private 생성자 class Person private constructor(a: Int) { }
Java 접근 제한자
접근 제한자는 Class 의 멤버에 대해 다른 Class 에서 접근하는 것을 제한하는 용도로 사용된다.
- public : 모든 Class 에서 접근 가능
- protected : 같은 패키지에 있는 Class, 그리고 다른 패키지의 자손 클래스에서 접근이 가능
- default : 같은 패키지에 있는 Class 들만 접근 가능
String name; // 아무런 접근 제한자도 지정되지 않았으므로 default 접근 제한자다.
코틀린에서 프로젝트(Project)는 모듈(Module), 패키지(Package), 파일(File)로 구성되어 있다. 프로젝트에는 모듈이 있고 모듈은 다시 패키지로 구성되어 있다.
Java는 파일명과 클래스명이 동일해야 하지만, 코틀린은 public 클래스는 하나만 사용 해야하는 규칙이 없다.
코틀린 파일은 .kt 확장자를 가지며 맨 위에는 이 파일이 어떤 패키지에 포함된 것인지 코틀린 컴파일러가 알 수 있도록 패키지 이름을 선언해야 한다. 만약 패키지 이름을 선언하지 않으면 그 파일은 자동으로 default 패키지에 포함된다. 파일이 패키지 안에 들어있어도 패키지 이름을 선언하지 않으면 default 패키지에 포함된 것으로 인식한다. 파일에 클래스가 여러 개 정의되어 있다면 파일은 단순히 클래스를 묶는 역할을 하고 kt 확장자가 붙게 된다. 코틀린에서는 파일명과 클래스의 선언 개수에 큰 의미를 두지는 않는다. 같은 파일에 있는 여러 개의 클래스는 모두 그 파일에서 지정한 패키지로 인식한다.
패키지 이름은 파일 맨 위에 적는다. 이때 패키지 이름 앞에 package라는 키워드를 함께 입력해야 패키지 이름으로 인식한다. 단, 패키지의 이름은 특수 문자나 숫자로 시작하면 안된다.
abstract class Person(name: String) { init { println("My name is $name.") } // 일반 메서드 fun displaySSN(ssn: Int) { println("My SSN is $ssn.") } // 추상 메서드 abstract fun displayJob(description: String) }
class Teacher(name: String): Person(name) { // 추상 메서드 구현 override fun displayJob(description: String) { println(description) } }
fun main(args: Array<String>) { val jack = Teacher("Jack Smith") jack.displayJob("I'm a mathematics teacher.") jack.displaySSN(23123) }
코틀린은 자바, C++과는 달리 클래스 선언 부분에서 이름에 괄호를 붙여 생성자를 만들 수 있다.
자바는 멤버변수를 field라고 하지만 코틀린 프로퍼티라는 용어를 사용한다. 자바는 생성자를 꼭 정의해야 하지만 코틀린은 생략이 가능하다.
코틀린에는 주 생성자와(primary constructor)와 부생성자가(secondary constructor)가 존재한다. 주 생성자는 class 선언과 함께 선언하고, 부 생성자는 추가적인 생성자가 필요할 때 사용한다. 생성자를 정의하지 않으면 기본으로 인자가 없는 생성자를 만들어 준다.
부모의 생성자는 super(), 클래스 내 생성자중 다른 생성자는 this()로 호출이 가능하다.
fun main() { var a = Person("박보영", 1990) //코틀린은 new 키워드 없이 인스턴스를 생성한다. var b = Person("전영경",1997) var c = Person("차은우") var d = Person("홍길동")
// 클래스 별로 여러 개를 가질 수 있다. constructor(name: String) : this(name,1997) }
기본 생성자
- 기본 생성자에 어노테이션 접근지정자 등이 있을 경우 constructor 키워드가 필요하다.
class Person public @Inject constructor(name: String) { .... }
클래스 상속
- 코틀린의 최상위 클래스는 Any 이다.
Any는 java.lang.Object 와는 다른 클래스이며, equals(), hasCode(), toString()만 있다.
- 상속 키워드는 콜론(:)이다. Java 의 상속 키워드는 extends
- 코틀린의 클래스와 메소드는 기본적으로 final이다. 즉 기본적으로 상속을 못하게끔 되어 있다.
Java에서는 클래스나 메서드에 final 키워드를 붙여 클래스를 더 이상 상속받지 못하게 하거나,
메서드를 재정의하지 못하게 할 수 있다. - 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
open 변경자가 없으면 final 로 자식 클래스에서 override가 불가능하다.
오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야 한다.
- 부모 클래스의 메소드를 자식 클래스에서 오버라이드 한 경우, 해당 메소드는 기본적으로 열려있다.
fun main() { var a = Animal("불독", 3,"개") var b = Dog("불독", 5)
a.introduce() b.introduce() b.bark()
var c = Cat("나갈래", 4) c.introduce() c.meow() }
open class Animal(var name:String, var age:Int, var type:String){ open fun introduce(){ // open 이므로 override 가능하다. println("저는 ${type} : ${name}, ${age} 살 입니다.") } }
class Dog(name:String, age:Int) : Animal (name,age,"개"){ // 상위 클래스 함수에 open 키워드를 명시하고, 서브 클래스 함수에 override 를 붙인다. override fun introduce(){ println("타입 : 개, ${name}, ${age} 살 입니다.") }
fun bark(){ println("멍멍멍") } }
class Cat(name: String, age: Int) : Animal(name, age, "고양이"){ fun meow() { println("야옹야옹") } }
fun foo2() { var ints = listOf(0, 1, 2, 3) // 람다식에서 return 시 nearest enclosing 함수가 return 된다. ints.forEach { if(it == 1) return print(it) } print("End") }
fun foo3() { var ints = listOf(0, 1, 2, 3) // 람다식에 대해서만 return 하려면 label 을 이용해야 한다. ints.forEach loop@{ if(it == 1) return@loop print(it) } print("End") }
fun foo4() { var ints = listOf(0, 1, 2, 3) // 람다식에 대해서만 return 하려면 label 을 이용해야 한다. // 암시적 레이블은 람다가 사용된 함수의 이름과 동일하다. ints.forEach { if(it == 1) return@forEach print(it) } print("End") }
fun foo5(): List<String> { var ints = listOf(0, 1, 2, 3) val result = ints.map{ if(it == 0){ return@map "zero" } "No $it" } return result }
public class GoogleSignInActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "GoogleActivity"; private static final int RC_SIGN_IN = 9001; private FirebaseAuth mAuth; private GoogleSignInClient mGoogleSignInClient;
// Configure Google Sign In GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build();
@Override public void onClick(View view) { switch (view.getId()) { case R.id.sign_in_button: signIn(); // 구글 로그인 버튼 클릭시 break; case R.id.sign_out_button: signOut(); break; } }
@Override public void onStart() { super.onStart(); // Check if user is signed in (non-null) and update UI accordingly. FirebaseUser currentUser = mAuth.getCurrentUser(); updateUI(currentUser); }
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data);
// Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...); if (requestCode == RC_SIGN_IN) { Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data); try { // Google Sign In was successful, authenticate with Firebase GoogleSignInAccount account = task.getResult(ApiException.class); firebaseAuthWithGoogle(account); } catch (ApiException e) { Log.w(TAG, "Google sign in failed", e); updateUI(null); } } }
private void signOut() { mAuth.signOut(); // Firebase sign out
// Google sign out mGoogleSignInClient.signOut().addOnCompleteListener(this, new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { updateUI(null); } }); }
private void revokeAccess() { // Firebase sign out mAuth.signOut();
// Google revoke access mGoogleSignInClient.revokeAccess().addOnCompleteListener(this, new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { updateUI(null); } }); }