# Plan optymalizacji — MyApplication (HA Widget Panel) Cel: maksymalna wydajność na AllWinner A13 (single-core 1.2GHz, 512MB RAM, Android 4.0, API 14). --- ## Spis treści 1. [Stack technologiczny](#1-stack-technologiczny) 2. [System designu — Bauhaus / Swiss Grid](#2-system-designu--bauhaus--swiss-grid) 3. [Konfiguracja buildu](#3-konfiguracja-buildu) 4. [Rendering — Custom View](#4-rendering--custom-view) 5. [GridView — lista widgetów](#5-gridview--lista-widgetów) 6. [Ikony — Canvas drawing](#6-ikony--canvas-drawing) 7. [StaticLayout — pre-obliczony tekst](#7-staticlayout--pre-obliczony-tekst) 8. [Animacje — GPU only](#8-animacje--gpu-only) 9. [Sieć — OkHttp + Retrofit](#9-sieć--okhttp--retrofit) 10. [JSON — TypeAdaptery zamiast refleksji](#10-json--typeadaptery-zamiast-refleksji) 11. [Wątki — HandlerThread](#11-wątki--handlerthread) 12. [Pamięć — LruCache + SparseArray](#12-pamięć--lrucache--sparsearray) 13. [Inicjalizacja przy starcie](#13-inicjalizacja-przy-starcie) 14. [Języki](#14-języki) 15. [Priorytety implementacji](#15-priorytety-implementacji) --- ## 1. Stack technologiczny | Element | Wybór | Powód | |---|---|---| | Język | **Kotlin** | Identyczny bytecode co Java — zero różnicy runtime | | UI framework | **Natywne Android API 14** | Zero overhead bibliotek, pełna kontrola | | Tema | **Theme.Holo** | Natywna, zero zależności, dostępna od API 11 | | Lista widgetów | **android.widget.GridView** | Natywna, wbudowane recycling widoków, brak Jetpack | | Sieć | **OkHttp 3.12.x + Retrofit 2.6.4** | Ostatnie wersje wspierające API 14 | | JSON | **Gson + TypeAdaptery** | Streaming parsing bez refleksji | | **ZABRONIONE** | AppCompat, Material Design, RecyclerView, Jetpack, Compose, CardView | Zbędny overhead / wymaga API 21+ | --- ## 2. System designu — Bauhaus / Swiss Grid ### Paleta kolorów ```kotlin object Colors { const val WHITE = 0xFFFFFFFF.toInt() // tło, tekst na ciemnym const val BLACK = 0xFF000000.toInt() // tekst, obramowania, cień const val RED = 0xFFE23A24.toInt() // błędy, alarmy, akcent krytyczny const val YELLOW = 0xFFFAD02C.toInt() // aktywny stan, akcent główny const val BLUE = 0xFF0056B3.toInt() // info, przyciski, akcent drugorzędny const val GRAY = 0xFF888888.toInt() // wyłączone, disabled } ``` Żadnych innych kolorów. Żadnych gradientów. Żadnego alpha blendingu poza animacjami opacity. ### Typografia — system monospace ```kotlin object Fonts { // Wbudowany w każde urządzenie z API 14 — zero overhead, zero KB w APK val REGULAR: Typeface = Typeface.create("monospace", Typeface.NORMAL) val BOLD: Typeface = Typeface.create("monospace", Typeface.BOLD) } ``` Każdy `TextPaint` używa `Fonts.REGULAR` lub `Fonts.BOLD`. Żadnych innych krojów. ### Skala typograficzna ``` Rola Rozmiar Waga Zastosowanie ──────────────────────────────────────────────────── value 20sp BOLD główna wartość encji (21.5°C, ON, 58%) label 12sp NORMAL nazwa encji secondary 10sp NORMAL jednostka, czas ostatniej aktualizacji ``` ### Siatka spacingu — tylko wielokrotności 4dp ``` Padding wewnętrzny karty: 8dp Odstęp między kartami: 4dp Padding ekranu: 8dp Grubość borderu: 1dp lub 2dp Szerokość lewego paska: 4dp ``` ### Geometria — ZERO zaokrągleń ```kotlin // ŹLE — zaokrąglone rogi są ZABRONIONE canvas.drawRoundRect(rect, 8f, 8f, paint) // DOBRZE — ostre rogi wszędzie canvas.drawRect(rect, paint) ``` `android:radius` i `corners` w drawable — ZABRONIONE. ### Hard shadow — technika Zamiast `elevation` — przesunięty czarny `View`: ```xml ``` --- ## 3. Konfiguracja buildu ### `app/build.gradle.kts` ```kotlin android { defaultConfig { minSdk = 14 // Multidex WYŁĄCZONY — bez AppCompat/Material szacowane ~25K metod, poniżej limitu 65K resourceConfigurations += listOf("pl", "en") } buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } // AllWinner A13 = ARM Cortex-A8 = armeabi-v7a // Wyklucza x86/arm64 → mniejszy APK, szybsza instalacja splits { abi { isEnable = true reset() include("armeabi-v7a") isUniversalApk = false } } } dependencies { // Sieć — OK, nie są to UI wrappery implementation(libs.okhttp) implementation(libs.okhttp.logging) implementation(libs.retrofit) implementation(libs.retrofit.gson) implementation(libs.gson) implementation(libs.mpandroidchart) // USUNIĘTE: AppCompat, Material, core-ktx, multidex, RecyclerView } ``` ### `res/values/themes.xml` ```xml ``` ### Dlaczego odpada multidex ``` AppCompat usunięty: -30 000 metod Material usunięty: -15 000 metod core-ktx usunięte: - 5 000 metod ────────────────────────────────────── Szacowany total: ~25 000 metod ← poniżej limitu 65K ``` --- ## 4. Rendering — Custom View **Zasady:** - `Paint` / `TextPaint` / `Rect` / `RectF` — pola klasy, nigdy zmienne lokalne w `onDraw` - `Fonts.REGULAR` / `Fonts.BOLD` przypisane raz przy inicjalizacji obiektu Paint - `drawRect` zamiast `drawRoundRect` — ostre rogi, szybsze - Stała wysokość karty w `onMeasure` — eliminuje kosztowne obliczenia `wrap_content` - Settery z `if (field == value) return` — bez zbędnych `invalidate()` ```kotlin class WidgetCardView(context: Context) : View(context) { private val dp = resources.displayMetrics.density private val paintBg = Paint(Paint.ANTI_ALIAS_FLAG) private val paintBorder = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = 2f * dp color = Colors.BLACK } private val paintLabel = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Fonts.REGULAR textSize = 12f * dp color = Colors.WHITE } private val paintValue = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Fonts.BOLD textSize = 20f * dp color = Colors.WHITE } private val paintSecondary = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { typeface = Fonts.REGULAR textSize = 10f * dp color = Colors.GRAY } private val paintStripe = Paint(Paint.ANTI_ALIAS_FLAG) private val rectBg = Rect() private val rectStripe = Rect() private var labelLayout: StaticLayout? = null private var valueLayout: StaticLayout? = null private var secondaryLayout: StaticLayout? = null var label: String = "" set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() } var value: String = "" set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() } var secondaryText: String = "" set(v) { if (field == v) return; field = v; rebuildLayouts(); invalidate() } var isActive: Boolean = false set(v) { if (field == v) return; field = v; invalidate() } var stripeColor: Int = Colors.BLUE set(v) { if (field == v) return; field = v; invalidate() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { val stripeW = (4f * dp).toInt() rectBg.set(0, 0, w, h) rectStripe.set(0, 0, stripeW, h) rebuildLayouts() } private fun rebuildLayouts() { if (width == 0) return val contentW = width - rectStripe.width() - (8 * dp).toInt() - (4 * dp).toInt() labelLayout = buildLayout(label, paintLabel, contentW) valueLayout = buildLayout(value, paintValue, contentW) secondaryLayout = buildLayout(secondaryText, paintSecondary, contentW) } private fun buildLayout(text: String, paint: TextPaint, w: Int): StaticLayout = StaticLayout(text, paint, w, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false) override fun onDraw(canvas: Canvas) { val dp8 = 8f * dp val dp4 = 4f * dp // Tło karty paintBg.color = if (isActive) Colors.YELLOW else Colors.BLACK canvas.drawRect(rectBg, paintBg) // Pionowy pasek koloru (domena encji) paintStripe.color = stripeColor canvas.drawRect(rectStripe, paintStripe) // Obramowanie canvas.drawRect(rectBg, paintBorder) // Kolor tekstu zależy od tła val textColor = if (isActive) Colors.BLACK else Colors.WHITE paintLabel.color = textColor paintValue.color = textColor val textX = rectStripe.width() + dp8 canvas.save() canvas.translate(textX, dp8) labelLayout?.draw(canvas) canvas.restore() val labelH = labelLayout?.height?.toFloat() ?: 0f canvas.save() canvas.translate(textX, dp8 + labelH + dp4) valueLayout?.draw(canvas) canvas.restore() val valueH = valueLayout?.height?.toFloat() ?: 0f canvas.save() canvas.translate(textX, dp8 + labelH + dp4 + valueH + dp4) secondaryLayout?.draw(canvas) canvas.restore() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val w = MeasureSpec.getSize(widthMeasureSpec) val h = when { resources.configuration.screenHeightDp < 480 -> (72 * dp).toInt() else -> (88 * dp).toInt() } setMeasuredDimension(w, h) } } ``` **Czego unikać:** - `ConstraintLayout` w itemach gridu — skomplikowany algorytm pomiaru - `CardView` — wymaga AppCompat, ma elevation - Zagnieżdżenie ViewGroup > 2 poziomów - `wrap_content` na wysokości itemów gridu --- ## 5. GridView — lista widgetów `android.widget.GridView` — natywny od API 1, zero zależności, wbudowane recycling widoków. Zastępuje `RecyclerView` (Jetpack — zabroniony). ```kotlin val gridView = GridView(this).apply { numColumns = when { resources.configuration.screenWidthDp >= 840 -> 4 // tablet landscape resources.configuration.screenWidthDp >= 600 -> 3 // tablet portrait else -> 2 // telefon } val dp4 = (4 * resources.displayMetrics.density).toInt() val dp8 = (8 * resources.displayMetrics.density).toInt() horizontalSpacing = dp4 verticalSpacing = dp4 setPadding(dp8, dp8, dp8, dp8) stretchMode = GridView.STRETCH_COLUMN_WIDTH adapter = widgetAdapter } ``` ### BaseAdapter ```kotlin class WidgetAdapter( private val context: Context, private var items: List ) : BaseAdapter() { override fun getCount() = items.size override fun getItem(pos: Int) = items[pos] override fun getItemId(pos: Int) = items[pos].entityId.hashCode().toLong() override fun hasStableIds() = true // GridView może reużywać widoki override fun getView(pos: Int, convertView: View?, parent: ViewGroup): View { // convertView = recycled view — ZAWSZE reużywaj jeśli istnieje val card = (convertView as? WidgetCardView) ?: WidgetCardView(context) val item = items[pos] card.label = item.friendlyName card.value = item.state card.secondaryText = item.lastUpdated card.isActive = item.isOn card.stripeColor = domainColor(item.domain) return card } fun update(newItems: List) { items = newItems notifyDataSetChanged() } // Bezpośrednia aktualizacja stanu encji bez przebudowy adaptera fun updateState(entityId: String, newState: String, isOn: Boolean) { val idx = items.indexOfFirst { it.entityId == entityId } if (idx == -1) return items = items.toMutableList().also { it[idx] = it[idx].copy(state = newState, isOn = isOn) } // Aktualizuj tylko widoczną kartę jeśli jest na ekranie val card = gridView?.getChildAt(idx) as? WidgetCardView if (card != null) { card.value = newState card.isActive = isOn } else { notifyDataSetChanged() } } private fun domainColor(domain: String) = when (domain) { "light" -> Colors.YELLOW "switch" -> Colors.BLUE "sensor" -> Colors.BLUE "binary_sensor" -> Colors.BLUE "script", "scene" -> Colors.RED else -> Colors.GRAY } } ``` **Uwaga:** `GridView` nie ma `DiffUtil` (Jetpack). Dla małej listy widgetów (< 30 pozycji) `notifyDataSetChanged()` przy zmianie konfiguracji jest akceptowalne. Przy aktualizacji samego stanu encji — bezpośrednia modyfikacja `WidgetCardView` bez przebudowy adaptera. --- ## 6. Ikony — Canvas drawing Zakaz bitmap (PNG/JPG). Ikony rysowane bezpośrednio na `Canvas` jako kształty geometryczne. Styl Bauhaus: proste figury, jeden kolor, zero dekoracji. ```kotlin object HaIcons { // Paint jako pole — zero alokacji przy każdym draw() private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = 2f } private val tmpRect = RectF() fun draw(canvas: Canvas, domain: String, x: Float, y: Float, size: Float, color: Int) { fillPaint.color = color strokePaint.color = color when (domain) { "light" -> drawLight(canvas, x, y, size) "switch" -> drawSwitch(canvas, x, y, size) "sensor" -> drawSensor(canvas, x, y, size) "binary_sensor" -> drawBinarySensor(canvas, x, y, size) "script", "automation" -> drawScript(canvas, x, y, size) else -> drawGeneric(canvas, x, y, size) } } private fun drawLight(canvas: Canvas, x: Float, y: Float, s: Float) { // Kwadrat + promienie — geometryczna "lampa" tmpRect.set(x + s*0.25f, y + s*0.25f, x + s*0.75f, y + s*0.75f) canvas.drawRect(tmpRect, fillPaint) canvas.drawLine(x + s*0.5f, y, x + s*0.5f, y + s*0.2f, strokePaint) canvas.drawLine(x + s*0.5f, y + s*0.8f, x + s*0.5f, y + s, strokePaint) canvas.drawLine(x, y + s*0.5f, x + s*0.2f, y + s*0.5f, strokePaint) canvas.drawLine(x + s*0.8f, y + s*0.5f, x + s, y + s*0.5f, strokePaint) } private fun drawSwitch(canvas: Canvas, x: Float, y: Float, s: Float) { // Prostokąt poziomy + blok po prawej tmpRect.set(x, y + s*0.35f, x + s, y + s*0.65f) canvas.drawRect(tmpRect, fillPaint) tmpRect.set(x + s*0.55f, y + s*0.15f, x + s*0.95f, y + s*0.85f) canvas.drawRect(tmpRect, fillPaint) } private fun drawSensor(canvas: Canvas, x: Float, y: Float, s: Float) { // Krzyż — pomiar tmpRect.set(x + s*0.4f, y, x + s*0.6f, y + s) canvas.drawRect(tmpRect, fillPaint) tmpRect.set(x, y + s*0.4f, x + s, y + s*0.6f) canvas.drawRect(tmpRect, fillPaint) } private fun drawBinarySensor(canvas: Canvas, x: Float, y: Float, s: Float) { // Kwadrat w kwadracie tmpRect.set(x + s*0.1f, y + s*0.1f, x + s*0.9f, y + s*0.9f) canvas.drawRect(tmpRect, strokePaint) tmpRect.set(x + s*0.3f, y + s*0.3f, x + s*0.7f, y + s*0.7f) canvas.drawRect(tmpRect, fillPaint) } private fun drawScript(canvas: Canvas, x: Float, y: Float, s: Float) { // Trzy poziome paski — lista komend val h = s * 0.12f val gap = s * 0.25f for (i in 0..2) { tmpRect.set(x, y + gap * i + gap * 0.5f, x + s, y + gap * i + gap * 0.5f + h) canvas.drawRect(tmpRect, fillPaint) } } private fun drawGeneric(canvas: Canvas, x: Float, y: Float, s: Float) { tmpRect.set(x + s*0.15f, y + s*0.15f, x + s*0.85f, y + s*0.85f) canvas.drawRect(tmpRect, fillPaint) } } ``` Ponieważ ikony są prostymi operacjami Canvas (kilka `drawRect` / `drawLine`), rasteryzacja jest natychmiastowa — nie potrzeba cache'owania bitmap. --- ## 7. StaticLayout — pre-obliczony tekst `canvas.drawText()` mierzy tekst przy każdym `onDraw`. `StaticLayout` robi to raz. ```kotlin private fun buildLayout(text: String, paint: TextPaint, widthPx: Int): StaticLayout = StaticLayout(text, paint, widthPx, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false) // W onDraw — zero mierzenia: canvas.save() canvas.translate(x, y) layout.draw(canvas) canvas.restore() ``` **Reguły:** - Przebuduj tylko gdy zmienia się tekst **lub** `onSizeChanged` - `if (field == value) return` w każdym setterze przed `invalidate()` - Guard `if (width == 0) return` w `rebuildLayouts()` --- ## 8. Animacje — GPU only Tylko właściwości animowane przez GPU via `ViewPropertyAnimator`. Zero animacji zmieniających layout — `width`/`height`/`margin`/`padding` wywołują `requestLayout()` co zabija wydajność. ```kotlin // Wysunięcie panelu konfiguracji z dołu fun revealPanel(panel: View) { panel.visibility = View.VISIBLE panel.translationY = panel.height.toFloat() panel.animate() .translationY(0f) .alpha(1f) .setDuration(200) .setInterpolator(DecelerateInterpolator()) .start() } // Chowanie panelu fun hidePanel(panel: View) { panel.animate() .translationY(panel.height.toFloat()) .alpha(0f) .setDuration(150) .setInterpolator(LinearInterpolator()) .withEndAction { panel.visibility = View.GONE } .start() } // Feedback dotknięcia karty fun tapFeedback(card: View) { card.animate() .alpha(0.4f) .setDuration(80) .setInterpolator(LinearInterpolator()) .withEndAction { card.animate().alpha(1f).setDuration(120).start() } .start() } ``` ### Tabela dozwolonych właściwości ``` Właściwość GPU Zastosowanie ────────────────────────────────────────────────── translationX/Y ✓ przesuwanie paneli, drawer scaleX/Y ✓ powiększenie aktywnego widgetu alpha ✓ fade, feedback dotknięcia rotation ✓ obrót ikony ładowania width/height ✗ requestLayout() — ZABRONIONE margin/padding ✗ requestLayout() — ZABRONIONE layout_weight ✗ requestLayout() — ZABRONIONE backgroundColor ✗ software render — zmień kolor przez Paint w onDraw ``` **Interpolatory:** tylko `LinearInterpolator` lub `DecelerateInterpolator`. **Czas trwania:** 80ms–250ms. Żadnych sprężyn, odbić, elastyczności. --- ## 9. Sieć — OkHttp + Retrofit Jeden singleton na całą aplikację — `ConnectionPool` jest drogi w tworzeniu. ```kotlin object NetworkModule { val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectionPool(ConnectionPool(3, 30, TimeUnit.SECONDS)) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .cache(Cache(File(context.cacheDir, "ha_http"), 2L * 1024 * 1024)) .addInterceptor(AuthInterceptor(token)) .build() } val retrofit: Retrofit by lazy { Retrofit.Builder() .baseUrl(baseUrl) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(buildGson())) .build() } } ``` --- ## 10. JSON — TypeAdaptery zamiast refleksji Gson domyślnie używa refleksji — na A13 odczuwalne przy każdym parsowaniu. TypeAdapter = ręczne parsowanie strumieniowe, 3-5x szybsze. ```kotlin class HaEntityTypeAdapter : TypeAdapter() { override fun read(reader: JsonReader): HaEntity { var entityId = ""; var state = ""; var friendlyName = "" reader.beginObject() while (reader.hasNext()) { when (reader.nextName()) { "entity_id" -> entityId = reader.nextString() "state" -> state = reader.nextString() "attributes" -> { reader.beginObject() while (reader.hasNext()) { when (reader.nextName()) { "friendly_name" -> friendlyName = reader.nextString() else -> reader.skipValue() } } reader.endObject() } else -> reader.skipValue() // nie parsuj niepotrzebnych pól } } reader.endObject() return HaEntity(entityId, state, friendlyName) } override fun write(out: JsonWriter, value: HaEntity?) {} } ``` --- ## 11. Wątki — HandlerThread Na single-core nie twórz wielu wątków — context switching jest kosztowny. Jeden `HandlerThread` na całą komunikację z HA. ```kotlin object HaWorker { private val thread = HandlerThread("ha-worker").also { it.start() } val handler = Handler(thread.looper) val mainThread = Handler(Looper.getMainLooper()) fun post(block: () -> Unit) = handler.post(block) fun postDelayed(ms: Long, block: () -> Unit) = handler.postDelayed(block, ms) fun cancelAll() = handler.removeCallbacksAndMessages(null) } ``` **Dlaczego `HandlerThread` zamiast `Executors.newSingleThreadExecutor()`:** - `postDelayed` dla retry z exponential backoff - `removeCallbacks` do anulowania zakolejkowanych zadań bez dodatkowej synchronizacji - Wbudowana kolejka z priorytetem --- ## 12. Pamięć — LruCache + SparseArray ### Cache stanów encji ```kotlin object EntityStateCache { private val cache = LruCache(50) fun get(id: String) = cache.get(id) fun put(id: String, s: EntityState) = cache.put(id, s) fun evictAll() = cache.evictAll() } ``` ### SparseArray zamiast HashMap dla kluczy Int ```kotlin // ŹLE — autoboxing Int→Integer przy każdym get/put val viewTypes = HashMap() // DOBRZE — zero alokacji, zoptymalizowane dla kluczy int val viewTypes = SparseArray() ``` ### onTrimMemory — oddaj pamięć gdy system prosi ```kotlin class MainActivity : Activity() { override fun onTrimMemory(level: Int) { super.onTrimMemory(level) if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) { EntityStateCache.evictAll() } } } ``` ### WeakReference w callbackach sieciowych ```kotlin // ŹLE — anonimowa klasa trzyma referencję do Activity client.newCall(req).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { textView.text = "..." // wyciek Activity po rotacji! } }) // DOBRZE class StateCallback(activity: MainActivity) : Callback { private val ref = WeakReference(activity) override fun onResponse(call: Call, response: Response) { ref.get()?.runOnUiThread { /* aktualizuj UI */ } } override fun onFailure(call: Call, e: IOException) {} } ``` --- ## 13. Inicjalizacja przy starcie ```kotlin class App : Application() { override fun onCreate() { super.onCreate() // Pierwsze połączenie w tle — tworzy connection pool przed pierwszym UI request HaWorker.post { warmupNetwork() } } private fun warmupNetwork() { try { NetworkModule.okHttpClient .newCall(pingRequest()) .execute() .close() } catch (_: IOException) {} } } ``` ### Co kiedy jest przeliczane ``` Zdarzenie Co się dzieje ────────────────────────────────────────────────────────────────── Start aplikacji warmup connection pool w tle onSizeChanged karty rebuild StaticLayout (1x na widget) onDraw drawRect + HaIcons.draw + layout.draw → ZERO alokacji, ZERO parsowania Zmiana stanu encji bezpośredni setter na WidgetCardView → tylko ta karta wywołuje invalidate() onTrimMemory (MODERATE+) EntityStateCache.evictAll() → RAM zwolniony Obrócenie ekranu stany encji z EntityStateCache (bez sieci) ``` --- ## 14. Języki ``` res/values/strings.xml angielski (default) res/values-pl/strings.xml polski ``` `resourceConfigurations += listOf("pl", "en")` wycina pozostałe tłumaczenia z bibliotek narzędziowych (~200KB oszczędności w APK). --- ## 15. Priorytety implementacji ``` Priorytet Optymalizacja Wpływ ────────────────────────────────────────────────────────────── KRYTYCZNY Brak AppCompat/Material → Theme.Holo RAM / metody Custom View — drawRect, brak zaokrągleń rendering Ikony jako Canvas draw (brak bitmap) rendering / RAM Jeden OkHttpClient singleton RAM DUŻY StaticLayout (pre-obliczony tekst) rendering TypeAdaptery Gson CPU / parsing HandlerThread (1 wątek tła) CPU GridView z BaseAdapter + hasStableIds rendering ŚREDNI EntityStateCache (LruCache) sieć / RAM SparseArray zamiast HashMap RAM onTrimMemory → czyszczenie cache RAM Animacje tylko GPU (ViewPropertyAnimator) jank Bezpośredni setter na kartę (bez adapter) rendering MAŁY ABI splits (armeabi-v7a only) APK size isShrinkResources = true APK size resourceConfigurations (pl + en) APK size WeakReference w callbackach memory leaks ``` --- ## Zależności po zmianach ```toml # gradle/libs.versions.toml # USUNIĘTO: AppCompat, Material, core-ktx, multidex [versions] okhttp = "3.12.13" retrofit = "2.6.4" gson = "2.10.1" mpandroidchart = "v3.1.0" [libraries] okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } mpandroidchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "mpandroidchart" } ```