28 KiB
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
- Stack technologiczny
- System designu — Bauhaus / Swiss Grid
- Konfiguracja buildu
- Rendering — Custom View
- GridView — lista widgetów
- Ikony — Canvas drawing
- StaticLayout — pre-obliczony tekst
- Animacje — GPU only
- Sieć — OkHttp + Retrofit
- JSON — TypeAdaptery zamiast refleksji
- Wątki — HandlerThread
- Pamięć — LruCache + SparseArray
- Inicjalizacja przy starcie
- Języki
- 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 wonDrawFonts.REGULAR/Fonts.BOLDprzypisane raz przy inicjalizacji obiektu PaintdrawRectzamiastdrawRoundRect— ostre rogi, szybsze- Stała wysokość karty w
onMeasure— eliminuje kosztowne obliczeniawrap_content - Settery z
if (field == value) return— bez zbędnychinvalidate()
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ć:
ConstraintLayoutw itemach gridu — skomplikowany algorytm pomiaruCardView— wymaga AppCompat, ma elevation- Zagnieżdżenie ViewGroup > 2 poziomów
wrap_contentna 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) returnw każdym setterze przedinvalidate()- Guard
if (width == 0) returnwrebuildLayouts()
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: 80ms–250ms. Ż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():
postDelayeddla retry z exponential backoffremoveCallbacksdo 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" }