Initial commit
This commit is contained in:
833
OPTIMIZATION_PLAN.md
Normal file
833
OPTIMIZATION_PLAN.md
Normal file
@@ -0,0 +1,833 @@
|
||||
# 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:** 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<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" }
|
||||
```
|
||||
Reference in New Issue
Block a user