아래 코드를 보면, Person 이라는 객체의 멤버 이름을 JSONObject 의 Key 값으로 치환해서 String 으로 변환해 줍니다.
거꾸로 이 String 을 JSONObject 로 변환할 수 있다면, 다시, 이전의 Person 이라는 객체로 만들어 줄 수도 있습니다.
fun main(args: Array<String>) {
val p1 = Person("John", 10)
val json: String = GsonBuilder().create().toJson(p1)
println(json)
val p2: Person = GsonBuilder().create().fromJson<Person>(json, Person::class.java)
println("name=${p2.name}, age=${p2.age}")
}
class Person(val name: String, val age: Int)
//----- Result -----
{"name":"John","age":10}
name=John, age=10
어떻게 이렇게 만들 수 있을까? 가 궁금해 집니다.
라이브러리 내부의 코드들은 더 복잡하고 많은 내용들을 담고 있지만, 간단하게 그 원리를 파악해보기 위해, 아래와 같은 코드를 실행해 보면, 어느 정도 짐작해 볼 수 있습니다.
코드에서는 클래스 내부에 선언된 필드들의 이름과 타입을 파악할 수 있는데, 이를 이용하면, 라이브러리를 사용하는 사람이 어떤 종류의 객체, 클래스를 생성하건 이를 이용해서 JSONObject 로 변환해 줄 수 있는 코드를 만들 수 있습니다.
- 물론, 아래와 같이 간단한 객체의 경우는 저 정도의 코드 만으로도 변환이 가능하겠지만, 실제 라이브러리의 코드를 참조하면, 더 많은 내용들이 담겨져 있고, 한 편으로는 더 많은 공부가 필요 하기도 합니다.
fun main(args: Array<String>) {
val c : Class<*> = Person::class.java
for(d in c.declaredFields){
println("Field Name = ${d.name}, Type = ${d.type}")
}
}
class Person(val name: String, val age: Int)
// ---- Result ----
Field Name = name, Type = class java.lang.String
Field Name = age, Type = int
이 글을 쓰게 된 것은 제목에서도 보이지만, 단순히 Gson 에 대해 소개하거나 설명하기 위한 글이 아닙니다.
Gson 이 Android 와 만났을 때, 그리고, Android 에서 난독화되었을 때의 문제를 풀어내기 위해서 입니다.
Android 의 proguard 는 클래스 필드명을 모조리 바꾸어 버립니다.
이를 테면, 위의 코드에서 사용했던 Person 같은 객체의 이름과 필드의 이름들은 아래와 같이 변경됩니다.
class A(val a: String, val b: Int)
// class Person(val name: String, val age: Int)
즉, 이런 식으로 바뀌면, 처음 {"name":"John","age":10} 으로 출력되었던 결과물은 {"a":"John","b":10} 으로 출력될 것입니다.
거꾸로 JSONOjbect 스트링을 객체로 변환하는 것은 당연히 실패합니다.
여기서 약간 난해한 상황은 기본 값을 갖는 객체가 생성되지 Error를 발생시킨다던가 하지 않습니다.
그래서, build variant 를 debug 로 실행할 때는 문제가 없었지만, release 로 실행할 때는 전혀 예상할 수 없는 결과가 나오게 되면 매우 당황하게 됩니다.
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
##---------------End: proguard configuration for Gson ----------
Android Library Project 로 consumer-proguard 를 추가했더라면, 상황이 달라질 수 있겠지만, Gson jar 파일로만 배포합니다.
어쨋거나, 위의 예제 프로젝트를 참고하여, proguard 파일에 똑같이 추가해주어야 별문제가 없을 것 같습니다.
그럼에도 여기서 가장 중요한 것은 아래 라인입니다.
저 부분에 Person 에 해당하는 class 파일(즉, 직접 추가한 클래스)을 넣어 주어야 합니다.
그래야, 위에서 얘기한 것처럼 난독화 할 때, name -> a 로 바뀌는 문제를 방지할 수 있습니다.
-keep class com.google.gson.examples.android.model.** { <fields>; }
# -keep class com.example.Person { <fields>; }
SVG, 스케일러블 벡터 그래픽스(Scalable Vector Graphics, SVG)는 2차원 벡터 그래픽을 표현하기 위한 XML 기반의 파일 형식으로, 1999년 W3C(World Wide Web Consortium)의 주도하에 개발된 오픈 표준의 벡터 그래픽 파일 형식입니다.
SVG 형식의 이미지와 그 작동은 XML 텍스트 파일들로 정의 되어 검색화·목록화·스크립트화가 가능하며 필요하다면 압축도 가능합니다. (출처 : WikiPedia)
간단히 설명하면, xml 로 그림을 그리는 표준 방식입니다.
이를 테면, Rect x=10 y=10 width = 100 height = 100, 이런 식으로 써놓으면, (10,10) 좌표에 너비=100, 높이=100 인 사각형을 그대로 그려주는 것입니다.
플레이 스토어에서 apk 혹은 aab 를 업로드한 뒤, 사용자에게 앱의 버전이 업데이트 되었음을 알리고, 업데이트하도록 독려하는 UX 를 구현하는 것은 그리 어려운 일은 아닙니다. 하지만, 이를 구현하는 것은 약간 귀찮은 일임에는 틀림없습니다. 앱이 플레이스토어에 업로드 된 앱 버전 이름 혹은 코드을 체크한 뒤, 현재의 앱 버전 이름 혹은 코드와 비교하여 일정한 차이를 보일 때,(혹은 항상) 업데이트를 유도하는 UX 를 추가하여야 합니다. 예전에는 별도의 서버 API 를 사용하는 경우도 있었고, 그 때 그 때 플레이스토어를 크롤링하는 방법을 사용하는 경우도 있었습니다.
주의해야할 점은 VERSION.SDK_INT >= 21 을 체크하셔야 합니다. 체크하지 않아도 앱이 죽지는 않지만 UX 를 해칠 수 있습니다.
그리고, 플레이 스토어가 설치되어 있지 않은 에뮬레이터에서는 당연히 동작하지 않습니다. 또한, 이런 속성이 있기 때문에, 플레이 스토어에 게재(Publish)된 apk 버전보다 versionCode 를 낮게 설정해놓아야 동작을 확인할 수 있습니다.
2. 추가할 코드
// Creates instance of the manager.
val appUpdateManager = AppUpdateManagerFactory.create(context)
// Returns an intent object that you use to check for an update.
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// For a flexible update, use AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// Request the update.
appUpdateManager.startUpdateFlowForResult(
// Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo,
// Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.IMMEDIATE,
// The current activity making the update request.
this,
// Include a request code to later monitor this update request.
1)
}
}
appUpdateInfo 로 부터 최신 VersionCode 를 가져올 수 있습니다. 이 값과 현재 앱 버전을 비교하여, update 버튼을 활성화시키고, 버튼 클릭에 따라, startUpdateFlowForResult 를 호출해주시면 적절한 update UX 를 구현할 수 있습니다.
appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
val versionCode = appUpdateInfo.availableVersionCode()
}
Android Studio 를 열고, 새로운 프로젝트 템플릿으로 간단한 안드로이드 앱 프로젝트를 만든 뒤, 아래와 같은 코드를 MainActivity 에 추가합니다. 그리고, 앱을 실행하면, 그림과 같은 화면이 보여질 것입니다. (다이얼로그의 스트링은 Android Resource 를 사용했습니다. string.xml 파일에 추가하기가 매우 귀찮아서요. --;)
기본으로 생성해주는 코드를 실행했을 때의 AlertDialog 의 모습은 아래와 같습니다. OK, Cancel 버튼의 색깔이 AppTheme 에서 설정한 colorAccent 의 값(#03DAC5)입니다.
간단하게 버튼의 색깔을 바꾸는 것은 colorAccent 값을 바꿔 주는 것만으로도 가능합니다. 이를 테면, 아래 코드처럼 activity theme 인 AppTheme.NoActionBar 에 colorAccent 아이템을 추가해주는 것 만으로도 쉽게 변경할 수 있습니다.
위에서 말한 모든 값들의 참조가 될 수 있는 것들의 힌트는 appcompat-1.1.0/res/values/values.xml 에 모두 있습니다. 알고보면, abc_alert_dialog_material.xml 레이아웃의 DialogTitle 과 message TextView 의 textColor 는 동일하게 ?android:textColorPrimary 로 설정되어 있다는 것도 확인할 수 있습니다.
이 글은 "AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan"(https://medium.com/androidiots/androidiots-podcast-e18-custom-lints-349f0651d458) 을 읽고 그대로 따라해 보면서 몇 가지 보충설명을 담아보았습니다. 이 글의 내용은 이 팀에서 UI 정책 같은 것들 때문에 RadioButton 을 Customize 한 IdiotRadioButton 사용하는데, 만약, RadioButton 을 그대로 사용할 경우, 경고 메시지를 주고, IdiotRadioButton 으로 수정/테스트하는 과정을 담은 것입니다.
Lint(린트) : 린트는 소스 코드를 분석하여 프로그래밍 오류, 버그, 문체 오류 및 의심스러운 구성을 표시하는 도구입니다.
2. Resource File 추가 하는 방법 : package name = com.example 일 경우,
Step 5. Custom Detector 클래스 만들기
class CustomCodeIssueDetector : Detector()
Step 6. Custom Detector Issue 만들기
Issue 를 생성할 때 필요한 값들에 대한 설명은 굳이 하지 않아도 대략 알 수 있도록 네이밍되어 있어 생략합니다. ^^
class CustomCodeIssueDetector : Detector() {
companion object {
val ISSUE = Issue.create(
id = "IdiotRadioButtonUsageWarning",
briefDescription = "Android's RadioButton should not be used",
explanation = "Don't use Android Radio button, be an idiot and use IdiotRadioButton instead",
category = Category.CORRECTNESS,
priority = 3,
severity = Severity.WARNING,
implementation = Implementation(
CustomCodeIssueDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}
}
Step 7. Scanner 추가하기
class CustomCodeIssueDetector : Detector(), XmlScanner {
override fun getApplicableElements(): Collection<String> {
return listOf(
"RadioButton"
) // will look for Radio Button //Text in all xml
}
override fun visitElement(context: XmlContext, element: Element) {
context.report(
issue = ISSUE,
location = context.getNameLocation(element),
message = "Usage of RadioButton is prohibited" // Error message
)
}
companion object {
val ISSUE = Issue.create(...) // Step 6 참고
}
}
Step 8. Lint (Quick) Fix 추가
class CustomCodeIssueDetector : Detector(), XmlScanner {
override fun getApplicableElements(): Collection<String> { ... } // Step 7 참고
override fun visitElement(context: XmlContext, element: Element) {
val idiotRadioButtonFix = LintFix.create()
.name("Use IdiotRadioButton")
.replace()
.text("RadioButton")
.with("com.androidiots.playground.IdiotRadioButton")
.robot(true)
.independent(true)
.build()
context.report(...) // Step 7 참고
}
companion object {
val ISSUE = Issue.create(...) // Step 6 참고
}
}
layout.xml 에서 RadioButton 을 사용하면 Warning 을 표시하는지 확인할 수 있는 코드
class CustomCodeIssueDetectorTest {
val testInputString = """
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<RadioButton android:id="@+id/someidlowercase" />
</LinearLayout>
"""
@Test
fun testRadioButton() {
lint().files(
xml(
"res/layout/layout.xml", testInputString
).indented())
.issues(CustomCodeIssueDetector.ISSUE)
.allowMissingSdk(true)
.run()
.expectWarningCount(1)
}
}
Lint 관련된 여러 글들을 보면, 안드로이드 그 자체의 Lint 기능이 강력해서 이딴 기능을 굳이 쓸필요가 있느냐하는 식의 멘트들이 많이 있습니다. 이 글의 사례, RadioButton -> IdiotRadioButton 으로 사용하는 것도 그닷 좋은 방법은 아니라 ,예제만 보아도 실제 이런 Lint 가 필요하다고 보기에는 조금 설득력이 떨어집니다. 개발에 참여하는 인원이 적을 수록 별로 의미가 없는 작업이지만, 안드로이드 앱 하나에 4~50명의 개발자가 동시에 개발할 경우, 코드 컨벤션처럼 강력하게 정착시켜야 할 것은 아니지만, 개발자들의 성향(?)에 따라 느슨한 룰같은 것들이 생기기 때문에, 큰 조직이라면 어쩌면 필요할 수도 있겠다는 생각이 듭니다. "Writing a Custom Android UI Inheritance Lint Rule"(https://medium.com/@roderiklagerweij/writing-a-custom-android-ui-inheritance-lint-rule-9af254480399) 에서 나온 것들이 아마도 대표적인 사례(i.e. IncorrectViewId)가 아닐까 싶습니다. 조금 익숙해지신 분들이라면, 글보다는 github(https://github.com/roderiklagerweij/AndroidCustomLint) 코드를 보시는 것이 훨씬 빠를 것입니다.
이 제약사항들 중 아마도 가장 중요한 것은 setOnClickPendingIntent 일 것입니다.
여기서 PendingIntent 에 대한 개발자 문서도 한 번 읽어보고 넘어가는 것이 좋습니다.
A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.
PendingIntent 는 시스템에 의해 유지되는 레퍼런스 토큰이라는 말이 나옵니다.
같은 intent action, data, category, component, flag 를 가지고 있다면 같은 PendingIntent 를 사용하게 된다는 의미로, PendingIntent를 생성할 때 사용하는 Intent 에 다른 Extra 값을 지정한다고 해서 다른 PendingIntent 가 생성되지는 않습니다.
그래서, widget Click 시 다른 동작을 지정하고자 할 때는 Extra 값이 아닌, Action 혹은 Data 를 변경해야 합니다.
이를 테면, 보통 A 라는 Activity 를 실행하기 위해 putExtra("extra",1), putExtra("extra",2) 로 구분해서 사용했다고 해서, PendingIntent 에 동일하게 적용하면 낭패를 보기 쉽습니다.
만약, PendingIntent 적용하고자 한다면, extra 값이 아닌, action = "com.tistory.develop_branch.widget.1", action = "com.tistory.develop_branch.widget.2" 와 같이 action 을 변경해서 사용해야 합니다.