onli blogging - Code
https://www.onli-blogging.de/
deSerendipity 2.5.0 - http://www.s9y.org/Thu, 01 Jan 1970 00:00:00 GMThttps://www.onli-blogging.de/templates/2k11/img/s9y_banner_small.pngRSS: onli blogging - Code -
https://www.onli-blogging.de/
10021Wie und warum ich Shariff modernisierte (Module, jQuery, Buildpipeline)
https://www.onli-blogging.de/2513/Wie-und-warum-ich-Shariff-modernisierte-Module,-jQuery,-Buildpipeline.html
Codehttps://www.onli-blogging.de/2513/Wie-und-warum-ich-Shariff-modernisierte-Module,-jQuery,-Buildpipeline.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=25136https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2513[email protected] (onli)
<p>Ich habe <a href="https://github.com/heiseonline/shariff">Shariff</a> umgebaut. <a href="https://github.com/onli/shariff-plus">Mein Fork</a> (<a href="https://github.com/richard67/shariff-plus">eines Forks</a>) nutzt ECMAScript statt CommonJS Module, verzichtet auf jQuery und reduziert die Buidpipeline massiv, sodass von <code>npm install</code> nun 253 statt 1307 Abhängigkeiten installiert werden – von denen noch dazu über 200 nur für die Tests notwendig sind. Der eingesandte PR ist durch die Verkürzung der Abhängigkeitsliste deutlich rot, +5.576 stehen gegen −36.613 Codezeilen, auch bei der Kernlogik wurde nichts aufgebläht. Einschränkungen für die Nutzer sollte der Umbau keine haben.</p>
<h4>Moment, worum geht es?
</h4>
<p>Shariff ist ein Javascriptprojekt, das vor einigen Jahren der Heiseverlag gestartet hatte. Es erstellt Buttons zum Teilen von URLs in sozialen Netzwerken so, dass nicht schon durch den Einbau der Buttons Leser von den Netzwerken verfolgt werden können. Mehrwert zu einfachen HTML-Buttons mit dem gleichen Vorteil sind die Zähler, die bei manchen der Netzwerken angezeigt werden können (und damals noch öfter konnten), die anzeigen wie oft ein Artikel bereits geteilt wurde.
</p>
<p>Ein cooles Projekt, das damals international Aufmerksamkeit bekam. Leider schlief im Laufe der Zeit die Entwicklung etwas ein. Das dürfte mehrere Gründe haben, pure Spekulation: Die Buttons werden von Lesern sehr selten genutzt, Entwicklungszeit hier rechnet sich kaum; Netzwerke haben nach und nach das Abrufen der Zählerstandes immer öfter unmöglich gemacht und so den Mehrwert Shariffs zu simpleren Ansätzen reduziert; und Heise nutzt Shariff (soweit ich sehen konnte) nicht mehr auf den eigenen Webseiten.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/shariff_release.webp'><!-- s9ymdb:2194 --><img class="serendipity_image_center" width="800" height="433" srcset="https://www.onli-blogging.de/uploads/shariff_release.1200W.serendipityThumb.webp 2600w,https://www.onli-blogging.de/uploads/shariff_release.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/shariff_release.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/shariff_release.serendipityThumb.webp" loading="lazy" alt=""></a>
</p>
<p><a href="https://www.heise.de/hintergrund/Ein-Shariff-fuer-mehr-Datenschutz-2467514.html">Die Ankündigung</a> von Shariff ist auf 2014 datiert. Technisch wurde das Projekt seitdem durchaus noch überarbeitet, aber es wurden nicht die Möglichkeiten genutzt, die 2025 solchen Projekten bietet: Durch die Weiterentwicklung der Browser sind viele Hilfsmittel von damals unnötig. Entfernt man sie, kann man die Buildpipelines ebenfalls entfernen oder reduzieren. Und kommt so im Idealfall wieder zu Javascriptprojekten, die direkt im Browser ausgeführt werden können, ohne die Entwicklung durch einen Buildschritt zu verkomplizieren.
</p>
<p>Das ist allerdings keine überall geteilte Meinung. Nicht alle Entwickler teilen das Ziel der Simplifizierung oder werten ihre Vorteile gleich hoch, oft wird auch über Frameworks wie React oder durch die Nutzung von Typescript statt Javascript eine Buildpipeline notwendig. Shariff aber nutzte weder Typescript noch solche Frameworks, war daher in meinen Augen ein guter Kandidat für eine Abstraktion vermeidende Überarbeitung.</p>
<div class="serendipity_imageComment_img"><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/serendipity_event_social.png'><!-- s9ymdb:1247 --><img class="serendipity_image_center" width="722" height="63" srcset="https://www.onli-blogging.de/uploads/serendipity_event_social.png 722w" src="https://www.onli-blogging.de/uploads/serendipity_event_social.serendipityThumb.png" loading="lazy" alt=""></a>
</div>
<figcaption class="serendipity_imageComment_txt">Diese Buttons kann Shariff erstellen.</figcaption><p>Ich war an Shariff interessiert, weil es für <a href="https://spartacus.s9y.org/index.php?mode=bygroups_event_en#serendipity_event_social">Serendipitys Socialplugin</a> benutzt wurde. Anlass in die Entwicklung zu schauen war der Versuch, einen Mastodonbutton hinzufügen. Dabei stolperte ich über den aktiven Fork <a href="https://github.com/richard67/shariff-plus">shariff-plus</a> von <a href="https://www.richard-fath.de/de/">Richard Fath</a>. Dafür begann ich die Vereinfachung. Zwischenzeitlich <a href="https://www.onli-blogging.de/2481/Socialplugin-1.0-Die-Variante-ohne-Shariff,-oder-neue-Sharebuttons-fuer-Serendipity.html">entfernte</a> ich Shariff zwar aus dem Socialplugin, aber die Modernisierung stellte ich kürzlich trotzdem fertig. Zu beachten ist, dass ich bei modernem Javascript durchaus Lücken habe, ich könnte also Details übersehen oder unnötig verkompliziert haben.</p>
<h4>jQuery
</h4>
<p>Das Helferskript jQuery zu entfernen war gleichzeitig die einfachste Operation, aber ist auch die, über die am wahrscheinlichsten Bugs verursacht wurden. Einige fand ich bereits und das sollten die gröbsten gewesen sein, aber weitere kann ich nicht ganz ausschließen.
</p>
<p>Mit jQuery werden manche Operationen einfacher, z.B. das Hinzufügen von Klassen zu einem DOM-Element. Früher war der große Vorteil auch das Überbügeln von Browserunterschieden, aber das ist heute eigentlich kein Thema mehr. Für ein Skript wie Shariff ist die jQuery-Abhängigkeit doof, weil wegen ihr die Webseitenbetreiber neben Shariff auch noch jQuery einbinden müssen, was die Webseitengröße erhöht.
</p>
<p>Zum Entfernen musste die <strong>shariff.js</strong> überarbeitet werden, die aber nur knapp 300 Zeilen lang ist. In ihr gab es Codestellen wie diese:</p>
<pre class="code">
var canonical = $('link[rel=canonical]').attr('href')</pre>
<p>Das wurde dann jeweils mit Browsermethoden ersetzt, hier:</p>
<pre class="code">var canonical = document.querySelector('link[rel=canonical]')?.href</pre>
<p>Davon waren manche Codeänderungen kniffliger, aber jQuery kann nichts was Browser nicht auch können, daher war alles mindestens theoretisch machbar.</p>
<h4>Module
</h4>
<p>Bei den Modulen war die Änderung weniger mit Codelogik verbunden. Denn die vorher genutzten CommonJS-Module (CJS) und die neuen <abbr title="ECMAScript Modules">ESMs</abbr> sind sich sehr ähnlich. Der Hauptunterschied ist, dass die ersteren von Browsern nicht verstanden werden. Um sie dann in ihnen zu nutzen müssen sie umgewandelt werden. Shariff nutzte CJS für die Funktionalität der Buttons, jeder Dienst war (und ist) sein eigenes Modul.
</p>
<p>Vorher gab es eine <strong>services/index.js</strong>, die so aussah:</p>
<pre class="code">module.exports = {
buffer: require('./buffer'),
…
}</pre>
<p>Daraus wurde:</p>
<pre class="code">export {default as buffer} from './buffer.js'
…</pre>
<p>Bei den Modulen selbst änderte sich ihre Definition, von</p>
<pre class="code">'use strict'
module.exports = function (shariff) {
…
}</pre>
<p>zu</p>
<pre class="code">'use strict'
export default function data(shariff) {
…
}</pre>
<p>Der Import der Module in der <strong>shariff.js</strong> änderte sich ebenfalls, aus</p>
<pre class="code">const services = require('./services')</pre>
<p>wurde</p>
<pre class="code">import * as services from './services/index.js';</pre>
<p>Das sind alles syntaktisch simple Änderungen, die man mechanisch und ohne tieferes Verständnis ausführen konnte. Problematisch aber war, dass die Module manchmal auch ein URL-Modul einbauten, was in Browsern so nicht existiert:</p>
<pre class="code">var url = require('url')
…
var shareUrl = url.parse('https://twitter.com/intent/tweet', true)</pre>
<p>Sowas musste dann umgebaut werden, hier:</p>
<pre class="code">var shareUrl = new URL('https://twitter.com/intent/tweet');</pre>
<p>Glücklicherweise waren keine weiteren Nodeisms im Code verteilt.
</p>
<p>Guter Effekt des Ganzen war, dass die <strong>shariff.js</strong> fortan direkt im Browser funktionierte, der konnte dann die Module laden ohne dass vorher ein Helferprogramm wie <em>webpack</em> den Code umformen musste. Optional ging das aber immer noch, so konnte <a href="https://rollupjs.org/">rollup</a> die <strong>shariff.complete.js</strong> weiterhin bauen, eine Datei mit dem Javascriptcode der äußeren <strong>shariff.js</strong> und der ganzen Module.</p>
<h4>Pipeline
</h4>
<p>Und hier machte ich einen Fehler: Ich hörte auf. Wenn <em>rollup</em> doch die gebrauchte <strong>shariff.complete.js</strong> erstellen konnte, Browser ansonsten direkt die <strong>shariff.js</strong> verstanden, ich das mit Less gebaute CSS erstmal nicht abändern wollte, dann müsste es das gewesen sein, oder? Keineswegs.
</p>
<p>In der <strong>package.json</strong> waren fünf Befehle definiert. Ich kann sie hier in Gänze zeigen:</p>
<pre class="code">"scripts": {
"dev": "webpack-dev-server --hot --inline --port 3000 --content-base demo --entry app=./demo/app.js",
"test": "eslint src && karma start --single-run",
"build": "rm -fr dist && webpack -p",
"build_zip": "7z a -tzip $BASE_NAME.zip ./dist/* && 7z a -ttar $BASE_NAME.tar ./dist/* && 7z a $BASE_NAME.tar.gz $BASE_NAME.tar",
"prepare": "husky install"
},</pre>
<p>Von denen war nur <code>build_zip</code> unbedenklich. Hauptproblem war webpack: Durch meine Änderungen an den Modulen funktionierte dessen Konfiguration nicht mehr. Und ich scheiterte selbst daran, die wieder hinzubiegen. Webpack aber, wie oben abzusehen, war zuständig für das Bauen des Javascript als auch des CSS, also zum Umwandeln der CJS-Module und der Lessdateien. Damit hatte meine Änderung beides blockiert.
</p>
<p>Zweites Problem: Die Abhängigkeiten waren ziemlich veraltet, es hagelte deprecated-Warnungen. Problematisch gerade dann, wenn man auf modernen JS-Code umgestellt hat, der eher nur mit modernen Varianten dieser Helfer zusammenspielt, wenn überhaupt.
</p>
<p>Außerdem waren noch Tests über, die jQuery testen wollten (was auch noch nicht als Abhängigkeit entfernt war), die auch noch ebenfalls auf das als deprecated markierte PhantomJS zurückgriffen, wobei auch die wenigen auf Shariff gerichteten Tests wegen den neuen Modulen zusätzlich noch etwas umgebaut werden mussten.
</p>
<p>Genau das habe ich dann getan, alles umgebaut. Die Befehle sehen in meinem Fork nun so aus, was die Änderungen schonmal sichtbar macht:</p>
<pre class="code">"scripts": {
"dev": "lessc src/style/shariff-complete.less demo/shariff.complete.css && npx http-server -p 3000 demo/",
"test": "eslint src && mocha -r jsdom-global/register",
"build": "rm -fr dist && mkdir dist && lessc src/style/shariff-complete.less dist/shariff.complete.css && rollup src/js/shariff.js -o dist/shariff.complete.js",
"build_zip": "7z a -tzip $BASE_NAME.zip ./dist/* && 7z a -ttar $BASE_NAME.tar ./dist/* && 7z a $BASE_NAME.tar.gz $BASE_NAME.tar"
},</pre>
<p>Die CSS-Datei wird also direkt von <code>lessc</code> erstellt, die <strong>shariff.complete.js</strong> von rollup. Das eingebaute <code>npx http-server</code> ersetzt <code>webpack-dev-server</code>. Generell ist bei <code>dev</code> weniger zu bauen, weil ich die Demodateien so umgebaut hatte, dass sie direkt auf die Quellcodedatei zeigen (die der Browser ja nun laden kann). Ganz entfernt hatte ich <em>husky</em>, da es kritische Warnungen wegen seiner Konfiguration schmiss und die Commits blockierte, damit fiel <code>prepare</code> weg. Erstmal, das Projekt könnte sowas leicht wieder einbauen.<br />
Bei den Tests ersetzte ich <em>karma</em> und <em>PhantomJS</em> mit <em>mocha</em> und <em>jsdom</em>, was dann leider einige Abhängigkeiten nach sich zog. Die direkte Abhängigkeitsliste ist aber nun hübsch klein. Vorher:</p>
<pre class="code">"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"jquery": "^3.4.1"
},
"devDependencies": {
"autoprefixer": "^8.6.5",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.17.3",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"husky": ">=6",
"karma": "^4.1.0",
"karma-chrome-launcher": "^2.2.0",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-phantomjs-launcher": "^1.0.4",
"karma-webpack": "^2.0.13",
"less": "^3.9.0",
"less-loader": "^4.1.0",
"lint-staged": ">=10",
"mocha": "^5.2.0",
"postcss-loader": "^2.1.6",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^3.12.0",
"webpack-dev-server": "^2.11.5"
},</pre>
<p>Jetzt:</p>
<pre class="code">"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2"
},
"devDependencies": {
"eslint": "^9.25.1",
"jsdom": "^17.0.0",
"jsdom-global": "^3.0.2",
"less": "^3.13.1",
"mocha": "^11.1.0",
"rollup": "^4.40.0"
},</pre>
<p>Das Ausmaß der Änderungen zeigt aber auch, dass der vorherige Umbau nur mit einigem Aufwand zu integrieren war. Den vom Projekt zu verlangen anstatt das selbst zu stemmen konnte das Rückspielen der sonstigen Änderungen nur blockieren.</p>
<hr /><p>Das waren die Kernzüge meiner Umbauten an Shariff, neben notwendigen Bugfixes und sonstigen Details. Mein Ziel ist nicht, Shariff selbst zu übernehmen, sondern das ganze soll dem aktiven Fork shariff-plus zugutekommen – oder dem Ursprungsprojekt, wenn dort Interesse ist. Aber bisher ist nur ein <a href="https://github.com/richard67/shariff-plus/pull/22">PR für shariff-plus</a> auf.
</p>
<p>Es ist auch nicht zwingend, dass der PR akzeptiert wird. Divergenz vom Originalprojekt kann immer auch ein Fehler sein, wenn von dort doch noch Änderungen kommen wird dann alles schwieriger. Die Lösung mit den Symlinks für die Demo funktioniert noch dazu meines Wissens nicht direkt auf Windows, sondern braucht <a href="https://stackoverflow.com/a/59761201">etwas Konfiguration</a>. Die Erstellung einer minifizierten Quellcodedatei fehlt auch noch, sie müsste bei Bedarf nachgereicht werden.
</p>
<p class="wl_nobottom">Doch so oder so, für mich war das ein interessanter Umbau. Ich sah es auch als Test, ob meine Annahme richtig ist und man Buildpipelines tatsächlich weitgehend vermeiden kann, zumindest bei solchen Projekten. Oder ob ich irgendwelche Anforderungen übersehe, die das blockierten. Mein Fazit: Klar kann man sie vermeiden, und die Wartbarkeit des Projekts profitiert von der Änderung massiv. Der Effekt würde nur noch stärker werden, wenn man Less zugunsten von echtem CSS auch noch rauswirft.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/01f1a9e4d6e24dc6af00734cea2271ce" width="1" height="1" alt="">
Mon, 05 May 2025 08:06:00 +0200https://www.onli-blogging.de/2513/guid.htmlMein liebster Pullrequest…
https://www.onli-blogging.de/2501/Mein-liebster-Pullrequest.html
Codehttps://www.onli-blogging.de/2501/Mein-liebster-Pullrequest.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=25010https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2501[email protected] (onli)
<p>…gefällt mir erstmal nicht wegen seiner Funktionalität, sondern wegen einer solchen Zusammenfassung:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/pr_loc.webp'><!-- s9ymdb:2163 --><img class="serendipity_image_center" width="157" height="29" srcset="" src="https://www.onli-blogging.de/uploads/pr_loc.serendipityThumb.webp" loading="lazy" alt="Plus 32, minus 16203"></a>
</p>
<p>Das sind die hinzugefügten und entfernten Codezeilen, es wurde also viel mehr Code gelöscht als hinzugefügt. Was die Zusammenfassung nicht verrät: In diesem Fall wurde nichtmal Funktionalität entfernt!
</p>
<p>Die Statistik ist aus <a href="https://github.com/s9y/additional_plugins/pull/189">diesem PR</a>, ein Update für das altehrwürdige Usergallery-Plugin für die <a href="https://docs.s9y.org/">Blogsoftware Serendipity</a>. Betonung liegt dabei auf alt, der PR entfernt unter anderem einen Sonderfall für die Nutzung unter PHP 4 (offizielles Supportende: 2008). Das war aber nicht die große Codeeinsparung, sondern die stammt aus der Entfernung einer gebündelten Library namens <em>JPEG_TOOLKIT</em> zum Auslesen von Exif-Tags. Die konnte mit einer Funktion aus Serendipitys Kern ersetzt werden, <code>serendipity_getMetaData</code>, die wiederum auf normalerweise einkompilierte PHP-Funktionen wie <code>iptcparse</code> und <code>exif_read_data</code> zurückgreift.
</p>
<p>Völlig ohne sichtbare Änderung ging das zwar nicht, <code>serendipity_getMetaData</code> gibt eine leicht andere Auswahl an Metadaten zurück. Und die Keys des zurückgegebenen Arrays sind auch teilweise anders, wodurch die Konfiguration zurückgesetzt werden musste (was ein paar der neuen Codezeilen erkennen und umsetzen). Doch das war es in meinen Augen definitiv wert, vor allem wenn man bedenkt, dass das <em>JPEG_TOOLKIT</em> unter PHP 8 (und möglicherweise auch früheren Versionen) nicht nur nicht funktionierte, sondern sogar kritische Fehler warf. Das halte ich auch meinem Gedanken entgegen, dass gebündelter Code nicht wirklich zählt: Oft stimmt das, wird er doch in einem anderen Projekt gepflegt. Aber hier wäre der Code von uns zu pflegen gewesen, da das Ursprungsprojekt nicht mehr aktiv zu sein scheint. Für mich zählt es dann doch.</p>
<hr /><p>Ich sehe sowas als Paradebeispiel für einen PR, wie ich ihn bei der Wartung im Idealfall anstreben will: Viel Code wurde entfernt, die Funktionalität dabei bewahrt. Diese Sichtweise sollte universell sein und kommt mir beim Aufschreiben trivial vor, aber man darf nicht vergessen: Das ist sie nicht. Berichte über Manager, die Entwickler nach Codezeilen bewerten wollen, findet man immer noch immer wieder. Insbesondere, wenn wie bei der <a href="https://www.onli-blogging.de/1960/Spamblock-Bayes-1.0-als-reduzierte-modernisierte-Version.html">Überarbeitung des Bayesplugins</a> sogar Funktionalität entfernt wird – zugunsten simpleren und wartbaren Code sowie einer klareren Bedienung – ist Verständnis nicht gesetzt. Und es ist ja auch eine Besonderheit der Programmierung, dass ein scheinbar geringeres Arbeitsergebnis letztendlich besser sein kann.
</p>
<p class="wl_nobottom">Der PR oben ist übrigens nur als Beispiel gemeint für diese Art von Codeänderungen, er ist nicht direkt mein Favorit. Ich erinnere mich an andere, bei denen ich an die hunderttausend Codezeilen entfernen konnte. Leider war das in einem privaten Repository.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/064b7bf3efab4bac8934dfdc27983489" width="1" height="1" alt="">
Mon, 31 Mar 2025 08:59:00 +0200https://www.onli-blogging.de/2501/guid.htmlserendipityMeine Spyfall-Lösung, oder: Bilder im Terminal
https://www.onli-blogging.de/2472/Meine-Spyfall-Loesung,-oder-Bilder-im-Terminal.html
CodeLinuxhttps://www.onli-blogging.de/2472/Meine-Spyfall-Loesung,-oder-Bilder-im-Terminal.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=24722https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2472[email protected] (onli)
<p>Im Oktober letzten Jahres hatte GNU/Linux.ch <a href="https://gnulinux.ch/herbstwettbewerb-spyfall">einen Programmierwettbewerb</a> für das Spiel Spyfall veranstaltet. Spyfall ist ein Diskussionsspiel, bei dem eine Gruppe von Spielern durch Fragen den Spion in der Gruppe identifizieren müssen. In jeder Spielrunde wissen alle außer dem Spion an welchem zugeteilten Ort sie sind (z.B. in einem Bunker) und stellen einander in einem Zeitlimit Fragen. Die Programmieraufgabe war nun, dafür ein Helferprogramm zu schreiben, das einem der Spieler die Spionrolle zuweist und allen anderen den Ort verrät.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/wezterm1.webp'><img class="serendipity_image_center" width="800" height="651" src="https://www.onli-blogging.de/uploads/wezterm1.serendipityThumb.webp" loading="lazy" alt=""></a>
</p>
<p>Mich hatte das (nicht nur wegen dem Preis) direkt interessiert, weil ich hier eine Möglichkeit sah Bilder im Terminal einzusetzen. Denn das geht überraschenderweise, wie mir kürzlich erst <a href="https://www.onli-blogging.de/2444/Ist-WezTerm-der-perfekte-Terminalemulator.html">WezTerm nochmal gezeigt hatte</a>. Ich erkläre im Folgenden meine in Bash implementierte Lösung im Detail.</p>
<h4>Spiellogik
</h4>
<p>Die Spiellogik können wir schnell abhandeln. Am Anfang wird die Spielerzahl eingegeben:</p>
<pre class="code">while [[ -z $players || 0 == $players ]];do
# ask for players and duration
echo "Wie viele Spieler nehmen teil? [3-10]"
read players
done</pre>
<p>Genauso wird die Rundenlänge abgefragt:</p>
<pre class="code">echo "Wie viele Minuten soll die Runde dauern? [Standard 8]"
read duration
if [[ -z $duration ]];then
duration=8
fi</pre>
<p>Mit den Informationen kann nun aus vorher vorbereiteten Arrays mittels Bashs Zufallsfunktion ein Ort und das passende Bild ausgewählt werden:</p>
<pre class="code"># Select a place
placeindex=$((RANDOM%${#places[@]})) # Zufallszahl mod der Anzahl möglicher Orte, sodass sie nie größer sein kann
place=${places[$placeindex]} # Der gewählte Index bestimmt dann den Ort
place_image=${images[$placeindex]} # Und die Bilder, siehe unten
place_image_ascii=${images_ascii[$placeindex]}</pre>
<p>Und um die Spielerrollen zuzuweisen muss nur der Spion ausgewählt werden:</p>
<pre class="code">spy=$((RANDOM%players))</pre>
<p>Die Spieler werden dann noch einer nach dem anderen über ihre Rolle informiert:</p>
<pre class="code">for i in $(seq 0 $((players - 1)));do
echo "Hey Spieler $((i+1)), bist du da und alleine? Bitte bestätige mit Enter:"
read confirmed
if [[ $spy == $i ]];then
# ask for players and duration
echo "Hey Spieler $((i+1)), du bist diese Runde der Spion. Bitte bestätige mit Enter:"
read confirmed
else
# ask for players and duration
# Hierhin kommt nachher noch der Code zum Bilderanzeigen, siehe unten
…
echo "Hey Spieler $((i+1)), diese Runde spielt in $place. Bitte bestätige mit Enter:"
read confirmed
fi
clear
done</pre>
<p>Das Skript hilft während des Spiels dabei das Zeitlimit anzuzeigen, was ich recht simpel durch ein sekündliches Herunterzählen des Countdowns umgesetzt habe:</p>
<pre class="code"># On start, start the timer
countdown=$((duration*60))
echo "Das Spiel startet nun. Ihr habt $duration Minuten!"
while [[ $countdown > 0 ]];do
sleep 1
countdown=$((countdown - 1))
echo "Noch $countdown Sekunden!"
done</pre>
<p>Am Ende soll ein Ton abgespielt werden. Das war gar nicht so einfach. Ich wollte erst nur den Beeper im PC tönen lassen, aber der ist oft deaktiviert, war es bei mir im Terminal beispielsweise. Daher versucht das Skript zusätzlich das oft auf Systemen vorhandene Programm <code>speaker-test</code> einzuspannen, um für einen Moment einen Ton über den Lautsprecher auszugeben:</p>
<pre class="code"># Now we signal the end of the game.
# First with the bell, but that might be disabled
echo -ne '\a'
# Now with speaker-test, to use the regular sound system. Note how we kill it quietly thanks to wait
if hash speaker-test 2>/dev/null ;then
speaker-test -t sine -f 1000 -l 1 > /dev/null &
speaker_pid=$!
sleep .2
kill -9 $speaker_pid
wait $speaker_pid 2>/dev/null
fi</pre>
<p>Der Test auf <code>hash speaker-test</code> ist dabei eine der Möglichkeiten um zu testen, ob ein Befehl auf einem System verfügbar ist. Mir ist nicht mehr klar, warum ich diesen Weg und nicht einen anderen wählte, aber er funktioniert.
</p>
<p>Das war der relevante Teil der Spielelogik. Er war verpackt in einer Funktion namens <code>main</code>, sodass ich außenrum noch ein paar Variablen anlegen konnte. Denn die wurden für das eigentlich interessante gebraucht.</p>
<h4>Bilder anzeigen
</h4>
<p>Denn wie kann ein solches Bashskript nun Bilder anzeigen? Es stellt sich raus, dass manche Terminals das einfach können. WezTerm beispielsweise bringt eine Befehlkombination <code>wezterm imgcat</code> mit, die man auf eine Bilddatei loslassen kann, sodass die dann im Terminal angezeigt wird. Das nutzt mein Skript so:</p>
<pre class="code">if [[ "$TERM_PROGRAM" == "WezTerm" ]];then
temp_file=$(mktemp)
echo "$place_image" | base64 -d > "$temp_file"
wezterm imgcat "$temp_file"
else</pre>
<p>Generisch scheint das Stichwort <a href="https://en.wikipedia.org/wiki/Sixel">Sixel</a> zu sein. Ein Protokoll, um Bilder an Terminals zu übertragen und von ihnen darstellen zu lassen. Darauf kann man testen, wobei ich mir von <code>lsix</code> abschaute wie ein solcher Test aussehen kann. Das nutzt mein Skript alternativ:</p>
<pre class="code"># We detect for sixel support, partly how lsix does it
stty -echo
IFS=";?c" read -a REPLY -s -t 1 -d "c" -p $'\e[c' >&2
for code in "${REPLY[@]}"; do
if [[ $code == "4" ]]; then
hassixel="yup"
break
fi
done
hash lsix 2>/dev/null
# hacky, but I did not want to fight with the syntax to have the hash inside the [[
if [[ $hassixel == "yup" && $? ]] ;then
temp_file=$(mktemp)
echo "$place_image" | base64 -d > "$temp_file"
lsix "$temp_file"
fi
</pre>
<p>Das funktioniert! Glaube ich, habe ich es doch letztendlich nur mit WezTerm getestet. Doch was hat es mit dem base64 auf sich, wo kommen die Bilder her?</p>
<h4>Bilder im Skript mit ausliefern
</h4>
<p>Ich wollte, dass die Bilder nicht separat im Dateisystem liegen müssen, sondern dass ich sie an Ralf mit dem Programmcode in einer Datei schicken kann. Zuerst erstellte ich dafür ganz normale grafische Bilder mit einem KI-Bildergenerator (Bing, <a href="https://www.onli-blogging.de/2450/Zu-Microsofts-Bing-Image-Creator-und-was-man-als-Blogger-mit-KI-Bildern-anfangen-kann.html">wie hier vorgestellt</a>). Doch dann nutzte ich <code>base64</code> um von der Bilddatei eine Textrepräsentation zu erstellen (es zu serialisieren) und es der Skriptdatei anzuhängen, z.B.so:</p>
<pre class="code">base64 Downloads/zoo.jpg >> spyfall.sh</pre>
<p>Um diese Serialisierung auch nutzen zu können packte ich sie in eine Variable, indem ich sie mit Anführungszeichen umstellte und den Variablennamen davorstellte, also so:</p>
<pre class="code">zoo="…"</pre>
<p>Diese Bilder landeten in der main in einem Array:</p>
<pre class="code"># Man beachte die wiederholte Nutzung der Anführungszeichen.
images=("$cave" "$spacestation" "$desert" "$disco" "$bunker" "$corn" "$antarktis" "$zoo")</pre>
<p>Das Array wurde im Spiel zur Auswahl des Bildes genutzt wie oben bei der Spiellogik gezeigt, indem nur die aktive Arrayposition festgelegt wurde.</p>
<h4>Die ASCII-Ausweichlösung
</h4>
<p>Aber nicht alle Terminals können grafische Bilder anzeigen. Für die wollte ich auch eine Ausweichlösung haben. Dafür griff ich auf <a href="https://github.com/radare/tiv"><code>tiv</code></a> zurück. Das ist ein Programm, das aus Bildern eine ASCII-Grafik zaubert, samt den Escapesequenzen um sie einzufärben (was in xterm mir übrigens am besten zu funktionieren schien).
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/xterm.png'><!-- s9ymdb:2127 --><img class="serendipity_image_center" width="800" height="583" srcset="https://www.onli-blogging.de/uploads/xterm.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/xterm.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/xterm.serendipityThumb.png" loading="lazy" alt=""></a>
</p>
<p>Ein Pluspunkt davon: Das Programm kann ich auf meiner Seite laufen lassen, es muss beim Spieler nicht installiert sein. Beim Spieler reicht es völlig, die gespeicherte Ausgabe des Programms auszugeben. Ich musste also nur wieder die Bildrepräsentation dem Skript anhängen:</p>
<pre class="code">tiv Downloads/zoo.jpg >> spyfall.sh</pre>
<p>Die wieder in einer Variable speichern:</p>
<pre class="code"># Man beachte das einzelne Anführungszeichen, um die Escapesequenzen zu bewahren.
zoo_ascii='…'</pre>
<p>Und wieder ein Array vorbereiten:</p>
<pre class="code">images_ascii=("$cave_ascii" "$spacestation_ascii" "$desert_ascii" "$disco_ascii" "$bunker_ascii" "$corn_ascii" "$antarktis_ascii" "$zoo_ascii")</pre>
<p>Nun ist es mit einem <code>echo</code> darstellbar:</p>
<pre class="code">else
# Our two graphical methods failed, so we fall back to the ascii images
echo "$place_image_ascii"
fi</pre>
<p>Somit läuft das Skript vernünftig in allen Terminals mit Bash.</p>
<hr /><p>Das Spiel selbst zu programmieren war nicht schwer. Sicher, man hätte das besser machen können, so war mein Ansatz mit <code>clear</code> einfach alte Ausgaben zu entfernen oder den Countdown schlicht mit echo untereinander herunterzuzählen nicht ideal. Hier hätte sich ein interaktives Terminalprogramm angeboten, das (wie nano, top, etc) seinen eigenen Platz schafft und alte Ausgaben überschreiben kann. Aber Bilder einzuarbeiten war mir neues genug.
</p>
<p>Es hat auch Spaß gemacht, sowas mal wieder in Bash umzusetzen. Damit gute Lösungen zu finden ist mehr noch ein Knobelspiel als sonst und das Ergebnis trotzdem kompakt und gut lesbar. Vorausgesetzt man verirrt sich nicht in den Bilderdefinitionen (die Skriptdatei ist übrigens 2,6 MB groß). Dass meine Implementierung dann auch noch als valider Gewinnspielkandidat <a href="https://gnulinux.ch/spyfall-die-loesungen">akzeptiert wurde</a> hat mich durchaus gefreut.
</p>
<p class="wl_nobottom">Wer sich das im ganzen ansehen will, ich habe das Skript <a href="https://gist.github.com/onli/8b78f1b4f219917522c09b3b24cdf98a">als Gist hochgeladen</a>.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/2427a992301f498fafca96dd62614223" width="1" height="1" alt="">
Mon, 13 Jan 2025 08:58:00 +0100https://www.onli-blogging.de/2472/guid.htmlFavicon als SVG, und wieviel Code man wirklich braucht
https://www.onli-blogging.de/2446/Favicon-als-SVG,-und-wieviel-Code-man-wirklich-braucht.html
Codeabouthttps://www.onli-blogging.de/2446/Favicon-als-SVG,-und-wieviel-Code-man-wirklich-braucht.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=24463https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2446[email protected] (onli)
<p>Man kann mittlerweile Vektorgrafiken für das Favicon einer Webseite benutzen. Dabei lässt sich direkt das Bild für den Dunkelmodus umfärben. Das ist eine gute Gelegenheit um die Anzahl der Favicons und der Zusatzanweisungen zu reduzieren – brauchte man vor einer Weile für verschiedene Betriebssysteme und Browser nämlich noch einiges an Code, reicht mittlerweile weniger.
</p>
<p>Hier im Blog ist das jetzt umgesetzt. Ich bin dabei in etwa <a href="https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs">diesem Artikel von evilmartians</a> gefolgt. Anlassgeber war übrigens <a href="https://benedikt.io/2024/10/neues-favicon/#comment-693421">Benedikts Antwort</a> auf meinen Kommentar zu seinem neuen Favicon.
</p>
<p><em>Update 06.01.2024:</em> Das hier verwendete SVG-Favicon wurde später nochmal überarbeitet. Zum einen wegen des Motivs, aber es stellte sich auch heraus, dass ein SVG als kleines Browsertabicon schnell unscharf werden kann wenn es beispielsweise zu feine Linien hat. Es empfiehlt sich daher wohl, das Icon als 32x32-Icon zu entwerfen und Details höchstens so anzubringen, dass sie herunterskaliert nicht auffallen. Diese Details der SVG-Skalierung sind in jedem Fall eine weitere Hürde für SVG-Favicons. Die Artikelkommentare erklären etwas mehr.</p>
<h4>Die Ausgangssituation
</h4>
<p>Hier im Blog war der Code für die Favicons relativ minimal. Es gab nur diese zwei Anweisungen:</p>
<pre class="code"><link rel="apple-touch-icon" href="https://www.onli-blogging.de/apple-touch-icon.png">
<link rel="shortcut icon" type="image/x-icon" href="https://www.onli-blogging.de/favicon.png" /></pre>
<p>Zusätzlich lag unter <strong>favicon.ico</strong> das <strong>favicon.png</strong> nochmal. Das vermeidete 404-Fehler in Browsern, die (damals?) das .ico immer auch abriefen. Dass es eine PNG-Grafik war, war trotz der Dateiendung irrelevant.
</p>
<p>Dabei war das <strong>apple-touch-icon.png</strong> 192x192 px groß und vll etwas unscharf, das <strong>favicon.png</strong> 16x16. Beide sind hier zu sehen:</p>
<ul class="s9y_gallery plainList"><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/favicon_alt.png"><!-- s9ymdb:2072 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/favicon_alt.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/favicon_192_alt.png"><!-- s9ymdb:2073 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/favicon_192_alt.serendipityThumb.png" alt=""></a></li>
</ul>
<p>Warum unscharf, und wo kommt das Icon überhaupt her? <br />
Ich habe es vor vielen Jahren Pixel für Pixel selbst gebaut, aber als 16x16-Icon, um in meinem IceWM-Desktop ein hübscheres Icon für Thunderbird zu haben. Als ich dann für diesen Blog ein Icon brauchte nutzte ich es einfach wieder. Immerhin war die Ähnlichkeit zum Thunderbirdlogo klein genug. Klar, eigentlich wäre ein Icon besser das irgendwie einen Bezug zum Blognamen oder Inhalt hat, aber ich wollte nach einer Weile den Wiedererkennungswert nicht verlieren und hatte für eine Kombination nie eine Idee. <br />
Die größere Variante könnte leicht unscharf sein, weil ich später mit einem fantastischen Skalierer für Pixelgrafiken aus der kleinen Vorlage ein größeres Icon gemacht habe. Das sollte ein Blogartikel werden, den schrieb ich aber scheinbar nie. Das Icon war meiner Erinnerung nach aber 256x256. Für das Apple-Touch-Icon habe ich das dann wieder etwas herunterskaliert. 192x192 war damals wohl eine akzeptabel erscheinende Zwischenlösung, die gerade auch für Android passte. Im evilmartians-Artikel wird jetzt 180x180 für dieses Icon empfohlen.</p>
<h4>Das neue SVG
</h4>
<p>Um diese Icons zu optimieren musste aber erstmal ein SVG des Favicons her. Erst versuchte ich mit Inkscapes "Trace Bitmap"-Funktion, aus meinem größeren alten Favicon ein SVG zu bauen. Das Ergebnis sah trotz mehreren Versuchen so aus:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/favicon_failed.svg'><!-- s9ymdb:2074 --><img class="serendipity_image_center" width="" height="" srcset="" src="https://www.onli-blogging.de/uploads/favicon_failed.svg" alt=""></a>
</p>
<p>Hat was, aber war nicht das gesuchte Ergebnis. Diese leeren Flächen müsste ich dafür manuell reparieren, was mir zu aufwändig war. Also recherchierte ich nochmal und fand eine Empfehlung für <a href="https://png-to-svg.com/">https://png-to-svg.com/</a>. Tatsächlich war dessen Ergebnis deutlich besser. Die einzelnen Flächen mussten immer noch übereinandergeschoben werden, weil zumindest in Inkscape weiße Linien zu sehen waren, aber es war viel weniger Leerraum zu füllen. Und ein paar Ecken bearbeitete ich dann noch manuell, wie den Schnabelübergang.
</p>
<p>Ich folgte noch der Empfehlung, das SVG mit <a href="https://github.com/svg/svgo">SVGO</a> zu optimieren:</p>
<pre class="code">npx svgo --multipass favicon.svg</pre>
<p>Dann hatte ich dieses Ergebnis:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/favicon_light.svg'><!-- s9ymdb:2075 --><img class="serendipity_image_center" width="" height="" srcset="" src="https://www.onli-blogging.de/uploads/favicon_light.svg" alt=""></a></p>
<h4>SVG mit Darkmode
</h4>
<p>Von einem Umschalten im Dunkelmodus ist da aber noch nichts implementiert. Aber SVGs sind Textdateien, mit einem Editor lässt sich das einbauen. Erstmal nahm ich wieder Inkscape zur Hand und versuchte mich an einem Entwurf:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/favicon_dark.svg'><!-- s9ymdb:2076 --><img class="serendipity_image_center" width="" height="" srcset="" src="https://www.onli-blogging.de/uploads/favicon_dark.svg" alt=""></a>
</p>
<p>Da werden also fast alle Farben auf eine gesetzt. Das lässt sich dann ins originale favicon.svg einbauen. Es bekommt ein neues Mediaquery für die Füllfarbe einer Klasse:</p>
<pre class="code"><style>@media (prefers-color-scheme:dark){.a{fill:#9da0ae !important}}</style></pre>
<p>Und alle SVG-Elemente, die umgefärbt werden sollen, bekommen diese Klasse, z.B.:</p>
<pre class="code"><path id="path1" d="… class="a" style="…" /></pre>
<p>Das resultierte in diesem finalen SVG:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/favicon.svg?v2'><!-- s9ymdb:2079 --><img class="serendipity_image_center" width="" height="" srcset="" src="https://www.onli-blogging.de/uploads/favicon.svg?v2" alt=""></a></p>
<h4>Erstellen von .png und .ico
</h4>
<p>Ob mit oder ohne Darkmodeumschalter, das SVG alleine reicht nicht. Erstmal sollen alte Browser eine Ausweichlösung bekommen. Für sie erstellte ich ein .ico:</p>
<pre class="code">inkscape ./favicon.svg --export-width=32 --export-filename="./tmp.png"
convert ./tmp.png ./favicon.ico</pre>
<p>Die Anleitung empfiehlt an der Stelle zu prüfen, ob das Icon herunterskaliert auf 16x16 auch noch gut aussieht, ansonsten sollte ein passgenaues erstellt und dem .ico (als Layer) hinzugefügt werden. Ich fand das bei meinem Icon nicht nötig.
</p>
<p>Außerdem braucht es ein größeres PNG, das z.B. im Appleuniversum benutzt wird. Dafür nutzte ich Gimp, und zwar so: Zuerst das SVG öffnen und als 140x140px große Grafik importieren. Dann unter <em>Image -> Canvas Size</em> das Canvas auf 180x180 vergrößern, dabei über den Button rechts den Inhalt zentrieren:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/favicon_resize.webp'><!-- s9ymdb:2077 --><img class="serendipity_image_center" width="498" height="548" srcset="https://www.onli-blogging.de/uploads/favicon_resize.webp 498w" src="https://www.onli-blogging.de/uploads/favicon_resize.serendipityThumb.webp" loading="lazy" alt=""></a>
</p>
<p>Das Ergebnis lässt sich dann als <strong>apple-touch-icon.png</strong> exportieren. Beide neuen Dateien so aus:</p>
<ul class="s9y_gallery plainList"><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/favicon_neu.ico"><!-- s9ymdb:2080 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/favicon_neu.serendipityThumb.ico" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/apple-touch-icon_neu.png"><!-- s9ymdb:2081 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/apple-touch-icon_neu.serendipityThumb.png" alt=""></a></li>
</ul>
<p>Zum Vergleich, hier nochmal die entsprechenden Dateien von vorher:</p>
<ul class="s9y_gallery plainList"><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/favicon_alt.png"><!-- s9ymdb:2072 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/favicon_alt.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/favicon_192_alt.png"><!-- s9ymdb:2073 --><img class="s9y_gallery_image" srcset="" src="https://www.onli-blogging.de/uploads/favicon_192_alt.serendipityThumb.png" alt=""></a></li>
</ul>
<h4>Der finale Code
</h4>
<p>Damit sind alle Dateien zusammen. <strong>favicon.svg</strong>, <strong>favicon.ico</strong> und <strong>apple-touch-icon.png</strong> lud ich dann auf den Server und passte den Code im head der Seite an:</p>
<pre class="code"><link rel="icon" href="https://www.onli-blogging.de/favicon.ico?v=1" sizes="32x32">
<link rel="icon" href="https://www.onli-blogging.de/favicon.svg?v=1" type="image/svg+xml">
<link rel="apple-touch-icon" href="https://www.onli-blogging.de/apple-touch-icon.png?v=1"></pre>
<p>Ich benutzte dafür bereits ein HTML-Nugget-Plugin von Serendipity, so brauchen die Templatedateien keine Anpassung. Das <code>?v=1</code> bei den Links zu den Dateien hilft Browsern dabei, nicht die im Cache liegenden alten Dateien zu verwenden.</p>
<hr /><p>Und das sollte es tatsächlich gewesen sein. Bei mir in Chrome und Firefox wird laut Netzwerkinspektor wirklich das <strong>favicon.svg</strong> geladen. Das sollte dann auch an verschiedenen Orten benutzt werden, die gerne größere Icons anzeigen, beispielsweise die Startseite von Firefox. Die fiel vorher auf das Apple-Touch-Icon zurück. Das zudem sollte nun etwas schärfer sein, aber das ist speziell für diesen Blog und hat mit dem SVG-Ansatz erstmal nichts zu tun. Wobei ich da auf Applenutzer angewiesen wäre: Taugt das Icon so wie es ist, oder braucht es beispielsweise noch einen farbigen Hintergrund?
</p>
<p>Warum habe ich anders als in der Vorlage beschrieben keine Manifestdatei erstellt? Weil diese Seite keine PWA ist, offline nicht funktioniert. Die Datei würde ich nachreichen, wenn in Android-Browsern oder -Launchern die Manifestdatei trotzdem nützlich wäre, um bessere Icons anzuzeigen. Das fand ich aber nirgends sauber beschrieben. Müsste ich also erstmal selbst testen.
</p>
<p>Auf den ersten Blick also eine gelungene Operation. Denn das neue Favicon wird angezeigt und müsste nun in mehr Situationen unabhängig der Größe scharf bleiben.<br />
Allerdings: Zwar war es interessant zu sehen, dass SVG-Favicons mittlerweile tatsächlich funktionieren. Aber mir fiel zumindest in meinen Browsern kein relevanter Qualitätsgewinn auf. Die vorherige Kombination aus großem Apple-Touch-Icon und kleinem regulären Favicon in Pixelgrafik war – so mein Stand jetzt – bereits keine schlechte Lösung. Vor allem, wenn das Icon so gestaltet wird, dass es sowohl auf hellem als auch auf dunklem Hintergrund funktioniert. Immerhin, das deckt sich mit der Vorlage, dernach man auf das <em>Windows Tile Icon</em>, das <em>Safari Pinned Icon</em> und die Einbindung des Favicons via <code>rel="shortcut"</code> verzichten kann. Egal, ob das eigene Favicon nun ein SVG ist oder nicht.
</p>
<p class="wl_nobottom">Abseits der Technik werde ich nun aber nochmal über das Iconmotiv nachdenken.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/0e8c3e43bfc74595af18b840049d06e3" width="1" height="1" alt="">
Mon, 28 Oct 2024 08:19:00 +0100https://www.onli-blogging.de/2446/guid.htmlExterne Links mit CSS kennzeichnen
https://www.onli-blogging.de/2384/Externe-Links-mit-CSS-kennzeichnen.html
Codehttps://www.onli-blogging.de/2384/Externe-Links-mit-CSS-kennzeichnen.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=23845https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2384[email protected] (onli)
<p>Ich hatte kürzlich einen <a href="https://danilafe.com/blog/blog_microfeatures/">Artikel zu netten Blogfeatures</a> <a href="https://www.onli-blogging.de/2382/Linksammlung-262024.html">verlinkt</a>, darunter waren auch kleine Icons für Links auf andere Seiten. Das gefiel mir besonders, als kleine informationsvermittelnde Spielerei die niemandem schaden dürfte. Ich habe hier im Blog das jetzt umgesetzt, rein mit CSS.
</p>
<p>Wer hier im Feedreader mitliest oder fürs Archiv, so sieht es aus:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/linkicon_example.webp'><!-- s9ymdb:1948 --><img class="serendipity_image_center" width="800" height="59" srcset="https://www.onli-blogging.de/uploads/linkicon_example.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/linkicon_example.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/linkicon_example.serendipityThumb.webp" loading="lazy" alt=""></a>
</p>
<p>Oder im Darkmode:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/linkicon_example_dark.webp'><!-- s9ymdb:1947 --><img class="serendipity_image_center" width="800" height="58" srcset="https://www.onli-blogging.de/uploads/linkicon_example_dark.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/linkicon_example_dark.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/linkicon_example_dark.serendipityThumb.webp" loading="lazy" alt=""></a></p>
<h4>Die Umsetzung 1: Mask-Image
</h4>
<p>Das Icon setzte ich so:</p>
<pre class="code">
@media screen {
.serendipity_entry_body a[href^="http"]::after,
.serendipity_entry_body a[href^="https://"]::after {
content: "";
width: 11px;
height: 11px;
margin-left: 4px;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
mask-size: cover;
background-color: black;
display: inline-block;
}
}</pre>
<p>Mit dieser ersten Anweisung bekommt jeder Link ein Icon hintendrangestellt. Das Icon ist dabei ein SVG, was hier praktisch ist weil SVGs nur Text sind. Entnommen ist das Icon <a href="https://christianoliff.com/blog/styling-external-links-with-an-icon-in-css/">diesem Artikel</a>, wie auch ein Teil des Ansatzes, stammt aber ursprünglich <a href="https://icons.getbootstrap.com/icons/box-arrow-up-right/">von Bootstrap</a>.
</p>
<p>Die erste Besonderheit ist das Setzen des Icons als <code>mask-im­age</code>, eine erst seit kurzem <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mask-image#browser_compatibility">breit unterstützte</a> CSS-An­wei­sung. Ich wollte es erst als <code>back­ground-im­age</code> oder als <code>content</code> setzen. Aber in beiden Fällen funktionierte in meinem Test die Anpassung der Farbe nicht, obwohl im SVG die Farbe als <code>currentColor</code> definiert war. Mit <code>mask-image</code> aber ging das, <code>back­ground-color</code> kontrolliert nun die Farbe des Icons und setzt sie in diesem Fall auf schwarz. Klar, alternativ hätte die Farbe ins SVG reingeschrieben und bei Bedarf das SVG ausgewechselt werden könne, aber das fand ich nicht elegant.
</p>
<p>Die zweite Besonderheit ist das drumrumgestellte Medienquery <code>@media screen {</code>. Dadurch taucht das Icon nicht <a href="https://www.onli-blogging.de/2028/Ein-Printstylesheet-fuer-den-Blog.html">im Printstylesheet</a> auf. Dort hätte es keine Funktion erfüllt, wird in diesem doch das Linkziel bereits als Text hinter Links geschrieben, außerdem kollidierte unter anderem die Breitenangabe mit dem dafür gesetzten ::after-E­le­ment.
</p>
<p>Im gleichen Medienquery ist als nächstes dieser Block:</p>
<pre class="code">@media screen {
…
.serendipity_entry_body a[href^="https://www.onli-blogging"]::after,
.serendipity_entry_body a:has(img)::after {
display: none !important;
}
}</pre>
<p>Damit entferne ich das Icon wieder in den Fällen, in denen es nicht passt. Zuerst sind das interne Links. Die ebenfalls mit dem Icon zu kennzeichnen würde ja die ganze Idee nutzlos werden lassen. Der zweite Selektor wählt alle Bilder, denen ein img-Element folgt. Bei Bildern macht das Icon ja funktional keinen Sinn. Der dafür genutzte Pseudoselektor <code>:has</code> ist relativ neu, wird aber ebenfalls seit Ende 2023 weitflächig von Browsern <a href="https://caniuse.com/css-has">unterstützt</a>.
</p>
<p>Im dunklen Modus wäre das schwarze Icon kaum sichtbar, dann die Farbe zu ändern war der letzte Schritt und geht dank dem Setzen als <code>mask-im­age</code> schnell:</p>
<pre class="code">@media (prefers-color-scheme: dark) {
.serendipity_entry_body a[href^="http"]::after,
.serendipity_entry_body a[href^="https://"]::after {
background-color: thistle;
}
}</pre>
<p>Diese Variante hat einen Nachteil, den ich nicht aufgelöst bekommen habe: Der Zeilenumbruch kann zwischen Link und Linkicon platziert werden. Das vermeidet der zweite, weniger elegante Ansatz.</p>
<h4>Die Umsetzung 2: Inline-Content
</h4>
<p>Die zweite und bis jetzt aktive Variante setzt das SVG-Icon direkt als Contentattribut des ::after-Elements:</p>
<pre class="code">@media screen {
.serendipity_entry_body a[href^="http"]::after,
.serendipity_entry_body a[href^="https://"]::after {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.7em' height='0.7em' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
margin-left: 4px;
white-space: nowrap;
}
}</pre>
<p>Der Preis ist die Anpassbarkeit der Farbe, die funktioniert so nicht mehr, <code>color</code> hat keinen Effekt. Dafür sorgt <code>white-space: nowrap;</code> tatsächlich dafür, dass zwischen Link und Linkicon nicht umgebrochen wird.
</p>
<p>Weniger schön ist auch die Größenanpassung. Denn auch die funktioniert nicht mehr. Als Inlineelement wird per CSS gesetztes height und width ignoriert, auch font-size hatte keinen Effekt. Deswegen die Größenangaben im SVG selbst, das <code>width='0.7em' height='0.7em'</code> wo vorher jeweils 16 stand.
</p>
<p>Der Code für die Bilder und für das Printdesign bleibt wie bei Methode 1, denn das Ausblenden funktioniert identisch. Aber Im dunklen Modus muss nun leider das SVG ganz ausgewechselt werden:</p>
<pre class="code">@media (prefers-color-scheme: dark) {
.serendipity_entry_body a[href^="http"]::after,
.serendipity_entry_body a[href^="https://"]::after {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.7em' height='0.7em' fill='rgb(135, 155, 234)' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
}
}</pre>
<p>Das ist fast das gleiche SVG, nur dass mit <code>fill='rgb(135, 155, 234)'</code> die Farbe hartkodiert wurde.
</p>
<p>Weniger elegant, aber letzten Endes zählt das Ergebnis. Und den Umbruch an der falschen Stelle vermeiden macht diese Lösung zu der zu bevorzugenden.</p>
<hr /><p class="wl_nobottom">Es dauerte ein bisschen, das passende CSS für dieses Designelement zu finden. Und natürlich hatte ich nicht im Voraus an den dunklen Modus oder das Printstylesheet gedacht. Aber das Rumschrauben am Blog machte mal wieder Freude und ich glaube, die entstandene Lösung sollte generell gut funktionieren. Rückmeldungen dazu sind immer willkommen.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/31636d84f2da4f53bb474bf9d6312b1f" width="1" height="1" alt="">
Mon, 08 Jul 2024 08:16:00 +0200https://www.onli-blogging.de/2384/guid.htmlPHP 5.6.40 von phpenv mit mod_php bauen lassen
https://www.onli-blogging.de/2344/PHP-5.6.40-von-phpenv-mit-mod_php-bauen-lassen.html
CodeLinuxhttps://www.onli-blogging.de/2344/PHP-5.6.40-von-phpenv-mit-mod_php-bauen-lassen.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=23442https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2344[email protected] (onli)
<p>Um alte PHP-Versionen auf moderne Li­nux-Dist­ri­bu­tion­en zu bekommen kann man <a href="https://github.com/phpbrew/phpbrew">phpbrew</a> oder <a href="https://github.com/phpenv/phpenv">phpenv</a> benutzen. Ich entschied mich für letzteres, weil <em>brew</em> mich glauben ließ, MacOS sei dort die Zielumgebung. Damit stand ich aber vor einem Problem, denn zum Bau des gebrauchten Apache-Mo­duls (mod_php) steht in der Readme derzeit nur das hier:</p>
<blockquote><p class="wl_notopbottom">Alternatively, you may still use the Apache php module by configuring php-build to build the libphp.so apache extension (directions to follow). libphp.so can then be found by apache under the <code>~/.phpenv/versions/$VERSION/libexec</code> folder. This file can be used for Apache's <code>LoadModule php5_module</code> directive and requires Apache to restart when changed.</p>
</blockquote>
<p><em>directions to follow</em> folgten nie, auch bei phpbuild fand sich nichts, außer Fehlerberichten. Aber es geht doch, nämlich so:</p>
<pre class="code">PHP_BUILD_APXS=/usr/bin/apxs PKG_CONFIG_PATH=$HOME/lib/openssl/lib/pkgconfig/ PHP_BUILD_CONFIGURE_OPTS="--with-icu-dir=/usr/local/icu-60 --with-openssl-dir=$HOME/lib/openssl/bin --with-apxs2" phpenv install 5.6.40</pre>
<p>Dieser Befehl baut die letzte PHP-5-Ver­sion, PHP 5.6.40, mit dem Modul für Apache. Man beachte die Zusätze: Weil die aktuelle Version von OpenSSL inkompatibel ist musste das vorher kompiliert werden, siehe <a href="https://stackoverflow.com/questions/72250611/rsa-sslv23-padding-undeclared-first-use-in-this-function-did-you-mean-rsa">hier</a>, und weil die aktuelle Version von icu inkompatibel ist musste noch dazu das kompiliert werden, siehe <a href="https://blog.hanhans.net/2020/06/08/php-compile-with-focal/">hier</a>. Bei letzterem hatte ich mir danach auch manuell einen Symlink angelegt, sodass mein Ubuntu 23.10 die <strong>.so</strong> finden konnte:</p>
<pre class="code">sudo ln -s /usr/local/icu-60/lib/libicudata.so.60 /usr/local/lib/</pre>
<p>Der obige php­env-Be­fehl packt das PHP-Modul leider nicht in den lib­exec-Ord­ner, sondern bricht vorher mit einem Fehler ab. Das war aber wohl der letzte Schritt, das Modul war komplett funktionsfähig. Es musste nur noch in der <strong>/etc/apache2/apache2.conf</strong> verlinkt und aktiviert werden, mit diesen Zeilen am Dateiende:</p>
<pre class="code"># Ersetze USER mit deinem Nutzernamen
LoadModule php5_module /home/USER/.phpenv/versions/5.6.40/usr/lib/apache2/modules/libphp5.so
AddType application/x-httpd-php .php</pre>
<p>Und nicht vergessen die .htaccess für den Zielordner zu aktivieren, in der <strong>/etc/apache2/sites-enabled/000-default.conf</strong>:</p>
<pre class="code"><VirtualHost *:80>
…
<Directory "/var/www/html">
AllowOverride All
</Directory>
</VirtualHost></pre>
<p>Damit das Modul mit Apache funktionierte musste ich dann auch noch den Ausführungsmodus von diesem ändern, das war:</p>
<pre class="code">sudo a2dismod mpm_event
sudo a2enmod mpm_prefork</pre>
<p>Da führen aber im Zweifel die Fehlermeldungen durch.
</p>
<p class="wl_nobottom">Das alles hat zwar funktioniert, aber jetzt am Ende zweifele ich, ob das oft eine gute Idee wäre. In meinem Fall machte so PHP 5 auf mein System zu bringen vieles einfacher, denn ich brauchte eine Kombination von dem alten PHP mit neuer Software und Datenbank für ein zweites System. Aber es wäre wohl generell geschickter, die kombinierte PHP-5/Apache­um­ge­bung in ein Dockerimage zu packen. Auf den ersten Blick sieht da <a href="https://github.com/alcalbg/docker-php5.6-apache">dieses Repo</a> sehr brauchbar aus, dessen Dockerfile Apache mit PHP 5 auf Debian stretch aufsetzt. Da dann das eigene PHP-Projekt hineinladen und man könnte sich das manuelle Kompilieren wahrscheinlich sparen. Nächstes mal wäre das mein Ansatz.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/612f634cc2ef4f30ac9134c6379a8ccf" width="1" height="1" alt="">
Tue, 13 Feb 2024 08:31:00 +0100https://www.onli-blogging.de/2344/guid.htmlEinfach- und Mehrfachfilter mit List.js
https://www.onli-blogging.de/2239/Einfach-und-Mehrfachfilter-mit-List.js.html
Codehttps://www.onli-blogging.de/2239/Einfach-und-Mehrfachfilter-mit-List.js.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=22393https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2239[email protected] (onli)
<p><a href="https://listjs.com/">List.js</a> ist ein netter Helfer, um Listen mittels Javascript durchsuch- und filterbar zu machen. Ich nutze es <a href="https://www.sustaphones.com/">auf sustaphones</a> für genau das.
</p>
<p>List.js’ Anwendung ist einfach zu verstehen. Du hast im HTML irgendeine List mit der id <em>target</em>. Was darin gefiltert werden soll bekommt Klassen, z.B. <code><span class="vendor">Der Wert</span></code>, deren Namen in ein options-Array gepackt und danach dem zu erstellendem List-Objekt übergeben werden:</p>
<pre class="code">var options = {
valueNames: [ 'devicename', 'vendor']
};
listObj = new List('target', options);</pre>
<p>Wenn jetzt auf <code>listObj</code> Funktionen wie <code>.search</code> oder <code>.filter</code> ausgeführt werden, passt das auch die HTML-Liste im Browser an, also was der Nutzer sieht. Beim Suchen via einem Texteingabefeld (hier mit der ID <em>search-field</em>) ist das trivial:</p>
<pre class="code">document.querySelector('#search-field').addEventListener('keyup', function(e) {
listObj.search(this.value);
});
</pre>
<p>Aber das Filtern ist etwas komplizierter.</p>
<h4>Einfachfilter
</h4>
<p>Für die verschiedenen Filter nehmen wir jeweils ein Select-Element:</p>
<pre class="code">
<select data-target="vendor">
<option>All</option>
<option>Vendor 1</option>
<option>Vendor 2</option>
</select></pre>
<p>Nun können wir auf ein Ändern der Auswahl reagieren und die Liste filtern:</p>
<pre class="code">
var filters = {};
document.querySelectorAll('select').forEach(function(select) {
select.addEventListener('change', function(e) {
if (e.target.selectedIndex === 0) {
// Der erste Eintrag wurde gewählt, also soll der Filter deaktiviert werden:
delete filters[select.dataset.target];
} else {
// Ansonsten setzen wir den Filter für "vendor" auf die gewählte Option:
filters[select.dataset.target] = e.target.value;
}
// Jetzt ist der Filter konfiguriert, wir können die Liste durchgehen und die Filter anwenden:
listObj.filter(function(item) {
for (var i=0; i < Object.keys(filters).length; i++) {
// Und hier ist die Hauptabfrage: Wenn der Wert ungleich ist geben wir false zurück, dadurch
// wird das Element aus der Liste entfernt
if (item.values()[Object.keys(filters)[i]].trim() != Object.values(filters)[i]) {
return false;
}
}
return true;
});
});
});</pre>
<h4>Mehrfachfilter
</h4>
<p>Mit dem oberen System können mehrere Filter gleichzeitig an sein, aber was ist, wenn mehrere Werte eines einzelnen Filters gleichzeitig angezeigt werden sollen? Das geht mit der obigen Lösung nicht – das Select-Element kann nur eine Option auswählen und der JS-Code macht einen Abgleich mit !=, erwartet also einen einzelnen String.
</p>
<p>Aber beides können wir ändern. <br />
Zuerst setzen wir das Select-Element auf Mehrfachauswahl:</p>
<pre class="code">
<select data-target="vendor" multiple>
<option>All</option>
<option>Vendor 1</option>
<option>Vendor 2</option>
</select>
</pre>
<p>Auf dem Desktop können jetzt Leute mit STRG + Klick mehrere Elemente auswählen, bei Mobilbrowsern hat das Select-Popup nun Checkboxen statt Radioboxen.
</p>
<p>Und beim JS-Code bauen wir uns ein Set und schauen, ob das jeweilige Element da drin ist:</p>
<pre class="code">var filters = {};
document.querySelectorAll('select').forEach(function(select) {
select.addEventListener('change', function(e) {
if (e.target.selectedOptions.length == 0 ||
(e.target.selectedOptions.length == 1 && e.target.selectedIndex === 0)) {
// Die Prüfung auf das erste "All" ist die zweite Bedingung. Ansonsten deaktivieren
// wir den Filter wenn die Auswahl leer ist
delete filters[select.dataset.target]
} else {
// Hier kommen alle aktive Elemente aus dem Select in ein Set (eine Liste ohne doppelte Werte)
filters[select.dataset.target] = new Set(Array.from(
e.target.selectedOptions).map(({ value }) => value)
);
}
listObj.filter(function(item) {
for (var i=0; i < Object.keys(filters).length; i++) {
// Bei der Filterung müssen wir jetzt nur prüfen, ob das Element im Set enthalten ist
if (! Object.values(filters)[i].has(item.values()[Object.keys(filters)[i]].trim())) {
return false;
}
}
return true;
});
});
});</pre>
<p>Hiermit sollten die Filter schon wie gewünscht funktionieren, sodass nach mehreren Werten für eine Kategorie gefiltert werden kann.</p>
<hr /><p class="wl_nobottom">Bei mir musste ich noch ein bisschen was drumrumstricken. So sehen diese Select-Element mit <code>multiple</code> etwas komisch aus, sodass ich sie erst nach Klick auf einen Button einblenden lasse. Und dieser Button brauchte dann einen Indikator um anzuzeigen, dass sein Filter aktiv ist. Aber die beste Lösung für solche Dinge wird in jedem Design anders sein, daher lasse ich das hier weg.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/81c4ed8d37954652840e88841a2a0be5" width="1" height="1" alt="">
Mon, 13 Feb 2023 08:56:00 +0100https://www.onli-blogging.de/2239/guid.htmlUnverstelltes Routing in Flutter: NamedRoutes mit Animationen
https://www.onli-blogging.de/2212/Unverstelltes-Routing-in-Flutter-NamedRoutes-mit-Animationen.html
Codehttps://www.onli-blogging.de/2212/Unverstelltes-Routing-in-Flutter-NamedRoutes-mit-Animationen.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=22123https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2212[email protected] (onli)
<p>Wie man in Flutter von einer Seite zur nächsten wechselt ist nicht ganz einfach, obwohl es eigentlich doch ganz simpel ist. Der Navigationsaufruf ist schnell geschrieben. Doch primär definiert man keine Seiten, sondern Widgets, und ob die jetzt alleine angezeigt werden oder nur als Teil eines anderen Widgets hängt einzig von ihrer Verwendung ab – schon das ist verwirrend. Dazu gibt es nicht den einen Weg, das Routing aufzusetzen, sondern viele: Das Flutter-Projekt selbst kennt mit dem <a href="https://api.flutter.dev/flutter/widgets/Navigator-class.html">Navigator</a> (1.0), dem <a href="https://api.flutter.dev/flutter/widgets/Router-class.html">Router</a> (2.0) und jetzt mit dem <a href="https://pub.dev/packages/go_router">go_router</a> (2.0+ ?) direkt drei offizielle Möglichkeiten, wobei sich hinter dem Navigator gleich mehrere Möglichkeiten verbergen und der Router 2.0 komplett vage ist. Dazu kommen die vielen Routing-Plugins auf pub.dev, alle mit ihren eigenen Vor- und Nachteilen.
</p>
<p>Dieser Artikel wird nicht alle diese Möglichkeiten vorstellen. Stattdessen zeige ich einen Weg, wie man völlig ohne Plugins strukturiert die Routen anlegen und ihre Übergänge animieren kann. Dazu gehört <a href="https://github.com/onli/flutter_navigation">dieses Git-Repo</a>, in dem der Code komplett nachvollzogen werden kann.</p>
<h4>Das Grundlagenbeispiel
</h4>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/flutter_main.png'><!-- s9ymdb:1653 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/flutter_main.png 720w" src="https://www.onli-blogging.de/uploads/flutter_main.serendipityThumb.png" loading="lazy" alt=""></a>
</p>
<p>Ich werde hier mit einer einfachen Flutter-Anwendung mit einer <strong>main.dart</strong> und drei Widgets <strong>view[123].dart</strong> benutzen. Die drei Widgets sollen jeweils als eigene Seiten aufgerufen werden. Sie sind so definiert:</p>
<pre class="code">import 'package:flutter/material.dart';
class View2 extends StatelessWidget {
const View2({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('View 2')),
body: const Center(
child: Text('View 2'),
),
);
}
}</pre>
<p>Und der Startbildschirm zeigt drei Buttons untereinander in einer Reihe:</p>
<pre class="code">class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => null,
child: const Text('View 1')),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => null,
child: const Text('View 2')),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () => null,
child: const Text('View 3')),
),
],
),
);
}
}</pre>
<h4>v0.1: Simple NamedRoutes
</h4>
<p>Diese drei Buttons sollen nun jeweils zu ihrem Widget navigieren.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/flutter_view2.png'><!-- s9ymdb:1654 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/flutter_view2.png 720w" src="https://www.onli-blogging.de/uploads/flutter_view2.serendipityThumb.png" loading="lazy" alt=""></a>
</p>
<p>Schauen wir uns also erstmal an, <a href="https://docs.flutter.dev/cookbook/navigation/named-routes">wie einfache NamedRoutes funktionieren</a>. Den Namen entsprechend bekommt hier eine Route einen Namen und wird darüber aufgerufen, ähnlich einer URL. Man erstellt dafür eine <strong>routes.dart</strong> mit einem solchen Inhalt:</p>
<pre class="code">import 'package:flutter/material.dart';
import 'package:flutter_navigation/view1.dart';
import 'package:flutter_navigation/view2.dart';
import 'package:flutter_navigation/view3.dart';
final routes = {
'/view1': (BuildContext context) => const View1(),
'/view2': (BuildContext context) => const View2(),
'/view3': (BuildContext context) => const View3(),
};
</pre>
<p>Die muss nun der Flutteranwendung zugewiesen werden:</p>
<pre class="code">class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
routes: routes, // genau hier
);
}
}</pre>
<p>Und schon können die Buttons eine Funktion bekommen:</p>
<pre class="code">ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/view2'),
child: const Text('View 2'),
)</pre>
<p><code>Navigator.of(context)</code> holt sich den Navigator, <code>pushNamed</code> navigiert zum zuvor angelegten Widget. Mit einem <code>Navigator.of(context).pop()</code> würde man wieder zurückkommen, es gibt hier also einen Navigations-Stack.</p>
<h4>v0.2: NamedRoutes mit Argumenten
</h4>
<p>Was aber, wenn bei der Navigation auch Daten an das Widget gegeben werden sollen?
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/flutter_view_dynamic.png'><!-- s9ymdb:1655 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/flutter_view_dynamic.png 720w" src="https://www.onli-blogging.de/uploads/flutter_view_dynamic.serendipityThumb.png" loading="lazy" alt=""></a>
</p>
<p>Dafür gibt es Argumente. Um genau zu sein gibt es ein Arguments-Objekt, in das alles beliebige gespeichert werden kann. Zum Beispiel hier eine Map mit einem String, den das Widget dann anzeigen soll:</p>
<pre class="code">ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/view1',
arguments: {
'content': 'Dynamic text',
}),</pre>
<p>Damit das Widget es auch bekommt, müssen wir seine Route in der <strong>routes.dart</strong> ändern:</p>
<pre class="code">'/view1': (BuildContext context) {
final args =
ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return View1(
content: args['content']!,
);
},</pre>
<p>Wir machen uns hier zunutze, dass Dart (zumindest seit der Version für Flutter 3) mit den Typen recht flexibel umgehen kann, sodass <code>content</code> nicht manuell in einen String umgewandelt werden muss. Das Widget kann mit dem Parameter direkt arbeiten:</p>
<pre class="code">import 'package:flutter/material.dart';
class View1 extends StatelessWidget {
final String content;
const View1({super.key, required this.content});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('View 1'),
),
body: Center(
child: Text(content),
),
);
}
}</pre>
<p>Das Widget kann nun dynamisch bei jedem Navigationsaufruf mit einem anderen Text befüllt werden.
</p>
<p>Schön an diesem Ansatz ist der geringe Aufwand. Es braucht keine eigene Klasse für die Widget-Argumente, weil sie einfach in eine Map gepackt werden. Und da bei der Map der Value-Typ auf <code>dynamic</code> gesetzt wurde kann beliebiges übertragen werden. Eine automatische Codegenerierung wie bei <a href="https://pub.dev/packages/auto_route">auto_route</a> hier draufzusetzen scheint unnötig.</p>
<h4>v0.3: NamedRoutes mit wählbaren Animationen (und Argumenten)
</h4>
<p>Die Animationen anpassen zu können dagegen wird für manche Anwendungen nötig sein. Ich zeige einen erweiterbaren Ansatz. Er wird mit <code>onGenerateRoute</code> der MaterialApp arbeiten.
</p>
<p><video controls width=250><source src="https://www.onli-blogging.de/uploads/animated_transitions.webm" type="video/webm"><a class="block_level opens_window" href="https://www.onli-blogging.de/uploads/animated_transitions.webm" title="animated_transitions.webm"><!-- s9ymdb:1656 -->animated_transitions.webm</a></video>
</p>
<p><br />
Das ist jetzt viel auf einmal:</p>
<pre class="code">return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
onGenerateRoute: (settings) {
if (routes.containsKey(settings.name)) {
final args = settings.arguments as Map<String, dynamic>?;
return PageRouteBuilder(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) =>
routes[settings.name]!(context),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
switch (args?['transition']) {
case Transitions.scale:
return ScaleTransition(scale: animation, child: child);
case Transitions.fade:
return FadeTransition(opacity: animation, child: child);
default:
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
child: child,
);
}
});
}
// Unknown route
return MaterialPageRoute(builder: (context) => Container());
},
);</pre>
<p>Also, was ist hier passiert:
</p>
<p>Zuerst wurde der <code>routes:</code>-Parameter entfernt. Wäre er noch da, hätte er Priorität und unser neuer Code in <code>onGenerateRoute</code> würde ignoriert.
</p>
<p>Der nächste Kniff ist <code>pageBuilder: (context, animation, secondaryAnimation) => routes[settings.name]!(context)</code>. Hiermit wird beim Seitenbau in <code>routes</code> nach einer Route gesucht. So kann die alte <strong>routes.dart</strong> weiterbenutzt werden, sie muss nichtmal editiert werden. Gut so, denn dadurch behalten wir einen festen Ort für übersichtliche Routendefinitionen.
</p>
<p>Es folgt der <code>transitionsBuilder</code>. Der schaut, ob bei dieser Navigation das Argument <code>transition</code> übergeben wurde. Wenn ja und es einen bestimmten Wert hat, wird eine der vordefinierten Übergangsanimationen gesetzt. Dafür wurde ein bislang nicht gezeigtes Enum angelegt:</p>
<pre class="code">enum Transitions { scale, fade }</pre>
<p>Die Navigation mit Animationsauswahl sähe nun nicht viel anders aus als zuvor:</p>
<pre class="code">ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/view1',
arguments: {
'content': 'Dynamic text',
'transition': Transitions.scale
}),
child: const Text('View 1')),
),</pre>
<p>Das ginge natürlich auch anders – so hatte ich eine Variante mit dem Plugin <a href="https://pub.dev/packages/page_transition">page_transition</a> <a href="https://github.com/onli/flutter_navigation/commit/631d72269e5590a2cf956a8f5f62c807222e77df">entwickelt</a>, die brauchte aber Änderungen an der <strong>routes.dart</strong>.</p>
<hr /><p>Dieser Artikel und Ansatz ist ein Nebenprodukt meiner Analyse von Flutters Rou­ting­si­tu­ation. Die vielen Lösungen waren chaotisch präsentiert, viele erschienen mir auch unnötig kompliziert. NamedR­outes stachen direkt als klar verständliche Lösung heraus, aber wie sie gut zu benutzen sind, samt Argumenten und Animationen, sah ich nicht erklärt. Ob sie dazu überhaupt taugen? Sie tun es, wie hoffentlich deutlich wurde.
</p>
<p class="wl_nobottom"><a href="https://docs.flutter.dev/development/ui/navigation#limitations">Laut Dokumentation</a> sollen Named­Routes Nachteile haben – bei Push-Be­nach­rich­ti­gung­en würden sie immer ihr Ziel öffnen, selbst wenn es schon offen sei (ist das in der on­Ge­ner­ate­Route wirklich nicht abfangbar?) und bei Webanwendungen als Kompilierziel der Vorwärtsbutton im Browser mit ihnen nicht funktionieren (kein Problem für mich, da ich Flutter für Webanwendungen ungeeignet finde). Wer darüber stolpern würde sollte sich wahrscheinlich als zweitbeste Lösung <a href="https://pub.dev/packages/go_router">den go_router</a> ansehen.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/aa53eb28b6c5484da9dac53f3c698bf6" width="1" height="1" alt="">
Tue, 15 Nov 2022 07:26:00 +0100https://www.onli-blogging.de/2212/guid.htmlflutterEin Jahr mit Flutter
https://www.onli-blogging.de/2076/Ein-Jahr-mit-Flutter.html
Codehttps://www.onli-blogging.de/2076/Ein-Jahr-mit-Flutter.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=20762https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2076[email protected] (onli)
<p>Gut ein Jahr ist es her, dass ich angefangen habe mit <a href="https://flutter.dev/">Flutter</a> zu arbeiten. Der Auftrag: Eine nutzerfreundliche Anwendung für Android und iOS mit einer gemeinsamen Codebasis zu bauen. Ich fand das Framework zur Mobilanwendungsentwicklung <a href="https://www.onli-blogging.de/1978/Flutter-Ein-tolles-Framework-fuer-mobile-Apps.html">zu Beginn</a> ziemlich toll. Wie sieht es jetzt aus?
</p>
<p>Ich finde Flutter immer noch gut, aber habe inzwischen auch mehr von den Schwachstellen mitbekommen. Eine Liste der positiven und der negativen Erkenntnisse folgt.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/good.webp'><!-- s9ymdb:1414 --><img class="serendipity_image_center" width="800" height="533" srcset="https://www.onli-blogging.de/uploads/good.1200W.serendipityThumb.webp 2600w,https://www.onli-blogging.de/uploads/good.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/good.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/good.serendipityThumb.webp" loading="lazy" alt=""></a></p>
<h4>Gut: Der Widgetbaum
</h4>
<p>Bei Flutter baut man die Oberfläche, indem die von der Sprache bereitgestellten Widgets in der Build-Funk­tion eigener Widgets kombiniert werden. Da hat dann eine Row zwei Columns, in denen jeweils drei Buttons sind, schon hat man ein Grid von Buttons mit zwei Spalten und drei Zeilen. Angelehnt am Ma­te­rial-De­sign gibt es alle wichtigen Widgets vordefiniert, wenn das nicht reicht können aus ihnen neue gebaut oder mit Plugins fertige eingebunden werden.
</p>
<p>Das funktioniert einfach sehr gut. Was in anderen Programmiersprachen in den imperativen Aufrufen unweigerlich zum Chaos wird, bleibt mit diesem Prinzip mit nur wenig benötigter Disziplin gut beherrschbar. Die so entstehenden UI-Files sind viel lesbarer und doch auch mächtiger, als wenn es XML-Da­tei­en wären, aber die Zustandsverwaltung und damit die Logik der App soll eindeutig extern definiert werden. Diese propagierte Trennung funktioniert gut, kann für einfache Screens aber trotzdem ignoriert werden, die Logik wird dann mit in die UI gepackt. Aber das macht man selten. Das ist genau die richtige Mischung aus einem guten Konzept und der benötigten Entwicklerfreiheit.</p>
<h4>Gut: Zustandsverwaltung mit GetX
</h4>
<p>Als ich anfing war <a href="https://pub.dev/packages/get">GetX</a> bei uns umstritten. Ich hatte mir das und als Alternative <a href="https://pub.dev/packages/flutter_bloc">Bloc</a> angesehen und dann darauf bestanden, dass wir GetX zumindest auch benutzen. Mittlerweile hat GetX sich voll bewährt.
</p>
<p>Mit GetX definiert man im Controller der UI spezielle Variablen. Wenn eine davon sich ändert, ändern sich auch die UI-Stelle an denen die Variable verwendet wird. Zum Beispiel hat man im Controller ein Set, baut in der UI daraus eine Liste, und wenn das Set ein Element mehr bekommt wird auch die UI-Liste länger. Das gelingt GetX auf der einen Seite mit möglichst wenig Deklarationen, aber gleichzeitig ist der Voo­doo-As­pekt viel geringer als bei Bloc. Dass es keine unnötigen abstrakten Konzepte wie Cubits hat kommt noch dazu.
</p>
<p>GetX hat ein paar Stolperfallen, so erkennt es nicht automatisch wenn sich der Inhalt eines Elements des Sets ändert. Da muss man nachhelfen, aber immerhin geht das. Insgesamt ermöglicht es eine effiziente Anwendungsentwicklung ohne Bullshit.</p>
<h4>Gut: Die Pluginauswahl
</h4>
<p>Mit <a href="https://pub.dev/">pub.dev</a> gibt es eine klare und hilfreiche Anlaufstelle für Plugins. Dort gibt es Plugins für alles was wir bisher gebraucht haben. Einen kompletten Kalender, Kryptographie, Loginformular, Benachrichtigungen, das oben erwähnte GetX – noch viel mehr und insgesamt alles wurde abgedeckt, mit oft hochqualitativ wirkenden und konfigurierbaren Plugins.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/flutter_pub_dev.png'><!-- s9ymdb:1413 --><img class="serendipity_image_center" width="800" height="445" srcset="https://www.onli-blogging.de/uploads/flutter_pub_dev.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/flutter_pub_dev.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/flutter_pub_dev.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/flutter_pub_dev.serendipityThumb.png" loading="lazy" alt=""></a>
</p>
<p>Diese zentrale Quelle zu haben ist viel besser als die mühsame Li­bra­ry-Su­che bei Android und auch besser als bei allen anderen Programmiersprachen bzw Frameworks, die ich kenne.</p>
<h4>Gut: Dart
</h4>
<p><a href="https://dart.dev/">Dart</a> hat mich nicht enttäuscht. Ich erwartete das anfangs: Zu hübsch und trotz der Typisierung Ruby-ar­tig war diese Sprache, das konnte nicht wahr sein. Aber es ist wahr. Dart ist einfach ganz wunderbar.
</p>
<p>Nicht so dynamisch wie Ruby zu sein schadet ihr an manchen Stellen, vor allem bei der fehlenden Serialisierbarkeit und dem immer noch äußerst umständlichen Umwandeln von Objekten nach JSON (und wieder zurück) schlägt das zu. Aber das erwartete überraschende Fehlverhalten, die frusterregenden Implementierungsfehler – all das blieb aus. Dart würde ich sogar außerhalb von Flutter verwenden, zumindest wenn Ruby nicht verfügbar wäre; vielleicht sogar wenn Ruby verfügbar wäre.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/bad.webp'><!-- s9ymdb:1415 --><img class="serendipity_image_center" width="800" height="533" srcset="https://www.onli-blogging.de/uploads/bad.1200W.serendipityThumb.webp 2600w,https://www.onli-blogging.de/uploads/bad.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/bad.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/bad.serendipityThumb.webp" loading="lazy" alt=""></a></p>
<h4>Schlecht: Respektlose Weiterentwicklung
</h4>
<p>Das größte Problem Flutters ist die respektlose Weiterentwicklung des Frameworks, daher fange ich damit an. Es fehlt den Goo­gle-Ent­wick­lern jeglicher Respekt vor der Zeit der Frameworknutzer, was sich in unverschämten und unnötigen Änderungen und völliger Missachtung der Abwärtskompatibilität niederschlägt.
</p>
<p>Flutter ist in diesem Jahr von 1.x auf 2.x geklettert. Die große – aber nicht die einzige – Änderung war Null–Safe­ty, was theoretisch optional bleibt aber faktisch durch Plugins erzwungen wird. Null-Safe­ty bedeutet, dass Variablen nicht mehr <code>null</code> werden können, sind sie nicht entsprechend deklariert worden. <code>int? x</code> darf ich auf null setzen – die ?-Syn­tax-De­kla­ra­tion ist neu –, <code>int x</code> nicht. Entsprechend muss all der Code der mit einem vorher definierten <code>int x</code> arbeitet angepasst werden, sodass er niemals x ein null zuweisen kann, oder die Deklaration angepasst und dann der Zustand von x an den Schnittstellen zu anderem null-sich­eren Code überprüft werden.
</p>
<p>Das vermeidet Abstürze, aber es ist ein Riesenwechsel für bestehende Anwendungen. Gut, es gibt <a href="https://dart.dev/null-safety/migration-guide#migration-tool">ein Migrationstool</a> – aber es funktioniert nicht. Schon bei unserer vergleichsweise simplen App war es überfordert und quittierte nach Tausend Fehlermeldungen den Dienste. Ich habe die Migration komplett per Hand machen müssen.
</p>
<p>Es ist auf der einen Seite verständlich, dass ein junges Framework sich noch ändert. Aber alten Code so unbrauchbar zu machen, Migrationstools beiseite zu stellen die schlicht nicht funktionieren, dafür gibt es keine Entschuldigung.
</p>
<p>Und bei dieser einen Änderung bleibt es ja nicht. Im gleichen Atemzug wurden beispielsweise auch einfach mal die bisherigen Buttonklassen <a href="https://flutter.dev/docs/release/breaking-changes/buttons">deprecated</a>. Und die neuen haben eine völlig andere API, sie werden mit viel kompliziertem Code angepasst. Es ist okay ein neues System für Buttons dem Framework hinzufügen, aber die alten abzuschaffen ist eine völlig unnötige Gängelung.
</p>
<p>Hier schlägt das System Google durch: Tausende Entwickler arbeiten am Framework, die kleinen frameworknutzenden Entwicklerstudios verbringen dann ihre Zeit damit den dauernden Änderungen hinterherzurennen. Das ist einerseits monopolsichernd und andererseits ein Strukturproblem der Organisation Google, aber die Ursache zu verstehen hilft in der Praxis wenig. <br />
Schon deswegen würde ich Flutter nicht für ein FOSS-Pro­jekt empfehlen, es ungern selbst in meiner Freizeit nutzen, sogar Firmen gegen die Nutzung raten. Wobei: Android und iOS nativ zu bespielen ist auch nicht besser, wenn eine Mobilanwendung wirklich sein muss ist Flutter immer noch besser als das. Aber was für eine vertane Chance Flutter doch ist, nur durch schlechtes Goo­gle-Pro­jekt­ma­nage­ment.</p>
<h4>Schlecht: Die Pluginweiterentwicklung
</h4>
<p>Was für die Sprache gilt, gilt für die Plugins nur noch mehr: Da wird in einem durch fröhlich alter Code kaputtgemacht. Es ist zwar alles auf dem Papier <a href="https://semver.org/lang/de/">SemVer</a>, doch in der Praxis hält das die wenigsten Entwickler davon ab mit Patch- und Funktionsupdates alten Code invalid zu machen. Migrationsguides sind dann unvollständig und <a href="https://github.com/aleksanderwozniak/table_calendar/issues/153#issuecomment-772881560">versteckt in Git­hub-Iss­ues</a>, eine <a href="https://github.com/dint-dev/cryptography">Kryptolibrary</a> kann den eigenen Ciphertext nicht mehr lesen, ein Router (ein Modul um die Ansicht zu wechseln) wird so vollständig umgebaut dass jeder Aufruf angepasst werden muss, ohne dass die Dokumentation das <a href="https://github.com/Milad-Akarie/auto_route_library/issues/498">auch nur erwähnt</a>. Und spätestens durch Null-Safe­ty wurden die Upgrades nicht-op­tio­nal, man kann sich ihnen nicht entziehen.
</p>
<p>Wenn man es vorher wüsste würde man nur Abhängigkeiten auf Module vernünftiger Entwickler aufbauen. Aber das sieht man im Vorhinein eben nicht immer. Und im Flutterland scheinen die besonders selten zu sein. Vielleicht kommt das aus der früheren Java­script-Her­kunft.</p>
<h4>Schlecht: Kompilierzeiten und -aufwand
</h4>
<p>Meine Firma macht die Flutterentwicklung mit Visual Studio Code und hat mir ein nettes Laptop gestellt, mit einem Ryzen 7 3700U und 16GB Ram. Das reichte nicht. Ich brauchte zuerst ein Ramupgrade, um beim Kompilieren (mit Emulator und Browser offen, klar) nicht immer wieder ein einfrierendes System zu haben. Und trotz dem modernen und starken Prozessor (und natürlich einer SSD) dauert das Bauen der Androidanwendung inzwischen einfach lange. Schon eher Minuten als Sekunden. Und dabei ist die von uns gebaute App nicht riesig.
</p>
<p>Es gibt zwar wie bei Android Studio ein <em>Hot Reload</em>, aber das bringt bei Änderungen der Controller wenig, denn die bleiben bei ihrem alten Zustand. Das genügt also nur in den seltensten Fällen, daher wird viel kompiliert und dabei gewartet. Ich müsste im Grunde an einer Thread­rip­per-Work­sta­tion arbeiten. Das macht bei meiner Dauer-Heim­ar­beit ja sogar Sinn, nur war die ungeplant und sollte das für die Mobilanwendungsentwicklung auch einfach nicht nötig sein. </p>
<h4>Schlecht: Webapps
</h4>
<p>Flutter kann auch Webapps bauen, aber das Ergebnis sind Javascriptmonster die sich an keine Webkonventionen halten. Nutzer können nichtmal Text kopieren, wird das Stand­ard-Text­wid­get benutzt. Völlig unverständlich, solche Macken blockieren komplett die Adaption des Frameworks im Webbereich.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/ambivalent.webp'><!-- s9ymdb:1416 --><img class="serendipity_image_center" width="800" height="600" srcset="https://www.onli-blogging.de/uploads/ambivalent.1200W.serendipityThumb.webp 2600w,https://www.onli-blogging.de/uploads/ambivalent.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/ambivalent.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/ambivalent.serendipityThumb.webp" loading="lazy" alt=""></a></p>
<h4>Ambivalent: Limitierungen der Mobilplattformen bleiben erhalten
</h4>
<p>Genug von den klar negativen Eigenschaften. So wie Flutter gute und schlechte Seiten hat, sind manche Eigenschaften des Frameworks weder klar negativ noch positiv.
</p>
<p>So umschifft Flutter die tieferen Limitierungen der mobilen Betriebssysteme einfach genau gar nicht. Arbeiten im Hintergrund beispielsweise geht unter Android manchmal, unter iOS praktisch nicht. Aber es das Framework hat keine Infrastruktur, um das doch zuverlässig zu können, nichtmal unter dem eigenen Betriebssystem Android. Anderes Beispiel: Geplante Benachrichtigungen beschränkt iOS auf 64, Android auf mehrere Hundert, Flutter und seine Plugins lässt den Entwickler in diesen Unterschied schlicht reinrennen. Unter Android kann man aus einem Formular Zurück-Swip­en und dann in einem Dialog vor verlorengehenden Daten warnen, unter iOS geht das nicht. 2018 hat ein Entwickler das Problem <a href="https://github.com/flutter/flutter/issues/14203#issuecomment-359572676">nicht verstanden</a>, seitdem ist es im Kern ungefixt.
</p>
<p>Wäre Flutter nicht von Google könnte man dem Framework solche Limitierungen nur schwer anlasten. Aber weil Google mit seiner Größe und Rolle als Androidentwickler die Möglichkeiten hätte hier Lösungen anzubieten, kann ich das dem Framework nicht einfach durchgehen lassen. Es stellt zwar in manchen Bereichen eine brauchbare gemeinsame Basis für die Entwicklung von iOS- wie Androidanwendungen her, das ist der positive Teil. Aber es übertüncht die Unterschiede ungenügend, aber macht es dabei trotzdem schwerer die Stärken von Android zu nutzen, wie dessen eigentlich funktionierende Hintergrundthreads via <a href="https://developer.android.com/topic/libraries/architecture/workmanager">WorkManager</a> oder Vorgängerlösungen. Und das macht Flutters Vorzüge etwas kaputt.</p>
<h4>Ambivalent: Die deklarative Widgetverwaltung
</h4>
<p>In Flutter werden Widgets einmal deklariert, dabei konfiguriert, später reagieren sie nur noch auf Interaktionen vom Nutzer oder Zustandsänderungen. Das ermöglicht den oben gelobten expliziten Widgetbaum, das ist positiv. Aber Widgets so gar nicht imperativ manipulieren zu können ist manchmal hochproblematisch. Wie schließt man zum Beispiel auf Knopfdruck das geöffnete Overlay eines Au­to­com­plete-Wid­gets? Nur durch einen Hack, indem man mit <a href="https://api.flutter.dev/flutter/widgets/FocusManager-class.html">dem FocusManager</a> dem Widget den Fokus entzieht.
</p>
<p>Andere Dinge gehen manchmal einfach gar nicht. Hier müssten die Entwickler eine bessere Mischung zwischen Konzepttreue und Entwicklerbefähigung finden.</p>
<h4>Ambivalent: Performance
</h4>
<p>Flutter <a href="https://flutter.dev/docs/perf/rendering/ui-performance">will</a> Anwendungen mit 60 FPS zeichnen, inzwischen sogar mit 120 FPS auf entsprechenden Geräten. Was ist mit 360 FPS bei Webanwendungen? Okay, bleiben wir fair. Aber auch das erklärte 60-FPS-Ziel ist illusorisch, wenn beim ersten Öffnen einer App es an allen Ecken und Enden laggt. Da müssen z.B. noch die Shader kompiliert werden, was gerade unter iOS massiv zu Einschränken führt. Erst die <a href="https://medium.com/flutter/whats-new-in-flutter-2-5-6f080c3f3dc">gerade veröffentlichte Version 2.5</a> schafft da vielleicht Abhilfe.
</p>
<p>Es stimmt, dass die gebauten Oberflächen ansonsten meist performant laufen. Deswegen ist der Punkt nicht unter den schlechten Seiten eingeordnet. Aber können das nicht im Grunde alle Frameworks zur mobilen Appentwicklung? Sobald etwas im Hintergrund rennt muss das auch bei Flutter manuell in einen Thread geschoben werden, was nicht immer geht, dazu kommen dann die Shader-Stot­ter. Vielleicht bewerte ich hier Flutter sogar zu positiv.</p>
<h4>Ambivalent: Die Testumgebung
</h4>
<p>Und schließlich ist da noch die Testumgebung. Flutter kann Unit-, Widget- und nun auch Integrationstests. Bei Unittests sollen einzelne Bestandteile des Codes getestet werden, mit Widgettests der gewünschte Aufbau der UI kontrolliert, mit Integrationtests der Ablauf der kompletten App. Das ist alles eingebaut und Tests zu schreiben ist mit dem System keine Qual, immerhin.
</p>
<p>Aber andererseits sind die Testumgebungen für Unit- und Widgetttests lächerlich beschränkt. Unittests können im Grunde nichts machen außer lokal Dart-Code ausführen, aber nicht mit dem System interagieren, keine HTTP-Re­quests senden. Soll eine API-kon­su­mier­ende Funktion getestet werden darf man die API dann mocken. Es gibt zwar Leute, die das für die richtige Art halten Unit-Tests zu schreiben. Aber ich halte das für eine irrige Position, denn es führt zu einen riesigen Aufwand und verhindert, dass Tests echte Fehlerfälle erkennen. <br />
Bei bei den Integrationtests gibt es diese Beschränkungen zwar nicht, aber dafür gibt es sie erst seit kurzem. Integrationtests funktionieren erst seit einem Umbau des Systems, der erst jetzt fertig wurde. Davor waren sie undokumentierterweise einfach kaputt, ich zumindest kriegte sie partout nicht zum Laufen. Und das ging wohl vielen so. Sie müssen auch immer noch auf simulierten oder echten Geräten laufen, was in einer CI-In­fra­struk­tur aufwendig und damit im Zweifel teuer ist.
</p>
<p>Flutter ermöglicht das Testen des Codes. Vielleicht macht es das besser als andere Frameworks in dem Bereich. Aber es ist viel komplizierter als ideal.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/fluttersite.png'><!-- s9ymdb:1417 --><img class="serendipity_image_center" width="800" height="445" srcset="https://www.onli-blogging.de/uploads/fluttersite.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/fluttersite.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/fluttersite.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/fluttersite.serendipityThumb.png" loading="lazy" alt=""></a></p>
<h4>Fazit: Die rosa Brille ist weg
</h4>
<p>Ich mag Flutter immer noch. Man muss sich vor Augen halten, wie hochproblematisch die native Anwendungsentwicklung sein kann, schon wirken die Probleme des Frameworks weniger gewichtig. Dart ist sogar toll. Insgesamt schafft mein kleines Team tolle Arbeit und auch ich fühle mich mit Flutter produktiv. Meine Nutzertests bestätigen den positiven Eindruck des Ergebnis, das war dann sogar großartig.
</p>
<p>Aber es ist dann eben doch ein Goo­gle-Frame­work, seine hässlichen Seiten passen genau dazu. Serendipity versucht wahrscheinlich seit fast 20 Jahren mit einem Mini-Team Abwärtskompatibilität beizubehalten, bei Flutter kriegt ein Milliardenunternehmen es nicht mein erstes aktive Jahr hin.
</p>
<p>Auch die Stärken passen: Cross-Platt­form­an­wen­dung­en mit wenig Aufwand und einem so guten Konzept bauen zu können passt zu der technischen Leistungsfähigkeit dieses Unternehmens. Die sieht man am Anfang, daher damals mein erster sehr positiver Eindruck. Die Schwächen sieht man dann später, daher mein jetzt etwas durchwachsenerer.
</p>
<p class="wl_nobottom">Ich hoffe, meine Bewertung kann dem einen oder anderen Leser bei seiner Entscheidung helfen, ob er Flutter verwenden sollte oder nicht. Ich möchte wirklich nicht zu stark von abraten – die beworbenen Stärken sind ja da. Nur sollte man sich der Schwächen des Frameworks ebenfalls bewusst sein.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/2e2c6b86fb924788a45dea073b4f0015" width="1" height="1" alt="">
Mon, 11 Oct 2021 08:38:00 +0200https://www.onli-blogging.de/2076/guid.htmlflutterYouTube-Videos einbinden, ohne dass die Seite lahm wird (+Serendipity-Plugin)
https://www.onli-blogging.de/2040/YouTube-Videos-einbinden,-ohne-dass-die-Seite-lahm-wird-+Serendipity-Plugin.html
Codehttps://www.onli-blogging.de/2040/YouTube-Videos-einbinden,-ohne-dass-die-Seite-lahm-wird-+Serendipity-Plugin.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=20402https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=2040[email protected] (onli)
<p>Wenn mehrere YouTube-Videos auf einer Seite landen wird diese ziemlich schwer. Zumindest, wenn man den Einbindungscode nutzt den YouTube selbst vorschlägt. Da das hier im Blog schnell mal passiert, so wie jetzt, da mehrere Artikel mit Videos auf der Hauptseite sind, habe ich mich nach Alternativen umgesehen.
</p>
<p>Normalerweise sieht das Iframe so aus:</p>
<pre class="code"><iframe width="560" height="315" src="https://www.youtube.com/embed/XhG-4zdVx0I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></pre>
<p>Auf den ersten Blick harmlos. Das Problem ist all das Javascript, das in diesem Iframe dann bezogen wird. Was sofort geschieht, da nach dem Laden die Iframe-Zielseite sofort den Player initialisiert.
</p>
<p><a href="https://dev.to/haggen/lazy-load-embedded-youtube-videos-520g">Auf dev.to</a> schlug Arthur vor, stattdessen ein alternatives Iframe zu nutzen:</p>
<pre class="code"><iframe
width="560"
height="315"
src="https://www.youtube.com/embed/XhG-4zdVx0I"
srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube.com/embed/XhG-4zdVx0I ?autoplay=1><img src=https://img.youtube.com/vi/XhG-4zdVx0I/hqdefault.jpg><span>▶</span></a>"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe></pre>
<p>Das ist schon von mir leicht verändert und der Artikel beschreibt verschiedene Zwischenschritte auf dem Weg zu dieser Lösung. Schaut euch das ruhig im Originalartikel an.
</p>
<p>Aber die Kernidee ist, dass in dem Iframe der Videoplayer erst nach einem Klick auf das Vorschaubild geladen werden wird. Statt einem halben MB an Code ein kleines Bildchen zu laden geht beim ersten Laden der Seite wesentlich schneller. Und da das Video nach einem Klick wie zuvor sofort startet, dank dem <code>autoplay=1</code>, wird dem Seitenbesucher der Unterschied kaum auffallen. Denn das ist das Ergebnis:
</p>
<p><iframe
width="560"
height="315"
src="https://www.youtube-nocookie.com/embed/XhG-4zdVx0I"
srcdoc="<style>*{padding:0;margin:0;overflow:hidden}html,body{height:100%}img,span{position:absolute;width:100%;height:auto;top:0;bottom:0;margin:auto}span{height:1.5em;text-align:center;font:48px/1.5 sans-serif;color:white;text-shadow:0 0 0.5em black}</style><a href=https://www.youtube-nocookie.com/embed/XhG-4zdVx0I?autoplay=1><img width=560 height=420 loading=lazy src=https://www.onli-blogging.de/index.php?/plugin/lazyoutubefetch_XhG-4zdVx0I><span>▶</span></a>"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe></p>
<h4>Einfluss auf die Seitenperformance
</h4>
<p>Ich stolperte über diese Baustelle, als ich die neue Version <a href="https://developers.google.com/speed/pagespeed/insights/">Googles Pagespeed</a> testete. Beim Prüfen dieses Blogs hier erschrak ich über das miserable Ergebnis:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_normal_tiny.png'><!-- s9ymdb:1313 --><img class="serendipity_image_center" width="1663" height="925" srcset="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_normal_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_normal_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_normal_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_normal_tiny.png" alt=""></a>
</p>
<p>Gut, die Mobilvariante des Tools ist häufig sehr hart, aber so schlecht hatte ich meinen Blog nicht gesehen. Ein Blick auf die Problemliste zeigte dann deutlich, dass Youtube-Videos die Ursache sind:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_cause_tiny.png'><!-- s9ymdb:1312 --><img class="serendipity_image_center" width="1663" height="925" srcset="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_cause_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_cause_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_cause_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_cause_tiny.png" alt=""></a>
</p>
<p>Was schlicht daran liegt, dass derzeit mehrere Artikel mit Videos von Youtube hier auf der Startseite sind.
</p>
<p>So sieht das Ergebnis jetzt aus, da alle diese Videos mit den neuen Iframes ersetzt sind:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_optimized_tiny.png'><!-- s9ymdb:1314 --><img class="serendipity_image_center" width="1663" height="925" srcset="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_optimized_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_optimized_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_optimized_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/lazyoutube/PageSpeed_Insights_Youtube_optimized_tiny.png" alt=""></a>
</p>
<p>Viel besser! Sogar für Telefone wird die Geschwindigkeit der Seite als hervorragend bewertet. Wie es bei einem einfachen Blog ja auch sein soll.</p>
<h4>Serendipity-Plugin
</h4>
<p>Ich wollte die regulären Iframes nicht per Hand ersetzen. Und generell wollte ich den originalen Code beibehalten. Ich gehe davon aus, dass die Kompatibilität mit dem derzeitigen Einbindungscode fast für ewig erhalten bleiben wird. Während bei dem angepassten Iframe Annahmen drin sind die sich ändern könnten, vor allem der Pfad zum Vorschaubild.
</p>
<p>Deswegen habe ich ein Plugin geschrieben, das die normalen Iframes umwandelt. Der Blogger schreibt also ganz normal seinen Artikel, bezieht den normalen Einbettungscode von Youtube, und das Plugin <strong>serendipity_event_lazyoutube</strong> wandelt dann automatisch diesen Code von Youtubes lahmen Standard-Iframe zu der hier gezeigten schnellen Alternative um, die den Videoplayer erst nach einem Klick auf das Vorschaubild lädt.
</p>
<p>Außerdem kann man damit, angelehnt an oEmbeds, den Einbettungscode vom Plugin erstellen lassen – falls einem das Bewahren des Original-Iframes im Artikelquellcode nicht so wichtig ist. Statt dafür nur einen Link zu setzen geht das mit [Youtube-Link], also [https://www.youtube.com/watch?v=XhG-4zdVx0I] für das Video von oben. <em>Update:</em> Diese Funktionalität habe ich in späteren Versionen wieder entfernt, das Ersetzen der regulären Iframes ist genug Aufgabe für ein Plugin.
</p>
<p>Nur damit der zu erstellende Code einfacher zu erzeugen ist habe ich oben auch das Iframe angepasst. Im Original ist da noch an zwei Stellen der Titel des Videos drin. Aber um den abzufragen müssen wir mit der Youtube-API arbeiten oder den Artikelschreiber das eintragen lassen, beides wollte ich vermeiden.
</p>
<p class="wl_nobottom">Das Plugin ist ganz frisch und es fehlt mindestens noch der Versuch, gewählte Anfangszeiten zu unterstützen. Deshalb liegt es bisher nur in einem eigenen <a href="https://github.com/onli/serendipity_event_lazyoutube">Github-Repo</a>. Hier im Blog ist es aber schon aktiv und Tester wären mir hochwillkommen.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/c8655e06a01e4536a599d65c21397fc1" width="1" height="1" alt="">
Fri, 04 Jun 2021 08:11:00 +0200https://www.onli-blogging.de/2040/guid.htmlserendipity7GUIs in Flutter (2/7)
https://www.onli-blogging.de/1984/7GUIs-in-Flutter-27.html
Codehttps://www.onli-blogging.de/1984/7GUIs-in-Flutter-27.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=19840https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=1984[email protected] (onli)
<p>Die <a href="https://eugenkiss.github.io/7guis/tasks/">sieben Aufgaben</a> von 7GUIs sollen typische Problemstellungen bei der Anwendungsentwicklung widerspiegeln. Dann können <a href="https://eugenkiss.github.io/7guis/implementations">die gebauten Lösungen</a> dafür benutzt werden, verschiedene Programmiersprachen und Toolkits miteinander zu vergleichen.
</p>
<p>Ich dachte, das ist eine gute Gelegenheit hier nochmal Flutter zu zeigen. <a href="https://www.onli-blogging.de/1978/Flutter-Ein-tolles-Framework-fuer-mobile-Apps.html">Die Vorstellung</a> letzten Monat zeigte relativ wenig von den damit baubaren Oberflächen.
</p>
<p>Flutter ist ja deklarativ aufgebaut, das heißt die Oberfläche baut sich immer wieder neu und reagiert dann auf die neuen Variablenwerte. Man kann dafür <code>StatefulWidgets</code> benutzen, die Variablen im Widget-Stateobjekt speichern und dann immer mit <code>setState</code> anzeigen, dass die Oberfläche sich doch bitte neubauen soll. Ich werde stattdessen <a href="https://pub.dev/packages/get">mit GetX</a> der Oberfläche einen Controller zur Seite stellen, in dem die Variablen leben und der dafür sorgt, dass auch Widgets ohne reguläres Zustandsobjekt interaktiv sein können. Aber seht selbst:</p>
<h4>1. Counter
</h4>
<p>Die erste Aufgabe ist ein Klickzähler. Doch einen Counter zu erstellen ist keine Herausforderung, das ist das Standardbeispiel auf der Flutterhomepage und bei Modulen wie GetX, die das Statemanagement übernehmen wollen. Entsprechend habe ich hier nur das GetX-Beispiel genommen und die Oberfläche angepasst.
</p>
<p>So sieht es aus:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/7guis/counter.png'><!-- s9ymdb:1167 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/7guis/counter.png 720w" src="https://www.onli-blogging.de/uploads/7guis/counter.serendipityThumb.png" alt=""></a>
</p>
<p>Die Oberfläche ist eine <code>Row</code>, in der ein Textfeld und ein Button sind.
</p>
<p>Das ist der Code:</p>
<pre class="code">import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
// Instantiate your class using Get.put() to make it available for all "child" routes there.
final Controller c = Get.put(Controller());
@override
Widget build(context) => Scaffold(
appBar: AppBar(title: Text('Counter')),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// Use Obx(()=> to update Text() whenever count is changed.
Obx(() => Text("Clicks: ${c.count}")),
RaisedButton(child: Text("Count"), onPressed: () => c.increment()),
],
)));
}
class Controller extends GetxController {
var count = 0.obs;
increment() => count++;
}</pre>
<p>Das Textfeld zeigt, da mit <code>Obx</code> umschlossen, immer den aktuellen Wert der Zählvariable im Controller an. Ein Druck auf den Button erhöht diesen Wert.
</p>
<p>Außer der Zählvariable im Controller und der Funktion <code>increment()</code> hat die App keine weitere Funktionalität.
</p>
<p>So funktioniert das dann in Bewegung:
</p>
<p><video src="https://www.onli-blogging.de/uploads/7guis/counter.webm" title="counter.webm" height="400" controls><a class="block_level opens_window" href="https://www.onli-blogging.de/uploads/7guis/counter.webm" title="counter.webm"><!-- s9ymdb:1169 -->counter.webm</a></video>
</p>
<p>Beachte auch, dass die Elemente wie die einer typischen Android-Anwendung aussehen. Das liegt schlicht daran, dass hier eine <code>MaterialApp</code> gestartet wird.</p>
<h4>2. Temperaturconverter
</h4>
<p>Die zweite Aufgabe ist eine Oberfläche zum Unwandeln von Celsius zu Fahrenheit und umgekehrt.
</p>
<p>So sieht meine Lösung aus:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/7guis/temp_converter.png'><!-- s9ymdb:1168 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/7guis/temp_converter.png 720w" src="https://www.onli-blogging.de/uploads/7guis/temp_converter.serendipityThumb.png" alt=""></a>
</p>
<p>Das ist eine einzelne <code>Row</code>, in der nacheinander je ein Texteingabefeld und ein Textanzeigefeld aufgereiht sind. Und ja, 0.0 bei beiden Feldern war nicht der richtige Defaultwert, wäre aber einfach änderbar.
</p>
<p>Der Code:</p>
<pre class="code">import 'package:flutter/material.dart';
import 'package:get/get.dart';
// ... main() und MyApp sind weggekürzt
class MyHomePage extends StatelessWidget {
final Controller c = Get.put(Controller());
@override
Widget build(context) => Scaffold(
appBar: AppBar(title: Text('Temperature Converter')),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
SizedBox(
width: 80,
child: Obx(() => TextFormField(
// to force Obx reload when value changes
key: Key("C" + c.celsius.string),
keyboardType: TextInputType.number,
initialValue: c.celsius.value.toStringAsFixed(1),
onChanged: (value) =>
c.fahrenheit.value = c.ctof(double.tryParse(value)),
))),
Text("Celsius ="),
SizedBox(
width: 80,
child: Obx(() => TextFormField(
key: Key("F" + c.fahrenheit.string),
keyboardType: TextInputType.number,
initialValue: c.fahrenheit.value.toStringAsFixed(1),
onChanged: (value) =>
c.celsius.value = c.ftoc(double.tryParse(value)),
))),
Text("Fahrenheit"),
],
),
));
}
class Controller extends GetxController {
var celsius = 0.0.obs;
var fahrenheit = 0.0.obs;
double ctof(double c) {
return c * (9 / 5) + 32;
}
double ftoc(double f) {
return (f - 32) * (5 / 9);
}
}</pre>
<p>Hier passiert jetzt schon ein bisschen mehr. Der GetX-Controller hat zwei Variablen, <code>celsius</code> und <code>fahrenheit</code>, und zwei Funktionen zum Umwandeln der Werte. In der UI wird immer, wenn im Texteingabefeld etwas eingeben wird, mittels <code>onChanged</code> im Controller der Wert des anderen Texteingabefeld geändert. Weil die Eingabefelder wieder mit <code>Obx</code> umschlossen sind baut dann Flutter direkt die Oberfläche neu, mit dem neuen Celsius/Fahrenheit-Wert als <code>initialValue</code> der Eingabefelder. Einen <code>key</code> zu setzen hilft flutter bzw Obx dabei, zu erkennen wann die Oberfläche neu gebaut werden muss.
</p>
<p>So sieht es in Bewegung aus:
</p>
<p><video src="https://www.onli-blogging.de/uploads/7guis/tempconvert.webm" title="tempconvert.webm" height="400" controls><a class="block_level opens_window" href="https://www.onli-blogging.de/uploads/7guis/tempconvert.webm" title="tempconvert.webm"><!-- s9ymdb:1170 -->tempconvert.webm</a></video></p>
<hr /><p class="wl_nobottom">Ich habe den Code auch <a href="https://github.com/onli/7GUIs_flutter">auf Github hochgeladen</a>, so kann jeder ihn direkt importieren und abändern. Es gibt noch fünf weitere Aufgaben, die immer komplizierter werden. Vielleicht mache eine Serie hierdraus, dann folgen sie später.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/f60f1624892a49f4933ffbcfc252e0bd" width="1" height="1" alt="">
Fri, 13 Nov 2020 17:57:00 +0100https://www.onli-blogging.de/1984/guid.htmlflutterFlutter: Ein tolles Framework für mobile Apps
https://www.onli-blogging.de/1978/Flutter-Ein-tolles-Framework-fuer-mobile-Apps.html
CodeLinuxhttps://www.onli-blogging.de/1978/Flutter-Ein-tolles-Framework-fuer-mobile-Apps.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=19780https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=1978[email protected] (onli)
<p><a href="https://flutter.dev/">Flutter</a> ist ein Framework zur Anwendungsentwicklung. Fokus sind Android und iOS-Apps, aber man kann stattdessen auch für das Web oder mittlerweile sogar den Desktop entwickeln. Es ist ein Google-Projekt, was aber nicht wirklich ein Nachteil ist wenn man mit Android sowieso für eine Google-Plattform entwickelt.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/flutter_seite.webp'><!-- s9ymdb:1144 --><img class="serendipity_image_center" width="800" height="444" srcset="https://www.onli-blogging.de/uploads/flutter_seite.1200W.serendipityThumb.webp 2600w,https://www.onli-blogging.de/uploads/flutter_seite.800W.serendipityThumb.webp 1200w,https://www.onli-blogging.de/uploads/flutter_seite.400W.serendipityThumb.webp 800w" src="https://www.onli-blogging.de/uploads/flutter_seite.serendipityThumb.webp" alt=""></a>
</p>
<p>Das Framework ist schon wegen der Konzepte eine ziemliche Umstellung zur gewöhnlichen Android-Entwicklung, zusätzlich schreibt man die Anwendung dann auch noch in einer neuen Sprache, <a href="https://dart.dev/">Dart</a>. Doch Dart entpuppt sich als großer Vorteil, dazu unten mehr.</p>
<h4>Flutter-Code und Konzepte
</h4>
<p>Flutter mag insgesamt eine große Umstellung sein, doch der Einstieg ist super simpel. Alles ist ein Widget, jede Oberfläche ist ein Widgetbaum, im Baum kombiniert man die Widgets frei. Zum Beispiel ist das hier das mitgelieferte Hello-World-Beispiel:</p>
<pre class="code">import 'package:flutter/widgets.dart';
void main() =>
runApp(
const Center(
child:
Text('Hello, world!',
key: Key('title'),
textDirection: TextDirection.ltr
)
)
);</pre>
<p>So sieht das aus:
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/Screenshot_1602869433.png'><!-- s9ymdb:1145 --><img class="serendipity_image_center" width="225" height="400" srcset="https://www.onli-blogging.de/uploads/Screenshot_1602869433.png 720w" src="https://www.onli-blogging.de/uploads/Screenshot_1602869433.serendipityThumb.png" alt=""></a>
</p>
<p>Auch als Anfänger ist das problemlos lesbar, die Elemente machen genau genau was man erwartet. <code>main()</code> ist der Startpunkt der Anwendung, <code>runApp</code> startet die App, das <code>Center-Widget</code> zentriert, <code>Text</code> zeigt Text an. Würde man den Code in eine Material-App packen, würde die App auch direkt aussehen wie eine typische Android-Anwendung.
</p>
<p>Aber es ist purer Code, während man bei der Entwicklung mit Android-Studio Oberflächen mit XML-Layouts zusammenbauen konnte. Die Trennung nicht zu haben birgt die Gefahr, UI-Code und Programmlogik zu verschmelzen – was ja aber auch bei regulären Androidanwendungen nur zu schnell passiert. Flutter, und mehr noch die Community drumrum, kompensiert diesen Nachteil über mit einem exzessiven Fokus auf Zustandsmanagement.</p>
<h5>Zustandmanagement
</h5>
<p>Zustandsmanagement ist die Verwaltung des States, das sind einfach die lokalen und globalen Variablen, die Einfluss auf die Oberfläche haben. Flutter versucht generell eine <a href="https://flutter.dev/docs/get-started/flutter-for/declarative">deklarative UI-Entwicklung</a> vorzugeben: Die UI wird immer wieder neu gezeichnet, und wenn der Zustand sich verändert hat sieht die Oberfläche entsprechend anders aus.
</p>
<p>Zuerst aber gibt es <a href="https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html">StatelessWidgets</a>, die keinen eigenen Zustand haben. Holen sie sich die Variablen nicht von außen sehen sie immer gleich aus.</p>
<pre class="code">class GreenFrog extends StatelessWidget {
const GreenFrog({ Key key }) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(color: const Color(0xFF2DBD3A));
}
}</pre>
<p>Das hier wäre einfach ein grünes Fenster.
</p>
<p>Dann gibt es <a href="https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html">StatefulWidgets</a>, die ihre eigenen änderbaren Variablen haben können. </p>
<pre class="code">class YellowBird extends StatefulWidget {
const YellowBird({ Key key }) : super(key: key);
@override
_YellowBirdState createState() => _YellowBirdState();
}
class _YellowBirdState extends State<YellowBird> {
@override
Widget build(BuildContext context) {
return Container(color: const Color(0xFFFFE306));
}
}</pre>
<p>Das zeigt jetzt auch nur einen farbigen Bereich an. Aber der State-Klasse könnte man jetzt Variablen hinzufügen. Die Doku zeigt dieses Beispiel:</p>
<pre class="code">class Bird extends StatefulWidget {
const Bird({
Key key,
this.color = const Color(0xFFFFE306),
this.child,
}) : super(key: key);
final Color color;
final Widget child;
_BirdState createState() => _BirdState();
}
class _BirdState extends State<Bird> {
double _size = 1.0;
void grow() {
setState(() { _size += 0.1; });
}
@override
Widget build(BuildContext context) {
return Container(
color: widget.color,
transform: Matrix4.diagonal3Values(_size, _size, 1.0),
child: widget.child,
);
}
}</pre>
<p>Das ist also ein Widget mit einer festen Größe, das eine Funktion mitbringt um zu wachsen. <code>setState(() { });</code> muss immer dann aufgerufen werden, wenn die Oberfläche die Zustandsvariablen neu evaluieren soll.</p>
<h6>GetX
</h6>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/getx_seite_tiny.png'><!-- s9ymdb:1146 --><img class="serendipity_image_center" width="800" height="444" srcset="https://www.onli-blogging.de/uploads/getx_seite_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/getx_seite_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/getx_seite_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/getx_seite_tiny.serendipityThumb.png" alt=""></a>
</p>
<p>Es gibt Unmengen Lösungsansätze für die Zustandsverwaltung. Zum einen weil sie wichtig ist, denn mit ihr steuert man ja direkt das Verhalten der App. Aber auch, weil da die Einflüsse der Entwicklergruppen kollidieren: Da kommt das übliche mentale Chaos aus dem Javascript-Land angeschwemmt, vermischt mit Enterprise-Architekturen – passt ja zu einem Google-Framework. <a href="https://pub.dev/packages/get">GetX</a> ist anders. Es ist ein pragmatisches Werkzeugset, das nur unter anderem Statemanagment löst. Mit dem Flutter-Modul kann man jeder Oberfläche einen Controller zur Seite stellen, und mit dem <code>Obx</code>-Widget die Oberfläche immer neu zeichnen lassen wenn sich eine der Variablen ändert. Im Flutter-Kontext ist das Magie, das Resultat ist einfach sauberer Code. So ist <a href="https://pub.dev/packages/get#counter-app-with-getx">das Counter-Beispiel</a> aus der Readme elegant und verständlich:
</p>
<p>Du hast einen Controller, in der die Variable und eine optionale Manipulierfunktion definiert wird:</p>
<pre class="code">class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}</pre>
<p>Und dazu das Widget, bei dem ein Druck auf den Button die Variable erhöht, wodurch direkt die Anzeige aktualisiert wird:</p>
<pre class="code">class Home extends StatelessWidget {
// Instantiate your class using Get.put() to make it available for all "child" routes there.
final Controller c = Get.put(Controller());
@override
Widget build(context) => Scaffold(
// Use Obx(()=> to update Text() whenever count is changed.
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),
// Replace the 8 lines Navigator.push by a simple Get.to(). You don't need context
body: Center(child: RaisedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
class Other extends StatelessWidget {
// You can ask Get to find a Controller that is being used by another page and redirect you to it.
final Controller c = Get.find();
@override
Widget build(context){
// Access the updated count variable
return Scaffold(body: Center(child: Text("${c.count}")));
}
}</pre>
<p>Das Beispiel zeigt noch zusätzlich die Navigationslösung, die man nicht nutzen muss. Trotzdem: Klarer geht es nicht.
</p>
<p>GetX hat Nachteile. So sind die Details nicht ganz so simpel wie es zuerst scheint - wie zum Beispiel umsetzen, dass auf eine Objektvariable eines Listenelements reagiert wird, wenn doch <code>List.obs</code> nur auf das Vergrößern und Verkleinern der Liste reagiert? Vor allem aber ist es ein junges Projekt, bei dem noch zu viele Commits hereinkommen, von zu wenigen Entwicklern, das Ding ist noch im Flux. Trotzdem ist es ein supermächtiges Werkzeug und erleichtert die Arbeit mit Flutter deutlich.</p>
<h5>Paketverwaltung
</h5>
<p>Richtig nett an Flutter ist auch das darum aufgebaute Entwickleruniversum. Man findet Erklärvideos und -artikel. Am wichtigsten aber sind die Pakete. Auf <a href="https://pub.dev/">https://pub.dev/</a> gibt es eine Suche, Module (für Flutter wie Dart) werden dort sauber vorgestellt, gewonnene Likes dienen als Orientierungshilfe. Bräuchte meine App beispielsweise eine animierbare Navigationszeile unten, ich könnte sie mit <a href="https://pub.dev/packages/convex_bottom_bar">convex_bottom_bar</a> direkt von dort beziehen. Eintragen in die pubspec.yaml, <code>flutter pub get</code> ausführen, den Code einbauen, fertig. Eine solche Paketübersicht habe ich bei der direkten Androidentwicklung mit Java schmerzlich vermisst.</p>
<h4>Dart
</h4>
<p>Dass Flutter direkt eine neue Programmiersprache erfordert ist erstmal total abschreckend. Doch Dart macht das ganz schnell wieder wett, denn die Sprache ist gelungen. Dart ist eine wilde Mischung, für mich fühlt es sich aber vor allem nach Ruby an. Was toll ist. Es fehlen allerdings die Blöcke. Dafür hat es async-Funktionen direkt mit dabei, samt await und Future. Wikipedia zählt Smalltalk und Erlang als weitere Einflüsse. Bestimmt ist da auch Python mit drin, Java und Javascript.
</p>
<p>Dart hat mich bisher noch in keiner Situation enttäuscht. Die Sprache hat für mich als Ruby-Programmierer immer eine gute Lösung parat gehabt und mich seltenst negativ überrascht.
</p>
<p><a class="serendipity_image_link" href='https://www.onli-blogging.de/uploads/dart_dev_tiny.png'><!-- s9ymdb:1147 --><img class="serendipity_image_center" width="800" height="444" srcset="https://www.onli-blogging.de/uploads/dart_dev_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/dart_dev_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/dart_dev_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/dart_dev_tiny.serendipityThumb.png" alt=""></a>
</p>
<p>Die folgenden Beispiele kann man auch alle <a href="https://dart.dev/">auf der Webseite</a> ausprobieren.
</p>
<p>Hallo-Welt:</p>
<pre class="code">main() {
print("Hello, World!");
}</pre>
<p>Zahlen:</p>
<pre class="code">int i = 1 + 2;
print(i); // => 3</pre>
<p>Strings:</p>
<pre class="code">final s = "abc" + "def";
print(s); // => abcdef</pre>
<p>Es gibt Listen und Maps (Hashes):</p>
<pre class="code">var testliste = [1, 2, 3];
print(testliste[1]); // => 2
var testmap = {1: 'a', 2: 'b', 3: 'c'};
print(testmap[2]); // => b </pre>
<p>Und map und fold:</p>
<pre class="code">var testliste = [1, 2, 3];
var result = testliste.map((element) => [element]);
print(result); // => [[1], [2], [3]]
var folded = result.fold(0, (prev, element) => prev + element.first);
print(folded); // => 6</pre>
<p>Dr ganze Bereich um async war ungewohnt und ist nicht einfach, aber <a href="https://dart.dev/codelabs/async-await#example-introducing-futures">das simple Beispiel</a> ist klar:</p>
<pre class="code">Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info from another service or database.
return Future.delayed(Duration(seconds: 2), () => print('Das hier folgt 2 Sekunden später'));
}
void main() {
fetchUserOrder();
print('Das ist die erste Ausgabe.');
}
</pre>
<p>Und natürlich Objekte mit Funktionen:</p>
<pre class="code">
class Sword {
int damage = 5;
use() => print("$this dealt $damage damage.");
}
main() {
var sword = Sword();
sword.use(); // => Instance of 'Sword' dealt 5 damage.
}</pre>
<p>Dazu kommt noch viel mehr: Generic, Mixins, Named Parameters, Lambda hatte ich erwähnt, die Standardklassen haben hilfreiche Funktionen definiert, chaining mittels <code>..</code> – also das Aneinanderreihen von Funktionen an ein Objekt, obwohl die Funktionen eigentlich <code>void</code> zurückgeben. Dart ist wirklich erstaunlich angenehm.
</p>
<p>Und die Flutter-Programme, die mit Dart gebaut werden, laufen nicht etwa in einem Wrapper, sondern werden kompiliert und dann <a href="https://dart.dev/platforms">nativ auf dem Gerät ausgeführt</a>. Dart kann auch nach Javascript transpilieren, was dann die Web-Unterstützung von Flutter erlaubt, aber das ist gefährlich – die so erstellten Webseiten sind bestimmt Javascript-Monster. Für einzelne Spezialfälle könnte aber auch das eine gute Lösung sein. </p>
<h4>Fazit: Beachtenswert
</h4>
<p>Flutter verspricht, mobile Anwendungen viel schneller entwickelbar zu machen als wenn man sie mit den Bordmitteln schreibt. Wenn man das Framework bereits beherrscht mag das stimmen. Ist man neu, ist es mit der Fixierung auf indirekte Interaktivität nicht so einfach zu beherrschen und dieser Lernprozess dann auch nicht schnell. Aber: Dann ist es mächtig, und ich habe definitiv den Eindruck, dass man hiermit bessere Oberflächen und somit auch Apps bauen kann. Dass die dann auf beiden großen mobilen Betriebssystemen laufen können kommt noch dazu.
</p>
<p>Dart ist dann die Krönung. Erwartet hatte ich ein alternatives Javascript – etwas, womit man arbeiten kann, aber woran man eher keine Freude hat. Stattdessen ist es nahe genug an Ruby und so ausgereift, dass ich fast frei meinen Programmcode schreiben kann. Sicher, mit der Zeit werden sich die Schwachstellen offenbaren, und eine so junge Sprache kann nicht an die Modulvielfalt von Ruby herankommen. Eine positive Überraschung war sie aber allemal.
</p>
<p>Wenn Flutter die entsprechende Entwicklung stabilisiert (<a href="https://flutter.dev/docs/deployment/linux">noch ist es alpha</a>) könnte ich mir das glatt auch für Linuxanwendungen vorstellen. Die Entwicklung mit wxWidgets, GTK und Qt ist deutlich komplizierter. Derzeit greifen Entwickler dann stattdessen oft zu Electron und anderen Javascript-Desktopwrappern. Flutter mit Dart könnte die bessere Lösung sein. Ich bin gespannt, ob sich das bewahrheitet. Derzeit hat sich die Alpha an snap gekoppelt, was komplett inakzeptabel ist solange es die einzige Lösung bleibt. Aber Flutter-Appimages oder schlicht Binaries? Könnten eine tolle Sache sein.
</p>
<p class="wl_nobottom">Wer jetzt für Android entwickeln will, für den ist Flutter mit Dart auch jetzt schon einen Versuch wert.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/e697ab1a2cf3461a8ea559871c8900dd" width="1" height="1" alt="">
Mon, 19 Oct 2020 08:43:00 +0200https://www.onli-blogging.de/1978/guid.htmlflutterDark Mode mit CSS
https://www.onli-blogging.de/1939/Dark-Mode-mit-CSS.html
Codehttps://www.onli-blogging.de/1939/Dark-Mode-mit-CSS.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=19392https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=1939[email protected] (onli)
<p>Selten, dass ich ein Feature umsetze das unter Linux nicht richtig funktioniert. Aber der dunkle Systemmodus der anderen Betriebssysteme ist eine gute Idee. Besonders, wenn Webseiten den Systemzustand erkennen und sich ebenfalls abdunkeln können.</p>
<h4>Ergebnis
</h4>
<p>Ich habe das <a href="https://www.pc-kombo.com/de/">auf pc-kombo</a> umgesetzt. So sah die Seite vorher immer aus:</p>
<ul class="s9y_gallery plainList"><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com1_tiny.png"><!-- s9ymdb:1017 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com1_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com1_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com1_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com1_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo1_tiny.png"><!-- s9ymdb:1011 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo1_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo1_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo1_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo1_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide1_tiny.png"><!-- s9ymdb:1013 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide1_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide1_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide1_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide1_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com1_tiny.png"><!-- s9ymdb:1009 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com1_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com1_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com1_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com1_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo1_tiny.png"><!-- s9ymdb:1010 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo1_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo1_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo1_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo1_tiny.serendipityThumb.png" alt=""></a></li>
</ul>
<p>So sieht sie jetzt bei aktiviertem Dark Mode aus:</p>
<ul class="s9y_gallery plainList"><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com_tiny.png"><!-- s9ymdb:1016 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Automatischer_PC-Hardwareempfehler_-_pc-kombo_com_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo_tiny.png"><!-- s9ymdb:1015 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Hardware_DB_-_pc-kombo_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide_tiny.png"><!-- s9ymdb:1014 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_The_pc-kombo_PC_Part_Picking_Guide_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com_tiny.png"><!-- s9ymdb:1012 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_PC_fuer_790_-_pc-kombo_com_tiny.serendipityThumb.png" alt=""></a></li><li class="s9y_gallery_item"><a class="serendipity_image_link" href="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo_tiny.png"><!-- s9ymdb:1008 --><img class="s9y_gallery_image" srcset="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo_tiny.1200W.serendipityThumb.png 2600w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo_tiny.800W.serendipityThumb.png 1200w,https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo_tiny.400W.serendipityThumb.png 800w" src="https://www.onli-blogging.de/uploads/darkmode/Screenshot_2020-06-13_Prozessor_Benchmark_-_pc-kombo_tiny.serendipityThumb.png" alt=""></a></li>
</ul>
<p>Nicht schlecht, oder?</p>
<h4>Aktivieren
</h4>
<p>Im Firefox setzt man in <strong>about:config</strong> <em>ui.systemUsesDarkTheme</em> auf 1. In Chromium gibt es in den Einstellungen der Entwicklerwerkzeuge die Option <em>Emulate CSS media feature prefers-color-scheme</em>. Unter Windows würde ich erwarten, dass die Browser automatisch die Systemeinstellungen aufgreifen.
</p>
<p>Die Webseite reagiert mit einem Mediaquery:</p>
<pre class="code">@media (prefers-color-scheme: dark) {
…
}</pre>
<p>Außerhalb des CSS bleibt alles gleich.</p>
<h4>Die Änderungen und ein paar Tricks
</h4>
<p>Am Ende der CSS-Datei angehängt überschrieb ich nun die CSS-Anweisungen, die der Webseite vorher ein helles Aussehen gaben. Manche davon kamen von mir, andere vom genutzten CSS-Framework <a href="https://picturepan2.github.io/spectre/index.html">Spectre</a>. Ich stolperte dabei über ein paar nette Tricks.</p>
<h5>Farben
</h5>
<p>Klar: Wenn vorher ein weißer Hintergrund gesetzt war musste der abgeändert werden. Normalerweise ist der Standardhintergrund hell und die Schrift dunkel:</p>
<pre class="code">body {
background-color: #fbfbfb;
color: #3b4351;
}</pre>
<p>Nun ist das umgedreht:</p>
<pre class="code">body {
color: #dcdfe5;
background-color: #37383a;
}</pre>
<p>Aber es ist nicht einfach weiß auf schwarz, so wie es ja auch vorher nicht einfach schwarz auf weiß war. Stattdessen ist es ein helles grau auf einem dunkleren grau. Der Kontrast <a href="https://webaim.org/resources/contrastchecker/?fcolor=DCDFE5&bcolor=37383A">ist hoch genug</a>, und anders als bei einer rein schwarz-weißen Seite kann immer noch mit Farben gearbeitet werden. So ist pc-kombo weiterhin auch blau.
</p>
<p>Es gab noch ein paar Farbanweisungen mehr zu setzen, aber es waren nicht viele und sie folgten dem gleichen Prinzip. Grau oder blau, abgedunkelte Versionen der vorher bereits genutzten Farben. An ein paar Stellen bewahrte ich auch die vorher genutzten blau-lilanen Hintergründe.</p>
<h5>Bilder abdunkeln
</h5>
<p>Viele Produktbilder haben einen hellen Hintergrund und sind nicht von mir bearbeitbar, aber auch meine Bilder will ich nicht alle bearbeiten. Zum Glück kann CSS hier helfen:</p>
<pre class="code">img {
opacity: .75;
transition: opacity .5s ease-in-out;
}
img:hover {
opacity: 1;
}</pre>
<p>Indem die Sichtbarkeit verringert wird werden die Bilder weniger hell, denn sie sind ja jetzt auf einem dunklen Hintergrund. Die Idee stammt aus <a href="https://css-tricks.com/dark-modes-with-css/">diesem Artikel</a>.</p>
<h5>Bildern Hintergrund geben
</h5>
<p>Andererseits waren Bilder mit einem transparentem Hintergrund und dunklem Bildinhalt nun schwer sichtbar, das Amazon-Logo zum Beispiel. Solche Bilder bekommen mit CSS einen hellen Hintergrund zugewiesen (der wegen dem obigen Schritt nicht zu hell wird), durch etwas Padding wirken sie nicht abgeschnitten, und ein kaum sichtbarer schwarzer Rahmen verbessert nochmal die Abgrenzung:</p>
<pre class="code">#priceDetails img, #recommendation img:not([src=""]) {
padding: 0.2em;
background: white;
border: 2px solid black;
}</pre>
<p>Das <code>not([src=""])</code> verhindert, dass vorher unsichtbare leere Bilder jetzt durch Rahmen und Padding sichtbar werden.</p>
<h5>Logo und Charts invertieren
</h5>
<p>Das Logo von PC-Kombo war nun dunkelgrau auf dunkelgrau, also nicht sichtbar. Und bei den Charts waren die schwarzen Linien und Bezeichnungen unsichtbar geworden. Hier hilft ein CSS-Filter:</p>
<pre class="code">.navbar-brand img, .apexcharts-canvas {
filter: invert();
opacity: 1;
}</pre>
<p>Die invertierten dunklen Farben sind jetzt gut sichtbar, die hellen bleiben sichtbar.</p>
<h5>Inputs einfärben
</h5>
<p>Da die inputs noch hell waren, mussten auch die angepasst werden. Mein CSS bezieht sich hier speziell auf Spectre:</p>
<pre class="code">.form-input, .input-group .input-group-addon {
border: .05rem solid #5e6b80;
}
.form-input, .input-group .input-group-addon {
background: #21325a;
color: #b7becb;
}
.form-input:focus, .form-input:not(:placeholder-shown):invalid:focus {
background: #0F1930;
}
.form-select:not([multiple]):not([size]) {
background: #21325a url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center/.4rem .5rem
}</pre>
<p>Grundsätzlich müssten sie aber auch geändert werden, wenn kein CSS-Framework ihnen bereits einen hellen Hintergrund und Grenzlinien gegeben hätte. Zumindest, solange auf Linux nicht auch diese Elemente automatisch abgedunkelt werden.
</p>
<p>Bei der Nummerneingabe waren die Hoch-Runter-Buttons weiterhin hell und ließen sich nicht abändern. Also blendete ich sie aus:</p>
<pre class="code">input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
margin: 0;
}
input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
}
</pre>
<p>Die Buttons sind zwar eigentlich nett, aber das ganze Design zu zerstören sind sie nicht wert.</p>
<h4>Einfacher als gedacht, aber Linux muss nachziehen
</h4>
<p>Ich war überrascht über den geringen Aufwand. Klar, es ist Designarbeit und jede Seite ist anders. Aber dank tollen Funktionen von CSS wie dem Farbfilter waren ansonsten problematische Ecken einfach zu lösen. Wenn das CSS einer Seite halbwegs organisiert ist oder Farben gar zentral definiert sind ist das Abdunkeln machbar.
</p>
<p>Allerdings ärgerlich, dass Linux keinen definierten Mechanismus hat um den Dunkelmodus zu aktivieren. Es müsste eine Datei <strong>~/.config/darkmode</strong> geben, die von Anwendungen und QT/GTK-Designs berücksichtigt wird. Klar kann man auch so ein dunkles Design auswählen, aber das weiß der Browserinhalt dann ja nicht.
</p>
<p class="wl_nobottom">Hier im Blog könnte ich jetzt ebenfalls relativ schnell ein dunkles Design anbieten, aber ich glaube ich warte damit bis Linux für den Dunkelmodus einen ordentlichen Mechanismus hat, oder bis die Browser die Einstellung regulär konfigurierbar machen.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/8f7f3247166045d8a629264147f11544" width="1" height="1" alt="">
Mon, 15 Jun 2020 08:26:00 +0200https://www.onli-blogging.de/1939/guid.htmlOga als Alternative zu Nokogiri
https://www.onli-blogging.de/1927/Oga-als-Alternative-zu-Nokogiri.html
Codehttps://www.onli-blogging.de/1927/Oga-als-Alternative-zu-Nokogiri.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=19270https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=1927[email protected] (onli)
<p>Der bekannteste XML-Verarbeitungshelfer im Rubyland ist sicherlich <a href="https://nokogiri.org/">Nokogiri</a>. Mit Nokogiri kann man schnell Informationen aus XML und HTML herausholen, per xpath oder CSS-Selektor. Ein Beispiel von der Webseite:</p>
<pre class="code">doc = Nokogiri::HTML(open('https://nokogiri.org/tutorials/installing_nokogiri.html'))
puts "### Search for nodes by css"
doc.css('nav ul.menu li a', 'article h2').each do |link|
puts link.content
end
puts "### Search for nodes by xpath"
doc.xpath('//nav//ul//li/a', '//article//h2').each do |link|
puts link.content
end</pre>
<p>Gegen Nokogiri spricht eigentlich nur die Installation. Basierend auf der libxml, ist es desöfteren mindestens eine zusätzliche Abhängigkeit, an die man im Zweifel bei der Servereinrichtung denken muss. Manchmal führt es auch zu <a href="https://www.onli-blogging.de/1438/Bundler-scheitert-an-Nokogiri-auf-Arch-Linux-ARM.html">ganz komischen Problemen</a>.
</p>
<p>Hier setzt <a href="https://gitlab.com/yorickpeterse/oga">Oga</a> an. Es hat diese Abhängigkeit einfach nicht. Das macht die Installation unproblematischer. Die API ist etwas anders als bei Nokogiri, einige der Unterschiede sind <a href="https://gitlab.com/yorickpeterse/oga/-/blob/master/doc/migrating_from_nokogiri.md">in einer Dokumentationsdatei beschrieben</a>. Es ist aber schon sehr ähnlich:</p>
<pre class="code">onli@fallout:~$ irb
2.5.3 :001 > require 'oga'
=> true
2.5.3 :002 > xml = Oga.parse_xml('<a><b test="def">abc</b></a>')
=> Document(
children: NodeSet(Element(name: "a" children: NodeSet(Element(name: "b" attributes: [Attribute(name: "test" value: "def")] children: NodeSet(Text("abc"))))))
)
2.5.3 :003 > xml.xpath('/a/b')
=> NodeSet(Element(name: "b" attributes: [Attribute(name: "test" value: "def")] children: NodeSet(Text("abc"))))
2.5.3 :004 > xml.at_xpath('/a/b')
=> Element(name: "b" attributes: [Attribute(name: "test" value: "def")] children: NodeSet(Text("abc")))
2.5.3 :005 > xml.at_xpath('/a/b').get('test')
=> "def"
2.5.3 :006 > xml.css('b')
=> NodeSet(Element(name: "b" attributes: [Attribute(name: "test" value: "def")] children: NodeSet(Text("abc"))))</pre>
<p>Überraschenderweise bin ich mit Oga selbst noch in keinerlei Probleme gelaufen, seitdem ich beispielsweise <a href="https://github.com/onli/ursprung">das Blogsystem ursprung</a> darauf umgestellt habe. Es fehlt allerdings an Dokumentation und Beispielen. Bei Nokogiri ist die offizielle Dokumentation auch schon spärlich, bei Oga ist es nochmal weniger, und es fehlen die zu Nokogiri vorhandenen vielen Stackoverflowantworten. Wie <a href="https://www.onli-blogging.de/1908/Mit-SAX-effizienter-XML-parsen.html">beim SAX-Parser</a>, als ich für Oga in den Quellcode schauen musste um die implementierten Events herauszusuchen und Nokogiri wenigstens einen erklärenden Absatz auf der Webseite hatte.
</p>
<p class="wl_nobottom">Trotzdem etabliert sich Oga mittlerweile als Bestandteil meiner Rubyprojekte. Installationsprobleme zu vermeiden ist mir sehr wertvoll. Wem das ähnlich geht oder wer eine Alternative zu Nokogiri sucht sollte Oga eine Chance geben.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/691388bd94a74fe797820af01984255d" width="1" height="1" alt="">
Fri, 22 May 2020 19:35:00 +0200https://www.onli-blogging.de/1927/guid.htmlrubyEffizienter CSV-Dateien verarbeiten, mit Ruby und generell
https://www.onli-blogging.de/1926/Effizienter-CSV-Dateien-verarbeiten,-mit-Ruby-und-generell.html
CodeInformatikhttps://www.onli-blogging.de/1926/Effizienter-CSV-Dateien-verarbeiten,-mit-Ruby-und-generell.html#commentshttps://www.onli-blogging.de/wfwcomment.php?cid=19260https://www.onli-blogging.de/rss.php?version=2.0&type=comments&cid=1926[email protected] (onli)
<p>Vor kurzem schrieb ich darüber, wie ich <a href="https://www.onli-blogging.de/1908/Mit-SAX-effizienter-XML-parsen.html">mit dem SAX-Parser</a> besser mit XML-Dateien umgehen konnte. Besser bedeutete, mit weniger Speicherbedarf schneller die gesuchten Informationen aus teils relativ großen XML-Dateien zu holen. Es half, aber der Server hatte immer noch spürbare Last durch die anderen Datenquellen: Den CSV-Dateien. Sie benutzen manche der Datenausgeber statt XML, und auch bei ihnen führte das naive Vorgehen zu extremen Speicher- und Prozessorbedarf.
</p>
<p>Das naive Vorgehen war grob so:</p>
<pre class="code">hardwares = Database.instance.getHardwares
hardwares.each do |hardware|
csv = cache.getset('csvApi') do
csvGz = open("https://url/zur/csv.gz")
unzippedCsv = Zlib::GzipReader.new(csvGz).read
csv = CSV.parse(unzippedCsv, :headers => true)
csv
end
return csv.detect{|line| line['id'] == hardware.id }
end
</pre>
<p>Es gab also ein Array mit den Bezugsobjekten, zu denen die Zeile mit ihrer ID aus der CSV-Datei gezogen werden soll. Optimiert ist da bereits, dass die CSV-Datei nicht mehrfach heruntergeladen wird. Dafür sorgt <a href="https://github.com/SamSaffron/lru_redux">der lru-cache</a>.
</p>
<p>Wie geht es besser?</p>
<h4>1. Speichereffizienter parsen
</h4>
<p>Der erste Schritt ist das Parsen der CSV-Datei. Der bisherige Code macht das in einem Rutsch und baut – ähnlich wie bei XML-Dateien – ein CSV-Objekt. Wenn wir stattdessen Zeile für Zeile durchgehen entsteht eine Chance, den Speicherbedarf zu reduzieren. Dalibor Nasevic hat dazu <a href="https://dalibornasevic.com/posts/68-processing-large-csv-files-with-ruby">Codebeispiele und Benchmarkergebnisse</a>. Der Code ändert sich so:</p>
<pre class="code">
unzippedCsv = Zlib::GzipReader.new(csvGz)
csvFile = CSV.new(unzippedCsv, headers: true)
while line = csvFile.shift
# do something
end
</pre>
<p>Der GzipReader liest nicht mehr die Datei auf einmal in den Speicher, mit diesem neuen Startpunkt geht der CSV-Parser zeilenweise durch die Datei. Wenn wir jetzt einfach das CSV-Objekt nachbauen bringt das nicht viel, aber es gibt uns die Möglichkeit etwas besseres zu bauen.</p>
<h4>2. Mit fastcsv schneller parsen
</h4>
<p>Doch bleiben wir erstmal beim Parsen selbst. Derzeit benutzt der Code das in Ruby integrierte CSV-Modul. Doch es gibt Alternativen, insbesondere <a href="https://github.com/jpmckinney/fastcsv">fastcsv</a>. Das Gem kann in vielen Fällen das normale CSV-Modul direkt ersetzen und war in meinen Tests etwa doppelt so schnell.</p>
<pre class="code">require 'fastcsv'
csvFile = FastCSV.new(unzippedCsv, headers: true)</pre>
<p>Nett, aber das Parsen der CSV-Datei war gar nicht das Problem. Das sparte ein paar Sekunden. Das eigentliche Problem war das spätere Durchsuchen des erstellten CSV-Objekts.</p>
<h4>3. Mit Hash nicht suchen, sondern nachschlagen
</h4>
<p>Das ist eine Optimierung, die in jeder Sprache funktionieren wird.
</p>
<p>Wenn <code>CSV.parse</code> ein CSV-Objekt erstellt, ist das im Grunde ein großes Array mit Arrays (<code>headers: false</code>) oder Hashs (<code>headers: true</code>) in den Arrayeinträgen. Entsprechend durchsucht der Code von oben dieses Array mit dem üblichen <a href="https://ruby-doc.org/core-2.6.1/Enumerable.html#method-i-detect">Enumerable.detect</a>. Doch das bedeutet, dass für jedes Suchobjekt die CSV-Struktur durchgegangen werden muss, bis etwas gefunden wurde. Oder bis die Struktur durch ist und eben nichts gefunden wurde. Wenn es nur eine Datenstruktur gäbe, die für eine ID direkt die passende Zeile ausgeben könnte…
</p>
<p>Die gibt es natürlich, genau das ist in Ruby der Hash. Da wir jetzt zeilenweise durch die CSV-Datei durchgehen und die Struktur selbst bauen können wir sie nutzen:</p>
<pre class="code">
csv = cache.getset('csvApi') do
…
csv = {}
while line = csvFile.shift
csv[line['id']] = line
end
csv
end
return csv[hardware.id]
</pre>
<p><strong>Das hier ist die große Optimierung</strong>. Anstatt mehrfach durch die riesige Datenmenge zu stöbern am Anfang mit etwas Mehraufwand die Hashstruktur zu erstellen spart danach so viel Zeit bei jedem Suchvorgang, dass minuten- bis stundenlange Prozesse in wenigen Sekunden fertig werden.</p>
<h4>4. Ungenutzte Datenfelder rausschmeißen
</h4>
<p>Moment, da gibt es noch eine mögliche Optimierung, wieder völlig unabhängig von Ruby. Eventuell braucht es später gar nicht alle Felder, die in der CSV-Datei gespeichert sind. Vielleicht wird später nur nach <em>price</em> und <em>available</em> geschaut. Wenn dem so ist, dann ist genau hier der Moment die überflüssigen Felder zu entfernen und so den Speicherbedarf zu senken:</p>
<pre class="code">while line = csvFile.shift
csv[line['id']] = line.to_h.keep_if{|k, _| k == 'price' || k == 'available' }
end</pre>
<p>Die Kombination dieser vier Schritte ist sehr mächtig. Was vorher viele Minuten rödelte und Prozessorkerne voll auslastete ist in ein paar Sekunden erledigt. Aber es ist ja auch ein Idealfall. Es gab genau eine ID, wir in einer Hashmap als Key nutzen und dann nachschlagen konnten. Was, wenn es mehr als einen Key gibt?</p>
<h4>5. SQLite für mehrere IDs
</h4>
<p>In meinem Anwendungsfall gab es manchmal neben der <em>id</em> noch die <em>sku</em>, also einen zweiten Key. Dann reicht ein Hash nicht, denn es gibt keine mir bekannte Möglichkeit, einen zweiten Key einzusetzen. Klar, wir könnten einen zweiten Hash erstellen. Aber würde das nicht den Speicherbedarf verdoppeln? Nein, es wäre besser einen zweiten Key als Index über die alte Hashmap zu legen. In Ruby wüsste ich nicht wie das geht (wenn du schon: Ein Kommentar wäre klasse!). Aber SQLite macht das mit links und ist in jeder Sprache verfügbar.
</p>
<p>Die Idee also ist: Statt einer Hashmap erstellen wir eine SQLite-Datenbank im Arbeitsspeicher. <code>Primary Key</code> wird die id, aber für die sku baut SQLite einen Index. Das Durchsuchen geht dann mit ein bisschen SQL. YAML serialisiert die CSV-Zeile, die im Zweifel auch wieder wie in Schritt 4 speicheroptimiert werden könnte.</p>
<pre class="code">csv = cache.getset('csvApi') do
…
csvFile = FastCSV.new(result, :headers => true)
db = SQLite3::Database.new(':memory:')
db.execute "CREATE TABLE csv(id TEXT PRIMARY KEY, sku TEXT, line TEXT)"
while line = csv.shift
db.execute("INSERT OR IGNORE INTO csv(id, sku, line) VALUES(?, ?, ?)", line['id'], line['sku'], YAML::dump(line))
end
db.execute "CREATE INDEX csv_sku ON csv(sku)"
db.execute "ANALYZE"
db
end
row = csv.execute("SELECT line FROM csv WHERE id = ?", hardware.id).first
unless row
row = csv.execute("SELECT line FROM csv WHERE sku = ?", hardware.sku).first
end
if row
return YAML::load(row[0])
end
</pre>
<p>SQLite ist unheimlich schnell, die CSV-Datei wird in sekundenschnelle durchsucht sein, je nach Größe natürlich.</p>
<h4>Fazit
</h4>
<p>Wenn man es mal richtig macht… Ich fand das ein gutes Beispiel für einen Anwendungsfall von Informatik-Grundkenntnissen. Statt ein Array zu durchsuchen die Datenstruktur zu ändern und eine Hashmap zu nehmen ist Grundlagenstoff des Studiums, Standardbeispiel für O(1) statt O(n). Aber ich brauchte einen Moment um zu erkennen, dass das hier möglich ist, das komfortable <code>CSV.parse</code> hatte mir das versteckt. SQLite einzubauen und nach einem schnelleren Gem zu schauen ist dann vielleicht etwas mehr aus praktischer Erfahrung gezogen, aber liegt wenn man mal optimiert und und nach dem Datenbankkurs auch nicht mehr fern.
</p>
<p>Mir hat dabei auch geholfen, diese Aufgabe als eigenes Projekt zu betrachten. Ursprünglich war das nur eine kleine Ecke im Code des Überprojekts (<a href="https://www.pc-kombo.com/de/">pc-kombo</a>), schnell mal gebaut, abgewandelt aus Code der eine REST-API nach den Informationen fragt (wo solche Optimierungen nicht möglich sind). Jetzt ist die Ecke ausgelagert in ihr eigenes Git-Repository und der Code ist auf genau diese Aufgabe reduziert. Das macht es einfacher, solche Optimierungsmöglichkeiten zu sehen.
</p>
<p>Auf jeden Fall lohnt sich der Aufwand. Zusammen mit der Reduzierung der Last durch XML-Dateien kann ich den großen Server bald wieder abschalten, der nach <a href="https://www.onli-blogging.de/1919/Scaleway-schaltet-ARM-Instanzen-ab,-ich-migrierte.html">dem Scaleway-Umzug</a> die temporäre Heimat dieses Mikroservice wurde. Aus dem Mikroservice wurde jetzt tatsächlich auch ein kleines Programm, das auf schmalerer Hardware wird laufen können. Das reduziert die Strom- oder die Hostingkosten dann schnell um ein paar hundert Euro im Jahr.</p>
<img src="https://ssl-vg03.met.vgwort.de/na/b8b2d72deb5149d483190c39b20556a8" width="1" height="1" alt="">
Wed, 20 May 2020 12:13:00 +0200https://www.onli-blogging.de/1926/guid.htmlruby