Files
Retro_HA/OPTIMIZATION_PLAN.md
Krzysztof Cieślik 22a3e0fe7e Initial commit
2026-06-13 21:43:53 +02:00

28 KiB
Raw Permalink Blame History

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
  2. System designu — Bauhaus / Swiss Grid
  3. Konfiguracja buildu
  4. Rendering — Custom View
  5. GridView — lista widgetów
  6. Ikony — Canvas drawing
  7. StaticLayout — pre-obliczony tekst
  8. Animacje — GPU only
  9. Sieć — OkHttp + Retrofit
  10. JSON — TypeAdaptery zamiast refleksji
  11. Wątki — HandlerThread
  12. Pamięć — LruCache + SparseArray
  13. Inicjalizacja przy starcie
  14. Języki
  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

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

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ń

// Ź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:

<FrameLayout android:padding="4dp">

    <!-- Cień: ten sam rozmiar co karta, przesunięty o 4dp -->
    <View
        android:background="#000000"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="4dp"
        android:layout_marginTop="4dp"/>

    <!-- Karta na wierzchu -->
    <com.example.myapplication.ui.WidgetCardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</FrameLayout>

3. Konfiguracja buildu

app/build.gradle.kts

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

<style name="AppTheme" parent="@android:style/Theme.Holo">
    <item name="android:windowBackground">@android:color/black</item>
    <item name="android:colorBackground">@android:color/black</item>
</style>

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()
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).

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

class WidgetAdapter(
    private val context: Context,
    private var items: List<WidgetConfig>
) : 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<WidgetConfig>) {
        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.

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.

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ść.

// 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: 80ms250ms. Żadnych sprężyn, odbić, elastyczności.


9. Sieć — OkHttp + Retrofit

Jeden singleton na całą aplikację — ConnectionPool jest drogi w tworzeniu.

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.

class HaEntityTypeAdapter : TypeAdapter<HaEntity>() {
    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.

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

object EntityStateCache {
    private val cache = LruCache<String, EntityState>(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

// ŹLE — autoboxing Int→Integer przy każdym get/put
val viewTypes = HashMap<Int, Int>()

// DOBRZE — zero alokacji, zoptymalizowane dla kluczy int
val viewTypes = SparseArray<Int>()

onTrimMemory — oddaj pamięć gdy system prosi

class MainActivity : Activity() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
            EntityStateCache.evictAll()
        }
    }
}

WeakReference w callbackach sieciowych

// Ź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

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

# 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" }