2023년 12월 15일 금요일

Android_min_ebook_kth

Android_min_ebook_kth

초보자를 위한 Android 개발기초 (기초편)

ⓒ 2021. Sugoi Group All rights reserved.
ⓒ 2021. provided by DX-NINJA, DX-JAPAN.

앱 개발 및 기타문의 : : cg99132@gmail.com
일본에서 프로그래머 취업하기 : (카카오톡 오픈 토크)

E-BOOK PDF 버젼 보기

1 안드로이드Android 개요

Android OS 는 미국의 Android 라는 회사가 처음 개발한 제품으로 2005년 구글이 이 회사를 인수하여 2008년에 일반공개에 한 이후 하드웨어의 성능향상과 더불어 현재는 모바일OS분야에서 가장 많은 사용자를 가지고 있다.

기본적으로 안드로이드는 Linux커널기반의 OS에 모바일단말기기를 쉽게 다룰수 있는Android는 Linux OS라는 매우 안정적이고 무료로 사용할수 있는 OS를 기반으로하였으며 추가로 모바일 단말장비에 장착된 여러가지 기능들을 모은 프레임워크이쉽게 다룰수 있도록 모바일용 프레임워크를 별도로 추가한 OS라고 할수있다.

구글사이트에서 찾아보면 안드로이드Android의 역사부터 현재 버젼, 기능개선에 주요 참여기업등 상세하게 나오므로 시간과 관심이 있다면 한번 찾아서 읽어 보는 것도 좋다.

1.1. 안드로이드Android 앱개발

모바일폰 초기에서는 단말기를 만드는 삼성이나 엘지같은 업체들이 사용하는 특수한OS에서 실행되는 프로그램을 만들기 위해서는 별도의 계약을 해야하는 등 사실상 개인이 앱을 만들어서 등록하기란 불가능했다.

하지만 안드로이드Android의 출현으로 정말 아무나 앱개발하여 앱마켓에 등록할수 있게 되었고, 이를 통한 수익도 창출하는게 가능하게 되었다.
초기에는 재미가 없는 앱뿐이어서 이동통신사의 다른 OS 처럼 될까 하는 우려도 있었지만, 개발자에게 오픈된 마켓과 JAVA로 앱을 만들수 있다는점. 안드로이드Android 프레임워크에서 단말기의 거의 모든 기능을 활용할수 있다는 점에서 개발자들의 폭풍같은 인기를 얻어 수많은 앱들이 마켓에 올림으로서 점점 생태계가 활성화 되었다.

구글도 개발자들이 안드로이드Android앱을 좀더 빠르고 쉽게 개발할수 있도록 전용에디터와 마켓, 무료사용가능한 클라우드등 여러가지로 지원을 하고 있어서 아이디어만 있으면 큰돈을 들이지 않고도 훌륭한 앱을 만들수 있다.

1.2. 구글의 안드로이드Android 개발자 지원환경

Java를 사용해서 안드로이드Android 앱을 개발하는것이 가능하기는 했지만, 아이폰 개발에 비하면 이렇다할 에디터(Eclise라는 구시대 에디터)와 에뮬레이터도 없어서 안드로이드Android 앱 개발자들의 개발 환경이 그리 좋지만은 않았었다.

그러던중 구글이 AndroidStudio 라는 것을 구글이 JetBrain사를 통해 제공하게 되어 안드로이드Android 개발자들은 좀더 편하고 통합된 환경에서 개발이 가능하게 되었다.

또한 Firebase 등과 같은 업체를 인수하여 안드로이드Android 앱개발자들이 서버쪽 환경구축에 좀더 편하도록 무료로 서비스를 제공하기도 한다.

1.3 새로운 언어 Kotlin!

현재 Android로 앱을 java 언어로 만들된 컴파일된 바이트코드가 생기고 이를 Dalvic(또는 v8)이라는 가상머신에서 하나의 프로그램형태로 실행하여 사용자에게 화면을 표시하도록 하고 있다.

최근에는 Jetbrain 사의 Kotlin이라는 새로운 언어로도 앱을 만들수 있는데, java에 비해 간결하고 유용한 방식으로 개발할수 있게 되었다.

2 Kotlin Android 프로그래밍

2.0 Java 언어로 개발 vs Kotlin 언어로 개발

예전에는 JAVA언어를 사용하여 개발을 했지만 최근에는 Kotlin언어로 개발을 정말 많이 한다. 새로운 서비스는 대부분 Kotlin언어를 사용하며 개발하고 Javaㄹ르를 사용하는 경우는 기존 앱의 유지보수 정도라고 생각하면 된다.
앱 배우기 시작하는 거라면 Kotlin 으로 시작하는걸 추천한다.

2.1 Android by Kotlin

본격적으로 안드로이드Android 프로그래밍을 시작하자.

모든 프로그램이 그렇듯이 안드로이드Android용 프로그램도 다음과 같은 것들을 실행하면서 프로그램 역활을 한다.

  • 무언가를 화면에 그린다.
  • 사용자의 요구에 반응한다.
  • 소리를 내거나 소리를 듣는다.
  • 인터넷에 접속해서 자료를 가져온다.
  • 데이터베이스와 연계해서 정보를 보관한다.
  • 다른 기기와 상호작용한다.
  • 등등…

이런 동작들은 사실 안드로이드Android 프레임워크와 기본라이브러이리 에서 기본적으로 제공해주기 때문에 개발자는 각각의 기능들을 호출해서 모바일기기에서 동작되도록 프로그래밍을 하기만 하면 된다.

2.2 Hello, Android

프로그램을 시작할때 정해진 관문처럼 여겨지는 “Hello” 프로젝트를 만들어 앱의 구조를 파악하고 안드로이드Android 에뮬레이터에서 화면으 직접 확인해보자

2.2.1 Android Studio에서 새로운 프로젝트 작성

구글의 공식 Android 개발 IDE 인 Android Studio에서 HelloAndroid 라는 프로젝트를 생성해보자.
참고로 Android Studio의 설치안내는 마지막장의 부록에 있다.
(Android Studio 설치는 하단의 " Android Studio 다운로드하기" 항목을 참고)

  1. -Step1 Android Studio 아이콘을 클릭해서 Android Studio 실행하고
    [Start a New Android Studio project ]를 클릭한다.

alt_text

  1. 새로 만든 프로젝트의 기본적인 정보가 보여지는데 다음과 같이 입력해본다.

alt_text

Application name 만들 앱의 이름을 입력한다.
Company domain 회사의 도메인으로 일단 대충입력한다.
project location 프로젝트의 파일들이 위치할 폴더를 지정한다.
packagename 패키지이름은 도메인.어플이름으로 자동구성되지만, edit 버튼을 눌러 사용자가 바꿀수 있다.
Include C++ support C++을 이용해서 개발한 라이브러이리등을 포함할 경우 사용한다. 체크안함
Include Kotlin support 코틀린Kotlin 언어를 지원할것인지 결정한다. java 언어로만 작성할경우에는 체크하지 않느는다. 체크
  1. 개발할 앱이 안드로이드Android의 어떤 기기와 버젼을 대상으로 작동할지는 결정한다.

alt_text

Phone and Tablet 안드로이드Android 폰과 타블랫 전용의 앱
wear 안드로이드Android 시계용 앱
tv 안드로이드Android 기반의 tv용 앱
android auto 자동차용 안드로이드Android 앱
android thins 그밖의 IoT 제품용 앱

안드로이드Android는 정말 많은 버젼과 기기에 탑재되어 있으므로 결정이 쉽지 않기때문에 [Help me choose]에서 버젼별 이용률을 참고한 화면을 제공한다. 이를 보고 어떤 기능을 제공하고 얼마나 많은 기기에서 실행가능하도록 할건지를 참고할수있다.
구글에게 권장하는 것은 항상 최신의 버젼을 대상으로 앱을 만들기를 권ㅎ장한다.

alt_text

왼쪽의 플랫폼을 선택하면 자세한 정보가 나온다.

alt_text

  1. 우리는 단순한 Hello, Android 만 출력하는 앱을 만들것이기 때문에 템플릿 선택화면에서는 Empty Actirvity 를 선택한다.

alt_text

  1. 앱을 실행할때 처음으로 보여지는 Activity의 이름을 지정한다.

alt_text

  1. Gradle 이 안드로이드Android 프로젝트를 자동으로 생성해준다. (완료때까지 기다리자

alt_text

  1. 새로운 프로젝트가 생성되었다.

alt_text

2.2.2 프로젝트 구성 과 디자인 편집

android studio에디터 화면의 중앙 가장 큰 화면에서는 MainActiviy 의 Kotlin 소스가 보인다. 앱의 실행과 동시에 보여질 activity 라고 전 단계에서 지정했으니, 이 파일에 처음실행시 해야될 것들을 처리하면 될것이다.

그 옆의 탭에는 activity_main.xml 파일이 보이는데 Android는 activity 소스와 xml 디자인 파일이 하나의 구성세트 이기 때문에 프로젝트 생성과 동시에 자동으로 생성된 레이아웃 파일이다.

화면 왼쪽에 프로젝트 구성요소가 보이는데, 실제 앱파일의 구성요소는 app 폴더모양을 더블크릭해서 세부파일들을 볼수 있다.

새로운 프로젝트 생성하기로 프로젝트를 생성했다면, app > java > 하위에 (생성시 입력한)패키지명,androidTest, test 이렇게 3개의 패키지가 생성된다. 실제앱은 생성시 작성한 패키지이름의 하위에 있는 MainActivity 에서 초기 실행화면이 보여진다.

개발자가 추가로 필요한 패키지나 액티비티Activity가 있다면 app> java 의 하위에 계속추가하면된다.

app> res 폴더에는 리소스파일 들이 위치하는데, 화면레이아웃, 아이콘,이미지,문자열등의 앱에 필요한 리소스들을 위치하는곳이다. 이폴더는 apk파일을 열어보는 프로그램에 의해 외부에서 아무나 볼수 있기 때문에 개인정보나 보안에 관련된 내용은 넣지 않도록 한다.

  1. app > java > 패키지명 > MainAvtivity파일의 아이콘이 코틀린Kotlin 아이콘인것을 확인하고 더블 클릭해본다. 현재 이미 화면 중앙의 편집화면에서 해당 파일이 열려져 있기 때문에 별다른 변화가 없을것이다.

alt_text

내용을 보면은 앱이 시작되고 MainAvticity가 생성되면서, 앱의 컨텐츠내용을 layout의 activity_main 이라는 파일의 구성요소를 보여(vView)준다고 적혀있는것이다.
R.layout.activity_main 의 의미는 Resource폴더의 layout폴더의 activity_main.xml을 열라는 의미이다.

  1. 이제 메인 화면의 디자인을 확인하기 위해서 app>res>layout>activity_main.xml 파일을 더블클릭한다.

alt_text

레이아웃 화면을 확인하고, 편집할수 있는 화면이 나타난다. 다른 화면이 나타나는 경우라면 하위의 [Design] 버튼을 클릭하면 위와 같은 화면이 나타난다.
최신 버젼의 AnndroidStudio 에서는 편집기 화면의 우측상단에 Code, Split, Design이 있다. 기존의 Text탭이 Code로 바뀌었고, 양쪽을 동시에 볼수 있는 항목 Split 가 추가되었다.

화면가운데 위치한 TextView (Hello World) 를 클릭하면 constraintlayout 의 상태가 보인다.

  1. TextView 의 문장을 바꾸기 위해서는 2가지 방법이 있다.
  • 오른쪽의 속성(Attribute)에서 text 항목에 값을입력,
  • 화면 하단의 [Text] 탭을 눌러서 xml 파일내에서 직접 수정(최신버젼의 Android Studio 에서는 Code탭으로 변경됨)

alt_text

alt_text

  1. Hello Android! 로 바꾸었으니, 이제 안드로이드Android 에뮬레이터 에서 실행을 시켜보자.
    실행 버튼을 클릭하여 타겟디바이스를 선택하는 화면이 나오면 추가된 가상 디바이스 또는 연결된 실제 안드로이드Android 폰을 선택한다. 하고 OK버튼을 누른다.
    가상 디바이스는 개발자가 실제폰없이도 개발이 가능하도록 해준다. 가상 디바이스에 탑재된 OS는 실제 기기에 탑재된 OS와 동일하기 때문에 개발시에 구현한 기능은 실제 기기에서 동일하게 동작한다. 따라서 개발시에는 다양한 크기,OS 별로 가상기기를 생성하여 해당기기에서의 도작을 미리 테스트해 볼수 있다.

alt_text

  1. 안드로이드Android 에물레이터에서 앱이 실행되었다.

alt_text

3 앱디자인 레이아웃과 각종 위젯

Android Studio에서 새로운 프로젝트를 생성하면 최소 한개이상의 layout파일이 만들어진다.

layout파일을 더블클릭하면 레이아웃 편집창이 열리는데 이곳에 앱에 표시될 텍스트, 버튼,이미지, 목록리스트, 지도 등의 위젯을 배치한 다음 프로그램으로 제어해서 인터랙티브한 앱을 만들수 있다.

alt_text

3.1 레이아웃 에디터 구성

레이아웃 편집화면은 다름과 같은 요소로 구성되어 있다.

  • palette : 각종 위젯과 뷰요소들을 모여 있다.
  • component Tree : 현재 레이아웃의 구성 요소들을 트리형태로 보여준다.
  • 미리보기 화면 : 뷰나 위젯들으 배치한 상태를 미리보기 해주는 화면으로 오른쪽의 blueprint화면에서는 구성요소들이 실제 차지하는 크기와 여백등을 쉽게 파악할수 있도록 해준다
    미리보기 화면의 상단에는 화면사이즈별로 변경이 가능하여 레이아웃이 어떻게 보일지를 파악할수 있고, 버젼별, 언어별로 어떻게 보일지를 파악할수 있다.
  • attribute : 위젯을 화면에서 선택하면 각 위젯에 맞는 속성설정들이굉장히 많이 보여진다.
  • 화면 스위치 (Code , Splite, Deisgn) : 레이아웃파일은 xml형태로 구성되어 있어서 xml code에서 직접파라메터를 수정하면서 미리보기 할수 있도록 화면 스위치 기능을 제공한다.

alt_text

3.2 Attributes

레이아웃. 뷰, 위젯, 컴퍼넌트등 안드로이드Android의 모든 디자인 요소들은 스타일, 위치, 색깔 , 투명도, 크기, 아이디등의 공통된 속성들과 각각의 특성에 맞는 속성들을 조절할수 가 있다. 각각의 속성을 다 설명하긴 어렵기 떄문에 직접 수정해 보면서 익히는게 좋다.
버튼, 텍스트와 같은 각각 Widget 들은 다양한 속성을 가지고 있다. 이들 속성에 따라 형태와 위치등이 달라지기도 한다.
Attributes은 Code탭의 xml 문장으로 직접 입력하는 방식과 아래 그림과 같이 Attribute판넬에서 직접 해당 속성의 값을 입력하는 방식이 이따있다.
Attribute판널

Code탭에서 xml 파일에 직접 입력( 개발자들은 보통 Split 탭화면모드에서 xml 파일에 직접 입력하고 확인하면서 작업한다. )

3.3 Layouts

layout에는 다양한 종류가 있지만 최근의 안드로이드Android 레이아웃은 ConstraintLayout을 기본으로 권장하고 있다.

  • ConstraintLayout:각 요소별로 서로간의 연결에 대한 제약(규역)을 정하여 지정함으로서 화면의 크기나 회전등에 좀더 유연하게 대응하도록 할수있다. constraintLayout은 아래의 별도 항목에서 설명한다.
  • LinearLayout(Horizontal , vertical) : 종/횡으로 위젯들을 나란히 배치할수 있도록 해준다.
  • FrameLayout : 마치 사진 프레임에 사진을 끼워서 배치하듯이, 하나의 프레임안의 자식뷰를 교체함으로서 컨텐츠 스위칭을 쉽게 있도록 해준다. 주로 프래그먼트Fragment를 이용한 탭메뉴등에서 프개르먼트가 보여지는 부분에 프레임레이아웃을사용한다. 또한 여러개의 위젯을 중첩배치할수도 있다.
  • TableLayout : html의 테이블태그처럼 테이블형식으로 위젯들을 배치할수있다. (개발시 거의 사용을 하지 않는다)
  • TableRow : TableLayout 에서 사용될 Row를 결정한다.
  • space : 위젯사이에 일정한 공간을 띄우고 싶을때 스페이스 위젯을 사용한다.

3.4 Widgets

이미지를 표시 하거나, 웹페이지를 보여주거나, 달력, 진행바 등 개발자가 직접만들고 구현하기 까다로운 특별한 기능을 하는 뷰를 위젯형태로 제공해 준다.

  • View : 뷰 위젯은 android.vView 클래스로서 화면에 무언가를 표시할때 가장기본이 되는 위젯이다. 뷰에는 여러가지 하위 뷰 또는 뷰그룹을 포함하여 하나의 또다른 위젯형태를 만들수 있게해준다.
  • ImageView : 이미지를 표시한다.
  • WebVIiew : webpage 를 표시한다.
  • VIViewoView : 동영상을 출력해준다.
  • CalendarVIiew : 달력을 출력해준다.
  • ProgressBar : 진행바를 표시해준다.
  • SeekBar : 값 조절바를 표시해준다.
  • RatingBar : 별점주기를 표시해준다.
  • SearchView : 검색하기 와 같은 형식으로 표시해준다.
  • TextureVIiew : 하드웨어 가속창에서 얻어온 정보를 빠르게 표시하기위한 View이다. camera 미리보기라든가 축소확대가 가능한 동여상 플레이어를 만들때 사용한다.
  • SurfaceView : 더블버퍼링을 지원하는 뷰로서 별도의 쓰레드를 통해 화면에 그릴 요소들을 그리고 표시한다. 별도의 게임엔진을 사용하지 않고도 간단한 게임등을 제작할수 있는 뷰이다.
  • Horizontal | Vertical Divider : 화면에 가로선 또는 세로 선을 그려준다.

3.5 Containers

컨테이너는 위젯보다 한층 복잡한 기능을 하는 뷰이다. 탭레이아웃, 탭바, 스크롤뷰, 리스트(리사이클부), 페이저뷰 등 여러가지 작은 기능들이 모여서 하나의 컨터이너 뷰를 구성하고 있다.

이들 컨테이너 들은 단순하게 사용하는게 아니라, 데이터또는ㄴ 다른 뷰하고의 상호작용을 하기위해서 제공되기대문에 각각의 설정값이나 사용방법은 다소 복잡할수도있다.

  • Spinner : 여러개의 값중에 하나를 돌려가며 선택할수 있도록 해준다.
  • RecyclerVIiew : 할일데이터목록, 영화나 음식목록, 등을 표시할때 사용하는 리스트형식의 뷰러서 데이터를 가로 또는 세로 또는 격자형태로 다양한 배치로 목록화면을 구성할수 있도록 해준다.
  • scrollView(HorizontalScrollVIiew) : 스크롤할없는 뷰에 스크롤이 가능하도록 해준다.
  • NestedSCrollVIiew : 중첩된 스크롤이 가능한 화면, 즉 전체 스크롤안에 상품리스트 뷰가 있을경우 상품리스트뷰와 전체스크롤과 같이 스크롤 되도록 해준다.
  • ViewPager2 : 페이지를 넘기는것처럼 여러개의 뷰를 하나씩 표시해준다. 인트로의 튜토리얼 ,또는 회전배너 표시등에 유용하다.
  • CardView : 하나의 카드처럼 뷰를 구성할수 있게 해준다. 티켓리스트드에서 하나의 티켓에 대한 뷰를 사용할때 유용하다.
  • AppBarLayout : 상단의 앱바를 자동으로 만들어 준다.
  • BottomAppBar : 하단 앱바를 자동으로 만들어 준다.
  • NavigationView : 앱내의 화면들을 안내하는 네이게이션 형식의 뷰를 만들수 있다. drawerLayout과 함께 사이드메뉴효과를 낼떄 사용한다.
  • BottomNavigationVIiew : 하단의 네비게이션 형식의 뷰를 만들수 있다. 하단 바로가기 버튼 모음 등 만들때 유용하다.
  • Toolbar : 상단의 툴바를 만들어 준다.
  • TabLayout : 탭전환이 가능한 형식의 뷰를 만들어 준다.
  • TabItem : TabLayout의 아이템 디자인용뷰를 만들어 준다.
  • ViewSub : 아무것도 표시되지 않는 비어있는 뷰로서 나중에 다른 레이아웃을 동적으로 include하여 표시할수 있도록 해주는 뷰이다. 당장은 화면에 표시되지 않아도 되지만 조건에 따라 나중에 다른 레이아웃이 불려질 필요가 있을때 사용된다. 또는 복잡한 구성의 앱의경우 일단 필요한 뷰 요소만 빠르게 표시한다음 점진적으로 뷰 요소들을 표시하여 사용자로 하여금 체감속도를 높이고자 할때 사용된다.
  • include : 다른 뷰를 삽입한다.
  • fragment : fragment 에서 사용하는 뷰이다. 하나의 레이아웃에 여러 fragment뷰를 사용할수도있다.
  • View : 사용자가 만든 custom View 또는 안드로이드Android의 다른 뷰클래스를 이용할수 있게 해준다.

3.6 Text, Buttons

일반 텍스트 와 버튼을 표시해 주는 뷰이다.

  • 텍스에는 입력받는 형식에 따라 일반텍스트, 이메일, 폰번호, 우편번호, 시간, 날짜 등 종류가 있다.
  • 버튼에는 체크박스,라이도그룹, 스위치버튼등 종류가 있다.

3.7 Legacy

이전 버젼의 안드로이등 레이아웃에서 사용되던 뷰 목록이다. 되도록 사용하지 않는편이 좋다.

4 복잡한 레이아웃 구성해보기(로그인 & 회원가입)

Hello Android 를 가상에뮬레이터 에서 출력해 보았으니 이제 앞으로 그런 작업들을 반복해 나가면서 점점 구성력 있는 앱을 만들 수 있을것이다.

6.1 에서 만든 앱은 액티비티Activity가 1개 였기 떄문에, 앱이라고 하기엔 너무 단순했다. 이번단원에서는 2개의 액티비티Activity를 구성하고, 새로운 화면의 레이아웃을 추가해 보겠다…

4.1 Constraint layout 사용하기

AndroidStudio 2.2 버젼부터 ConstraintLayout 란 새로운 Layout 이 도입되었다. 앱을 개발할때 의외로 시간이 걸리고 생각대로 잘 안되는 게 화면구성인데 이를 개선하고자 새롭게 제공되는 레이아웃전용 라이브러리이다.

ConstraintLayout 라이브러리를 이용해서 작성한 Layout 은 프로젝트 빌드시에 자동으로 androidStudio 가 최적화된 안드로이드Android 레이아웃형태로 제작해준다.

예전버젼의 안드로이드Android부터 사용해 왔던 개발자는 Relativelayout, LinearLayout 등을 이용하여 위젯들을 어렵게 배치했었다. 각 Layout을 중첩해서 화면배치를 하다보니 불필요하게 여러겹으로 되어 있거나, 화면변화에 대응하지 못하는 다소 불안한 레이아웃의 상태인경우가 많이 있었다.

ConstraintLayout을 이용하면 위젯간의 “제약조건” 을 통해서 좀더 직관적이고 유연한 레이아웃배치가 가능하다.

4.1.1 layout_constraint[Top,Bottom,Left,Right]_to[Top,Bottom,Left,Right]Of

위젯의 상,하,좌,우 측면에 대해 to 로 지정된 위젯에서 상,하,좌,우 방향중 어느쪽에 위치시킬건지를 지정하는속성이다.

<Button 
    android:id="@+id/button1"
...
    app:layout_constraintRight_toRightOf="parent"/>

이렇게 되어 있다면, button1 위젯의 오른쪽측면은 parent 위젯의 가장 오른쪽과 일치시킨다는 의미이다. 따라서 button1 은 부모위젯크기 범위 내에서 오른쪽에 항상위치하게 된다.

<Button

    android:id="@+id/button1"
...

    app:layout_constraintRight_toLeftOf="parent"/>

위와 달리 이렇게 한다면 반대로 부모위젯의 왼쪽에 위치하게된다.
아래와 같이 design탭에서 속성을 선택/입력 가능하며, 미리보기 화면에서도 보여준다.

4.1.2.Chain

체인은 위젯들을 서로 묶어서 균일한 간격으로 배치를 가능하게 도와준다.

물론 위젯에 각각의 속성을 주고 일정한 간격의 배치가 가능하지만, 체인속성을 이용하면 좀더 유연하고 간편하게 위젯들의 배치를 할수있다.

체인에는 3가지 속성이있다.

  • Spead Chain
    Spread Chain은 위젯들의 균일한 간격을 자동으로 계산하여 , 간격만큼 위젯들을 배치시켜준다.

  • Spread Inside Chain
    위젯들간의 내부 간격을 균일한 간격을 자동으로 계산하여 , 간격만큼 위젯들을 배치시켜준다. Spread Chain 는 위젯의 바깥간격까지 균일하게 계산하여 주는것이고 이것은 내부 공간만 계산해준다.

  • Packed Chain
    Spread Inside Chain 과는 반대로 위젯들의 외부 간격을 균일한 간격을 자동으로 계산하여 , 간격만큼 위젯들을 배치시켜준다. 따라서 위젯간의 간격은 0가되어 서로 붙어있는 모양이된다.

  • app:layout_constraintHorizontal_bias : Packed Chain 을 하게되면 위젯들이 기본적으로 가운데 위치하게 되는데, 왼쪽이나, 오른쪽으로 배치하고 싶을때는 이 속성값을 변경해주면 된다.(가중치,치우침)

4.1.3.Guidelines

화면에 보이지 않는 가상의 가이드라인을 만들어 위젯들이 가이드라인을 참조하여 레이아웃을 결정하도록 도와준다.
화면에는 렌더링되지 않으나 가상의 가이드라인을 설정하여 해당라인을 기준으로 Constraint 제약설정을 할수 있도록해준다. 실무에서 사용하면 꽤 유용하다.

layout_constraintGuide_percent

4.1.4.배리어

그렇다. 배리어, 즉 장벽이다. 배리어는 지정된 위젯들의 최대 외곽영역의 경계를 자동으로 측정해서 레이아웃을 결정해준다.

    <android.support.constraint.Barrier

        android:id="@+id/barrier"

        app:barrierDirection="start"

        app:constraint_referenced_ids="textView1,textView22"

        app:layout_constraintStart_toStartOf="parent" />

위 처럼 하면, `TextView1,TextView22 의 컨텐츠 영역크기를 계산해서 최외곽의 크기만큼의 영역에 barrier 를 형성해서 각 위젯이 해당 영역내에서 영향을 받고록 한다.

4.1.5.DimensionRatio

위젯의 크기를 가로세로의 비율을 지정할수있다.

app:layout_constraintDimensionRatio=“h,15:9”

4.1.6. GoneMarginXXX

B에 GoneMarginXXX값을 설정하면 연결되어있는 A가 Gone상태가 되었을때 B의 마진을 강제로 재설정할수있다.
GoneMargin의 설명

4.1.7. Group

B에 GoneMarginXXX값을 설정하면 연결되어있는 A가 Gone상태가 되었을때 B의 마진을 강제로 재설정할수있다. Group 은 속성이 아니라 별도의 헬퍼위젯으로 androidx.constraintlayout.widget.Group 태그로 지정한다.
그룹에 참여시킬 뷰들의 id를 나열하면 된다. 기본적인 속성을 거의 다 적용할수있다.

<androidx.constraintlayout.widget.Group  
 
 android:layout_width="wrap_content"  
 
 android:layout_height="wrap_content"    
 
 android:elevation="10dp"
  app:constraint_referenced_ids="imageView, imageView3"  
 
 />

4.1.8. 회전배치

layout_constraintCircleXXX 속성은 특정 위젯을 중심으로 회전반경과 각도를 지정한 방식으로 회전배치를 할수있다.
그림에서처럼 이미지위젯을 중심으로 45,135,290도의 3개의 텍스트위젯을 회전배치했다.

enter image description here

4.1.9 Layer

레이어는 Group과 비슷하게 constraint_referenced_ids로 지정된 뷰들을 하나의 덩어리로 묶어준다.
Group은 하나의 실제뷰를 생성안하지만 Layer는 뷰를 생성하기 때문에 배경색과 여백등을 지정할수 있다.
Layer는 constraint_referenced_ids로 지정된 뷰들을 합친 외곽의 크기만큼자동으로 사이즈가 지정된다.
프로그램에서 레이어를 이용하여 레이어안의 위젯들에게 한꺼번에 애니메이션효과를 지정할수있다.

  • 배경색지정
  • 고도높이 elevation 지정
  • 알파값지정
  • 뷰애니메이션지정
  • 패딩지원

4.1.10 MockView

아직 개발중인 CustomView를 대신해서 레이아웃작업을 먼저 진행할수있도록 도와주는 가짜뷰 MockView를 제공해 준다.
Helpers > MockView에 있다.

4.2 Tools

tools 속성은 레이아웃작업을 할때 유용한 여러가지 속성을 제공한다.

  • tools:ignore ->
    속성을 지정하면 Layout의 Lint 를 무시할수 있다.다국어 앱을 만들때, string 에서 해당 언어를 지정하지 않을때는 Lint의 에러가 나는데, 이런 메시지를 무시할 수 있다.
    <string name=“app_name” tools:ignore=“MissingTranslation”>알람</string>
    또는 이미지위젯에서 설명을 적지않았을때,Image without “contentDescipriton” 이라는 에러가 나오는데,tools:ignore="ContentDescription"를 지정하면 에러가 표시되지 않는다.
  • tools:text -> Text가 표시되는 위젯의 경우 프로그램을 실행하기전에 텍스트가 화면에 표시되는것을 레이아웃에서 미리보고 싶을때가 있는데, 이속성을 지정하면 layout 편집화면에서 미리볼수있다.
  • tools:visibility -> 레이아웃 편집화면에서 위젯을 보이거나 숨겨보고 싶을때 사용한다.
  • tools:listitem , tools:listheader , tools:listfooter ->
    RecyclerView나 ListView를 이용하는 경우 각 item 이 화면에 어떻게 표시될지 미리보기 하고 싶을때가 있는데, 이때, 사용한다.
  • tools:itemCount -> RecyclerView나 를 이용할경우 미리보기의 itemcount 를 지정한다.
  • tools:layout -> 지정된 layout 파일으 미리보기해준다. 주로 fragment 로지정된 부분이 화면에 어떻게 보일지 확인하기 위해서 사요한다.
  • tools:showIn -> <include>Tag 를 이용해서 다른 뷰에서 이용될때, include 하는 뷰에서 어떻게 보일지 미리보기 가능하도록한다.
  • tools:locale -> 표시될 언어(다언어지원일경우)를 지정한다.
  • tools:context -> 해당 레이아웃이 context 화면에 쓰일건지 지정한다. 이렇게 지정하면 되면, xml 파일과 Activity 간의 관게를 프로그램이 알수 있기 때문에, 편집기에서 자동으로 지정가능한 액션,이벤트(onClick등) 등을 추천해준다.
  • tools:targetApi -> 대상 API level을 지정한다.
  • tools:shrinkMode , tools:keep , tools:discard ->shrinkMode 를 사용하면 사용하지 않는 코드 및 리소스를 축소시켜준다. keep 은 리소스가 사용하지 않더라고 절대로 없어지지 않도록 한다. discard 는 빌드시에 리소스를 없애준다.
  • tools:menu -> appbar 에 표시 될 메뉴를 지정한다.
  • tools:minValue , tools:maxValue ->NumberPicker 위젯의 최소값과 최대값을 지정한다.
  • tools:openDrawer -> openDrawer의 액션을 지정한다.
  • tools:sample/* ->layout inspector에서 프로젝트의 sample 폴더하위에 있는 데이터들을 가져와서 미리보기 할수 있도록 해준다. 리스트뷰의 아이템등이 어떻게 표시되는지 확인하고 싶을때 사용한다.

4.3 레이아웃 구성 변경

기존의 Hello Android는 layout 에 단순 문자열 출력하는 TextView밖에 없었기 때문에 너무 단순했다.

이것을 앱실행시 마치 로그인 화면인것처럼 레이아웃을 바꾸어 본다.

  1. activity_main.xml 파일을 design Mode에서 열고 Hello Android 의 textView 를 선택하고 삭제한다.

alt_text

  1. 로그인 화면에 필요한것은 입력이 가능한 회원아이디, 비밀번호 폼과 클릭이 가능한 로그인버튼, 회원가입, 회원약관동의서 등이 되겠다. Design Mode의 Pallete 영역에서 다양한 콤포넌트가 제공되므로 이용 이용해서 원하는 폼을 만들수 있다.
    먼저 회원 로그인 영역은 보통 위에서부터 아이디,비밀번호,로그인버튼,회원가입버튼,회원약관동의 체크박스 의 수직 직선 순서대로 구성 있으니까 LinearLayout 의 Vertical형식으로 기본 레이아웃을 구성하고 그 안에 해당 폼들을 순서대로 배치할 예정이다.

일단 Palleter > Layourt > LinearLayout(Vertical) 을 선택하고 오른쪽의 미리보기 디자인 화면의 중앙에 마우스를 이용해서 배치한다. 배치할때 화면의 정중앙근처에 놓으면 자동으로 가로와 세로선이 생긴다.

alt_text

  1. Linear Layout 이 화면중앙에 배치되면서 자동으로 화면크기만큼의 가로 세로 값과 Constraint 값이 설정된다.

alt_text

디자인 모드

alt_text

xml 모드

  1. 자동으로 화면크기만큼 Linear Layout 의 크기가 지정된것도 좋지만, 로그인 버튼은 항상 화면중앙에 필요한 만큼만 화면을 차지 하는게 보기 좋으니까 Attribute영역에서 layout_width, layout_height값을 조절한다.

alt_text

  1. 자 이제 레이아웃의 내부에 아이디 입력,비밀번호 입력, 로그인버튼, 회원가입버튼, 약관동의 체크박스 를 배치해 보자.

alt_text

아이디입력 : Palette > Text > TextView
비밀번호 입력 : Palette > Text > Password
로그인: Palette > Buttons> Button (Attributes의 TextView에서 text를 “로그인 으로 변경)
회원가입: Palette > Buttons> Button (Attributes의 TextView에서 text를 “회원가입 으로 변경)

alt_text

  1. 실행버튼을 클릭해서 다시 앱을 실행해 보면 hello Android화면의 레이아웃이 로그인 화면 레이아웃으로 바뀐것을 볼수 있다.

alt_text

5 Activity 가 두개 이상인 앱(로그인 & 회원가입)

두개의 액비티비와 디자인을 만들어 두었다. 이제 이 두개의 액티비티Activity가 서로 화면을 호출하고 , 또 다른 새로운 액티비티Activity로부터 필요한 값을 받는 등을 연습해보자

액티비티는 안드로이드Activity는 Android에서 하나의 화면구성과 프로그램처리로 구성된다. 현재의 액티비티Activity에서 다른 화면으로 이동하고자 다른 액티비티Activity를 호출하면 기본적으로는 그릇이 쌓이듯이 새로운것이 위로 쌓여 사용자는 제일 위에 있는 화면만 볼 수 있는것이다.

이를 스택구조 라고 하는데 액티비티Activity의 호출은 이 스택구조대로 진행된다.

다만 모바일 기기의 적은 메모리와 실행환경 특성상, 액티비티Activity를 실행할때마다 무조건 스택의 맨꼭대기에 새로운 액티비티Activity를 쌓는것이 아니라, 새로운 액티비티Activity가 호출될때, 기존에 쌓여있던 스택상의 액티비티Activity들을 어떻게 할것인지 정할수도 있다.

새로운 액티비티Activity가 호출될때 스택에 어떻게 배치될것인지는 개발자가 정하기 나름이다.

안드로이드 에서는 액티비티Android 에서는 Activity의 호출시 가능한 플래그는 다음과 같다.

-Task 와 Flag의 설명은 부록을 참고한다. 글 하단의 ETC -> 2.ACTIVITY FLAG 항목을 참고하자.

앱의 전반적인 흐름을 생각해서 적절한 플래그를 지정해주는게 좋다.

  • 안드로이드Android 콤포넌트 이벤트

    안드로이드Android 의 레이아웃 콤포넌트들은 자신의 적절한 이벤트 속성을 가지고 있다. 따라서 개발자는 해당 콤포넌트들이 가지고 있는 이벤트에 대한 이벤트발생시점과 이벤트발생시 처리되야할 내용들을 기술해서 인터랙티브한 앱을 만들수 있다.

예를들어 button 콤포넌트의 경우, 클릭되었을때와 ,눌렸을때, 눌린다음 띄었을때 등의 이벤트가 미리 안드로이드Android에 정의되어 있기때문에, button 콤포넌트가 있는 layout 이라면 프로그래머가 해당 버튼에 대한 이벤트처리를 적을수 있다.

5.1 액티비티Activity 추가

로그인 화면을 만들었으니 회원가입화면을 만들어서 회원이 가입할수 있도록 해보자.

프로젝트 생성때는 1개의 액티비와 레이아웃이 자동으로 생성되었지만, 프로그램에서는 1개이상의 액티비티Activity 화면을 이동하면서 실행하게 된다. 따라서 사용자가 추가로 액티비티Activity를 추가해줘야 한다.

  1. 에디터 화면에서 MainActirt.kt 파일의 탭을 선택한다

alt_text

  1. 상단의 파일메뉴에서 File-> New ->Activity->Emprty Activity 를 선택한다.

alt_text

  1. 새로운 Activit 입력창이 나타나면 다음과 같이 입력한다.

alt_text

  1. 추가된 파일은 app>java>JoinFormActivity 와 app>res>layout>activity_join_form.xml 파일 이며 그림과 같다.

alt_text

  1. activity_join_form.xml 탭을 선택하고, 6.2.1 에서 했던 것처럼 회원가입에 필요한 입력폼인 이름,아이디,비밀번호와 버튼인 회원가입, 체크박스인 약관동의 를 추가한다.

alt_text

  1. 이제 우리앱에서는 MainActivity 와 JoinFormActivity 두개의 화면이 구성되어 있다. 하지만 이렇게 두개 이상의 액티비티Activity와 화면 구성을 헀어도, 개발자가 액티비티Activity간의 상호작용을 프로그래밍 하지 않으면 아무런 동작도 하지 않는다. JoinFormActivity 와 activity_join_form.xml은 단지 만들어만 둔것뿐이기 때문이다.
    6.3 에서 두개 이상의 액티비티Activity가 서로 호출하고 값을 전달 받는 방법을 연습해보자

5.2 여러개의 Activity 가 AndroidManifest.xml에 선언되었는지 확인

앞서 새로운 액티비티Activity가 추가한 후에는 반드시 확인해야 될것이 있었는데, app>manifests>AndroidManifest.xml파일이다. AndroidManifest 파일에는 앱에서 사용할 액티비티Activity에 대한 정의가 반드시 정의되어있어야한다.
만일 개발자가 새로운 액티비티Activity를 직접 추가하고 프로그램에서도 새로운 액티비티Activity가 열리도록 처리했는데도 오류가 발생한다면, 제일먼저 AndroidManifest.xml 에 해당액티비티Activity가 선언되어 있는지 확인하는 것이 좋다.

alt_text

5.3 하나의 Activity 에서 다른 Activity 열기

앱을 실행하면 처음으로 보이는 MainActivity 에서 회원가입 버튼을 누르면 JoinFormActivity 의 화면이 열리도록 할것이다.

“회원가입” 이라는 버튼을 클릭할때 안드로이드Android는 버튼에 대해 onclick 이벤트가 발생했다고 통지하는데, 개발자는 이때의 이벤트에 대해 적절한 처리과정을 적어주면 된다.

JoinFormActivity 화면이 열리도록 할예정이기 때문에, 다른 Activity 를 실행하는 명령어로서 StartActivity 함수를 실행한다.

  1. app>res>layout>activity_main 파일을 더블클릭해서 편집기화면에 보이도록하고 회원가입의 버튼을 클릭해서 attribute 에서 ID를 확인한다. 현재는 button2 라는 아이디이다.

alt_text

  1. app > java > MainActivity 를 더블클릭해서 편집기에서 MainActivity.kt 가 열리도록 한다. 소스를 보면 액티비티Activity가 생성되면서 제일 먼저 실행되는 onCreate 에는 현재 activity_main 레이아웃을 화면에 보여주는 setContentView 만 실행되고 있다.
    이 하단에 위에서 확인해두었던 “회원가입” 버튼을 레이아웃에서 찾아서 객체로 저장한다.

alt_text

Android Studio 에서는 intelisense라는 기능으로 함수나 리소스의 첫글자만 적어도 자동으로 사용가능한 목록이 나타나도록 편리한 기능을 지원한다.

  1. 과정 2처럼 하게되면 findViewById<Button> 부분에서 Button 관련 위젯이 import 되어 있지 않아서 에러표시가 될것이다. 이때는 소스파일의 상단부분에 import 지시어로 Button 위젯을 임포트 하거나, <button>부분에 마우스커서를 위치하고 Alt+Enter 를 누르면 Android Studio에서 자동으로 import 코드를 삽입하여 준다.

alt_text

Import 지시어로 Button위젯을 추가한다.

alt_text

  1. 이제 buttonJoinFormActivity를 클릭할때 JoinFormActivity 가 열리도록 클릭이벤트를 걸어보자.
    위에서도 말했듯이 , 안드로이드Android OS 에서는 화면에서 사용자가 무언가를 터치했을때, 터치한 것이 무엇인지에 대해 항상 대기하고 있다. 따라서 사용자가 화면을 터치한순간 안드로이드Android는 터치한 위치에 있는 버튼 위젯이 터치되었다고 알려주기 때문에, 개발자는 button위젯에 이벤트를 대기시켜 이벤트 발생시 처리될것을 프로그래밍 하면된다.

alt_text

  1. 버튼에 대해 click이벤트를 대기상태로 만들었으니, 실제로 이벤트가 발생할때 처리될것을 만들엊보자.
    이번장에서는 단지 다른 액티비티Activity를 실행할것이기 때문에, startActivity 함수를 이용한다.
    다른 액티비티Activity를 실행하기 위해서는 인텐트 라는 것을 이용해야한다.

alt_text

단, MainActiry 에는 Intent 가 import 되지 않았기 때문에, 위 처럼 붉은색으로 표시되는데, 과정3 에서 처럼 붉은색 글씨위에서 alt+Enter키를 누르면 자동으로 intent 가 import된다.
startActivity 함수로 인텐트를 지정하여 액티비티Activity를 시작한다.

package com.example.sample.helloandroid

import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       val buttonJoinFormActivity = findViewById<Button>(R.id.button2)

       buttonJoinFormActivity.setOnClickListener {
           //이곳엔 버튼이 클릭되었을때 처리할 것을 입력
           val intent = Intent(this, JoinFormActivity::class.java)
           startActivity(intent)
       }
   }
}
  1. 좀더 응용하고 싶다면, JoinFormActivity.kt 의 레이아웃에서 가입취소 버튼의 ID 를 찾아서 가입취소 버튼이 클릭되었을때, 현재의activity를 종료하는 코드를 넣어보자.
    참고로 현재의 Activity 를 종료하는 코드는 finish이다.
package com.example.sample.helloandroid

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class JoinFormActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_join_form)


       val buttonCancleActivity = findViewById<Button>(R.id.button4)

       buttonCancleActivity.setOnClickListener {
           //이곳엔 버튼이 클릭되었을때 처리할 것을 입력
           finish()
       }
   }
}
  1. 결과는 다음과 같다.

alt_text
alt_text
alt_text

액티비티Activity 실행-> 회원가입 클릭-> 가입취소 클릭-> 처음화면

5.4 Intent

안드로이드Android에서는 앱사용자가 앱내의 어떤 기능을 호출할떄 Intent(의도) 라는 클래스를 통해 호출한다.
같은 앱내에 있는 액티비티Activity라도 intent를 통해 명시적인 액티비티Activity이름(클래스이름)을 지정하여 주어 실행할수 있다.
만일 외부의 다른 서비스 또는 앱을 호출하고자 할때는 인텐트함수에 액션과 데이터를 넘겨주면 안드로이드Android가 해당 액션과 데이터처리가 가능하도록 선언된 앱들을 찾아서 연결할 앱목록을 보여준다. 이를 암시적 인텐트 라고 하는데 외부의 다른 앱의 패키지명과 클래스명을 알수 없을떄 이와같이 암시적으로 액션과 데이터를 안드로이드Android에게 던져주고 안드로이드Android가 알아서 찾도록 한다.

명시적으로 앱내다른 액티비티Activity를 호출

val myIntent = Intent(this, MySecondScreen::class.java)
startActivity(myIntent)

암시적으로 다른 앱을 호출

val intent = Intent(Intent.ACTION_VIEWiew, Uri.parse("http://m.naver.com"))
startActivity(intent)

암시적 인텐트를 이용하거나 외부에 내앱의 특정 기능을 제공하기위해서는 intent filter 와 action을 적절히 지정해야한다.

5.5 다른 Activity 호출후 결과값을 받아오기

다른 액티비티Activity를 호출한후 해당 액티비티Activity에서 처리한 결과값을 현재 액티비티Activity가 받아오고 싶은경우가 있다. 이때는 새로운 액티비티Activity를 호출할때 startActivityResult라는 함수를 사용한다.

이번에는 약관동의 화면 액티비티Activity를 추가해서 회원가입 액티비티Activity에서 약관동의 체크박스를 클릭했을때, 약관 내용의 액티비티Activity를 추가생성해서 약관동의 버튼의 클릭여부에 따라 회원가입진행여부를 판단하는 부분을 만들어보자.

  1. 액티비티Activity를 추가해보자

alt_text

app>java>AgreementActivity 파일과 app>res>layout>activity_agreement.xml 이 생성된다.

  1. app>res>layout>activity_agreement.xml 파일을 더블클릭해서 아래 그림과 같이 레이아웃을 구성하자
    alt_text
    참고로,
    약관동의 버튼의 ID는 : btnAgree 이며
    동의안함 버튼의 ID는 : btnCancle이다.
    약관동의 내용은 inputType : textMultiLine형식이다.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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=".AgreementActivity">

   <LinearLayout
       android:layout_width="368dp"
       android:layout_height="551dp"
       android:orientation="vertical"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent">

       <EditText
           android:id="@+id/editText4"
           android:layout_width="match_parent"
           android:layout_height="446dp"
           android:ems="10"
           android:inputType="textMultiLine"
           android:text="약관동의 약관동의 약관동의" />

       <Button
           android:id="@+id/btnAgree"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="약관 동의" />

       <Button
           android:id="@+id/btnCancle"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:text="동의 안함" />
   </LinearLayout>
</android.support.constraint.ConstraintLayout>
  1. 이제 JoinFormActivity에서 [약관동의] 체크박스를 클릭했을때 AgreementActivity 가 실행되도록 할것이기 때문에 체크박스에 대한 이벤트 리스너를 소스에서 구현한다.
package com.example.sample.helloandroid

import android.app.Activity
import android.content.Intent
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.CheckBox
import android.widget.Toast

class JoinFormActivity : AppCompatActivity() {
   private val REQUEST_CODE = 1

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_join_form)


       val buttonCancleActivity = findViewById<Button>(R.id.button4)

       buttonCancleActivity.setOnClickListener {
           //이곳엔 버튼이 클릭되었을때 처리할 것을 입력
           finish()
       }

       val checkAgreeActivity = findViewById<CheckBox>(R.id.checkBox)

       checkAgreeActivity.setOnClickListener {
           //이곳엔 버튼이 클릭되었을때 처리할 것을 입력
           val intent = Intent(this, AgreementActivity::class.java)
           startActivityForResult(intent, REQUEST_CODE)
       }
   }

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       when (resultCode) {
           Activity.RESULT_CANCELED -> {
               Toast.makeText(this@JoinFormActivity, "약관에 동의하지 않으면 가입할 수 없습니다.", Toast.LENGTH_LONG).show()
               Log.d("TAG", "resultCode $resultCode")
               finish()
           }
       }
   }
}

체크박스에 대한 click이벤트를 작성하고, 이벤트처리내용은 AgreementActivity 를호출한다.
이전의 MainActivity 에서 JoinFormActivity 를 실행할때는 startActivity를 사용했지만, 이번에는 호출한 액티비티Activity로부터 결과코드를 돌려받기 위해서 startActivityResult 함수를 이용해서 호출했다.

또한 호출한 결과값에 따른 처리를 하기 위해서 onActiviryResult 함수를 오버라이드 해서 , 결과값이 RESULT_CANCLE 의 경우 가입폼을 종료한다.
따라서 약관동의를 하지않으면 처음 로그인 화면으로 돌아가게 된다.

  1. 결과는 다음과 같다.

alt_text

alt_text

alt_text

alt_text

6 Fragement 이용한 앱

안드로이드Android 버젼 Android 3.0(API 레벨 11)에서 부터는 안드로이드Android탭 등과 같은 좀더 커진 화면에 컨텐츠를 효과적으로 배치하고 대응하기 위해서 Fragment(조각,파편)라는 것을 도입했다.
하나의 액티비티에 Activity, 즉 한화면단위에 여러개의 작은 Fragment 를 배치할수 있으며, 각 Fragment는 독립적인 라이프 사이클을 가지고 동작하게 됩니다. 따라서 하나의 Fragment 는 화면의 일부만 점유해도 동작될수 있으며 이를 이용하여 다수의 액티비티Activity에서 동일한 Fragment의 내용을 표시할수있다.

단, 플래그먼트는 액티비티Fragment는 Activity 위에서만 표시될수 있으며 혼자서는 화면에 출력될수 없다.

플래그먼트는 액티비티Fragment는 Activity와는 별도의 자신만의 라이프사이클을 가지고 있다. 액티비티Activity 라이프사이클에는 없는 onaAtteach, onDetach,onCreateView, onActivityCreated 등 액티비티Activity에 Fragment가 추가되거나 제거될때의 시점에 의존하는 별도의 라이프사이클도 가지고 있다.

대표적으로 신문기사앱을 만들때, 안드로이드Android 탭과 스마트폰용 앱를 따로 만드는 것이 아니라, 기사의 목록과 기사의 내용을 별도의 fragment로 구성하여 화면크기에 따라 적절한 레이아웃이 작동되도록 만들면 하나의 앱으로도 다양한 기기로의 대응이 가능하다.

  • FragementManager : Activity 내에서 Fragment들을 관리하기 위한 메소드
  • FragTransacction : Fragment연산의 집합클래스
  • Fragment에 Tag 지정하면 태그이름으로도 찾을수있다. : MainActivity 에서 replace 의 마지막 변수가 테그이름
  • Fragment 에서 Bundle로 값전달 방법 :
Bundle b _ new Bulder(); 
b.putString(“name”,”YSI”) 
f.setArguments(b)  

사용은

Bulder bfrom = getArguments(); \
if(bfrom != null)String name = bfrom.getStrting()
  • Fragment 에서 속한 Activity 로 전달하는방법 :
  1. 확장성이 떨어지나 일반적인방법: getActivity() 해서 거기서 함수를 따로 추가해서 함수에 던짐.
  2. 인터페이스를 만들고 전달하는 패턴
  • Fragment에서 새로운 Activity띄우기 : startActivirty, startActivityResult(새로띄운 앱으로 부터 결과값을 받을 필요가 있을 때 사용),

6.1 Fragment Manager

생성한 프래그먼트를 액티비티Fragment를 Activity와 상호 작용하기 위해서는 프래그먼트Fragment 매니저를 사용한다.
프래그먼트 매니저는 프래그먼트Fragment 매니저는 Fragment의 기본적인 제어와 액티비티Activity와의 통신등의 작업을 할수있다.

  • 프래그먼트Fragment 트랜잭션 add, replace
    프래그먼트Fragment의 추가/삭제/교체등을 동적으로 할수 있게해준다.
val transaction = supportFragmentManager.beginTransaction();
// 레이아웃의 fragment부분에 사용자의 프래그먼트Fragment로 교체한다.

// 레이아웃의 fragment부분에 사용자의 프래그먼트를 프래그먼트Fragment를 Fragment 스택에 추가한다.
transaction.add(R.id.myfragment, MyFragment());

// 또는 레이아웃의 fragment부분에 사용자의 프래그먼트Fragment로 교체한다.
transaction.replace(R.id.myfragment, MyFragment());

transaction.addToBackStack(null);

//commit을 해야 반영된다.
transaction.commit();
  • 플래그먼트에서 액티비티Fragment에서 Activity 자원 접근하기
    activity(java에서는 getActivity())를 통해 접근할수 있다.
    사용예 : 프래그먼트Fragment 클래스에서 activity?.findViewById<TextView>(R.id.hello)

  • 액티비티에서 프래그먼트 자원 접근하기
    프래그먼트Activity에서 Fragment 자원 접근하기
    Fragment 메니져를 통해서 먼저 접근하고자 하는 프래그먼트Fragment를 찾은후에 자원에 접근할수 있다.
    사용예 : 액티비티Activity 클래스에서 supportFragmentManager.findFragmentById(R.id.myfragment)

  • show, hide
    프래그먼트Fragment 보이지 않거나 보이도록 할수 있다.

7 ContentProvider 로 외부공개하기

다른 앱에 데이터를 제공하거나 제공받기 위해서는 안드로이드Android에게 정한 ContentProvider 란 기능을 이용하여 같은 인터페이스를 통해 가능하다.
앱에서 안드로이드Android의 사진기능을 불러내서 촬영후 촬영된 이미지를 받아서 자신의 앱에 사용한다든지, 주소록에 있는 사용자목록을 받아서 처리한다든지 라는게 가능한 이유가 카메라앱, 주소록앱 등에서 ContentProvider를 제공해주기 때문이다.

ContentProvider 가 제공하기 위한 거라면, 제공받는 쪽은 ContentResolver를 이용한다.

자신의 앱의 일부데이터를 다른앱에서 사용할수 있도록 할수도있다.
URI형태는 다음과 같다.

  • contetn://패키지정보/파일경로/자료ID
  • 또는 contetn://패키지정보/테이블이름/자료ID

안드로이드Android의 기본 컨텐츠 프로바이더는 아래와 같다.

  • android.provider.Contacts.Phones.CONTENT_URI
  • android.provider.Contacts.Photos.CONTENT_URI
  • android.provider.CallLog.Calls.CONTENT_URI
  • android.provider.Calendar.CONTENT_URI
  • …MediaStore,Images,

개발자가 자신의 앱에서 컨텐츠를 제공하고자 Content Provider를 만들 때는 ContentProvider 클래스를 상속받아서 다음 6개의 함수를 구현해주면 된다.

query() , insert() , update() , delete() , getType() , onCreate()

자료를 제공하는 ContentProvider 에서는 다음과 같이 설정한다.

  1. AndroidManifest.xml에서 프로바이터 속성추가
<application >
<provider  
 
 android:name=".MyContentProvider"  
 
 android:authorities="com.sugoijapaneseschool.mytestcontentprovider1.MyContentProvider"  
 
 android:exported="true"  
 
 android:enabled="true"/>
  1. 외부에서 다른앱이 자료조회,갱신등을 요청할경우에 적절한 데이터처리를 위한 클래스와 처리로직을 생성한다.
 class MyContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val queryBuilder = SQLiteQueryBuilder()
        val db = MyDatabase.getInstance(context!!)
        queryBuilder.tables = "mytable"
        val cursor =
            queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder)
        cursor.setNotificationUri(context!!.contentResolver, uri)
        return cursor
    }

    override fun getType(p0: Uri): String? {
        return null
    }

    override fun insert(p0: Uri, p1: ContentValues?): Uri? {
        val db = MyDatabase.getInstance(context!!)
        db.insert("mytable", null, p1)
        return null
    }

    override fun delete(p0: Uri, p1: String?, p2: Array<out String>?): Int {
        val db = MyDatabase.getInstance(context!!)
        return db.delete("mytable",p1,p2)
    }

    override fun update(p0: Uri, p1: ContentValues?, p2: String?, p3: Array<out String>?): Int {
        val db = MyDatabase.getInstance(context!!)
        return db.update("mytable",p1, p2, p3)
    }
}

3.이번예제는 데이터베이스를 이용하므로 앱에 데이터베이스를 생성한다.

class MyDatabase {
        companion object {
            private const val DATABASE_NAME = "mydatabase.db"
            private const val DATABASE_VERSION = 1
            private var sInstance: DatabaseHelper? = null

            @Synchronized
            fun getInstance(context: Context): SQLiteDatabase {
                if (sInstance == null) {
                    sInstance = DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION)
                }
        }
        return sInstance!!.writableDatabase
            }

            class DatabaseHelper(
                context: Context?,
                name: String?,
                factory: SQLiteDatabase.CursorFactory?,
                version: Int) : SQLiteOpenHelper(context, name, factory, version) {
                override fun onCreate(_db: SQLiteDatabase?) {
                    _db?.execSQL("CREATE TABLE mytable"
                            + "(_id INTEGER PRIMARY KEY AUTOINCREMENT, "
                            + "username TEXT NOT NULL);")
                    _db?.execSQL("insert into mytable(username) values('kim')")
                }

    }

            override fun onUpgrade(_db: SQLiteDatabase?, _oldVersion: Int, _newVersion: Int) {
                    _db?.execSQL("DROP TABLE IF EXISTS mytable")
                    onCreate(_db)
                }
            }
    }
    }
}
  1. android.net.Uri.Builder 클래스와 android.content.ContentUris 클래스를 사용해서 만드는 방법
  2. 앱을 설치한다.

이제 ContentProvider앱은 데이터를 외부에서 조회해볼수 있도록 준비가 되었다.
외부의 앱은 ContentResolver를 통해 데이터를 조회, 갱신하면된다.

1.Android Q이상이라면 AndroidManifest.xml에 프로바이더 권한을 추가한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sugoijapaneseschool.mytestcontentresolver1">
    <queries>
        <provider android:authorities="com.sugoijapaneseschool.mytestcontentprovider1.MyContentProvider"/>
    </queries>

2.프로바이더가 제공하는 전용주소를 ContentResolver를 통해 요청하면 된다.


class MainActivity : AppCompatActivity() {

    private val PROVIDER_URI = "content://com.sugoijapaneseschool.mytestcontentprovider1.MyContentProvider"
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    ...
    
        val c: Cursor? = contentResolver.query(
                Uri.parse(PROVIDER_URI),
              null,
                null, null, null, null)

        while (c?.moveToNext() == true) {
            var tv = findViewById<TextView>(R.id.mytext)
            tv.text = (c.getString(1))
        }
    }
    c?.close()
    }
}

8 Dialog 창띄우기

Dialog 는 사용자에게 추가정보를 요구하거나 결정에 대한 반응을 요구할때 작은 창을 띄워서 프롬프트를 보내는 작은 창이다.

다이아로그는 보통 AlertDialog, DatePickerDialog, TimePickerDialog 서브클래스사용한다. 다이아로그를 표시할때 액티비티Activity와 프라그먼트를 사용한다.

AlertDialog 를 사용해서 사용자가 직접 레이아웃을 디자인할수도 있다. 예를 들어 대화상자 안에 영상을 넣는다는지, 여러개의 체크박스 달력같은 것도 넣어서 표시할수도 있다.

8.1 AlertDialog

기본형태의 대화상자이다. 예/아니오/취소 같은 속성의 총 3개의 버튼까지 지정이 가능하다. 별다른 디자인을 생각하지 않고 알림을 띄우고 싶을때 사용한다.

val builder = AlertDialog.Builder(this@MainActivity)
   .setTitle("title")
   .setMessage("message")
   .setPositiveButton("Yes"){dialog, which ->
       toast("OK")
   }
   .setNeutralButton("cancel") {dialog, which ->
       toast("cancel")

   }
   .setNegativeButton("No") {dialog, which ->
       toast("No")

   }
builder.show()

8.2 DatePicker, TimePickerDialog

datepicker, timepickkerDialog는 AlertDialog를 상속받아서 달력또는 시간표시 기능을 추가하여 사용기 편하도록 만들어진 대화창이다.

val mcurrentTime = Calendar.getInstance()
val year = mcurrentTime.get(Calendar.YEAR)
val month = mcurrentTime.get(Calendar.MONTH)
val day = mcurrentTime.get(Calendar.DAY_OF_MONTH)

val datePicker = DatePickerDialog(this, object : DatePickerDialog.OnDateSetListener {
   override fun onDateSet(vView: DatePicker?, year: Int, month: Int, dayOfMonth: Int) {
       toast(String.format("%d / %d / %d", dayOfMonth, month + 1, year))
   }
}, year, month, day);


val mTimePicker: TimePickerDialog
val mcurrentTime = Calendar.getInstance()
val hour = mcurrentTime.get(Calendar.HOUR_OF_DAY)
val minute = mcurrentTime.get(Calendar.MINUTE)

mTimePicker = TimePickerDialog(this, object : TimePickerDialog.OnTimeSetListener {
   override fun onTimeSet(vView: TimePicker?, hourOfDay: Int, minute: Int) {

       toast(String.format("%d : %d", hourOfDay, minute))
   }
}, hour, minute, false)

mTimePicker.show();

8.3 CustomDialog

대화상자를 좀더 깔끔하고 내용을 다양한 구성으로 하고 싶을 때 layout을 직접만들어서 alertdialog의 vView로 지정하면된다. 커스텀 뷰에는 이미지, 버튼, 리스트, 체크박스등 다양한 위젯을 배치할수 있다.

alert_custom.xml 레이아웃 생성

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">


   <TextView android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:gravity="center"
       android:textSize="30sp"
       android:textStyle="bold"
       android:textAllCaps="true"
       android:padding="6dp"
       android:textColor="@android:color/white"
       android:background="@android:color/holo_blue_light"
       android:text="Sign"/>

   <androidx.appcompat.widget.AppCompatEditText
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:id="@+id/userName"
       android:hint="Username"
       android:layout_marginTop="16dp"
       android:layout_marginLeft="4dp"
       android:layout_marginRight="4dp"
       android:layout_marginBottom="4dp"/>

   <androidx.appcompat.widget.AppCompatEditText
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:id="@+id/password"
       android:inputType="textPassword"
       android:hint="Password"
       android:layout_marginTop="16dp"
       android:layout_marginLeft="4dp"
       android:layout_marginRight="4dp"
       android:layout_marginBottom="4dp"/>


   <LinearLayout
       android:layout_width="match_parent"
       android:orientation="horizontal"
       android:layout_marginTop="6dp"
       android:layout_height="wrap_content">

       <androidx.appcompat.widget.AppCompatButton
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="1"
           android:id="@+id/cancel"
           android:text="Cancel"/>

       <androidx.appcompat.widget.AppCompatButton
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_weight="1"
           android:id="@+id/login"
           android:text="Login"/>

   </LinearLayout>


</LinearLayout>

소스코드예

val builder: AlertDialog.Builder = AlertDialog.Builder(this@MainActivity)
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val customView = inflater.inflate(R.layout.alert_custom, null, false)

builder.setView(customView)

val alertDialog: AlertDialog = builder.create()
alertDialog.show()

customView.findViewById<Button>(R.id.cancel).setOnClickListener({
   alertDialog.dismiss()
})

customView.findViewById<Button>(R.id.login).setOnClickListener({
   Toast.makeText(this@MainActivity, customView.findViewById<Button>(R.id.userName).text, Toast.LENGTH_SHORT).show()
   alertDialog.dismiss()
})

9 네트워크통신하기

앱에서 외부api서버등을 통해 데이터를 가져오거나, 서버에 사진등을 업로드 할때 네트워크를 통해 통신을 해야한다. 네트워크통신을 하기위해서는 네트워크프로토콜을 이용하여 데이터를 전/수신해야되는데 http 통신을 가장 많이 사용한다.

Android에서는 HttpUrlConnection이라는 java표준 라이브러리를 api 1 부터 지원하기 때문에 별도의 라이브러리를 추가하지 않고도 네트워크 통신을 할수있다.

HttpUrlConnection은 사용하기가 좀 번거로운점이 있어서 volley, retrofit 등의 라이브러리를 활용하는게 좋다.

단, androidManifest.xml에 퍼미션을 추가하지 않으면 실행시에 에러가 난다.

<uses-permission android:name="android.permission.INTERNET" />

9.1 HttpURLConnection

java표준 라이브러리로서 api 1부터 지원한다. 이것만 가지고도 충분히 네티트워크통신을 할수 있지만, 결과값처리, 버퍼처리, 데이터전송하기, 오류처리,재시도등 불필요하게 손이 많이 가기 때문에 잘사용하지않고 개발시에는 volley, retrofit과 같은 잘만들어진 라이브러리를 사용한다.

기본적으로 네트워크 통신은 응답대기 특성이 있어서 메인쓰레드에서는 하지 않는다. 그래서 안드로이드Android에서는 thread, Async등을 사용해서 백그라운드에서 처리했었는데, 코틀린Kotlin에서는 코루틴으로을 이용하여 간단하게 백그라운드에서 처리가 가능하다.

CoroutineScope(Dispatchers.IO).launch {
   // 새로운 CoroutineScope 로 동작하는 백그라운드 작업
   val connection = URL("https://jsonplaceholder.typicode.com/todos/1").openConnection() as HttpURLConnection
   try {
       val data = connection.inputStream.bufferedReader().use { it.readText() }
       print(data)
   } finally {
       connection.disconnect()
   }
}

9.2 Volley

volley는 안드로이드Android에서 지원하는 라이브러리이다. HttpUrlConnection은 너무 불편하고 사용하려면 부가적인 코드를 만들어야 되었기 때문에 별도의 라이브러리를 지원이를 좀더 간편하게 사용하도록 별도의 라이브러리인 Volley 를 제공한다.

volley는 앱에서 쉽고 빠르게 네트워크통신을 할수 있게 해주는데 네트워크요청의 우선순위, 동시요청, 캐시지원,요청취소, SPDY프로토콜지원, protobuffer,json,xml등 다양한 응답형식지원 등 다양한 기능을 제공한다.

사용하기 위해서는 volley라이브러리를 build.gradle(Module:app)에 의존성 추가해야한다.

dependencies {
...
   implementation 'com.android.volley:volley:1.2.0'
}

requestQueue, add, QuestMethod, Response, Listener등 한결 보기편하게 코딩할수 있다. 또한 httpUrlConnection때 사용하던 coroutine 을 사용하지 않아도 volley 자체가 별도의 쓰레드에서 수행되도록 되어 있어 사용과 관리가 한결 쉬워졌다.

Volley.newRequestQueue(this).add(StringRequest(Request.Method.GET,"https://jsonplaceholder.typicode.com/todos/1", Response.Listener {
   println("Response : $it")
},null))

9.3 Retrofit ( OKHttp)

volley도 충분히 좋은 라이브러리지만 속도 면에서 Retrofit이 응답속도가 빠르고 좀더 편리한 점이 있기 때문에 더 많이 사용하는 라이브러리다.

주요특징으로는 typesafe(응답데이터를 필요한객체 data class 로 받을수있다), interceptor를 추가가능, 어노테이션을 통한 정의 등이 있다.

사용하기 위해서는 retrofit라이브러리를 build.gradle(Module:app)에 의존성 추가해야한다.

dependencies {

...
   implementation 'com.squareup.retrofit2:retrofit:2.9.0'
   implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
}
val retrofit = Retrofit.Builder()
       .baseUrl("https://jsonplaceholder.typicode.com/")
       .addConverterFactory(GsonConverterFactory.create())
       .build()

val server: MyService = retrofit.create(MyService::class.java)
server.gettRequest().enqueue(object : Callback<ResponseDTO> {
   override fun onFailure(call: Call<ResponseDTO>?, t: Throwable?) {
       Log.e("retrofit", t.toString())
   }


   override fun onResponse(call: Call<ResponseDTO>, response: retrofit2.Response<ResponseDTO>) {

       print("retrofit ${response} ")
   }
})


interface MyService {

   //post1
   // 매개변수를 미리 정해두는 방식
   @GET("todos/1")
   fun gettRequest(
   ): Call<ResponseDTO>

}

data class ResponseDTO (
       var userId: Int,
       var id: Int,
       var title: String,
       var completed: Boolean
)

10 firebase 사용하기

구글에서 제공하는 firebase 는 BaaS(Backend as Service)로서 개발자가 서버를 구축하지 않고도 앱을 개발할수 있도록 데이터베이스부터 인증, 푸시알림, 호스팅등까지도 제공하는 서비스이다.

Firebase는 사용과 설치가 매우 간단하고 속도 또한 매우 빠르기 때문에 소기업이나 개인개발자들은 서버를 직접 구성하는것보다 파이어베이스의 이용을 권장한다.

10.1 설치

AndroidStudio에서 tools->firebase 를 선택하면 제공하는 여러가지 기능목록이 나오는 데 이중 원하는 기능을 선택하면 step by step으로 체크해주기때문에 따라하면서 작업하면 어렵지않게 설정을 마루리 할수 있다.

10.2 firebase analytics

analytics 는 앱내에서 사용자의 행동패턴을 분석하고자 할때 사용한다. 사용자의 행동패턴을 분석해서 특정 사용자에게만 앱내 상품 구매를 유도한다든지, 앱개선시에 좀더 좋은 ux로 개선할수 있도록 데이터를 취합해준다.

analytics에서는 크게 두가지 정보를 log로 서버에 기록(logging)할수 있게 해준다.

  • Event : 각종이벤트 예를들면 앱기동, 로그인, 레벨업, 상품구매, 카트추가 등 앱활동에 있을법한 것들을 미리 정의된 상수값을 사용하여 기록할수 있다.
  • 사용자속성 : 앱운영사가 수집하고자 하는 별도의 사용자정보등을 키-밸류 로 지정하여 서버에 기록할수 있다. 이벤트에도 붙여서 같이 기록도 가능하다.

fb analytics를 이용하여 사용자행동을 기록하면 앱활성자수,평균수익률, 앱이용률, 사용자 단말버젼 및 종류 파악등 다양한 각도로 분석할수 있도록 해준다.

사용예: 로그인시에 정보를 기록하기

val bundle = Bundle()
bundle.putString(FirebaseAnalytics.Param.ITEM_ID, "id")
bundle.putString(FirebaseAnalytics.Param.ITEM_NAME, "name")
mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, bundle)

6.8.3.firebase auth

앱에서 사용자 로그인등 인증을 구현하기 위해서는 별도의 인증 서버와 사용자 데이터베이스, 또는 연계할 각종 외부인증서비스와 연동모듈 등을 준비해야 되는데 firebase auth 기능을 이용하면 비교적 쉽게 인증서비스를 구현할수 있다.

기존적인 password 인증부터, 전화번호, 구글ID연동, 페이스북연동, 트위터연동등 많이 쓰이는 로그인 연동서비스부터 OAuth 2.0, OpenOID connect와 같은 표준인증방식들도 같이 사용할수 있다.

또한 로그인시구현시에 반드시 구현해야하는 로그인 화면 UI 쉽게 구성할수 있도록 기본 화면을 제공해 준다.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

  
 
  var gso= GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
           .requestIdToken(getString(R.string.default_web_client_id))
           .requestEmail()
           .build();

   googleSigneInClient= GoogleSignIn.getClient(this, gso)
   var mFirebaseAuth:FirebaseAuth = FirebaseAuth.getInstance();

   val signInIntent =googleSigneInClient.signInIntent
   startActivityForResult(signInIntent, 100)

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)

   if(requestCode==100){
       val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)
       if(result?.isSuccess == true){
           //구글 로그인이 성공했을 경우
           val account = result.signInAccount
           val credential = GoogleAuthProvider.getCredential(account?.idToken, null)
           FirebaseAuth.getInstance().signInWithCredential(credential)
       }
   }
}

10.3 firestore & firebase database

앱내에서 데이터를 저장하거나 보관된 데이터를 가져와서 앱내의 정보를 영속화 시킬수 있도록 도와주는 서비스이다.

firestore는 NoSQL 데이터베이스로서 data,document,collection 으로 구성되어 있다. 데이터의 저장도 key-value형식으로서 테이블스키마 정의가 따로 없다.

Android에서 firebase데이터베이스를 이용하면 데이터의 변경에 대한 이벤트를 파이어베이스 라이브러리에서 감지하여 UI에 반영할수 있는 onDataChange등의 리스너를 제공하여 주기 때문에 실시간으로 데이터가 변경되었을 때 사용자에게 곧바로 반영되도록 할수 있는 장점이 있다.

사용할수 있는 데이터베이스는 두가지로 firestore와 firebase realtme database 가 있다.

  • realtime database : 구형 데이터베이스 형식이다.
// Write a message to the database
val database = FirebaseDatabase.getInstance()
val myRef = database.reference.child("myusers").child("kim")
//val myRef = database.getReference("message")

myRef.setValue("Hello, kim!")
  • firestore : realtime database를 개선한 새로운 데이터베이스 형식이다.
val firestore = FirebaseFirestore.getInstance()
var userDTO= UserDTO("kim","Hello, kim in firestore")
firestore?.collection("User")?.document("document1")?.set(userDTO)
       .addOnCompleteListener {
           if(it.isSuccessful)
               Toast.makeText(this, "succeed", Toast.LENGTH_SHORT).show()
}

10.4 Firebase Cloud Messaging

앱에 Push통지(Notification)을 하기 위해서는 GCM또는 APNS(iOS) 각각의 코드를 구현하고 별도의 서버에서 사용자단말 ID에 메시지를 전송하여 사용자가 알림메시지를 받도록 구현해야 한다.

FCM은 플랫폼에 상관없이 앱운영사가 사용자에게 간편하게 메시지를 보낼수 있도록 서비스를 제공한다.

Android에서 FCM 메시지를 수신하기 위해서는 AndroidManifest.xml 파일에 service 를 등록하고 , 등록한 서비스가 처리해야될 내용이 구현된 class 가 있어야 한다.

<service
   android:name=".MyFirebaseMessgaeService"
   android:enabled="true"
   android:exported="true">
   <intent-filter>
       <action android:name="com.google.firebase.MESSAGING_EVENT" />
   </intent-filter>
</service>

class MyFirebaseMessagingService : FirebaseMessagingService() {
   val TAG = "FirebaseMessagingService"

   @SuppressLint("LongLogTag")
   override fun onMessageReceived(remoteMessage: RemoteMessage) {
       Log.d(TAG, "msg received: ${remoteMessage.from}")
       if (remoteMessage.notification != null) {
           showNotification(remoteMessage.notification?.title, remoteMessage.notification?.body)
       }
   }
   override fun onNewToken(token: String) {
       Log.d("TAG", "Refreshed token: $token")
   }
   private fun showNotification(title: String?, body: String?) {
       val intent = Intent(this, MainActivity::class.java)
       intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
       val pendingIntent = PendingIntent.getActivity(this, 0, intent,
           PendingIntent.FLAG_ONE_SHOT)
       val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
       val notificationBuilder = NotificationCompat.Builder(this)
           .setSmallIcon(R.mipmap.ic_launcher)
           .setContentTitle(title)
           .setContentText(body)
           .setAutoCancel(true)
           .setSound(soundUri)
           .setContentIntent(pendingIntent)
       val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
       notificationManager.notify(0, notificationBuilder.build())
   }
}

10.5 Firebase etcs

이밖에도 아래와 같은 다양한 기능들이 있다

  • 앱의 오류 보고 Cracshytics
  • 사용자 선호도 테스트 A/B 테스트
  • 앱내광고 Admob
  • 배포관리 Distribution
  • 앱테스트 TestLab
  • 등등

11 로컬 저장소 사용

Android에서 정보를 로컬에 저장하는 방법으로는 여러가지가 있지만 아래와 같은 방법들을 일반적으로 사용한다.

  • SharedPpreference : Android에서 간편하게 제공하는 저장방식으로 적은 데이터단위로 쉽게 사용할수있다.
  • SsqlLite : 모바일용 데이터관리 RDBMS로서 전통진 sql 문장을 사용하여 CRUD하여 데이터를 사용할수 있다.
  • File 저장 : image 나 raw 데이터같은 경우 file에 직접 저장하여 사용한다.

앱의 규모나 목적에 따라서 사용하면된다.

11.1 sSharedPpreference

앱내에서 데이터를 간단히 저장하고 또 이값을 앱의 여기저기서 불러내서 사용하고자 할때 사용한다. 앱이 삭제되지 않는한 계속 존재하는 앱내부의 공유저장소이다.

데이터는 xml 형식으로 저장되기 떄문에 입력데이터는 string,int,bool 등으로 한정적이다.

val prefs: SharedPpreferences = getSharedPpreferences("myprefes", MODE_PRIVATE)
var myEditText: String?
   get() = prefs.getString("myvalue1", "")
   set(value) = prefs.edit().putString("myvalue1", value).apply() //or commit()

위와 같이 데이터 저장과 호출은 매우 단순하다. 공유자원이기 때문에 context가 제공되는 어떤곳에서도 불러서 사용할수 있다.

11.2 sqllLite

sSharedpreference로는 앱내에서 발생한 다소 복잡한 데이터들을 저장하기에는 다소 무리가 있는데, 이때 사용하는것이 SsqllLite이다. SsqllLite 는 안드로이드Android에 기본으로 탑재되어있는 경량 관계형데이터베이스로서 별도의 라이브러리를 추가히않고도 바로 사용할수 있다.

또한 로컬데이터베이스에 저장되어 있기때문에, 네트워크가 끊긴 환경에서 앱을 실행해도 데이터를 표시할수 있다.

sql을 사용해봤고 CRUD(Create, Read, Update, Delete)를 할줄 아는 개발자라면 SsqlLite를 사용해보는 것도 고려해볼만하다.

class DBHelper(context: Context?, name: String?, factory: SQLiteDatabase.CursorFactory?, version: Int) :
   SQLiteOpenHelper(context, name, factory, version) {
   override fun onCreate(db: SQLiteDatabase) {
       var sql : String = "CREATE TABLE if not exists mytable (" +
               "_id integer primary key autoincrement," +
               "title text);";

       db.execSQL(sql)
   }

   override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
       val sql : String = "DROP TABLE if exists mytable"

       db.execSQL(sql)
       onCreate(db)
   }

}


// insert row
val dbHelper = DBHelper(this, "newdb.db", null, 1)
val databaseWrite = dbHelper.writableDatabase

val values = ContentValues().apply {
   put("title", "My Title")
}
val newRowId = databaseWrite?.insert("mytable", null, values)



// select row
val databaseRead = dbHelper.readableDatabase
val projection = arrayOf("title")

val selection = "title = ?"
val selectionArgs = arrayOf("My Title")

val sortOrder = "title DESC"

val cursor = databaseRead.query(
   "mytable",
   projection,
   selection,
   selectionArgs,
   null,
   null,
   sortOrder
)

if (cursor.moveToFirst()) {
   do {
       var title = cursor.getString(cursor.getColumnIndex("title"))
      println(title)
   } while (cursor.moveToNext())
}

*현재 Android 에서는 SQLlsqlLite보다 쓰기 편한 Room 사용을 공식적으로 권장하고 있다.
*저장된 SQL 데이터베이스 파일은 단말기에 접근 가능만 한다면 누구라도 읽어 볼수있기 때문에 보안이 중요한 데이터는 저장하지 않도록 하는게 좋다.

11.3 file 저장 / 읽기

이미지나 raw데이터 같은 경우 데이터베이스나 sSharedpreference에 넣기에는 무리가 있기때문에 로컬저장소에 파일형태로 저장하여 사용할수 있다.

파일저장은 안드로이드Android 내부저장소와 외부저장소를 지정하여 선택할수 있다.

단, 파일저장/읽기 할때는 메인쓰레드에서 하게되면 ANR(App Not Responsive : 앱응답없음오류)이 되기 때문에 코루틴의 IO쓰레드에서 실행하기를 권장한다.

raw데이터의 경우 FileOutputStream을 사용해서 저장한다.

openFileOutput("myfile.txt", MODE_PRIVATE).use {
   it.write("myfile message".toByteArray())
}

val file = File(filesDir, "myfile.txt")
val contents = file.readText() // Read file

println(contents);

12 서비스 & 브로드캐스트Broadcast 다루기

12.1 서비스

안드로이드에서Android Service는 유저 화면에 보이지 않지만 지속적으로 뭔가를 실행해야 될때 서비스라는 것을 이용한다. 서비스는 백그라운드에서 작업을 수행하기 때문에, 사용자가 앱사용을 끝내더라도 계속 뭔가를 수행되도록 할수도있다.
안드로이드 최신버젼에서는 단말기의 사용자 환경을 최적화 하기 위해서 각 앱들이 백그라운드에서 실행하는 것을 사용자가 결정할수 있도록 하고 있어서 주의가 필요하다.

서비스의 타입은 3가지가 있다.

  • 포어그라운드 타입 : 알림창에 서비스가 실행중이라는 것을 지정해 놓고 유저가 해당 서비스가 실행중이라는것을인지할수 있기 때문에 ,안드로이드Android가 강제로 종료하지않는다.
  • 백그라운드 타입 : 알림창등에 표시않지않고 백그라운드에서 수행한다. 사용자는 해상서비스가 수행중인지 인지할수 없기때문에, 안드로이드Android가 시스템상황과 백그라운드서비스제한정책 에 따라서 강제로 종료하는 경우도 있다.
  • 바인드 서비스 : 서로 다른 서비스(다른앱 서비스도 포함)들이 서로 연계(bind되어 서버-클라이언트와 같은 구성이됨) 되어 서비스를 구동한다. 바인딩된 서비스는 통신이 가능하다. 주로 앱의 기능을 다른 외부에 제공할때 사용된다.구글의 인앱결제 IBinder등을 참고.

사용예

AndroidManifest.xml

<service android:name=".MyBindService"/>

MainActivity

var mBindService: MyBindService? = null;
private val mConnection: ServiceConnection = object : ServiceConnection {
   override fun onServiceConnected(name: ComponentName, service: IBinder) {
       val binder: MyBindService.BindServiceBinder = service as MyBindService.BindServiceBinder
       mBindService = binder.service
       mBindService!!.registerCallback(mCallback)
   }
   override fun onServiceDisconnected(name: ComponentName) {
       mBindService = null
   }
}

private val mCallback: MyBindService.ICallback = object : MyBindService.ICallback {
   override fun remoteCall() {
       Log.d("MainActivity", "called by service")
   }
}

override fun onDestroy() {
   super.onDestroy()

   unbindService(mConnection);
}
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)


   val service = Intent(this, MyBindService::class.java)
   bindService(service, mConnection, BIND_AUTO_CREATE)

   val scope = CoroutineScope(Dispatchers.IO)
   scope.launch{
       delay(1000)
       mBindService?.myServiceFunc();
   }
}

MyBindService

class MyBindService : Service() {

   inner class BindServiceBinder : Binder() {
       val service: MyBindService
           get() = this@MyBindService
   }

   private val mBinder: BindServiceBinder = BindServiceBinder()
   private var mCallback: ICallback? = null

   override fun onBind(intent: Intent?): IBinder? {
       return mBinder
   }

   interface ICallback {
       fun remoteCall()
   }

   fun registerCallback(cb: ICallback?) {
       mCallback = cb
   }

   fun myServiceFunc() {
       mCallback!!.remoteCall()
   }
}

12.2 브로드캐스트

안드로이드Broadcast

Android는 단말의 여러가지 정보에 대해 단방향으로 메시지를 보내는데 이것을 브로드캐스트라고 한다.

브로드캐스트Broadcast라고 한다.

Broadcast는 시스템전반에 걸쳐 보내지는 메시지이기 때문에 수신자가 메시지형식만 알면 메시지를 수신할수 있다.

개발자도 자신의 브로드캐스트Broadcast를 만들어서 송신하고 이를 다른곳에서 수신할수 있도록 할수 있다.

등록된(registered) 브로드캐스트는 액티비티, 프래그먼트Broadcast는 Activity, Fragment, 서비스 등에서 보낸 메시지를 수신할수있다.

Oreo버젼부터는 동적으로 리시버를 생성하여 Context가 살아있는 동안에만 수신할수있도록 제한했다.

LocalBroadcastManager를 이용하여 내앱에서만 브로드캐스트Broadcast를 허용할수 있도록 등록하기때문에 다른 앱이 수신하는것을 방지할수 있다.

lateinit var myBroadCastReceiver:MyBroadCastReceiver
override fun onResume() {
   super.onResume()

   myBroadCastReceiver = MyBroadCastReceiver()
   val intentFilter = IntentFilter().apply {
       addAction("test.broadcast")
   }
   registerReceiver(myBroadCastReceiver, intentFilter)


   val boardIntent = Intent().apply {
       setAction("test.broadcast")
   }
   sendBroadcast(boardIntent)
}
override fun onPause() {
   super.onPause();
   unregisterReceiver(myBroadCastReceiver);

}

13 WorkManager

Android 에서 백그라운드 서비스, 브로드캐스트Broadcast등을을 하기위해서 Thread, Async, JobScheduler,알람매니져등이 있지만 최근에는 WorkManager를 이용하기를 권장한다.

WorkManager는 주로 백단 에서 작업해야하는 것들에 대해 사용하는데, 앱이 종료되거나 다시시작되더  WorkManager가 작업을 다시 시작해주기 때문에 안정적인 서비스 개시가 가능하도록 해준다.

그렇다고 모든 서비스, 쓰레드에 WorkManager를 쓰라는건 아니고 , 즉각실행해야 될거는 coroutines(또는 rxjava등) , 정확한 시간에 가동되야 하는거는 AlarmManager, 기기가 다시 시작되어도 실행되어야 할 백그라운드 작업등은 WorkManager를 쓰라는 얘기다.

WorkerManager 는 API레벨과 앱상태같은 요건에 근거해서 내부적으로 적절한 방법으로 백그라운작업을 어떻게 할지 자동으로 선택하다.

https://developer.android.com/guide/background

WorkerManger에는 다음과 같은 개념이 있다.

  • Worker : Abstract클래스로서 이 클래스를 확장받은 일할놈(?)이 어떤일을 할지를 구체적으로 기술한다. 자신의 일이 잘 끝났다 못끝냈다는 Success, Failure, Retry 3개의 값을 반환함으로서 workmanager가 전반적인 work를 관리할수 있도록 한다.
  • WorkRequest : 작업할놈이 결정되었다면 이 클래스를 통해서 작업요청을 해야하는데, OneTimeWorkrequest, PeriodicWorkRequest가 있다.
  • WorkerManager : WorkRequest를 받은 Worker가 를 큐에 넣거나 빼는 등의 전체 work 에 대한 작업상태를 모니터링 한다.
  1. 앱이 백그라운드에서 log 를 출력하기.

https://github.com/sugoigroup/android_workmanager_example/commit/d9662cc3e3fa38c01079a7bac25003b5b42fd46f

dependencies {
...

    def work_version = "2.5.0"
    implementation "androidx.work:work-runtime:$work_version"

}

build.gradle (Module:… .app)에 workerManager 의존성추가

  1. 샘플워커 클래스를 만들고, workRequest->workerManager에 등록

https://github.com/sugoigroup/android_workmanager_example/commit/d9662cc3e3fa38c01079a7bac25003b5b42fd46f



---UploadWorker.java (일할놈)를 만들어서 등록


import android.util.Log;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class UpladWorker extends Worker {
    private int count = 0;

    public UpladWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        while (count < 10) {
            count++;
            try {
                Log.i("worker", "now background" + count);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
   }
     }

        return Result.success();

    }
}

—workerRequest -> workerManager에 worker

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

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        OneTimeWorkRequest oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .build();

        // 매니져에게 일할거라고 등록한다.
        WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);
    }


I/worker: now background1

I/worker: now background2

I/worker: now background3

I/worker: now background4 --> 이 시점에서 Home 버튼을눌러 앱을 보이지않는 백그라운드상태로 전환했다.

I/worker: now background5 --> 백그라운드상태이지만 워커는 계속움직여 로그가 찍힌다.

I/worker: now background6

I/worker: now background7

I/worker: now background8

I/worker: now background9

I/worker: now background10 
  1. 백그라운드 상테에서 알림창으로 통지가 오는지 확인해보자.

https://github.com/sugoigroup/android_workmanager_example/commit/3dbb0496cf0427cbdb6c710987f8efb3a1fa1d4e

–worker (일할놈)이 로그찍는일에서 통지하는일로 역활을 바꾸어 보자.

public class UpladWorker extends Worker {
    private int count = 0;

    public UpladWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        while (count < 10) {
            count++;
            try {
               // Log.i("worker", "now background" + count);
                showNotification("worker", "now background" + count);
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
   }
     }

        return Result.success();

    }

    private void showNotification(String task, String desc) {
        NotificationManager manager = (NotificationManager) getApplicationContext().getSystemService(
                Context.NOTIFICATION_SERVICE);
        String channelId = "my_channel";
        String channelName = "my_name";
        // Oreo 부터는 노티에 알림하려면 채널이 있어야 한다.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new
                    NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
        }
  }
      NotificationCompat.Builder builder =
                new NotificationCompat.Builder(getApplicationContext(), channelId)
                        .setContentTitle(task)
                        .setContentText(desc)
                        .setSmallIcon(R.mipmap.ic_launcher);
        manager.notify(1, builder.build());
    }
}

핸폰 알림창에 3초마다 알림이 갱신된다.

  1. workManager에게 특정태그로 요청된 workRequest를 통해 중지명령을 내려서 worker가 명령이후에는 작업하지않도록 해보자.

일단 간단하게 hello textvView 를 클릭하면 클릭한 시점부터 worker가 일을 안하도록 하자.

https://github.com/sugoigroup/android_workmanager_example/commit/b44eaa1059d44fa718d089053c63f2ad81005fe6

-------------
UploadWorker.java

    public Result doWork() {

        while (count < 10) {
            if (isStopped()) {
                continue;
            }
            count++;
            try {
               // Log.i("worker", "now background" + count);
                showNotification("worker", "now background" + count);
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

   }

     return Result.success();

    }
-------------------
MainActivity.java


        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        OneTimeWorkRequest oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        WorkManager.getInstance(this).enqueue(oneTimeWorkRequest);

Hello world라는 텍스트뷰를 클릭하면 알림창이 더이상 안온다…

  1. WorkerManager는 각 workRequest에 대해 모두 기록하고 중간에 멈춰질때 다시 복귀하도록 관리하고 있다. 따라서 이번 예제에서 setRequiresCharging(work의 일하는 제약을 충전중일경우에만) 이라고 한정지었기 때문에 충전중이 아닌상태로 앱을 시작했다가 , 충전중상태로 다시 앱을 재가동하면 충전중이 아닐때의 work 까지 총 두개의 work 가 일하게 된다.

때문에 이를 막기 위해서 beginUniqueWork라는 함수로 workManager를 가동하면 KEEP, REPLACE, APPEND 라는 3개의 옵션을 통해 같은 이름을 가진 작업 queue가 있을때, 나중에 들어온 같은 이름의 작업을 무시할건지, 중간에 교체할건지, 순서대로 진행할건지를 결정할수 있도록 해준다

https://github.com/sugoigroup/android_workmanager_example/commit/4bd3b10667b4f09879a317f124d68066b44c1f3e

protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView tv = (TextView) findViewById(R.id.hello);
        tv.setOnClickListener(e -> {
            startQueue();
        });
        startQueue();
    }

    private void startQueue() {

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        //
    //
    WorkManager.getInstance(this)
                .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
                .enqueue();
    }

같은 WorkRequest를 A ,실행중에 B 실행 에 대해

KEEP : B는 무시된다.(단 A가 끝난상태면 B는 진행한다.)

REPLACE : B로 새롭게 시작한다.(A가 어디까지 진행된지 상관없다)

APPEND : A가끝날때 까지 기다렸다가 B를 진행한다.

APPEND OR REPLACE : A가진행중이면 B를 뒤에 추가하고 시작하되, A가 실패로 끝난상태거나 취소가 이루어졌다면 B로 새롭게 시작한다.

hello world text 뷰를 클릭하면서 위의 네가지를 바꾸어 보면 알림창에 해당 옵션에 따라서 적절하게 queue가 진행될거다.

  1. 서로 다른 workRequest를 순차적으로 일시키자.

https://github.com/sugoigroup/android_workmanager_example/commit/255afd504af3c17417a39f4f00801e408f896c26

------
LogWorker.java
public class LogWorker  extends Worker {

    public LogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
         Log.i("worker", "Wow All Done!");
        return Result.success();
    }
}

-------
MainActivity.java


        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  logWorkerRequest =  new OneTimeWorkRequest.Builder(LogWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        // then 으로 순차적인 workRequest를 실행할수 있다.
        WorkManager.getInstance(this)
                .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
                .then(logWorkerRequest)
                .enqueue();
    }
  1. workRequest(일요청)에 자료를 넣고(input), 일이 끝나면 처리한 결과(output)을 받아서 textvView에 뿌려보자.

백그라운드 쓰레드에서 작동하는 workManager 에 데이터를 전달하고 이를 다시 받아서 UI쓰레드에서 ui를 변경하는것은 참 귀찮은 작업인데, 화면변경은 livedata 를 통해서 전달 할수 있어서 간편하다. 이밖에도 Future 인터페이스를 통해 해당 workRequest 작업이 끝나는 시점에만 특정한 로직을 실행한다든지 할수도있다.

https://github.com/sugoigroup/android_workmanager_example/commit/b97b03e7743c6d86001a7df8a821a80bd115325f

------
LogWorker.java
public class LogWorker  extends Worker {

    public LogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {

        String detector = getInputData().getString(KEY_DETECTOR);

        Log.i("worker",   detector + " detector is founding a key!");

        //내보낼 값을 정하자
        Data outputData = new Data.Builder()
            .putString(FOUND_OUT_KEY, "elzzup")
            .build();
        return Result.success(outputData);
}

-------
MainActivity.java


        private void startQueue() {

        // 충전중일때만 worker 가 실행되도록 제한을 건다.
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(true)
                .build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  oneTimeWorkRequest =  new OneTimeWorkRequest.Builder(UpladWorker.class)
                .setConstraints(constraints)
                .addTag(CANCEL_ME)
                .build();

        //보낼값을 정하자.
        Data whoIsTheDetector = new Data.Builder().putString(KEY_DETECTOR, "kim").build();

        // 이번 한번만 일하도록 한다.
        final OneTimeWorkRequest  logWorkerRequest =  new OneTimeWorkRequest.Builder(LogWorker.class)
                .setConstraints(constraints)
                .setInputData(whoIsTheDetector)
                .addTag(CANCEL_ME)
                .build();

        // 매니져에게 일할거라고 등록한다.
        // then 으로 순차적인 workRequest를 실행할수 있다.
        WorkManager.getInstance(this)
                .beginUniqueWork("iamUnique", ExistingWorkPolicy.APPEND_OR_REPLACE, oneTimeWorkRequest)
                .then(logWorkerRequest)
                .enqueue();

        // 결과를
        foundKeyByWorkerGuy(logWorkerRequest.getId());
    }

    private void foundKeyByWorkerGuy(UUID workerUUID) {
        LiveData<WorkInfo> lf = WorkManager.getInstance(this).getWorkInfoByIdLiveData(workerUUID);

        lf.observe(this, workInfo -> {
            if (workInfo.getOutputData().getString(FOUND_OUT_KEY) != null) {
                tv.setText("The Key is  " + workInfo.getOutputData().getString(FOUND_OUT_KEY));
            } else {
                tv.setText("Finding the key");
            }
        });
    }

workManager 는 백그라운드 서비스용도로 사용되기 위한 기능이므로써 쓰레드비슷한 형식으로 자겁해서 UI에 변화를 줘야되는 즉, 화면이 떠있는 상태로 foreground작업에는 쓰레드나 coroutine을 사용하는게 맞다.

14 멀티미디어 기능 다루기,

안드로이드Android에는 기본으로 카메라, 오디오 기능이 탑재되어 있다.

14.1 카메라

단말에 기본으로 장착되어 있는 카메라를 이용하여 사진을 촬영후 촬영된 데이터를 받아서 뷰에 출력하거나 파일로 저장할수 있다.

카메라를 연동할때 미리보기 화면을 커스터마이징해서 촬영버튼등을 다른아이톰으로 한다든지, 카메라 설정을 조절하고자 할떄는 sSurfacevView 를 활용하는 방법도 있다.

val REQUEST_IMAGE_CAPTURE = 1
private val PERMISSION_REQUEST_CODE: Int = 101

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   if (requestCode == REQUEST_IMAGE_CAPTURE) {
       val photo: Bitmap = data?.extras?.get("data") as Bitmap
       imageView.setImageBitmap(photo)
   }
}

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   if (checkPersmission()) {
       val intent: Intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
       startActivityForResult(intent, REQUEST_IMAGE_CAPTURE)
   } else {
       requestPermission()
   }
}

14.2 비디오

Android에서는 비디오 재생을 쉽고 편하게 하기위해서 MediaPlayer라는 것을 제공한다. 재생/멈춤 등의 기본적인 제어가 가능하다.

layout.xml

<VideoView
   android:id="@+id/screenVideoView"
   android:layout_width="match_parent"
   android:layout_height="210dp"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent"></VideoView>

사용예

val videoView = findViewById<VideoView>(R.id.screenVideoView)
videoView.setVideoPath("https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4")
videoView.start()

14.3 오디오

MediaPlayer 에서는 오디오 재생도 쉽고 간편하게 할 수 있다.

val mediaPlayer = MediaPlayer.create(this, Uri.parse("https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3"))
mediaPlayer.setOnPreparedListener {
   mediaPlayer.start()
}

15 위치정보와 구글맵

휴대폰의 GPS 위치정보 센서를 이용해서 현재 사용자의 위치를 구글맵이나 다른 지도정보서비스에 표시할수 있다.

https://console.cloud.google.com/apis/ 에서 신청하자

AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

strings.xml

<resources>
   <string name="app_name">coroutines</string>
   <string name="google_maps_api_key">abcdefg012345678909</string>
</resources>

layout.xml

<fragment
   android:id="@+id/myfragment"

   android:name="com.google.android.gms.maps.SupportMapFragment"        android:layout_width="match_parent"
   android:layout_height="match_parent"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   tools:ignore="MissingConstraints" />

MainActivity.kt

class MainActivity : AppCompatActivity() , OnMapReadyCallback {

private val REQUEST_ACCESS_FINE_LOCATION: Int = 101

private fun checkPersmission(): Boolean {
   return (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) ==
           PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
       this,
       android.Manifest.permission.ACCESS_FINE_LOCATION
   ) == PackageManager.PERMISSION_GRANTED)
}

private fun requestPermission() {
   ActivityCompat.requestPermissions(
       this,
       arrayOf(ACCESS_FINE_LOCATION),
       REQUEST_ACCESS_FINE_LOCATION
   )
}



override fun onMapReady(googleMap: GoogleMap) {
   mMap = googleMap
}

//define the listener
private val locationListener: LocationListener = object : LocationListener {
   override fun onLocationChanged(location: Location) {

       val dhaka = LatLng(location.latitude , location.longitude)
       mMap?.let {
           it.addMarker(MarkerOptions().position(dhaka).title("Marker in Dhaka"))
           it.moveCamera(CameraUpdateFactory.newLatLng(dhaka))
       }
   }
   override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
   override fun onProviderEnabled(provider: String) {}
   override fun onProviderDisabled(provider: String) {}
}

// inside a basic activity
private var locationManager : LocationManager? = null
private var mMap: GoogleMap? = null
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)


   if (checkPersmission()) {
       // Create persistent LocationManager reference
       locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
       // Request location updates
       locationManager?.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0f, locationListener)


       val mapFragment =
           supportFragmentManager.findFragmentById(R.id.myfragment) as SupportMapFragment
       mapFragment.getMapAsync(this)
   } else {
       requestPermission()
   }
}


}

16 하드웨어 정보 연동하기

16.1 센서

현재 안드로이드Android기기에 들어있는 센서는 3종류로 나뉜다. 레벨14이상이면 다 지원한다.

  • 동작 센서(Motion Sensor)
    x,y,z 3축을 기준으로 기기의 움직임에 의한 가속력과 회전력을 측정해 준다. 운동센서로는 조깅앱, 핸폰들고 푸샵, 균형잡기 앱, 기울기 게임등에 활용할만한 다음과 센서들이 준비되어있다.
    • 중력 센서(gravity sensor) : 단말의 현재 방향(세워졌던가, 누워있던가)등과 중력(?) 감지
    • 선형 가속도계(linear accelerometer) : 단말의 이동속도등 감지
    • 자이로스코프(gyroscope) ,회전 벡터센서(rotational vector sensor): 단말의 회원각을 감지
    • 중요움직임센서(Use the significant motion sensor) : 걷기,이동,자동차타기 등 위치변화에 큰 움직임등 감지
    • 걸음걸이관련센서(step detector sensor,step counter sensor) : 몇발자국걸었는지 감지
      등이 있다
  • 위치 센서(Position Sensor)
    단말의 위치에서 측정되는 센서값으로 게임용 회원벡터센서(게임상에서 핸폰의 초기위치를 맞추기 위해 임의로 Y축을 조절하도록 함), 방향센서(나침반센서) , 근접센서(일반적으로 5cm가 넘으면 먼값으로 인정된)등이 있다.
  • 환경 센서(Environment Sensor)
    빛, 온도, 상대습도,기압 등을 감지할수 있다.

플랫폼별 센서 사용가능여부는 다음과 같다(구글참고)

https://developer.android.com/guide/topics/sensors/sensors_overvView

표 2. 플랫폼별 센서 사용 가능 여부

센서 Android 4.0

(API 레벨 14)

Android 2.3

(API 레벨 9)

Android 2.2

(API 레벨 8)

Android 1.5

(API 레벨 3)

TYPE_ACCELEROMETER
TYPE_AMBIENT_TEMPERATURE 해당 없음 해당 없음 해당 없음
TYPE_GRAVITY 해당 없음 해당 없음
TYPE_GYROSCOPE 해당 없음1 해당 없음1
TYPE_LIGHT
TYPE_LINEAR_ACCELERATION 해당 없음 해당 없음
TYPE_MAGNETIC_FIELD
TYPE_ORIENTATION 2 2 2
TYPE_PRESSURE 해당 없음1 해당 없음1
TYPE_PROXIMITY
TYPE_RELATIVE_HUMIDITY 해당 없음 해당 없음 해당 없음
TYPE_ROTATION_VECTOR 해당 없음 해당 없음
TYPE_TEMPERATURE 2

예2라고 써있는 것만 지원이 중단되었고, 나머진 14레벨이상이면 다쓸수 있다.

센서에는 측정값을 얻는 공통 인터페이스 함수가 있다.

Public Methods
float getMaximumRange() 얻을수 있는 최대 측정값
int getMinDelay() 현재측정값과 다음측정값사이의 최소지연 초ms
String getName() 센서이름
float getPower() 전력사용량
float getResolution() 센서가 얻을수 있는 해상도(센서의 단위에따라다름)
int getType() 센서타입
String getVendor() 제조사(센서칩)
int getVersion() 버젼
String toString()Returns a string containing a concise, human-readable description of this object. 센서에 대한 정보

센서가 반응하는속도(빈도) 설정은 다음과 같다.

반응빈도 설정값
SENSOR_DELAY_FASTEST 0 가장빈번하게 측정 속도
SENSOR_DELAY_GAME 1 게임에 적합한 측정 속도
SENSOR_DELAY_UI 2 사용자조작에 맞는 측정 속도
SENSOR_DELAY_NORMAL 3 일반적인 측정 속도

센서의 측정값(STATUS)에 대해서 신뢰도는 아래와 같이 넘어온다.

신뢰도 설정값
SENSOR_STATUS_ACCURACY_HIGH 3 측정 신뢰도 높음
SENSOR_STATUS_ACCURACY_MEDIUM 2 측정 신뢰도 보통
SENSOR_STATUS_ACCURACY_LOW 1 측정 신뢰도 낮음
SENSOR_STATUS_UNRELIABLE 0 측정 신뢰도 없음

근접센서의 예

퍼미션추가

<uses-permission android:name="android.hardware.sensor.proximity"/>

근접센서 사용예

private var sensorManager: SensorManager? = null

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
}
override fun onResume() {
   super.onResume()
   sensorManager!!.registerListener(
       this,
       sensorManager!!.getDefaultSensor(Sensor.TYPE_PROXIMITY),
       SensorManager.SENSOR_DELAY_NORMAL
   )
}

override fun onPause() {
   super.onPause()
   sensorManager!!.unregisterListener(this)
}

override fun onSensorChanged(p0: SensorEvent?) {

   if(p0?.sensor?.getType() == Sensor.TYPE_PROXIMITY) {
       if(p0.values[0] == 0f) {
           Log.d("Sensor", p0?.values[0].toString());
       } else {
           Log.d("Sensor", p0?.values[0].toString());
       }
   }

}

override fun onAccuracyChanged(p0: Sensor?, p1: Int) {
   when (p1) {
       SensorManager.SENSOR_STATUS_UNRELIABLE -> Toast.makeText(
           this,
           "UNRELIABLE",
           Toast.LENGTH_SHORT
       ).show()
       SensorManager.SENSOR_STATUS_ACCURACY_LOW -> Toast.makeText(
           this,
           "LOW",
           Toast.LENGTH_SHORT
       ).show()
       SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> Toast.makeText(
           this,
           "MEDIUM",
           Toast.LENGTH_SHORT
       ).show()
       SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> Toast.makeText(
           this,
           "HIGH",
           Toast.LENGTH_SHORT
       ).show()
   }

}

공식 사이트를 참고하자 https://developer.android.com/guide/topics/sensors/sensors_motion?hl=ko

16.2 바이브레이션

핸드폰 진동 알림효과이다.

<uses-permission android:name="android.permission.VIBRATE"/>
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val vibrationEffect = VibrationEffect.createOneShot(3000, 100)
vibrator.vibrate(vibrationEffect)

16.3 기타서비스 (Settings.System)

android의 시스템 하드웨어를 접근하기 위해서는 별도의 승인화면을 거쳐야한다. 사용자가 승인화면에서 on 을 하면 해당 앱에서는 하드웨어 관련 설정을 수정할수 있다. 안드로이드Android 버젼에 따라 제약이 많이 지긴 했지만, 링톤설정, 화면밝기 , 화면 폰트 크기등 조절이 가능하다.

참고 : https://developer.android.com/reference/android/provider/Settings.System

화면밝기예

fun setBrightness(context:Context, value:Int):Unit{
   Settings.System.putInt(
       context.contentResolver,
       Settings.System.SCREEN_BRIGHTNESS,
       value
   )
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
   super.onActivityResult(requestCode, resultCode, data)
   setBrightness(this, 10)
}


@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   if (!canWriteSystem(this)) {
       allowWritePermission(this, packageName)
   } else {

       setBrightness(this, 10)
   }
}

17 앱 빌드

앱을 배포하기 위해서는 소스코드, 라이브러리, 이미지, 레이아웃 xml , 각종 설정파일들을 하나의 파일에 압축하ㅓ고 또 이를 개발자전용키로 싸인을 한다음 난독화라는 과정을 거쳐서 최종적으로 aar(jar) 파일로 만들어야 구글마켓에 등록할수 있다.

이러한 복잡한 빌드(aar, jar파일을 만든다는의미)과정을 체계적이고 쉽게 할수 있도록 도와주는 ant, maven 라는 툴들이 예전부터 있었다. 그런데 ant,maven조차도 사용법이 매우 복잡하고 의존관계의 다른 라이브러리 설정이 매우 까다로워서 많은 개발자들이 빌드와 배포는 매우 귀찮고 까다로운 작업이라는 인식이 있었다.

Android Studio 에 기본으로 포함된 빌드프로그램인 gradle은 매우 쉬운 사용법과 쉽게 의존성관리를 할수 있고 다는 점에서 안드로이드Android 개발자에게는 gradle이 가장 적합한 빌드툴이 되었다.

물론 경우에따라서 ant와 maven을 사용하기도 한다.

17.1 Gradle

gradle은 groovy라는 별도의 언어를 사용하여 빌드에 필요한 스크립트를 작성하여 빌드를 한다. groobvy가 별도의 언어이긴 하지만 문법과 형식이 자바Java와 비슷해서 이질감없이 빌드스크립트를 작성할수 있다.

Android Studio에서는 새로운 프로젝트를 생성할때 프로젝트에 대한 gradle.build스크립트와 app또는 module에 대한 gradle.build스크립트 등을 만들어 주어서 개발자가 별도의 스크립트를 생성하지 않고도 안드로이브 프로젝트를 빌등애서 실행할수 있도록 편의를 제공한다.

프로젝트의 gradle.build 파일에는 주로 의존성관련과 의존패키지들을 가져올 레포지트리를 설정한다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
   ext.kotlin_version = "1.4.30"
   repositories {
       google()
       jcenter()
   }
   dependencies {
       classpath "com.android.tools.build:gradle:4.1.2"
       classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
       classpath 'com.google.gms:google-services:4.3.5'

       // NOTE: Do not place your application dependencies here; they belong
       // in the individual module build.gradle files
   }
}

allprojects {
   repositories {
       google()
       jcenter()
   }
}

task clean(type: Delete) {
   delete rootProject.buildDir
}

app모듈읠 gradle.build 파일에는 안드로이드Android앱을 빌드하기 위해 필요한 버젼, 라이러리, 정의등을 한다. 만일 하나의 앱프로젝트에 여러개의 작은 모듈을 추가해서 개발했다면 각 모듈별 gradle.build파일이 있게된다.

plugins {
   id 'com.android.application'
   id 'kotlin-android'
   id 'com.google.gms.google-services'
}

android {
   compileSdkVersion 30
   buildToolsVersion "30.0.2"

   defaultConfig {
       applicationId "com.sugoijapaneseschool.coroutines"
       minSdkVersion 23
       targetSdkVersion 30
       versionCode 1
       versionName "1.0"

       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
   }

   buildTypes {
       release {
           minifyEnabled false
           proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
       }
   }
   compileOptions {
       sourceCompatibility JavaVersion.VERSION_1_8
       targetCompatibility JavaVersion.VERSION_1_8
   }
   kotlinOptions {
       jvmTarget = '1.8'
   }
}

dependencies {

   implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
   implementation 'androidx.core:core-ktx:1.3.2'
   implementation 'androidx.appcompat:appcompat:1.2.0'
   implementation 'com.google.android.material:material:1.3.0'
   implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
   testImplementation 'junit:junit:4.+'
   androidTestImplementation 'androidx.test.ext:junit:1.1.2'
   androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

   implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'


}

android{…} 항목에는 안드로이드Android앱에 포함되어야할 버젼정보와 빌드조건(Release, Debug등)에 따른 설정값등을 지정한다.

defendencies{…}에는 사용하고자 하는 내부/외부 라이브러리를 지정한다. 라이브러리는 프로젝트의 gradle.build에 적혀있는 레포지트리에서 참조하여 가져온다.

gradle의 빌드작업단위는 task이다. 만일 안드로이드Android용 라이브러리프로젝트를 만들면서 만들고 난 라이브러리를 공용의 레포지트리에 올리고 싶다면 벼롣의 task 를 스크립트에 추가하면 된다.

task uploadToJcenter(dependsOn: previousMyApp) {
    doLast {
        println "Upload to Jcenter"
    }
}

빌드에 대해 자세히 알고 싶다면 공식문서를 참고하자.https://developer.android.com/studio/build?hl=ko

17.2 Proguard

난독화(암호화가 아니라 난독화 이다, 즉 읽기 어렵게 만들어주는것을 말함)되어있지 않은 앱의 apk패키지를 디컴파일프로그램으로 보면 모든 소스가 그대로 보이게 된다.

Proguard 는 앱을 패키징할떄에 소스코드를 분석하여 난독화시켜주어 디컴파일링해도 원본의 형태를 이해하기 어렵도록 해준다.

또한 소스코드 분석시에 사용하지 않는 클래스나 기타 리소스 등을 패키지에 포함되지 않도록 하여 apk 화일의 용량을 최적화 시켜주기도 한다.

안드로이드Android 스큐디오 에서 프로젝트를 생성했다면 프로가드는 기본으로 빌드파일에 설정되어 있어서 release시에 꼭 프로가드가 설정될수 있도록 해준다.

buildTypes {
   release {
       minifyEnabled false
       proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
   }
}

프로가드가 앱을 분석하여 코드와 리소스를 최적화 시켜주기는 하지만 자동으로 해주는 기능이다 보니 JNI에서 호출되는 코드또는 동적으로 참조되는 메소드나 클래스 외부 라이브러리의 클래스등을 당장의 소스코드에서 필요없는것 처럼 판단해 패키지에서 빠트리는 경우어서 앱실행시 오류가 나는 경우가 있다.

프로가드 에서는 이런 현상을 방지할수 있도록 프로젝트폴더 하위에 proguard-rules.pro 라는 파일을 제공해 주어 파일에적혀진 클래스와 속성들은 반드시 패키징에 포함되게 할수 있다.

-keep public class <MyClass>

Android Gradle 플러그인 3.4.0이상버젼에서는 gradle.properties 파일에 android.enableR8=true 을 지정하면 프로가드 프로그램을 사용하지 않고 R8 컴파일러를 사용하여 난독화와 최적화 작업을 수행한다.

앱축소,난독화 및 최적화에 대한 구글 공식문서를 참고하자

17.3 Signing

구글 플레이에 등록하기위한 안드로이드Android 앱 APK파일에는 반드시 개발자가 서명파일을 이용하여 서명한수 등록하여야한다. 해당앱에 대한 서명키가 있는 개발자만이 앱을 업데이트하거나 관리등을 할수 있기 위해서이다.

서명키는 개발자의 컴퓨터에서 직접 생성하여 APK파일에 서명툴로 서명해주면 된다.

Android Studio를 사용한다면 APK생성시에 서명키를 생성하거나 선택하여 자동으로 서명된APK를 만들수 있게 도와준다.

최근까지는 이렇게 개발자가 직접 서명키를 보관하여 사용하여 APK파일을 만들었었지만 개발자가 퇴사하거나, 컴퓨터를 포맷하는 등의 부주의한 실수로 서명키를 잃어버린다면 해당앱은 구글 스토어에서 절대로 업데이트를 할수 없는 절망적인 상황에 빠르게 된다.

이를 개선하고자 구글에서는 새로운 방식의 앱의 서명방식을 제공하는데 , 최종 APK의 서명은 구글이 관리를 하되 개발자는 개발한 APK의 업로드키 라는 것으로 개발자 인증만 하도록 하는 방식이다.

이로서 개발자는 APK에 서명된 키에 관여하지 않아도 되고, 자신의 업로드키만 관리하면 된다. 업로드키는 잃어버려도 이메일등으로 다시 발급받을수 있기때문에 분실로 인한 앱업데이트불가 에 대한 걱정도 안해도 된다.

18.NDK

개발을 하다보면 C ,C++등으로 작성된 외부의 라이브러리를 사용해야 할경우가 생긴다. Android에서는 NDK라는 것을 통해 컴파일된 라이브러리를 가져와서 java에서 사용할수 있도록 해준다.

대표적으로 opencv, cocos2dx같은 라이브러리는 NDK연동해서 컴파일 해야 사용할수 있다.

  1. 액티비티Activity 클래스에서 먼저 static 으로 만들고자 하는 ndk 모듈을 로딩한다.

JniTwo 라는 이름은 C++ 모듈이름으로 Android.mk 에 [ LOCAL_MODULE := jniTwo ] 라고 나중에 똑같이 적으면 된다.

   static {
        System.loadLibrary("jniTwo");
    }
  1. 모듈을 로드했으면 액티비티Activity 클래스에서 쓸 함수를 이름만 정의한다.
private native String getStr(String str);
  1. src->main->jni 폴더를 만든다.

  2. 그 안에 cpp 파일을 만든다. * 헤더는 나중에 수동을 만들거다.

//
// Created by kimtaeho on 2019-10-12.
//

#include <string.h>

JNIEXPORT jstring JNICALL Java_com_example_myapplication_MainActivity_getStr(JNIEnv *env, jobject thiz, jstring str) {
    char buf[255];
    char appendMsg[] = " JNI Hello";

    const char *ch1 = env->GetStringUTFChars(str, 0);
    strcpy(buf, ch1);
    strcat(buf, appendMsg);

    return env->NewStringUTF(buf);

 }
  1. 액티비티Activity 클래스에서 함수를 사용해본다.
       TextView ttt = (TextView)findViewById(R.id.ttt);
        ttt.setText(getStr("Kim"));
  1. Android Studio Setting 에서 Tools->External Tools 에 새로 생성해서 다음과 같이 적는다.
Program : C:\Program Files\Java\jdk1.8.0_191\bin\javah.exe

Arguments : -classpath "$Classpath$" -v -jni $FileClass$

Working Directory : C:\workspace\MyApplication2\app\src\main\jni
  1. 이 상태에서 Build->make Project 를 해본다.

  2. 모듈을 로드하는 Activity Class 에서 마우스오른쪽 [External Tools] 에서 6에서 생성한 것을 선택하면 복잡한 h 파일이 생성된다. 해당 파일의 맨 밑에 cpp 에서 만든 함수가 정의되어 있는것을 확인하고 파일이름을 적당히 바꾸고 cpp 에서 include 한다.

  3. jni 폴더에 Android.mk 를 만들어 모듈이름고 소스 파일이름을 지정한다.

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jniTwo
LOCAL_SRC_FILES := Two.cpp
LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)
  1. build.gradle(app) 에 다음과 같이 추가.


import org.apache.tools.ant.taskdefs.condition.Os



// Project Structure에서 설정한 NDK 경로를 읽어 들여 Return.
def getNdkBuildPath() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    def command = properties.getProperty('ndk.dir')
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        command += "\\ndk-build.cmd"
    } else {
        command += "/ndk-build"
    }
    return command
}


Android {

 ...



    sourceSets.main {
        // Compile된 Native Library가 위치하는 경로를 설정.
        jniLibs.srcDir 'src/main/libs'
        // 여기에 JNI Source 경로를 설정하면 Android Studio에서 기본적으로 지원하는 Native
        // Library Build가 이루어짐. 이 경우에 Android.mk와 Application.mk를
        // 자동으로 생성하기 때문에 편리하지만, 세부 설정이 어렵기 때문에 JNI Source의
        // 경로를 지정하지 않음.
        jni.srcDirs = []
    }

    ext {
        // 아직은 Task 내에서 Build Type을 구분할 방법이 없기 때문에 이 Property를
        // 이용해 Native Library를 Debugging 가능하도록 Build할 지 결정.
        nativeDebuggable = true
    }

    // NDK의 ndk-build 명령을 이용하여 Native Library를 Build하기 위한 Task를 정의.
    //noinspection GroovyAssignabilityCheck
    task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
        if (nativeDebuggable) {
            commandLine getNdkBuildPath(), 'NDK_DEBUG=1', '-C', file('src/main').absolutePath
        } else {
            commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath
        }
    }



    // App의 Java Code를 Compile할 때 buildNative Task를 실행하여 Native Library도 같이
    // Build되도록 설정합니다.
    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn buildNative
    }

    // NDK로 생성된 Native Library와 Object를 삭제하기 위한 Task를 정의.
    //noinspection GroovyAssignabilityCheck
    task cleanNative(type: Exec, description: 'Clean native objs and lib') {
        commandLine getNdkBuildPath(), '-C', file('src/main').absolutePath, 'clean'
    }

    // Gradle의 clean Task를 실행할 떄, cleanNative Task를 실행하도록 설정.
    clean.dependsOn 'cleanNative'



}
  1. gradle.properties 에 추가
android.useDeprecatedNdk=true

alt_text

ETC

1.ANDROID TASK

https://developer.android.com/guide/components/activities/tasks-and-back-stack?hl=ko

안드로이드Android에서 말하는 Task(작업) 은 activity 들을 모아놓은 하나의 작업대이다. 앱에서 화면단위로 Activity를 실행하거나 이동하면 마치 카드가 쌓이듯이 stack형태로 쌓여지게 되는데 이떄 쌓일 장소를 지정하는 게 task 이다.

task를 여러개 둔다는 것은 작업대를 여러개 두어 각각 작업대에서 activity를 별로도 쌓아두겠다는 거다. 이렇게 별도로 쌓인 activity들은 각각의 task 안에서만 영향을 받게 된다.

앱을 생성하고 activity를 추가한뒤 아무것도 안했다면 앱생성시 입력한 패키지 이름으로 task 가 강제지정된다.

만일 새로운 activity를 추가할때 task 이름을 따로 지정한다면 앱에서는 두개의 각기다른 작업대에서 각각의 activity들이 존재하게 된다.

셀제로 task를 다르게 지정했을때 어떻게 되는지 보자.

새로운 Activity를 생성하여 AndroidManifest.xml에서 아래와 같이 지정하여 [android:taskAffinity] 항목을 추가해보자.

<activity

android:name=".MyNewTaskActivity"

android:label="@string/title_activity_my_new_task"

android:theme="@style/Theme.MyApplication.NoActionBar"

android:taskAffinity="com.sugoijapaneseschool.myapplication2.MyNewTaskActivity2"></activity>

처음실행되는 런쳐 액티비티Activity에서 다음과 같이 일단 강제로 새로운 액티비티Activity를 띄워보자.

val intent = Intent(this, MyNewTaskActivity::class.java)

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

startActivity(intent)

홈버튼 등을 눌러서 task목록을 보면 놀랍게도 두개의 태스크가 표시되는것을 알수 있다.

즉 같은 앱에서도 작업에 따라서 별도의 태스크로 지정하여 해당 액티비티Activity들만 삭제되도록 하거나 할수 있다.

예를 들어 공부앱에서 영어 강좌를 듣던 사용자가 일본어 강좌를 듣기 시작했을때, 일본어관련 액티비티Activity를 또하나의 다른 태스크로 지정하면 사용자는 두개의 작업 태스크를 가지게 되고, 필요없는 태스크만 제거 할수도 있다.

여기서 중요한 것은 activity의 android:taskAffinity 속성인데, 친화력이란 뜻으로 현재 액티비티Activity가 속한 태스크를 지정하는 것이라고 생각하면된다.

또한 Activity를 start 시킬때 FLAG_ACTIVITY_NEW_TASK 플래그를 지정하여 새로운 태스크에서 액티비티Activity가 실행되도록 해야한다.

2.ACTIVITY FLAG

https://developer.android.com/guide/components/activities/tasks-and-back-stack?hl=ko

Activity를 start할때 Task내의 stack의 맨위에 들어가게 되는데, 무작정 들어가는게 아니라 이미 쌓여있던 애들을 어떻게 처리할것인지에 대한 결정을 하는 FLAG이다.

Intent에 추가하거나 설정 한다.

Intent.addFlags() : 플래그를 여러개 지정할때쓰인다. Task 관련 플래그는 두개이상지정해야 적용이 되는것도 있기에 해당작업을 할때쓴다.

Intent.setFlags() : 지정된 플래그로 stack을 정리한다음에 새로운 액티비티Activity를 올려놓는다.

  • FLAG_ACTIVITY_BROUGHT_TO_FRONT
    지정이 안될때의 기본값, 같은 액티비티Activity가 이미 스택에 있는 경우 해당 액티비티Activity를 맨앞으로 꺼내보인다.
  • FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    백그라운드에서 있던 태스크(즉 비활성되어있던 태스크)가 포어그라운드(화면전면에 나오는 것)로 전환될때 자동으로 불리는 것으로 FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 태그와 함께 불리면 현재 불리우는 액티비티이후의 액티비티Activity이후의 Activity들을 모두 제거된다.
  • FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
    현재 태스트내의 호출된 액티비티부터 위의 액티비티Activity부터 위의 Activity가 모두 삭제됩니다.
  • FLAG_ACTIVITY_CLEAR_TOP
    호출하는 액티비티를 최상위 액티비티Activity를 최상위 Activity로 하기위해서 그 위에 쌓인 액티비티Activity들을 모두 삭제한다.
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    안드로이드Android의 태스크목록에서 보면 최근 실행된 activity 화면이 보이는데, 이 플래그를 지정하면 현재 액티비티Activity 화면이 최근 목록에서 안보이게 된다.
  • FLAG_ACTIVITY_FORWARD_RESULT
    startActivityForResult를 바로 호출한 액티비티Activity가 아닌, 포워딩하여 result값을 전달받을수 있도록 해준다.
  • FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
    최근 실행목록에서 실행되게 되면 자동으로 설정된다.
  • FLAG_ACTIVITY_MULTIPLE_TASK
    FLAG_ACTIVITY_NEW_TASK와 사용하여 기존의 태스크에 호출액티비티Activity를 쌓는게 아니라 기존태스크를 제거하고 새로운 태스크를 생성하여 액티비티Activity를 쌓는다.
  • FLAG_ACTIVITY_NEW_TASK
    새로운 테스크를 생성하여 그 테스크안에 액티비티Activity를 추가한다. 만일 기존의 같은 태스크가 있다면 해당 태스크에 추가한다.
  • FLAG_ACTIVITY_NO_ANIMATION
    Activity 전환 시 애니메이션 무시한다.
  • FLAG_ACTIVITY_NO_HISTORY
    Activity가 Stack에 쌓이지 않게 한다. 일회성 액티비티Activity인경우 사용한다.
  • FLAG_ACTIVITY_SINGLE_TOP
    호출되는 Activity가 최상위에 있을 경우 해당 Activity를 다시 생성하지 않고, 있던 Activity를 다시 사용한다.
  • FLAG_ACTIVITY_REORDER_TO_FRONT
    호출되는 Activity가 Task에 있으면 해당 액티비티Activity를 스택의 가장상위로 순서를 재정렬해준다.

Android Studio 다운로드하기

Android Studio 는 JetBrain이라는 개발자툴 전문 제작사에서 구을 통해 제공하는 무료 안드로이드Android 앱 개발 에디터 프로그램이다.
완전 무료인데도 기능이 충실하기 때문에 안드로이드Android 개발에는 전혀 문제가 없다. 오히려 다른 개발툴이 안나올 정도로 유일한 안드로이드Android 앱개발 에디터라고 생각해도 된다. 무조건 쓰란 얘기다.

공식 웹사이트의 https://developer.android.com/ 에 가면 첫화면부터 다운로드 하라는 버튼이 있으니 다운받아서 적절한 폴더에 설치하면 된다.

가상 디바이스 추가/관리

앱을 실행할때 가상 디바이스를 선택하여 실제기기가 없어도 앱의 실행/동작 상태를 볼수있다.
가상 디바이스는 Android Studio에서 추가 생성/관리 할수 있는데 메뉴는 아래와 같은 위치에 있다.

  1. 상단메뉴에서 Tools > Device Manager선택
  2. 앱실행아이콘의 디바이스 선택 탭에서 하위 메뉴인 Device Manager선택
  3. 오른쪽 측면 메뉴에서 Device Manager선택

어느것을 선택하든 아래와 같은 화면이 보이게 되는데, [Create Device]버튼을 클릭해서 디바이스를 추가해 보자.

디바이스추가 화면이 나타나는데, 우리는 Phone용 앱을 주로 제작하므로 Phone을 선택하고, 화면크기를 맘에드는것으로 선택하자.

Next을 눌러서 가상기기에 설치될 OS를 선택해보자. OS리스트는 안드로이드Android의 지금까지의 모든 OS목록이 보여지며, 해당 OS를 선택해서 가상기기를 실생하면 실제 안드로이드Android에 설치되는 것과 같은 OS가 탑재된 가상 디바이스가 추가된다.

만일 OS목록에 위와같이 모두 뿌옇게 나온다면 OS이름옆의 아래 화살표 버튼을 눌러서 다운로드 한다음 해당 OS를 선택해보자.
구글은 항상 최신 OS를 대상으로 하는 앱을 개발하기를 원한다. 사용자단말기도 실제로 자동으로 항상 새로운 OS로 업그레이드되기 때문에 개발자도 개발시점에서의 최신 OS를 선택하여 개발하는게 좋다.

해당 OS의 다운로드가 끝났다면 Next버튼을 눌러서 마지막 확인화면에서 선택한 화면크기, OS가 맞는지 확인하고, 가로모드앱인지, 세로모드 앱인지 결정한후에 FInish로 가상 디바이스를 생성해준다.

이제 가상디바이스가 추가되었으므로 앱실행 아이콘옆의 디바이스 선택 리스트에 그림과 같이 해당 디바이스가 나타난다.
여러개의 디바이스를 추가했다면 앱실행시 원하는 가상 디바이스를 선택하면된다.

가상디바이스 구동 화면

가상 디바이스에서 구동되긴 하지만 일부 기능은 실제기기에서나 테스트 가능한 것들이 있다. 예를 들어 센서,카메라,위치정보등이다.
센서,카메라 등 실제기기에서나 가능한 기능인 경우에는 에뮬레이터내부에서 가짜로 해당 기능이 동작되는거 처럼 설정을 해주는 옵션들이 있다. 아래 그림처럼 점세개(Extended Control)을 클릭하여 옵션창을 보이도록 하자.
아래 그림에서처럼 위치, 배터리상태, 전화걸림, 카메라, 자이로 센서 등 실제기기에서나 취득가능한 값들을 에뮬레이터에서 가짜로 만들어서 앱제작시에 해당 데이터를 이용하여 개발이 가능하도록 도와준다.


ⓒ 20213. Sugoi Group All rights reserved.

수정 및 최신 내용 갱신:https://yunhos.blogspot.com/2021/03/androidminebookkth.html

0 comments:

댓글 쓰기