Share
Embed
Реанимация приложения
Никита Клещин, Delivery Club – MOSDROID #16 Sulfur
Опыт починки, фичерения, рефактора, обновления приложений с многолетней историей в условиях современного бизнеса и ограниченных ресурсов. Простыми словами — хотим поделиться теми вещами, которые помогают нам не сойти с ума, делать продукт и внедрять фичи типа Slices и In-App Update.
MOSDROID
March 30, 2019
Transcript
28 slides
-
НАЧАЛЬНЫЕ УСЛОВИЯ Прощай FoodPanda! Новые глобальные цели Android команда выросла
до 4 человек +2 приложения Новое приложение работает только c FoodPanda API Текущее приложение полгода стояло на холде Разрабатывалось на аутсорсе до 2014 года, не было рефакторинга Команда QA не выросла У команды API тоже много новых задач и новые проекты -
ПРОДУКТОВАЯ РАЗРАБОТКА • Рефактор в счет продуктовых задач • “Предсказание”
изменений • Технологии для решения проблемы • Contract First -
ЧТО ДЕЛАТЬ С КОДОМ? • Прятать внешние библиотеки за свои
интерфейсы • Бить логику “на уровни” • Приводить код к одному виду • Trade-off -
ПОЛЕЗНОСТИ ДЛЯ РАЗРАБОТКИ + Репозиторий, задачки, документация + Swagger +
Beta / HockeyApp + Jenkins / Bamboo + Artifactory + Аналитика + Firebase Performance Monitoring + Fiddler / Charles + Lynx + Hugo + Custom Crash Tracker + Debug Settings Activity -
private void openLynxActivity() { LynxConfig lynxConfig = new LynxConfig(); lynxConfig.setMaxNumberOfTracesToShow(4000)
.setFilter("WTF"); Intent lynxActivityIntent = LynxActivity .getIntent(this, lynxConfig); startActivity(lynxActivityIntent); } ... public class YourApplication extends Application { @Override public void onCreate() { super.onCreate(); new LynxShakeDetector(this).init(); } } * Lynx: https://github.com/pedrovgs/Lynx -
@DebugLog public String getName(String first, String last) { SystemClock.sleep(15); //
Don't ever really do this! return first + " " + last; } Log V/Example: ⇢ getName(first="Jake", last="Wharton") V/Example: ⇠ getName [16ms] = "Jake Wharton" * Hugo: https://github.com/JakeWharton/hugo -
Stacktrace Fatal Exception: java.lang.RuntimeException: createContext failed: 12291 at com.google.maps.api.android.lib6.gmm6.vector.ba.a(...) at
com.google.maps.api.android.lib6.gmm6.vector.bb.run(...) -
public class Application extends AbstractApplication { @Override public void onCreate()
{ super.onCreate(); registerActivityLifecycleCallbacks(new LifecycleHandler()); } ... } public class LifecycleHandler implements Application.ActivityLifecycleCallbacks { @Override public void onActivityStarted(Activity activity) { tracker().onStart(activity) Logger.i(TAG, "onStart: " + activity.getClass().getSimpleName()); } ... } * https://developer.android.com/reference/android/app/Application.ActivityLifecycleCallbacks -
Stacktrace Fatal Exception: java.lang.RuntimeException: createContext failed: 12291 at com.google.maps.api.android.lib6.gmm6.vector.ba.a(...) at
com.google.maps.api.android.lib6.gmm6.vector.bb.run(...) Log # Crashlytics - Custom logs # Platform: android # Issue: 5c5f6122f8b88c296345f87f # Session: 5C9105C2015300011623986FFABB2A46_DNE_0_v2 # Date: Tue Mar 19 2019 18:08:00 GMT+0300 (Москва, стандартное время) 0 | 6:07:49 PM | D/Event ... is registered 1 | 6:07:53 PM | D/Event SplashActivity is registered 2 | 6:07:53 PM | I/LifecycleHandler onStart: SplashActivity 3 | 6:07:53 PM | D/Event ... is registered 4 | 6:07:53 PM | D/counter amountSession: 1 5 | 6:07:54 PM | D/Event SplashActivity is unregistered 6 | 6:07:54 PM | D/Event AddressActivity is registered -
Manifest. BuildType: Debug <manifest ...> <application ...> <activity ... android:name="com.github.pedrovgs.lynx.LynxActivity"
/> <activity ... android:name=".businesslayer.activities.DebugActivity" /> <activity-alias ... android:name="DeveloperOptions" android:targetActivity=".businesslayer.activities.DebugActivity" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> </application> </manifest> -
public interface ITracker ... public class AbstractTracker implements ITracker ...
public class Tracker1 extends AbstractTracker ... ... public class TrackerN extends AbstractTracker ... public class LifecycleHandler implements Application.ActivityLifecycleCallbacks { @Override public void onActivityStarted(Activity activity) { tracker().onStart(activity) Logger.i(TAG, "onStart: " + activity.getClass().getSimpleName()); } ... } public class Tracker implements ... -
public class Tracker implements InvocationHandler { public static final String
TAG = "Tracker"; protected static final ClassLoader LOADER = ITracker.class.getClassLoader(); protected static final Class<?>[] INTERFACES = {ITracker.class}; public static ITracker newInstance(Collection<ITracker> trackers, Executor executor) { return (ITracker) Proxy.newProxyInstance(LOADER, INTERFACES, new Tracker(trackers, executor)); } ... @Override public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { mExecutor.post(() -> { for (ITracker tracker : mTrackers) { try { method.invoke(tracker, args); } catch (Throwable e) { Logger.e(TAG, e.getMessage(), e); } } }); return null; } } * InvocationHandler https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/InvocationHandler.html -
ПОЛЕЗНЫЙ ФУНКЦИОНАЛ + Неразрывный контекст + Размер приложения + Hard/Soft
update + Feature toggle + A/B Tests -
final Intent intent = activity.getIntent(); if (intent.getData() == null) {
branch.initSession(listener, activity); } else { branch.initSession(listener, intent.getData(), activity); } ... protected class BranchListener implements Branch.BranchUniversalReferralInitListener { @Override public void onInitFinished(BranchUniversalObject o, LinkProperties link, BranchError e) { DeepLink deeplink = null; if (e == null && o!= null) { if (o.getMetadata().containsKey(ANDROID_DEEPLINK_PATH)) { String link = object.getMetadata().get(ANDROID_DEEPLINK_PATH); if (!TextUtils.isEmpty(link)) { handle(DeepLinkUtil.deeplink(link)); } } } } } * Branch.io: https://docs.branch.io/apps/android/ -
GOOGLE ГОВОРИТ, ЧТО ЕСТЬ * Top 7 Takeaways for Android
App Bundles: https://www.youtube.com/watch?v=st9VZuJNIbw -
ЧТО ДЛЯ ЭТОГО ЕСТЬ? + ProGuard (R8/D8) + VectorDrawable /
Compat + VectorAnimation / Compat + AppBundle? + Modularization? -
ОБНОВЛЕНИЕ ПРИЛОЖЕНИЯ Предусматривали два вида: • Мягкое обновление • Критическое
обновление Собственный механизм • Отправлять пользователя в маркет • Другая механика во время Staged Rollouts -
УДАЛЕННОЕ УПРАВЛЕНИЕ + Загружаемые настройки + Firebase Remote Config *
Martin Fowler: Feature Toggles https://martinfowler.com/articles/feature-toggles.html -
А КАК ОБЫЧНЫЕ ТЕСТЫ? + Appium + Espresso + Caliper
(под dk.ilios:spanner) -
@RunWith(SpannerRunner.class) public class UnitTestBenchmarks { @BenchmarkConfiguration public SpannerConfig configuration =
new SpannerConfig.Builder() .saveResults(resultsDir, fileName) .useBaseline(baseLineFile) .medianFailureLimit(Float.MAX_VALUE) .addInstrument(RuntimeInstrumentConfig.defaultConfig()) .build(); @Benchmark public boolean instanceOf(int reps) { boolean result = false; for (int i = 0; i < reps; i++) {result = (testClass instanceof Object);} return result; } @Benchmark public boolean directComparison(int reps) { boolean result = false; for (int i = 0; i < reps; i++) {result = testClass == Object.class;} return result; } } * dk.ilios:spanner - https://github.com/cmelchior/spanner -
ДЕЛАЙТЕ ПРОДУКТ, А НЕ ЗАДАЧИ - В таком режиме мы
сделали свыше 20 релизов, не считая хотфиксов и псевдо хотфиксов - Подтянули ANR-free и Crash-free почти к 100% - На один релиз приходилось, в среднем, по 23 задачи, из которых большая часть - про продукт - При этом наша аудитория растет