Share
Embed
SDK 개발자로 살아남는 법
앱을 만드는 것 보다, 여러 앱에 들어갈 수 있는 SDK를 만들고 싶어서 입사했다. 그런데 뭔가 이상하다. 그동안 쓰던건 못 쓰고, 안하던 일은 해야하고, 아무튼 안드로이드 스튜디오는 똑같이 쓰고 있는데 뭔가 다르다. SDK 개발을 하면서 앱 개발과 다르게 어려웠던 일들과 이를 어떻게 헤쳐나갔는지 공유해 봅니다.
JoonHo Park
February 02, 2018
Transcript
44 slides
-
– द ೠߣ, ޖधೞݶ ਊхפ. “ غযחѱ ݆ਸ
Ѣۄ झఋস જই.” -
࢚ • ਬ܂ѱ ѐߊೞח ٣ణ ֢݃٘ • ୭न ӝࣿ ӓੋ
بੑ • ӝࣿ ࠶۽Ӓ৬ ೣԋೞח #ࣗా • ೡ ࣻ ب۾ ࣻ۴ػ ݃ੌझహ • زചػ ࠽٘ / పझ / ߓನ • ղ ѐߊр ഝߊೠ ܻ࠭ • ղо ೧ࠄ -
അप • ృӔী ݒੋ ҵੋ न࠙ • ۨѢद ٘৬ ޅ
ॳח ۄ࠳۞ܻ • ࠗܰݶ оঠೞח ӝࣿਗ • बबೞݶ Ҋ ٜযয়ח ਃҳࢎ೦ • ֢ز ডੋ ࠽٘ / పझ / ߓನ • ഒ݅ Ҋةೠ উ٘۽٘ ࣛ • ղо ೠ -
• ృӔী ݒੋ ҵੋ न࠙ • ܻಂష݂ ߂ ઓࢿ ѐࢶ
• API ޙࢲച৬ ٘ о٘ ѐࢶ • ழޭפா࣌ ѐࢶ • ࠽٘ / పझ ѐࢶ ߂ زച • ѐߊ ݽفо ೣԋೞח ܻ࠭ޙച • ղо ೠ, ઑӘ ؊ ಞೞѱ ރࠗܿ -
ழޭפா࣌ ѐࢶ ழޭפா࣌ਸ ѐࢶ೧ঠ ೞח ਬ • ࠁా
ղо ೞӝ फѤ թب ೞӝ फ. • ীࢲ ࠛಞೠ ࠗ࠙ਸ ೧Ѿೞݶ ਗ ࣻ ݅ఀ ٙਸ ࠄ. • ੋೞ ޅ ೞח оؘ ߈ࠂغח दрਸ ݶ ೡ ੌী ೡ ࣻ . • ࠛಞઙо աۄࢲ ѪѪ ٜ०दѱ ؽ • рױೠ ݾ : “ ਸ Ѫ эইࢲ ٜযщ؊פ ؊ۄ” -
যࠁפ ҙܻ Slack : GitHub / Zapier / WebHook Integrations
/ Channel Order Asana : Custom Property / Team Color / Weekly Project Confluence : GitHub Integration / Plugins GitHub : GitHub Organization / Protected Branch / Git-Flow Tech Blog : GitHub Page + Jekyll ழޭפா࣌ ѐࢶ -
Hello, Gradle! “Aীо ۠ ӝמ ػҊ ফӝ೮חؘ” “Bীࢲח ۠ ӝמ
֍য׳ۄחؘ" “Cی ೞ۰ݶ ۠ ӝמ ֍যঠѷחؘ” “ࢲߡীࢲ ӝמ ୶о೮חؘ పझ೧ࠊঠ ೡ Ѫ эؘ" “যীࢲ ӝמ ୶о೮חؘ प ӝӝ పझо ਃೡ Ѫ эؘ” “ ী ݅ٚ పझ জ લחؘ, ઁ ਃೠ জੋ ݽܰѷ֎” “పझ ਊਵ۽ח ѐߊࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח पࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח դةചب ೞҊ ۽Ӓب ࡐঠೞחؘ” “…” -
Hello, Gradle! “Aীо ۠ ӝמ ػҊ ফӝ೮חؘ” “Bীࢲח ۠ ӝמ
֍য׳ۄחؘ" “Cی ೞ۰ݶ ۠ ӝמ ֍যঠѷחؘ” “ࢲߡীࢲ ӝמ ୶о೮חؘ పझ೧ࠊঠ ೡ Ѫ эؘ" “যীࢲ ӝמ ୶о೮חؘ प ӝӝ పझо ਃೡ Ѫ эؘ” “ ী ݅ٚ పझ জ લחؘ, ઁ ਃೠ জੋ ݽܰѷ֎” “పझ ਊਵ۽ח ѐߊࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח पࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח դةചب ೞҊ ۽Ӓب ࡐঠೞחؘ” “…” Version Tag -
Hello, Gradle! “Aীо ۠ ӝמ ػҊ ফӝ೮חؘ” “Bীࢲח ۠ ӝמ
֍য׳ۄחؘ" “Cی ೞ۰ݶ ۠ ӝמ ֍যঠѷחؘ” “ࢲߡীࢲ ӝמ ୶о೮חؘ పझ೧ࠊঠ ೡ Ѫ эؘ" “যীࢲ ӝמ ୶о೮חؘ प ӝӝ పझо ਃೡ Ѫ эؘ” “ ী ݅ٚ పझ জ લחؘ, ઁ ਃೠ জੋ ݽܰѷ֎” “పझ ਊਵ۽ח ѐߊࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח पࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח դةചب ೞҊ ۽Ӓب ࡐঠೞחؘ” “…” Product Flavor -
Hello, Gradle! “Aীо ۠ ӝמ ػҊ ফӝ೮חؘ” “Bীࢲח ۠ ӝמ
֍য׳ۄחؘ" “Cی ೞ۰ݶ ۠ ӝמ ֍যঠѷחؘ” “ࢲߡীࢲ ӝמ ୶о೮חؘ పझ೧ࠊঠ ೡ Ѫ эؘ" “যীࢲ ӝמ ୶о೮חؘ प ӝӝ పझо ਃೡ Ѫ эؘ” “ ী ݅ٚ పझ জ લחؘ, ઁ ਃೠ জੋ ݽܰѷ֎” “పझ ਊਵ۽ח ѐߊࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח पࢲߡ ࠢৈঠ ೞחؘ” “ߓನೡ ٸח դةചب ೞҊ ۽Ӓب ࡐঠೞחؘ” “…” BuildType -
Hello, Gradle! static String computeVersionName() { String tag = "git
describe --tags --abbrev=0 --exact-match".execute().text.trim() String commit = "git rev-parse HEAD".execute().text.trim().substring(0, 7) if (tag?.trim()) return tag else return commit } -
Hello, Gradle! def (STRING, APP_NAME, APP_KEY) = ["string", "app_name", "app_key"]
flavorDimensions "bitsound" productFlavors { master { dimension "bitsound" applicationIdSuffix ".master" resValue STRING, APP_NAME, "Master ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } slave { dimension "bitsound" applicationIdSuffix ".slave" resValue STRING, APP_NAME, "Slave ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } develop { dimension "bitsound" applicationIdSuffix ".develop" resValue STRING, APP_NAME, "Develop ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } publish { dimension "bitsound" resValue STRING, APP_NAME, "Publish ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } } <resources> <!-- Gradle Controlled : DO NOT OVERRIDE --> <!--<string name="app_name"></string>--> <!--<string name="app_key"></string>--> </resources> -
Hello, Gradle! def (STRING, APP_NAME, APP_KEY) = ["string", "app_name", "app_key"]
flavorDimensions "bitsound" productFlavors { master { dimension "bitsound" applicationIdSuffix ".master" resValue STRING, APP_NAME, "Master ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } slave { dimension "bitsound" applicationIdSuffix ".slave" resValue STRING, APP_NAME, "Slave ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } develop { dimension "bitsound" applicationIdSuffix ".develop" resValue STRING, APP_NAME, "Develop ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } publish { dimension "bitsound" resValue STRING, APP_NAME, "Publish ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } } <resources> <!-- Gradle Controlled : DO NOT OVERRIDE --> <!--<string name="app_name"></string>--> <!--<string name="app_key"></string>--> </resources> -
Hello, Gradle! def (STRING, APP_NAME, APP_KEY) = ["string", "app_name", "app_key"]
flavorDimensions "bitsound" productFlavors { master { dimension "bitsound" applicationIdSuffix ".master" resValue STRING, APP_NAME, "Master ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } slave { dimension "bitsound" applicationIdSuffix ".slave" resValue STRING, APP_NAME, "Slave ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } develop { dimension "bitsound" applicationIdSuffix ".develop" resValue STRING, APP_NAME, "Develop ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } publish { dimension "bitsound" resValue STRING, APP_NAME, "Publish ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } } -
Hello, Gradle! def (STRING, APP_NAME, APP_KEY) = ["string", "app_name", "app_key"]
flavorDimensions "bitsound" productFlavors { master { dimension "bitsound" applicationIdSuffix ".master" resValue STRING, APP_NAME, "Master ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } slave { dimension "bitsound" applicationIdSuffix ".slave" resValue STRING, APP_NAME, "Slave ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } develop { dimension "bitsound" applicationIdSuffix ".develop" resValue STRING, APP_NAME, "Develop ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } publish { dimension "bitsound" resValue STRING, APP_NAME, "Publish ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" } } -
Hello, Gradle! buildTypes { debug { minifyEnabled false buildConfigField BOOLEAN,
LOGGING, TRUE } production { initWith(buildTypes.release) minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), '../ proguard-rules-sdk.pro' buildConfigField BOOLEAN, LOGGING, FALSE } release { minifyEnabled false buildConfigField BOOLEAN, LOGGING, TRUE } } -
Hello, Gradle! applicationVariants.all { variant -> variant.outputs.all { output ->
if (output != null && output.outputFileName.endsWith('.apk')) { def outputDir = "../../../../../../release-${computeVersionName()}/apk/${archivesBaseName}" def outputName = "${archivesBaseName}-${variant.name}-${computeVersionName()}.apk" output.outputFileName = "${outputDir}/${outputName}" } } } libraryVariants.all { variant -> variant.outputs.all { output -> if (output != null && output.outputFileName.endsWith('.aar')) { def outputDir = "../../../../release-${computeVersionName()}/aar/${variant.flavorName}" def outputName = "io.bitsound${variant.buildType.name == "production"? "" : "-$ {variant.buildType.name}"}-${computeVersionName()}.aar" output.outputFileName = "${outputDir}/${outputName}" } } } task prepare() { subprojects { project -> if (project.name.startsWith("bitsound")) { new File("${project.buildDir}/outputs/aar").mkdirs() } } } -
Hello, Gradle! release-v4.0.0/ !"" aar # !"" develop # #
!"" io.bitsound-debug-v4.0.0.aar # # !"" io.bitsound-release-v4.0.0.aar # # %"" io.bitsound-v4.0.0.aar # %"" publish # !"" io.bitsound-debug-v4.0.0.aar # !"" io.bitsound-release-v4.0.0.aar # %"" io.bitsound-v4.0.0.aar %"" apk !"" apitestapp # !"" apitestapp-developDebug-v4.0.0.apk # !"" apitestapp-developProduction-v4.0.0.apk # !"" apitestapp-developRelease-v4.0.0.apk # !"" apitestapp-masterDebug-v4.0.0.apk # !"" apitestapp-masterProduction-v4.0.0.apk # !"" apitestapp-masterRelease-v4.0.0.apk # !"" apitestapp-publishDebug-v4.0.0.apk # !"" apitestapp-publishProduction-v4.0.0.apk # !"" apitestapp-publishRelease-v4.0.0.apk # !"" apitestapp-slaveDebug-v4.0.0.apk # !"" apitestapp-slaveProduction-v4.0.0.apk # %"" apitestapp-slaveRelease-v4.0.0.apk %"" usertestapp !"" usertestapp-developDebug-v4.0.0.apk !"" usertestapp-developProduction-v4.0.0.apk %"" usertestapp-developRelease-v4.0.0.apk -
ೞҊर ੌ ѐߊ زച పझ ܻ ಂ ష ݂ জѐߊ
ృӔ ਫ਼ӝ ӝࣿ࠶۽Ӓ ࢿ -
೧ঠೞח ੌ ѐߊ ӝࣿਗ জѐߊ!!! ƁźƁ ঠӔ ҳഅӝദ పझ
పझ ޙࢲ ࢿ -
పझ উࢿ పझܳ ਤ೧ ࢎਊೡ ࣻ ח пઙ ߑߨٜ •
Java à JUnit • Android à Espresso / UI Automator / … • Mass Coverage à Firebase Test Lab / AWS Device Farm / … • Utility à Stetho / Firebase Remote Config / Firebase Cloud Messaging / … • Crash Reports à Fabric Crashlytics / Firebase Crash Reports / … -
పझ উࢿ పझܳ ਤ೧ ࢎਊೡ ࣻ ח пઙ ߑߨٜ •
Java à JUnit : ҅ਊ ঌҊ્ܻ Ѩૐী ਊೣ • Android à Espresso / UI Automator / … • Mass Coverage à Firebase Test Lab / AWS Device Farm / … • Utility à Stetho / Firebase Remote Config / Firebase Cloud Messaging / … • Crash Reports à Fabric Crashlytics / Firebase Crash Reports / … -
పझ উࢿ పझܳ ਤ೧ ࢎਊೡ ࣻ ח пઙ ߑߨٜ •
Java à JUnit : ҅ਊ ঌҊ્ܻ Ѩૐী ਊೣ • Android à Espresso / UI Automator / … • Mass Coverage à Firebase Test Lab / AWS Device Farm / … • Utility à Stetho / Firebase Remote Config / Firebase Cloud Messaging / … • Crash Reports à Fabric Crashlytics / Firebase Crash Reports / … -
పझ উࢿ పझܳ ਤ೧ ࢎਊೡ ࣻ ח пઙ ߑߨٜ •
Java à JUnit : ҅ਊ ঌҊ્ܻ Ѩૐী ਊೣ • Android à Espresso / UI Automator / … • Mass Coverage à Firebase Test Lab / AWS Device Farm / … • Utility à Stetho / Firebase Remote Config / Firebase Cloud Messaging / … • Crash Reports à Fabric Crashlytics / Firebase Crash Reports / … -
పझ ೞ٘ਝয ݃ܳ ࢎਊೠ SDK పझ • ীޯۨఠ ࢚ীࢲ ݃
ࢎਊ ࠛо • ࢎਊ оמೞ؊ۄب पઁ ݃ܳ ਊ೧ ٛחѱ ইפۄ ৻ࠗੑ۱ ഋక • ݃ ೞ٘ਝয पઁ ࢿמ ӝӝ݃ ܴ • ӝఋ पઁ ജ҃ীࢲ ߊࢤೞח নೠ ग৬ ஏ ࠛоמೠ ߸ࣻ • ѾҴ ೡ ࣻ߆ী হ -
పझ ೞ٘ਝয ݃ܳ ࢎਊೠ SDK పझ • ীޯۨఠ ࢚ীࢲ ݃
ࢎਊ ࠛо • ࢎਊ оמೞ؊ۄب पઁ ݃ܳ ਊ೧ ٛחѱ ইפۄ ৻ࠗੑ۱ ഋక • ݃ ೞ٘ਝয पઁ ࢿמ ӝӝ݃ ܴ • ӝఋ पઁ ജ҃ীࢲ ߊࢤೞח নೠ ग৬ ஏ ࠛоמೠ ߸ࣻ • ѾҴ ೡ ࣻ߆ী হ 1. ӝࠄ పझজ + ElasticSearch + Kibana (3~4ݺ 4दр ࢚) -
పझ ೞ٘ਝয ݃ܳ ࢎਊೠ SDK పझ • ীޯۨఠ ࢚ীࢲ ݃
ࢎਊ ࠛо • ࢎਊ оמೞ؊ۄب पઁ ݃ܳ ਊ೧ ٛחѱ ইפۄ ৻ࠗੑ۱ ഋక • ݃ ೞ٘ਝয पઁ ࢿמ ӝӝ݃ ܴ • ӝఋ पઁ ജ҃ীࢲ ߊࢤೞח নೠ ग৬ ஏ ࠛоמೠ ߸ࣻ • ѾҴ ೡ ࣻ߆ী হ 1. ӝࠄ పझজ + ElasticSearch + Kibana (3~4ݺ 4दр ࢚) 2. Kotlin + RxJava2 + Firebase Remote Config ࢎਊ (2~3ݺ 2दр ࢚) -
పझ ೞ٘ਝয ݃ܳ ࢎਊೠ SDK పझ • ীޯۨఠ ࢚ীࢲ ݃
ࢎਊ ࠛо • ࢎਊ оמೞ؊ۄب पઁ ݃ܳ ਊ೧ ٛחѱ ইפۄ ৻ࠗੑ۱ ഋక • ݃ ೞ٘ਝয पઁ ࢿמ ӝӝ݃ ܴ • ӝఋ पઁ ജ҃ীࢲ ߊࢤೞח নೠ ग৬ ஏ ࠛоמೠ ߸ࣻ • ѾҴ ೡ ࣻ߆ী হ 1. ӝࠄ పझজ + ElasticSearch + Kibana (3~4ݺ 4दр ࢚) 2. Kotlin + RxJava2 + Firebase Remote Config ࢎਊ (2~3ݺ 2दр ࢚) 3. FCM ਊೠ Master-Slave పझজ (1~2ݺ 1दр о) -
Master - Slave Test Application Master - Slave Test Application
• ӝઓ API పझজ ӝמ ৮ ਬ • Product Flavorܳ ਊೞৈ Master, Slave ӝמ ੑ • Slave • জ दद FirebaseTokenਸ ࢲߡী ١۾ • FCMਵ۽ View IDܳ ߉ਵݶ ܼ प೯ • Master • ߡౡ ܼ द ࢲߡ۽ View ID ࣠ • ࢲߡח ١۾ػ ݽٚ Slave FirebaseTokenਵ۽ View ID ࣠ • Relay Server • ݈ рױೠ View ID ҅ӝ • Django 2.0 + Django Rest Framework on AWS -
Master - Slave Test Application flavorDimensions "bitsound" productFlavors { master
{ dimension "bitsound" applicationIdSuffix ".master" buildConfigField BOOLEAN, MASTER, TRUE buildConfigField BOOLEAN, SLAVE, FALSE resValue STRING, APP_NAME, "Master ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } slave { dimension "bitsound" applicationIdSuffix ".slave" buildConfigField BOOLEAN, MASTER, FALSE buildConfigField BOOLEAN, SLAVE, TRUE resValue STRING, APP_NAME, "Slave ${computeVersionName()}" resValue STRING, APP_KEY, "01234567-89ab-cdef-0123-456789abcdef" matchingFallbacks = ["develop"] } } -
class FirebaseInstanceIDService : FirebaseInstanceIdService() { private val api by lazy
{ Api.requests(ApiService::class.java) } override fun onTokenRefresh() { FirebaseInstanceId.getInstance()?.token?.let { token -> Storage.put(token to Storage.FIREBASE_TOKEN) try { Observable.just(this.applicationContext) .map<Optional<String>>({ ctx -> GoogleApiAvailability.getInstance()?.isGooglePlayServicesAvailable(ctx) .takeIf { it == ConnectionResult.SUCCESS } .let { try { optionalOf(AdvertisingIdClient.getAdvertisingIdInfo(ctx)?.id) } catch (e: Throwable) { Timber.e(e) emptyOptional() } } }) .filter { it.exists } .map { Storage.put(it.value to Storage.ADID) } .filter { BuildConfig.SLAVE } .flatMap { api.register(it as String, token, Build.MODEL)} .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ Timber.d(if (it.success) "Success" else "Failure") }, { Timber.e(it) }) } catch (e: Exception) { Timber.e(e) } } } } Master - Slave Test Application -
object FirebaseCloudMessageForSlave { var id: Int = 0 set(value) {
field = value publisher.onNext(field) } val publisher: PublishSubject<Int> by lazy { PublishSubject.create<Int>() } } Master - Slave Test Application -
class FirebaseCloudMessageService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage?) {
remoteMessage?.run { Timber.d("onMessageReceived($this)") for ((key, value) in this.data) Timber.d("[data] $key => $value") this.data["id"]?.toInt()?.let { if (BuildConfig.SLAVE) FirebaseCloudMessageForSlave.id = it } ?: Timber.d("Value Not Found : ${this.data} -> ${this.data["id"]}") } } } // in activity private val disposables by lazy { CompositeDisposable() } override fun onResume() { super.onResume() FirebaseCloudMessageForSlave.publisher .filter { BuildConfig.SLAVE } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ id -> this.findViewById<View?>(id)?.performClick() }, { Timber.e(it) }) .also { disposables.add(it) } } Master - Slave Test Application -
private fun View.taskOnClick(title: Any, block: () -> Unit) { this.setOnClickListener
{ logging(title, TextLogData.Type.TITLE) block() if (BuildConfig.MASTER) { logging("Sending Click Command ${this.javaClass}(#${this.id}) to All Slaves...") api.clickAll(this.id) .map { it.slaves } .flatMapIterable { slaves -> slaves } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ logging("Command Success to ${it.model}”) }, { Timber.e(it) }) } } } // in activity btn_bitsound_start_single_detection.run { Picasso.with(activity).load(R.drawable.ic_start_single_detection_white_24dp) .transform(ColorFilterTransformation(GREEN)) .into(this) this.taskOnClick(R.string.bitsound_single_start) { Bitsound.with(this).startSingleDetection(BitsoundContentsReceiver::class.java) echo activity } } Master - Slave Test Application