2024년 12월 겨울.. 홍콩 여행을 계획을 하게 되었습니다.

무려 반년 전 홍콩이나 갈까??라는 아내와의 얘기로 항공권과 호텔을 미리 예약을 하고 기대하던 12월이 다가왔습니다.

일정은 12월 12일 목요일부터 15일 일요일까지 3박 4일 일정이고 터보젯을 타고 하루는 마카오도 방문 예정입니다.

 

주차

오전 8시 10분 출발이라 인천공항 2 터미널로 일찍 출발을 합니다. 차량은 인천공항 발레파킹을 예약해서 주차대행을 맡기기로 했는데, 이건 신용카드 서비스 중에 인천공항 발레파킹 무료 혜택이 있어서 이용하기로 했습니다. 주차대행 비용이 2만 원인데 신용카드 혜택으로 아낄 수 있으니 좋더라고요, 두 아이들을 데리고 멀리 장기주차장에 주차하고 셔틀을 타고 오는 시간도 아깝고 반대로 귀국했을 때 다시 셔틀 타고 장기주차장까지 가는 것도 힘들기도 합니다. 그리고 장기주차장이 하루 최대 9천 원이지만, 다자녀 이거나 차량이 친환경 차량이면 주차비도 감면받습니다. 50%!!.. 그리고 12월의 홍콩 날씨는 가을 날씨라고 해서 공항 지하에서 차량을 맡기로 가벼운 옷차림으로 올라갈 수 있다는 장점도 ^^

 

로밍

따로 usim이나 e-sim을 구입하진 않고 T로밍 서비스를 이용하기로 했습니다. usim을 갈아 끼우거나 e-sim 설정하는 것도 귀찮고 T로밍으로 온 가족 로밍을 한다면 추가로 3천 원만 더 지불했을 때 다 같이 로밍서비스를 이용할 수 있어서 항상 해외에 나갈 땐 가족들이 T로밍을 이용하고 있습니다.

 

출발

홍콩 여행에서 많이들 사용하는 옥토퍼스 카드는 따로 국내에서(클룩, 와드 같은 곳) 구매해 가지 않고 홍콩 공항에 가서 직접 구입했습니다. 차이가 있다면 보증금 정도일 것으로 보이고, 따로 환불할 생각은 없고 카드 털기??로 마지막날 공항에서 다 사용했습니다. 방법은 차차 설명하기로 하고,

 

이번 홍콩행으로 갈 비행기 그리고 기내식입니다. 비행기 안에서 먹는 기내식은 그래도 맛있었습니다. 

 

 

첫째날의 계획은 홍콩에 도착해서 숙소에 짐을 맡기고, 익청빌딩과 미드레벨 에스컬레이터, 그리고 피크트램을 이용해서 홍콩의 야경을 구경할 생각이고 12일 일기예보가 홍콩에 비가 온다는 예보가 있어서 피크트램을 따로 예약을 하고 가진 않았습니다. 티켓을 따로 구매하는 것이 좋다고는 하지만 날자 지정인데 비가 왔을 때 야경을 보기 힘들듯 싶고,. 아니면 지정하지 않은 걸로 구매하더라도 당일 구매해도 되기 때문에,. 

 

홍콩에 도착하고 나서 짐을 찾고, 입국심사 후 옥토퍼스 카드를 구입합니다. 카드 구입하는 곳은 짐을 찾고 나오자마자 바로 앞에 있습니다. 단 현금으로만 구입할 수 있고 카드로 구입은 좀 더 나오면 공항 안에서 자판기 같은 것으로 구입하면 됩니다. 어른은 보증금 50달러에 150달러 충전이 되어서 200달러, 어린이는 50달러 보증금에 충전은 50달러 그래서 100달러입니다.

 

버스 타러 이동을 하고 셩완까지 이동해야 하기에 A11번 버스를 타러 이동합니다. 버스 타러 가는 곳은 어렵지 않게 찾을 수 있을 거라 봅니다. 이번 홍콩 여행에서는 따로 택시를 이용하진 않았고 전철과 트램을 많이 이용하게 되었는데, 전철만으로도 이동이 어렵지 않았고 어느 정도 거리는 구경하면서 걸으면 어렵지 않았던 거 같군요.

A11

 

A11번 버스입니다. 2층 버스이고 캐리어는 1층 공간에 둘 수 있고, 나름 CCTV도 있어서 위에서도 짐을 볼 수 있지만 그렇게 가져가는 사람은 없을 거 같아요. 큰 캐리어가 아니고 짐을 둘 공간이 없다면 2층까지 가지고 올라오시는 분들도 있기는 합니다.

 

공항에서 A11버스를 타고 호텔 근처 정거장은 마카오 페리 터미널인데, 대부분의 사람들이 해당 정거장에서 내립니다. 대략 50분 정도 걸리니 참고하세요.

 

 

이번에 지내게 되는 호텔은 iclub AMTD Sheung Wan Hotel으로 나름 가성비가 좋다고 알려진 호텔이기도 합니다. 반년 전에 예약을 해서 그런지 비싼 홍콩물가에서 싸게 예약했다고 생각했는데, 출발하기 전 호텔 가격을 보니 예약하기 전보다 많이 올랐더군요. 참고로 iclub호텔이 작은 길을 두고 서로 길건너에 하나씩 있습니다. 하나는 iclub AMTD, 하뉴는 iclub 셩완.. 그래서 어디를 예약했는지 잘 보고 들어가셔야 합니다. 찾아보면 잘못 들어가신 분들이 상당히 많이 있는 거 같아요. 여러 호텔을 찾아보다가 이 호텔을 찾게 된 이유도 페리 터미널이 근처에 있다 보니 하루 마카오를 가기 위해서 배를 타러 가는 것도 가깝고, 센트럴도 가깝기에 걸어서 10분 정도?이다 보니 접근성이 좋다고 생각했기에 이곳을 선택했어요. 그리고 간단한 조식을 제공하는데 3층 라운지에서 토스트와 커피, 주스를 제공해 줍니다.

 

체크인을 미리 하고 짐을 객실에 둔 뒤에 오늘 계획했던 여행을 시작해 봅니다.

익청빌딩, 미드레벨 에스컬레이터, 피크트램... 이렇게 3개를 생각했고.. 아 우선 배가 고프니 점심을 먹으러..

 

점심을 호텔 근처 피치드래건이라는 식당을 갈려고 했는데, 잘못해서 그 옆에 가게에 들어갔어요. 마지막날 오전에 이 식당에 가긴 했지만 잘못 들어간 식당이 구글맵에 나오지 않는군요. 메뉴도 다 중국어라고 해야 하나, 한문이라 알아보지도 못했는데.. 그래도 아내가 중국어 전공이라서 그런지 알아서 잘 시켜줍니다.

음식사진이 왜 이거 하나밖에 없는지..

 

이거와 밥을 같이 주는데, 뭔가 밋밋하기도 하고.

하지만 마라 소스 같은 거와 간장 같은 거? 있어서 찍어먹으니 나름 괜찮긴 했습니다. 그리고 면 같은 것도 시켰는데 아이들이 닭은 안 먹지만 그래도 면은 먹더군요. 잘못 들어간 식당치고는 그래도 현지식??으로 괜찮게 먹었던 거 같아요.

 

 

 

 

 

자 이제 전철을 타고 익청빌딩으로 향합니다.

 

익청빌딩

홍콩의 익청빌딩은 트랜스포머 영화에서도 잠깐 나왔었죠,.  셩완역에서 전철(Island Line)을 타고 Tai Koo에서 내려서 조금만 걸어가면 됩니다. 9개 정거장으로 대략 18분 정도 걸립니다. 자세한 정보는 구글맵을 보면서 길을 찾으면 자세하게 나옵니다.

 

 

익청빌딩, %커피

익청빌딩 1층 상가에 보면 응 커피가 있습니다. 한잔 마셔주며,, 커피를 시키고 나오기까지 기다리고 있는데 안에서 커피마시는 분들은 전부 한국분들 이었습니다.

 

이제 미드레벨 에스컬레이터를 보러 가봅니다.

 

미드레벨 에스컬레이터

미드레벨 에스컬레이터

다시 Tai Koo 역에서 Central 역으로 전철을 타고 이동합니다. 영화 중경삼림 에서 나와서 유명해졌으며 많은 분들이 홍콩에 가면 여기도 들리더군요. 도심 한가운데에 이런게 있을 수 있는지, 그리고 주변 건물들과 조화도 이뤄지면서. 구경 열심히 하고 이제 피크트램을 타러 이동해 봅니다. 미드레벨 에스컬러에터에서 피크트램까지도 걸어갈만 합니다. 주변 구경도 하고 가면서 이것저것 사먹기도 하면서. 걸어가는데 대략 15분~20분 정도 소요 됩니다.

 

피크트램

피크트램은 직접 가서 티켓을 구입할 수도 있지만 국내에서 클룩과 같은 곳에서도 구입해서 갈 수 있습니다. 미리 구입해서 가면 티켓이 있는 줄과 구입줄이 따로 있어서 QR코드를 찍고 들어갈 수 있지만, 가서 구입하더라도 티켓 구입후 그곳에서 바로 진입시켜 줘서 사람이 많다면 별반 달라보이진 않았던거 같습니다. 어두워 지기 시작할 때 라서 그런지 사람들이 너무 많아요. 피크트램을 타면 올라갈 때는 오른쪽, 내려올 때는 왼쪽에 앉으라고 합니다. 그래야 구경거리가 더 많기 때문에.

피크트램

이 많은 인원들과 같이 트램을 타고 피크타워로 올라갑니다.

약간 부촌?? 의 아파트 들이 보이는거 같기도 합니다. 익청빌딩과는 사뭇 다른,.

 

그리고 피크타워에서 야경을 구경해 줍니다.

 

홍콩 날씨가 비가 올 수도 있다고 해서 따로 미리 예약을 하진 않고 당일날 구입해야지 했는데, 홍콩에 도착하니 날씨가 너무 좋더군요, 그래서 결국 클룩에서 기간없이 사용할 수 있는 티켓으로 구입했어요. 가격 차이는 크진 않지만 지정 날자로 티켓을 미리 구입한다면 조금 더 싸긴 합니다. 

 

이렇게 홍콩 야경을 구경해 주고 다시 피크트램을 타고 내려와서는 숙소로 돌아갑니다. 아이들도 피곤하기도 하고 쉬면서 맥주 한잔 먹어줘야 하기도 해서.

 

숙소까지의 이동은 트램을 타보기로 했습니다. 아이들이 타보고 싶어 하기도 했고 이걸 타면서 또 건물들 구경하면서 가는 재미도 있더라구요. 버스와 전철, 그리고 숙소까지 이동하는 트램까지 모두 옥토퍼스 카드를 이용했습니다.

 

다음날은 홍콩의 디즈니랜드와 야시장을 가볼려고 합니다.

android library를 만들고 배포할려면 maven repository가 필요한데, 이게 준비가 되어 있지 않다면 aar파일을 직접 전달해야 하는 번거러움이 있습니다. 그런데 github packages에 apache maven을 지원해주기에 간단하게 aar library를 배포하여 사용할 수 있습니다.

github packages에 대한 설명은. 해당 링크로..

github - personal access tokne 생성

github의 자신의 계정에서 Settings-> Developr settings -> Personal access tokens 으로 가서 access token을 생성 합니다.
write packages, read packages 를 체크 합니다.

properties

project의 root 위칭에 github.properties 를 만들고 그 안에
github에서의 username과 access token생성한 것을 적용해 줍니다.
그리고 해당 파일은 github에 올라가지 않도록 .gitignore 에 추가해 주는 것이 좋습니다. 보안상...

github_username={username}
github_access_token={access_token}

Publish script

library module에 있는 위치에 publish.gradle 를 만듭니다.
그리고 그 안의 내용에는

apply plugin: 'maven-publish'

def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties")))

def LIB_GROUP_ID = {library_group_id}
def LIB_ARTIFACT_ID = {artifact_id}
def LIB_VERSION = {version_name}

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    classifier "sources"
}

publishing {
    repositories {
        maven {
            name = 'GithubPackages'
            url = uri("https://maven.pkg.github.com/{user_name}/{project_name}")
            credentials {
                username = githubProperties['github_username'] ?: System.getenv("github_username")
                password = githubProperties['github_access_token'] ?: System.getenv("github_access_token")
            }
        }
        maven {
            name = 'CustomMavenRepo'
            url = "file://${buildDir}/repo"
        }
    }
    publications {
        deploy(MavenPublication) {
            groupId LIB_GROUP_ID
            artifactId LIB_ARTIFACT_ID
            version LIB_VERSION
            artifact("$buildDir/outputs/aar/${aar_library_name}.aar")
            artifact(sourceJar)

            pom.withXml {
                def dependenciesNode = asNode().appendNode('dependencies')

                //Iterate over the compile dependencies (we don't want the test ones), adding a <dependency> node for each
                configurations.api.allDependencies.each {
                    def dependencyNode = dependenciesNode.appendNode('dependency')
                    dependencyNode.appendNode('groupId', it.group)
                    dependencyNode.appendNode('artifactId', it.name)
                    dependencyNode.appendNode('version', it.version)
                }
            }
        }
    }
}

그리고 나서, library project에 있는 build.gradle 에 아래와 같이 추가해 줍니다.

apply from: file('publish.gradle')

이와 같이 추가하고 나면

그림과 같이 gradle에 publishing 에 대한 부분이 생깁니다.

library를 build후 publishDeployPublicationToCustomMavenRepoRepository 하고 나면, local repo에 배포가 되는 부분을 확인할 수 있습니다.

실질적으로 배포를 할려면 publishDeployPublicationToGithubPackagesRepository 로 하면 github의 지정된 username/project의github packages에 배포가 되게 됩니다.

library 사용

root에 있는 build.gradle 에 다음과 같이 repositories에 추가해 줍니다.

def githubProperties = new Properties()
githubProperties.load(new FileInputStream(rootProject.file("github.properties")))

repositories {
        google()
        jcenter()

        mavenLocal()
        maven {
            name = {maven_repository_name}
            url = uri("https://maven.pkg.github.com/{user_name}/{project_name}")
            credentials {
                username = githubProperties['github_username'] ?: System.getenv("github_username")
                password = githubProperties['github_access_token'] ?: System.getenv("github_access_token")
            }
        }
    }

그리고 application 에 있는 build.gradle 에 maven repository에 있는 library 사용할 때 처럼.

implementation '{library_group_id}:{artifact_id}:{version}'

으로 해서 사용하면 됩니다.

maven repository를 만들고 관리하는 것 또한 비용인데, 이걸 github packages를 이용한다면 간단히 해결되는 것 같습니다.

하나의 RecylerView에서 다양한 viewholder화면을 보여줄려면, 이전에는 하나의  adapter안에서 type들을 지정하고, 해당 position에 대해서 type에 맞는 viewholder들을 가져와서 보여줬습니다.

하지만, 여러 type들이 존재하거나 할 경우는 adapter안에는 다양한 type과 viewholder내용들이 존재하게 됩니다.

header,. 그리고 item들이 순차적으로 있다고 해도, adapter에 대한 position의 계산도 틀려집니다.

그래서 이번에 concatadapter를 볼려고 합니다.

developer.android.com/reference/androidx/recyclerview/widget/ConcatAdapter

 

ConcatAdapter  |  Android 개발자  |  Android Developers

ConcatAdapter public final class ConcatAdapter extends Adapter An RecyclerView.Adapter implementation that presents the contents of multiple adapters in sequence. MyAdapter adapter1 = ...; AnotherAdapter adapter2 = ...; ConcatAdapter concatenated = new Con

developer.android.com

다양한 adapter들을 결합에서 하나의 recyclerview에서 내용을 보여줄 수 있습니다.

header, item, footer 가 존재한다면,

headeradapter, itemadapter, footeradapter를 만들고, 그걸 concatadapter에 결합해서 사용하면 됩니다.

그리고 notify할 경우도, 각 adapter에서 따로 진행할 수도 있습니다.

간단하게 만든 sample은.

github.com/drcarter/ConcatAdapterTest

 

drcarter/ConcatAdapterTest

Contribute to drcarter/ConcatAdapterTest development by creating an account on GitHub.

github.com

으로 간단히 만들어 봤습니다.

 

Assert   : 9777A9
Debug    : 6A98B9
Error    : FF6B68
Info     : 6A855A
Verbose  : BBBBBB
Warning  : BC7739

이정도 색상으로 변경해서 사용하면,,.

요즘 화재의 맥북인 apple silicon인 M1 칩이 들어간 air가 생겼습니다.

개발을 하기 위한 설정과 여러 package들을 설치하기 위헤서 homebrew를 설치하기 위해 작업을 하다, 여러 내용이 있지만,

arm cpu인 M1 에 맞는 내용은 ...

/bin/bash -c "$(curl -fsSL https://gist.githubusercontent.com/nrubin29/bea5aa83e8dfa91370fe83b62dad6dfa/raw/48f48f7fef21abb308e129a80b3214c2538fc611/homebrew_m1.sh)"

이와 같은 방법으로 하는게, 제일 좋군요.

 

프로젝트의 시간이 길어질 수록 사용하지 않는 resource가 많아지는 문제가 있습니다.

리펙토링 하면서 이전 리소스에 대해서 지울 수도 있지만, 명시적으로 지워준다면 id를 만들지 않기 때문에 더 좋을 수도 있죠.

build.gradle 에서

android {
    buildTypes {
        release {
            shrinkResources true
        }
    }
}

으로 빌드시에 resource정리를 하는 방법도 있지만, 명시적으로 지우는 것을 해볼까 합니다.

android studio 에서 Refactor -> Remove Unused Resources... 라는 메뉴가 있습니다.

해당 기능을 통해서 layout, string, drawable, color, dimen 등등 사용하지 않는 resource를 지울 수 있습니다.

단, 문제가 하나 있는데,

dynamic resource로 접근하는 부분에 대해서는 걸러주지 않습니다.

그 부분을 해결하기 위해서는

resource 유지를 위해서 keep 처리를 해야 하는데, tools:keep 을 적용하면 됩니다.

예를 들어서, resource.getIdentifiericon_position_의 prefix로 되는 resource들을 다 유지하고 싶다면,

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" 
    tools:keep="@drawable/icon_position_*">
</resources>

으로 xml resouce파일을 만들어서 관리하면 됩니다. 이 방법은 shrinkResources 로 dynamic resource에 접근하는 것들에 대한 유지 방법과 같습니다.

Firebase Crashlytics에서 custom UnCaughtExceptionHandler를 적용하기 위해선 초기화 하는 방법을 변경해 줘야 합니다.

Fabric에서 하는 방법은 별다른 방법이 없어도 잘 동작했지만, fabric에서 firebase crashlytics로 넘어가면서 초기화 순서가 중요해 졌습니다.

firebase의 초기화는 contentprovider를 통해서 초기화 됩니다.

contentprovider의 속성중 순서를 정할 수 있는 부분이, android:initOrder입니다. 이에 대한 설명은

동일한 프로세스에서 호스팅하는 다른 콘텐츠 제공자에 상대적으로 콘텐츠 제공자를 인스턴스화해야 하는 순서입니다. 콘텐츠 제공자 사이에 종속성이 있는 경우 제공자별로 이 속성을 설정하면 종속성에서 요구하는 순서대로 생성됩니다. 값은 단순 정수이며 숫자가 높을수록 먼저 초기화됩니다.

으로 되어 있습니다.
custom exception handler가 firebase 보다 먼저 초기화 되어야 하는데, 그렇지 않아서 정상적으로 동작하지 않는 부분입니다.

그럼 해결방법은.
custom exception handler를 초기화 하는 conentprovider를 등록하고 android:initOrder 의 값을 firebase보다 크게 지정하면 됩니다. firebase 의 initOrder값은 100으로 되어 있습니다.

override fun onCreate(): Boolean {
    val myUncaughtExceptionHandler = UncaughtExceptionHandler(
        Thread.getDefaultUncaughtExceptionHandler()
    )
    Thread.setDefaultUncaughtExceptionHandler(myUncaughtExceptionHandler)

    return true
}
<provider
    android:name=".UncaughtExceptionHandlerContentProvider"
    android:authorities="${applicationId}"
    android:exported="false"
    android:grantUriPermissions="false"
    android:initOrder="101" />

이와 같이 manifest 에 등록하고 사용하면 됩니다.

val items = listOf(1, 2, 3, 4, 5)
// actual sum of 15

이와 같은 collection에서 전체 합을 구해보기.

 

1. foreach

@Test
fun sumTest_1() {
    var total = 0
    items.forEach {
        total += it
    }
    Assert.assertEquals(total, actual)
}
/**
 * Performs the given [action] on each element.
 */
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

 

가장 무난하게 많이??이들 사용했을것 같은 부분.

 

 

2. sum()

@Test
fun sumTest_2() {
    val total = items.sum()
    Assert.assertEquals(total, actual)
}
/**
 * Returns the sum of all elements in the collection.
 */
@kotlin.jvm.JvmName("sumOfInt")
public fun Iterable<Int>.sum(): Int {
    var sum: Int = 0
    for (element in this) {
        sum += element
    }
    return sum
}

각 element들의 합계를 바로 구한다. foreach의 내용이 바로 sum의 내용과 같음.

 

 

3. reduce

@Test
fun sumTest_3() {
    val total = items.reduce { acc, i -> acc + i }
    Assert.assertEquals(total, actual)
}
/**
 * Accumulates value starting with the first element and applying [operation] from left to right to current accumulator value and each element.
 * 
 * @sample samples.collections.Collections.Aggregates.reduce
 */
public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
    val iterator = this.iterator()
    if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
    var accumulator: S = iterator.next()
    while (iterator.hasNext()) {
        accumulator = operation(accumulator, iterator.next())
    }
    return accumulator
}

첫번째 element가 기본값으로 operation의 block내용을 가져와서 합계를 구함. 

 

 

4. fold

@Test
fun sumTest_4() {
    val total = items.fold(0) { acc, i ->
        acc + i
    }
    Assert.assertEquals(total, actual)
}
/**
 * Accumulates value starting with [initial] value and applying [operation] from left to right to current accumulator value and each element.
 */
public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R {
    var accumulator = initial
    for (element in this) accumulator = operation(accumulator, element)
    return accumulator
}

reduce와의 차이는 초기값을 지정할 수 있음.

+ Recent posts