2017/3/10 #DroidKaigi 2017 でお話した「フリル」のチーム開発に関する資料です
4ଓ͘ΞϓϦʹ͓͚ΔνʔϜ։ൃ@cutmailϋογϡλάɿ#DroidKaigi2
View Slide
ରऀ• AndroidΞϓϦͷνʔϜ։ൃΛ͍ͯ͠Δ• AndroidΞϓϦͷνʔϜ։ൃʹڵຯ͕͋Δ• AndroidΞϓϦͷνʔϜ։ൃΛ͍ͯͯ͠Կ͔ࠔ͍ͬͯΔ
ΞδΣϯμ• ϑϦϧAndroid൛ͷي• ϑϦϧʹ͓͚Δ։ൃ• νʔϜ։ൃΛ͏·͘ճͨ͢Ίʹ͖ͬͯͨ͜ͱ
@cutmail• גࣜձࣾFablic Co-Founder/Engineer• Android / iOS / Ruby on Rails
ϑϦϧ
ϑϦϧ• ຊॳͷϑϦϚΞϓϦ• ॳঁੑݶఆɺͷͪʹஉੑʹղ์• 20127݄ʹiOS൛͕ϦϦʔε
ϑϦϧ for iOS• ࣌Titanium MobileͰ։ൃ։࢝• ։ൃظؒ3ϲ݄• ͷͪʹ༷ʑͳࣄͰωΠςΟϒԽ
͓΅͍͑ͯ·͔͢ Titanium Mobile
Titanium Mobile• ࣌JavascriptͰΫϩεϓϥοτϑΥʔϜ։ൃ͕Ͱ͖ΔͱݴΘΕ͍ͯͨ• ࣌ͷTitanium MobileͰͷAndroidΞϓϦ։ൃ
ϑϦϧ for AndroidJava!
ϑϦϧ for Android• 201211݄6 ॳgitίϛοτ• 20131݄29 v1.0ϦϦʔε• ࠷ۙKotlin͕ಋೖ͞Ε·ͨ͠
v1.0
Լλϒ!!!
• ϑϦϧAndroid൛ͷي• ϑϦϧʹ͓͚Δ։ൃ• νʔϜ։ൃΛ͏·͘ճͨ͢Ίʹ͖ͬͯͨ͜ͱΞδΣϯμ
• v1.0• v2.1• ެࣜγϣοϓػೳՃ• v2.3• ActionBarԽ• σβΠϯϦχϡʔΞϧ• v2.5• FrilAPIClientͷҠߦ• v3.0• UIϦχϡʔΞϧ• v3.6• ͓͢͢ΊϢʔβʔϦχϡʔΞϧ• v4.0• UIϦχϡʔΞϧ• v4.1.2• RxAndroid1.0ʹߋ৽• v4.3• Android WearΞϓϦͷՃ• v5.0• ৭ݕࡧػೳͷՃ• v5.2• ͕͢͞ը໘ϦχϡʔΞϧ• v5.3.2• νϟοταϙʔτ• v5.5.0• FCMͷҠߦ• v5.7.0• λΠϜϥΠϯɾ͕͢͞ը໘ϦχϡʔΞϧ• v6.0• v6.0.0 BIϦχϡʔΞϧɾϒϥϯυ&ੑผબഇࢭ• v6.4• SMSೝূ
• v1.0• v2.1• ެࣜγϣοϓػೳՃ• v2.3• ActionBarԽ• σβΠϯϦχϡʔΞϧ• v2.5• FrilAPIClientͷҠߦ• v3.0• UIϦχϡʔΞϧ• v3.6• ͓͢͢ΊϢʔβʔϦχϡʔΞϧ• v4.0• UIϦχϡʔΞϧ• v4.1.2• RxAndroid1.0ʹߋ৽• v4.3• Android WearΞϓϦͷՃ• v5.0• ৭ݕࡧػೳͷՃ• v5.2• ͕͢͞ը໘ϦχϡʔΞϧ• v5.3.2• νϟοταϙʔτ• v5.5.0• FCMͷҠߦ• v5.7.0• λΠϜϥΠϯɾ͕͢͞ը໘ϦχϡʔΞϧ• v6.0• BIϦχϡʔΞϧɾϒϥϯυ&ੑผબഇࢭ• v6.4• SMSೝূ
ΞϓϦΞΠίϯͷมԽ
ϑϦϧͷྺ࢙ϦχϡʔΞϧͷྺ࢙
ϦχϡʔΞϧ༷ʑͳԠ͕ى͖Δ
https://speakerdeck.com/shoby/yuzanishou-keru-rerare-wen-ti-woqi-kosiduraida-gui-mo-riniyuarufalsejin-mefang
• v1.0~• v2.0~• v3.5~
։ൃମ੍• AndroidΞϓϦΤϯδχΞ 1.5໊• ࣗiOSΛΓͳ͕ΒยखؒAndroid• σβΠφʔ 1໊v1.0~v1.1♂♂
։ൃڥ• Eclipse• Bitbucket «v1.0~v1.1
։ൃϑϩʔ• ϓϧϦΫΤετɺίʔυϨϏϡʔͳ͠• developϒϥϯν͔ΒͦΕͧΕϒϥϯνΛ֤ͬͯࣗͰϚʔδ• جຊతʹiOSͷػೳΛͦͷ··Ҡ২v1.0~v1.1
ΞʔΩςΫνϟ• Activity• DB• Content Provider• ը૾ಡΈࠐΈ• URLConnectionʹΑΔࣗલ࣮v1.0~v1.1
ΞʔΩςΫνϟ• API• AsyncTaskLoaderϕʔε• ը໘ؒͷΠϕϯτ௨• startActivityForResult• ্෦ͷόʔࣗલv1.0~v1.1
v1.0~v1.1public Loader onCreateLoader(int index, Bundle args) {HashMap params = new HashMap();params.put("method", "0");params.put("grid_flag", "0");params.put("pos", "0");Loader loader = new JSONLoader(this, "POST",“/timeline", params);loader.forceLoad();return loader;}@Overridepublic void onLoadFinished(Loader arg0, JSONObject response) {if (response == null) return;mAdapter.loadFromJSON(response);if (mAdapter.getCount() > 0) {mListView.setAdapter(mAdapter);mListView.invalidate();}}AsyncTaskLoaderAPIίʔϧAPIϨεϙϯε
• v1.0~v1.1• v2.0~• v3.5~
v2.3
v3.0
ମ੍• AndroidΞϓϦΤϯδχΞ 5໊• σβΠφʔ 1໊v2.0~v3.4♂♂♂♂♂
։ൃڥ• GitHub• Android Studio• Gradle• CI (Travis-CI)v2.0~
։ൃϑϩʔ• ϓϧϦΫΤετಋೖ• ίʔυϨϏϡʔಋೖv2.0~
ΞʔΩςΫνϟ• Activity + Fragment• ԣը໘ɺλϒϨοτରԠͷͨΊʹFragmentͷಋೖ• DB• Content Provider• ը૾ಡΈࠐΈ• Picassov2.0~
ΞʔΩςΫνϟ• API• android-async-http• EventBus• Ottov2.0~
public RequestHandle getItemDetail(int itemId, final SingleModelCallbackcallback) { RequestParams params = new RequestParams(baseParams); params.put("item_id", String.valueOf(itemId)); return httpClient.get(baseUrl + "/api/item", params, new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { ItemDetail itemDetail = gson.fromJson(response.toString(), ItemDetail.class); callback.success(itemDetail); } @Override public void onFailure(int statusCode, Throwable e, JSONObject errorResponse) { callback.failure(statusCode, e, errorResponse); } }); }android-async-http v2.0~
private void getItemDetail() {apiClient.getItemDetail(mItem.getId(),newFrilAPIClient.SingleModelCallback() {@Overridepublic void success(ItemDetail itemDetail) {setDetailView(itemDetail);}@Overridepublic void failure(int statusCode, Throwable error,JSONObject errorResponse) {// Τϥʔॲཧ}});}android-async-http v2.0~
v4.0
։ൃମ੍• AndroidΞϓϦΤϯδχΞ 2໊• σβΠφʔ 1໊v3.5~♂♂
։ൃڥv3.5~• GitHub• Android Studio• Gradle• CI (CircleCI)
ΞʔΩςΫνϟ• Activity + Fragment• DB• Content Providerv3.5~
ΞʔΩςΫνϟ• API• Retrofit + RxJavav3.5~
Observable observable =FrilServiceCreator.createFrilService(activity).getItemDetail(itemId); return observable.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { @Override public void call(ItemDetail itemDetail) {setItemDetail(itemDetail); } }, new Action1() { @Override public void call(Throwable throwable) { Timber.e(throwable, throwable.getMessage()); } });Retrofit + RxJava v3.5~@GET("/api/item") Observable getItemDetail( @Query("item_id") Integer itemId );
ΞʔΩςΫνϟͷมભ
ΞʔΩςΫνϟͷมભ ωοτϫʔΫ "TZOD5BTL-PBEFSBOESPJEBTZODIUUQ3FUSPpUඇಉظॲཧΠϯλϑΣʔε-PBEFS $BMMCBDL 3Y+BWB&WFOU#VT TUBSU"DUJWJUZ'PS3FTVMU 0UUP 0UUPը૾ 63-$POOFDUJPO 1JDBTTP 1JDBTTP
RxJavaΛಋೖ͢Δ·Ͱ…
ྫ͑ɺΞϓϦͷϚελσʔλʢۜߦɺϒϥϯυʣͷߋ৽
ྫʣϚελσʔλͷߋ৽ॲཧ1. Ϛελσʔλͷߋ৽ΛνΣοΫ 2. ߋ৽͕͋Εඞཁͳ߲Λߋ৽͢Δ 3. શͯͷσʔλͷߋ৽͕ྃͨ͠Βݺͼग़͠ݩʹ͢
RxJavaಋೖલpublic class MigrateMasterDataTask extends AsyncTask {@Override protected Boolean doInBackground(Void... params) {final FrilService frilService = FrilServiceCreator.createFrilService(context); //ϦϞʔτ͔ΒͷऔΓࠐΈ͕ඞཁ͔νΣοΫ Call call = frilService.checkNeedDatabaseUpdateSync( AppPrefs.getBrandVersion(context), AppPrefs.getBankVersion(context)); DatabaseUpdate databaseUpdate = call.execute().body(); if (databaseUpdate == null) { return false; } // ϦϞʔτ͔ΒͷऔΓࠐΈ͕ඞཁ boolean updateBrand = databaseUpdate.isNeedForBrand(); boolean updateBank = databaseUpdate.isNeedForBank(); if (!updateBrand && !updateBank) { return true; } //ϦϞʔτͷσʔλΛ SQLite ʹҠߦ return syncFromRemote(updateBrand, updateBank);}}
RxJavaಋೖલprivate boolean syncFromRemote(boolean isNeedUpdateBrand, boolean isNeedUpdateBank) {if (isNeedUpdateBrand) {// ωοτϫʔΫ͔ΒϒϥϯυϚελΛಉظऔಘ}if (isNeedUpdateBank) {// ωοτϫʔΫ͔ΒۜߦϚελΛಉظऔಘ}if (brandVersion != null) {// ΞϓϦʹ࠷৽ͷόʔδϣϯใΛอଘ}if (bankVersion != null) {// ΞϓϦʹ࠷৽ͷόʔδϣϯใΛอଘ}return true;}
ॲཧͷྲྀΕ͕͍ͮΒ͍https://www.flickr.com/photos/eughenes/3758142701
RxJavaͷಋೖ
RxJavaಋೖޙpublic Completable migrateMasterData() { final String brandVersion = AppPrefs.getBrandVersion(context); final String bankVersion = AppPrefs.getBankVersion(context); return frilService.checkDatabaseUpdateNeeded(brandVersion, bankVersion) .flatMap(update -> { List completables = new ArrayList<>(); if (update.isNeedForBrand()) { completables.add(migrateBrandsFromRemote().subscribeOn(Schedulers.io())); } if (update.isNeedForBank()) { completables.add(migrateBanksFromRemote().subscribeOn(Schedulers.io())); } return Completable.merge(completables).toObservable(); }) .onErrorResumeNext(throwable -> { return Observable.error(throwable); }) .toCompletable(); }Subscription subscription = helper.migrateMasterData() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::dismissProgressDialog, throwable -> { Timber.e(throwable, throwable.getMessage()); }); subscriptions.add(subscription);
ωοτϫʔΫϨΠϠͷඋʹΑΓɺॲཧͷྲྀΕ͕Θ͔Γ͘͢ͳͬͨ
RetrofitʹΑΓΤϯυϙΠϯτఆٛͷίʔυ͕গͳ͘ͳͬͨ
ΞʔΩςΫνϟυΩϡϝϯτͷඋ͕νʔϜͷ։ൃʹӨڹ͢Δ
͜͜·Ͱ͕ϑϦϧAndroid൛ͷي
• ϑϦϧAndroid൛ͷي• ϑϦϧʹ͓͚Δ։ൃ• νʔϜ։ൃΛ͏·͘ճͨ͢Ίʹ͖ͬͯͨ͜ͱɺͬͯྑ͔ͬͨ͜ͱΞδΣϯμ
ϑϦϧAndroid൛ͷ։ൃ• Issueཧ• ϓϧϦΫΤετ• ίʔσΟϯάنɺυΩϡϝϯτͷඋ• CI• ϢʔβʔΠϯλϏϡʔ• QA• ϦϦʔε࡞ۀ• ࣈͷܭଌ
Issueཧ• LabelΛ׆༻• readyίʔυϨϏϡʔͯ͠OK
ϑϦϧAndroid൛ͷ։ൃ• Issueཧ• ϓϧϦΫΤετ• ίʔσΟϯάنɺυΩϡϝϯτͷඋ• CI• ϢʔβʔΠϯλϏϡʔ• QA• ϦϦʔε• ࣈͷܭଌ
ϓϧϦΫΤετ• GitHubͷςϯϓϨʔτΛ׆༻• ֬ೝखॱ͕ॏཁ
ίʔσΟϯάنͱυΩϡϝϯτͷඋ• ίʔσΟϯάنΛఆΊ͍ͯ·͢• CONTRIBUTING.md• ϓϧϦΫΤετΛग़͢ࡍʹϦϯΫ͕දࣔ͞ΕΔ
جຊతʹCookpadͷίʔσΟϯάنʹ४ڌ͢Δ
https://github.com/cookpad/styleguide
ࡉ͔͍ͱ͜ΖͰ໎Θͳ͍Α͏ʹίʔσΟϯάنΛඋ
READMEͷඋ
JavaDocͷඋ
@colorͷඋ
AnnotationΛੵۃతʹ͏dependencies { compile 'com.android.support:support-annotations:24.2.0' }void setActionBarAlpha(@IntRange(from = 0x0, to = 0xFF) int alpha) {
AnnotationΛੵۃతʹ͏
https://developer.android.com/studio/write/annotations.htmlImprove Code Inspectionwith Annotations
ϝϯςφϯε͍͢͠ঢ়ଶʹ͓ͯ͘͠
։ൃ͕εϜʔζʹਐΈग़͢ͱى͖Δͷ͕
ϓϧϦΫΤετཷ·Δ
ϓϧϦΫΤετཷ·Δ• ཷ·͍ͬͯΔϓϧϦΫΤετΛSlackʹ௨• ϨϏϡʔ͠ͳ͍ͱຖͲΜͲΜ૿͍͑ͯ͘
CI
http://in.fablic.co.jp/entry/circle-ci-in-android-project
ϕʔλ൛ͷࣾ• Fabric betaͰࣾϢʔβʔʹ• developϒϥϯνʹmerge͞ΕΔʹ࠷৽൛͕͞ΕΔ
Fabric betaʹΑΔβ൛webhookpush
ϦϦʔε൛ͱผΞϓϦͱͯ͠
ϢʔβʔΠϯλϏϡʔ
ϢʔβʔΠϯλϏϡʔ• iOS൛ϦϦʔεͷࡍʹ100ਓ͘Β͍ϢʔβʔΛั·͑ͯΠϯλϏϡʔΛ࣮ࢪ• ϑϦϧϢʔβʔʹฉ͘จԽ
ϢʔβʔΠϯλϏϡʔ• ຖճͰͳ͍͕େ͖ͳϦχϡʔΞϧͳͲͷࡍߦ͏• ࣾͷCSελοϑશһϑϦϧϢʔβʔͷͨΊɺ͙͢ʹΠϯλϏϡʔ͕Մೳ• ελϯσΟϯάσεΫͰΧδϡΞϧʹΠϯλϏϡʔ
ϢʔβʔΠϯλϏϡʔ• QAલޙʹΠϯλϏϡʔΛͨ͠Γ͢Δ• ΠϯλϏϡʔͷ݁Ռɺ༷Λม͑Δ͜ͱ͋Δ• ੈʹग़Δલʹ࣮ࡍͷϢʔβʔͷ͕ฉ͚ΔͨΊΤϯδχΞͱͯ҆͠৺
QA• ΤϯδχΞɺCSελοϑɺσβΠφʔɺϓϩμΫτΦʔφʔ͕ࢀՃ• ςετγʔτΛࣄલʹΤϯδχΞ͕࡞͠ɺ߲ʹԊͬͯಈ࡞νΣοΫΛ͍ͯ͘͠• ΞϓϦͷΫΦϦςΟνΣοΫ• ༷֬ೝ݉Ͷ͍ͯΔ
ςετγʔτ
QAͷྲྀΕࣄલ४උ1. git-pr-releaseͰϦϦʔεʹ͚ͯQA༻ͷϓϧϦΫΤετΛ࡞Δ2. ΤϯδχΞ͕ϓϧϦΫΤετͷίϝϯτΛϕʔεʹQA։࢝·ͰʹςετγʔτΛهࡌ͢Δ3. SlackͰQAΛґཔ1. ςετͷ֓ཁΛઆ໌2. ςετγʔτʹ͕ͨͬͯ͠ςετΛ͍ͯ͘͠3. ؾ͍ͮͨ͜ͱ͕͋ΕશͯϝϞΛ͢
ϦϦʔε.git-pr-templateʹϦϦʔεखॱΛهࡌ
ΞϓϦΠϕϯτͷܭଌhttps://www.flickr.com/photos/helenanormark/9421984744
ΞϓϦΠϕϯτͷܭଌ• Google Analytics• Facebook Analytics• Fabric• Firebase→BigQuery• ࠂSDK
ෳͷSDKΛೖΕ͍ͯΔཧ༝• ֤αʔϏεʹΑͬͯݟ͑Δͷ͕ҧͬͨΓɺࢪࡦΛߟ͑ΔࡍʹݟΔࣈͷ͕֯ҧͬͨΓ͢Δ• Ұ͚͕ͭͩͣΕͨΓͯ͠ݕ͢Δ͜ͱ͕Ͱ͖Δ• αʔϏε্ॏཁͳKPIಠࣗͷཧը໘Λ࡞ͬͯΥον
εΫϦʔϯΠϕϯτͷܭଌ• ΤϯδχΞͷख࡞ۀͰ֤ActivityʹܭଌίʔυΛຒΊࠐΜͰ͍Δ• ͔ͭͯiOSͰػցతʹຒΊࠐΉ͜ͱΛ͕ͨ͠ɺूܭ݁Ռ͕ͪ͝Όͪ͝Όʹͳͬͯ͠·ͬͨ͜ͱ͕͋ͬͨ
εΫϦʔϯΠϕϯτͷॏཁੑ• ػೳΛվળͨ͠Γɺ͢ɺ͞ͳ͍ͷٞΛ͢Δͱ͖ͷࡐྉʹͳΔ• ࣈΛϕʔεʹٞΛ͢Δ
• ϑϦϧAndroid൛ͷي• ීஈͷ։ൃͷྲྀΕ• νʔϜ։ൃΛ͏·͘ճͨ͢Ίʹͬͯྑ͔ͬͨ͜ͱΞδΣϯμ
తผνʔϜԽ
~2015ࠒ·Ͱ• iOSνʔϜɺAndroidνʔϜɺServerνʔϜɺσβΠφʔͷΑ͏ͳઐ৬ͰΘ͔Ε͍ͯͨiOS Android Server♂♂♂Design♂♂♂ ♂♂♂ ♂♂♂
৽ػೳՃ͍͚ͨ͠Ͳɺ Android MରԠ͠ͳ͍ͱ…
2015ࠒ~• AνʔϜɺBνʔϜͱ͍͏తผͷνʔϜʹશͯͷ։ൃϝϯόʔ͕ॴଐ• ҕһձ੍• ΫϥΠΞϯτҕһձ• αʔόʔҕһձ• σβΠϯҕһձ• ੳҕһձ
తผνʔϜBA♂♂♂♂ ♂ ♂♂♂ ♂ ♂♂♂♂♂ ♂ ♂♂♂ ♂ ♂♂♂♂♂iOS Android Server Design
తผνʔϜԽ• KPIผͷνʔϜʹͨ͜͠ͱͰඪ͕໌֬ʹͳͬͨ• Android͚ͩͱ͔Ͱͳ͘ɺඪΛୡ͢ΔͨΊʹ෯͍εΩϧΛٻΊΒΕΔΑ͏ʹͳͬͨ
తผνʔϜԽ• ٕज़త՝ͳͲΛҕһձ͕Λ࣋ͭ͜ͱͰɺൣғ͕ΑΓ໌֬ʹ• ྫʣAndroid 6.0ରԠɺRailsΞοϓσʔτ
αϙʔτରԠͷ൪੍• CS͔Β࣭ͳͲ͕͋ͬͨΒΘ͔Δਓ͕͍͑ͯͨ• 2016ΑΓσΠϦʔͷ൪੍Λಋೖ• ຖேbot͕2໊બͼɺબΕͨਓͦͷҰαϙʔτ͔Βͷ࣭ʹ͑Δ• Θ͔Βͳ͍߹Θ͔ΔਓʹΤεΫϩʔ• ରԠ༰Ͱ͖ΔݶΓwikiʹ͢
αϙʔτରԠͷ൪੍• ࣝͷଐਓԽ͕ݮͬͨ• αʔϏε༷ͷཧղ͕ਂ·ͬͨ• ྫ͑औҾपΓͷ༷ͳͲ
ϦϦʔεϊʔτΛΤϯδχΞ͕ॻ͘
ϦϦʔεϊʔτΛΤϯδχΞ͕ॻ͘• ࣮Λ୲ͨ͠ΤϯδχΞΛத৺ͱͯ͠ϦϦʔεϊʔτΛॻ͘• ਓؒຯͷ͋Δจষ• ࠷ऴతʹϥΠςΟϯάελοϑʹϨϏϡʔͯ͠Β͏
PlayετΞͷϨϏϡʔ• AppFollowͱ͍͏αʔϏεΛಋೖ
PlayετΞͷϨϏϡʔ• Slackͷνϟϯωϧʹਵ࣌ඈΜͰ͘Δ• ωΨςΟϒͳϨϏϡʔ͕͋Ε։ൃɺCSνʔϜ͕रͬͯվળʹཱͯΔ• CSνʔϜʹϨϏϡʔʹฦ৴͢Δ୲Λஔ͍͍ͯΔ• ΞϓϦͷධՁΛྑ͍ঢ়ଶʹอͭ
·ͱΊ
͘ଓ͘ΞϓϦΛ։ൃ͠ଓ͚Δʹ• ఆظతʹΞʔΩςΫνϟΛݟ͢• نυΩϡϝϯτΛඋ͢Δ• ૾Ͱͳ͘ɺQAϢʔβʔΠϯλϏϡʔͳͲͰ࣮ࡍͷϢʔβʔͷΛฉ͖ɺαʔϏεʹө͢Δ
͝੩ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠