소개


안드로이드 어플리케이션은 클래스 파일, 레이아웃, 문자열 데이터, 해상도 별 이미지 등 수많은 데이터 조합으로 이루어져 있었다. 하지만 안드로이드가 발전하면서 새롭게 등장하는 앱들의 평균 사이즈 또한 증가하면서 사용자들이 용량이 큰 앱은 설치하지 않거나 설치 후에도 용량 확보를 위해 금방 삭제하는 현상이 증가하고 있다고 발표했다.



용량이 큰 앱은 다운로드와 설치 시간이 길고 업데이트 설치 비율이 낮아진다.


기존 안드로이드 앱 설치 방식은 모든 데이터를 시스템에 복사한다. 해상도 별 이미지, 제공하는 모든 국가별 문자열 데이터 등 모두 같이 설치한다. 해상도를 변경하거나 시스템 언어를 변경하지 않은 이상 현재 사용하는 설정 이외의 모든 데이터를 쓸모가 없어진다. 즉 사용자는 쓰지도 않는 이미지나 문자열 데이터를 앱 때문에 가지고 있는 것이다. 


안드로이드 개발팀은 이러한 단점을 보완하고자 새로운 앱 모델을 선보였는데 바로 앱 번들이다. 앱 번들은 구글 플레이 서비스에서 시스템이 사용하는 설정 데이터(해상도나 언어)만을 조합하여 앱을 설치하는 형태로 진행된다. 필요한 데이터로만 구성된 앱을 설치하기 때문에 앱 크기를 줄일 수 있고 다운로드, 설치, 업데이트 시간을 단축할 수 있게 된다.



좌측은 기존의 방식으로 모든 데이터를 가져와 설치한다.

우측의 경우 앱 번들에서 시스템 해상도, 언어, 아키텍처의 데이터만 가져와 설치한다.


이로 인해 같은 앱이더라도 기존 방식에 비해 최대 35%까지 앱 크기를 줄일 수 있게 된다. 그런데 만약 앱을 사용하다가 중간에 시스템 언어를 바꾸거나 해상도를 변경할 경우 새로 설치해야 하나? 답은 아니다. 언어나 해상도나 변경될 경우 구글 플레이 서비스가 인식하여 알아서 필요한 파일을 다운로드한다. 때문에 시스템 설정이 변경되어도 재설치 없이 그대로 사용할 수 있게 된다.


앱 번들은 Split APK 방식으로 설치하기 때문에 구글 플레이 서비스에서 앱 번들에서 필요한 데이터만 조합한 뒤 재-서명 과정을 거치게 되는데 이를 위해 개발자 키가 개발자 콘솔에 등록되어야 한다. 다만 보안 문제는 걱정할 것이 없는 이유가 구글에서 개발자 키를 자신들이 사용하는 구글 키와 동일한 저장소에 보관한다고 하였기 때문이다.


구조 및 사용법


앱 번들은 안드로이드 스튜디오 3.2 이상부터 사용 가능하다. 앱 번들을 사용하기 위해 기존 개발 구조가 크게 바뀌거나 추가할 필요는 없다. 간단하게 APK로 서명하던 것을 Bundle 형태로 서명하면 된다. Bundle로 빌드 시 기존 APK 파일 형태가 아닌 AAB(Android App Bundle)로 배포되며 APK와 다른 구조를 가지게 된다.



출처

https://developers-kr.googleblog.com/2018/05/google-io-2018-whats-new-in-android.html

https://developer.android.com/guide/app-bundle/

https://www.youtube.com/watch?v=QdoEcfibGs&list=PLWz5rJ2EKKc8WFYCR9esqGGY0vOZm2l6e&index=15

by JamesY 2018. 11. 12. 23:36

Oreo 버전부터는 일반적인 Background 서비스를 사용하기 까다로워 졌다.


일단 서비스 실행 후 앱이 보이지 않거나 (액티비티가 화면에 나오지 않는 등) 


Foreground 서비스를 사용하지 않거나


IME, 배경화면 서비스, 알림 리스너, 음성 또는 텍스트 서비스 중 하나를 사용하지 않는 이상


서비스는 오래가지 않아 종료된다.


Foreground 서비스는 서비스 실행과 동시에 알림(Notification)을 보여주어 사용자가 서비스가 동작 중이라는 것을 인지시켜 주며 실행을 유지한다.


- Foreground 서비스를 실행

startService(Intent(context, Service::class.java))

- 서비스 내에서 Foreground 실행 함수를 호출해야한다.

startForeground(FOREGROUND_ID, notification)

이렇게 사용하면 서비스는 알림을 띄우며 동작하게 된다.


이후 서비스를 종료하기 위해서는 서비스 클래스 내에서 stopSelf() 함수를 호출하거나


외부에서 context.stopService(intent)를 호출하면 된다.


종료하는 과정에서는 알림 역시 제거된다. 하지만 알림을 제거하지 않고 서비스를 종료하기 위해서는 클래스에서 또는 클래스 내에서 onDestroy 함수에서 

stopForeground(Service.STOP_FOREGROUND_DETACH)

를 추가하면 된다.


주의해야할 사항은 위 함수는 Nougat(API 24)이상부터 사용가능하다.


또한 boolean값을 받는 같은 함수가 존재하는데 false를 주어도 서비스 종료 시 알림이 제거된다.

by JamesY 2018. 10. 23. 22:56

Admob을 포함한 광고 플랫폼은 개발자에게 용돈을 주는 고마운(?) 존재이다. 


물론 앱에 광고를 너무 많이 넣진 않고 미관을 해치지 않는 선에서만 추가한다.


처음부터 Admob을 추가해 앱을 개발하는 것을 고려했기 때문에 개발 초기에는 앱 실행 시 속도 차이를 크게 못느꼈다.


그러나 업데이트를 하면서 이런저런 기능을 추가하게 되면서 Admob이 실행 속도에 영향을 주기 시작했다.


참고로 앱의 실행 방식은 3가지가 존재한다.

Cold start, Hot start, Warm start


자세한 정보는 아래 링크에 자세히 서술되어 있다.

https://developer.android.com/topic/performance/vitals/launch-time


여기서 실행 속도가 느리다는 것은 Cold start이다.


Cold start 속도가 3초 후반대에서 4초 대로 늘면서 원인을 찾아본 결과 Admob이었다.


이러한 문제를 해결하기 위해서 검색한 결과


1. https://stackoverflow.com/a/31636336


위 링크에서 제공하는 다양한 방법이 있다.


2. https://stackoverflow.com/a/19446364


위 링크는 1번 링크의 첫번째 링크의 내용이다.


Handler를 사용하여 광고를 불러오는 소스 코드를 1초 딜레이를 주고 실행한다.


정말 단순히 Handler를 추가했을 뿐인데 2초에서 3초 사이로 실행 시간을 줄일 수 있었다.


by JamesY 2018. 7. 27. 23:52

1. Adapter의 onBindViewHolder 함수에서 holder의 adapterposition 값을 저장하지 말 것

- Drag and drop 기능 구현 시 adapterposition을 저장한 값을 사용하면 이전 값을 가져오거나 잘못된 값을 가져올 수 있음


2. Adapter의 onBindViewHolder 함수에서 특정 이미지나 텍스트를 변경 시 조건문으로 만들 것

- 기본적으로 recyclerview는 아이템을 재사용하기 때문에 재사용된 데이터가 다른 위치에서 보일 수 있다. 하지만 정확히 조건문을 구현하여 변경 전과 후를 구현하면 방지할 수 있다.


3. Adapter 내 getItemCount 함수와 getItemId 함수를 구현할 것

- 2번과 비슷한 이유로 재사용 이슈로 인해 일부 잘못 표기되는 경우가 존재한다.


4. Listview의 head/foot view는 Recyclerview에선 view type으로 구현 가능

- Recyclerview는 listview와 별개로 별도의 head/foot view를 만들 수 있는 함수가 없다. 좀 더 확장한 개념의 view type을 사용하여 단순히 head/foot view 뿐만 아니라 리스트 데이터의 따라 다양한 형태로 view를 구현 가능하다.


5. Recyclerview에서 drag and drop 기능과 swipe 기능 구현

https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf


* 개발하면서 배우는 점을 지속적으로 업데이트할 예정

by JamesY 2018. 7. 22. 21:56

출처 : https://developer.android.com/training/basics/intents/filters.html?hl=ko


* Android studio를 사용하는 경우 Tools - App Links Assistant를 통해서도 생성이 가능하다.


-  AndroidManifest.xml에서 intent-filter를 등록

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="https"
android:host="*" />
</intent-filter>


- App Links Assistant를 사용하면 테스트도 가능하다.

by JamesY 2018. 2. 13. 19:23

Google Maps Utility KML


적용 및 사용 방법 : https://developers.google.com/maps/documentation/android-api/utility/kml?hl=ko


1. 현재 공식 지원 중인 KML Utility는 베타 버전으로 일부 태그는 부분적으로 지원하거나 지원되지 않음


2. 1번의 이유로 Style 태그는 지원하지만 StyleMap 태그는 부분적으로 지원함 - 강조(Hightlight)는 지원안됨


3. KmlContainer는 하나의 Document 또는 Folder를 지칭함


4. 일반적으로 Style 관련 태그는 첫번째 Container에 포함되어 있음


5. Placemark의 style을 가져오기 위해서는 Style 태그가 포함된 Container에서 getStyle 함수를 통해 가져올 수 있음 


6. normal 상태와 highlight 상태를 가지는 StyleMap의 id는 불러오면 오류 발생


7. StyleMap id 뒤에 '-normal' 혹은 '-highlight'를 붙이면 getStyle 함수를 통해 데이터를 가져올 수 있음


8. layer에서 addLayerToMap 함수는 normal 상태의 style만 적용


9. addLayerToMap로 불러와도 색이 적용되지 않는 이유는 데이터에 내용이 누락됨 (이유는 알 수 없음)


* 이 외 style과 관련된 팁을 발견하면 추가 예정


* 이 팁들은 구글 내 지도 서비스에서 KML 내보내기를 통해 얻은 KML 파일을 기준으로 작성되었습니다.

by JamesY 2018. 1. 22. 19:38

Google Maps utility library란?

https://developers.google.com/maps/documentation/android-api/utility/?hl=ko


안드로이드에서 Google Maps을 사용하여 개발하다 보면 두 좌표 간 거리,  GeoJSON, KML 등 부족한 부분이 존재한다.


구글에서 이 부분을 메워줄 수 있는 공식 유틸리티 라이브러리를 제공한다.

GeoJSON 계층이 있는 지도

지도에 GeoJSON 가져오기

지형지물을 GeoJSON 형식으로 저장하고 이 유틸리티를 사용하여 지도 위의 계층으로 렌더링할 수 있습니다. addLayer()를 호출하여 지도에 GeoJSON 데이터를 추가합니다. addFeature()를 호출하고 GeoJsonFeature 객체에 전달하여 개별 지형지물을 추가할 수도 있습니다.

자세한 내용은 Google Maps Android GeoJSON 유틸리티에 관한 문서를 참조하세요.

KML 계층이 있는 지도

지도에 KML 가져오기

이 유틸리티를 사용하면 KML 객체를 지리적 셰이프로 변환하고 지도 위의 계층으로 렌더링할 수 있습니다. addLayerToMap()을 호출하여 지도에 계층을 추가합니다. Placemark, GroundOverlay, Document 또는 Folder에서 getProperties()를 호출하여 KML 객체의 속성에 액세스할 수 있습니다.

자세한 내용은 Google Maps Android KML 유틸리티에 관한 문서를 참조하세요.

열지도가 있는 지도

지도에 열지도 추가

열지도를 사용하면 뷰어가 지도에서 데이터 지점의 분포와 상대적 강도를 쉽게 이해할 수 있습니다. 열지도는 각 위치에 마커를 배치하는 대신 색상과 셰이프를 사용하여 데이터의 분포를 나타냅니다. HeatmapTileProvider를 생성하고 지도의 관심 지점을 나타내는 LatLng 객체 컬렉션에 전달합니다. 그런 다음 새 TileOverlay를 생성하여 열지도 타일 제공자에 전달하고, 지도에 타일 오버레이를 추가합니다.

자세한 내용은 Google Maps Android 열지도 유틸리티에 관한 문서를 참조하세요.

버블 아이콘이 있는 지도

버블 아이콘을 통해 마커 사용자 지정

IconGenerator를 추가하여 마커에 정보 조각을 표시합니다. 이 유틸리티는 마커 아이콘을 정보 창처럼 보이게 해서, 여기에 텍스트와 다른 콘텐츠를 담는 방법을 제공합니다. 정보 창은 한 번에 하나만 열 수 있지만 이 유틸리티는 하나 이상의 마커를 동시에 열 수 있다는 장점이 있습니다. 또한, 마커의 스타일을 지정하고, 마커 및/또는 콘텐츠의 방향을 변경하고, 마커의 배경 이미지/나인 패치를 변경할 수 있습니다.

클러스터형 마커가 있는 지도

마커 클러스터 관리

ClusterManager를 사용하면 다양한 확대/축소 수준에서 여러 개의 마커를 관리할 수 있습니다. 즉, 지도의 가독성을 해치지 않으면서도 지도에 많은 마커를 넣을 수 있습니다. 사용자가 높은 확대/축소 수준에서 지도를 보면, 개별 마커가 지도에 나타납니다. 사용자가 낮은 확대/축소 수준으로 축소하면, 마커가 클러스터로 모여서 지도를 보기 쉽게 해줍니다.

자세한 내용은 Google Maps Android 마커 클러스터링 유틸리티를 참조하세요.

인코딩된 폴리라인이 있는 지도

폴리라인 인코딩 및 디코딩

PolyUtil은 인코딩된 폴리라인과 폴리곤을 경도/위도 좌표로 변환하거나 그 반대로 변환할 때 유용합니다.

Google 지도에서 폴리라인이나 폴리곤을 정의하는 경도와 위도 좌표는 인코딩된 문자열로 저장됩니다. 폴리라인 인코딩에 대한 자세한 설명을 참조하세요. Google Maps Directions API와 같은 Google API의 응답에서 이 인코딩된 문자열을 수신할 수 있습니다.

Google Maps Android API 유틸리티 라이브러리에서 PolyUtil을 사용하여 일련의 경도/위도 좌표('LatLngs')를 인코딩된 경로 문자열로 인코딩하고, 인코딩된 경로 문자열을 일련의 LatLngs로 디코딩/인코딩할 수 있습니다. 이는 Google Maps API 웹 서비스와의 상호운용성을 보장합니다.

지도 상에서 두 지점 간의 거리 계산

구면 기하학을 통해 거리, 영역 및 방향 계산

SphericalUtil의 구면 기하학 유틸리티를 사용하여 경도와 위도를 기반으로 거리, 영역, 방향을 계산할 수 있습니다. 다음은 유틸리티에서 사용할 수 있는 몇 가지 메서드입니다.

  • computeDistanceBetween() – 두 개의 위도/경도 좌표 사이의 거리를 미터 단위로 반환합니다.
  • computeHeading() – 두 개의 위도/경도 좌표 사이의 베어링을 도 단위로 반환합니다.
  • computeArea() – 지구 위의 닫힌 경로의 영역을 제곱미터 단위로 반환합니다.
  • interpolate() – 주어진 두 지점 사이의 거리에서 지정된 부분에 있는 지점의 경도/위도 좌표를 반환합니다. 예를 들어, 이 메서드를 사용하여 두 지점 사이의 마커를 애니메이트할 수 있습니다.

유틸리티에 포함된 메서드의 전체 목록은 참조 문서를 참조하세요.


GitHub : https://github.com/googlemaps/android-maps-utils

by JamesY 2018. 1. 15. 19:19

Constraint Layout을 이용하여 간단한 이동 애니메이션을 구현할 수 있다.


먼저 Constraint Layout을 어떤식으로 사용하는지 알아야 하기 때문에 모를 경우 배우고 난 뒤 읽는 것을 추천


결과물





1. Floating action button을 Constraint Layout에 넣기

<android.support.constraint.ConstraintLayout
android:id="@+id/menu_layout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="bottom|end" >

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@android:drawable/ic_dialog_alert"
app:fabSize="mini" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@android:drawable/ic_dialog_info"
app:fabSize="mini" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@android:drawable/ic_dialog_email" />

</android.support.constraint.ConstraintLayout>

fab은 메뉴 버튼, fab1과 fab2는 fab을 누를 시 나오는 항목들


2. fab 버튼 이벤트 구현

class MainActivity : AppCompatActivity() {

private var isMenuCollapsed = true

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

fab.setOnClickListener { view ->
val constraintSet = ConstraintSet()
constraintSet.clone(menu_layout)

if(isMenuCollapsed) {
// Set fab1's position to top of fab
constraintSet.connect(fab1.id, ConstraintSet.BOTTOM, fab.id, ConstraintSet.TOP)
// Set fab2's position to top of fab1
constraintSet.connect(fab2.id, ConstraintSet.BOTTOM, fab1.id, ConstraintSet.TOP)
}
else {
// Set fab1's position back to bottom
constraintSet.connect(fab1.id, ConstraintSet.BOTTOM, fab.id, ConstraintSet.BOTTOM)
// Set fab2's position back to bottom
constraintSet.connect(fab2.id, ConstraintSet.BOTTOM, fab.id, ConstraintSet.BOTTOM)
}
val transition = AutoTransition()
transition.duration = 300
transition.interpolator = AccelerateDecelerateInterpolator()

TransitionManager.beginDelayedTransition(menu_layout, transition)
constraintSet.applyTo(menu_layout)

isMenuCollapsed = !isMenuCollapsed
}
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}


이 방법은 menu_layout의 클론(constraintSet)을 만들고 fab1과 fab2의 새로운 위치를 지정 해주고 Transition을 한다. 결과적으로는 두 개의 menu_layout을 통해 애니메이션을 구현한 것이다.


Constraint를 잘 활용하면 예제처럼 아래서 위로 가는 것뿐만 아니라 오른쪽에서 왼쪽 또는 대각선 까지 다양한 방향 전환이 가능하다.


GitHub : https://github.com/Hot6ix/FloatingActionButton

참조 : https://robinhood.engineering/beautiful-animations-using-android-constraintlayout-eee5b72ecae3

'Android > Kotlin' 카테고리의 다른 글

커스텀 리스트뷰 ( Custom ListView)  (0) 2017.12.21
Volley를 이용한 간단한 네트워크 통신  (0) 2017.12.21
by JamesY 2018. 1. 9. 23:45

1. 기본 환경 세팅


- 두 액티비티 모두 Google Map 적용

- 사용자지정 액태비티 전환을 위한 코드 삽입

https://developer.android.com/training/material/animations.html?hl=ko#Transitions


2. Google Map에 적용

두 액티비티의 소스 코드 중 초기 Google Map Fragment를 가져오는 부분에 추가

val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.view!!.transitionName = resources.getString(R.string.shared_element_map)

Fragment의 View를 가져와 TransitionName을 넣는다. 

넣은 후 액티비티 전환을 위한 Intent의 Options을 정의할 때 다음과 같이 추가한다.

var options = ActivityOptionsCompat.makeSceneTransitionAnimation(this,
Pair<View, String>(map.view, ViewCompat.getTransitionName(map.view)))


by JamesY 2018. 1. 8. 17:54

Kotlin으로 작성된 커스텀 리스트뷰 예제



MainActivity.kt

import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

val list: ArrayList<CustomItem> = ArrayList()
list.add(CustomItem("James", 23, "Male"))
list.add(CustomItem("Jamie", 20, "Female"))
list.add(CustomItem("John", 30, "Male"))

listview1.adapter = CustomAdapter(this, list)
}

private class CustomAdapter(context: Context, array: ArrayList<CustomItem>): BaseAdapter() {

private var list: ArrayList<CustomItem> = array
private var inflater: LayoutInflater = LayoutInflater.from(context)

override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {

val view: View?
val viewHolder: ViewHolder

if(p1 == null) {
view = this.inflater.inflate(R.layout.listview_item, p2,false)
viewHolder = ViewHolder(view)
view.tag = viewHolder
}
else {
view = p1
viewHolder = view.tag as ViewHolder
}

viewHolder.name.text = list[p0].name
viewHolder.age.text = list[p0].age.toString()
viewHolder.gender.text = list[p0].gender

return view!!
}

override fun getItem(p0: Int): Any {
return list[p0]
}

override fun getItemId(p0: Int): Long {
return p0.toLong()
}

override fun getCount(): Int {
return list.size
}

}

private class ViewHolder(view: View?) {

val name: TextView = view?.findViewById(R.id.nameTextView) as TextView
val age: TextView = view?.findViewById(R.id.ageTextView) as TextView
val gender: TextView = view?.findViewById(R.id.genderTextView) as TextView
}

data class CustomItem(var name: String, var age: Int, var gender: String)
}


listview_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />

<TextView
android:id="@+id/ageTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />

<TextView
android:id="@+id/genderTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="TextView" />
</LinearLayout>


GitHub : https://github.com/Hot6ix/SimpleCustomListView

by JamesY 2017. 12. 21. 17:52
| 1 2 |