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

834 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<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`
```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
<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()`
```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<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.
```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:** 80ms250ms. Ż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<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.
```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<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
```kotlin
// Ź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
```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" }
```