Firefox 瀏覽器在看虎牙火貓看直播時,經(jīng)常大概率過一小段時間( 20 分鐘左右)直播網(wǎng)頁就會直接空白轉(zhuǎn)圈,刷新都沒用,只能關(guān)閉標簽頁再重新打開,真的無語,用 chrome 就不會有問題,改 firefox 的 ua 照樣還是沒用,難道 Firefox 用戶在國內(nèi)已經(jīng)少到可以忽略了嗎?
例如我想把設(shè)置頁中 "清除歷史記錄" 按鈕放在書簽工具欄上.https://tiebapic.baidu.com/forum/pic/item/d01373f082025aaf9b25d1a1ecedab64034f1a40.jpg
https://github.com/ustclug/mirrorrequest/issues/251
如何投票
請在 Github Issue 頁面使用 Emoji ?? 表情來投票
macOS 和 Firefox 都是最新版,不知道是哪個版本開始出現(xiàn)這個問題。
不管是用 Proxy SwitchyOmega 還是內(nèi)置的代理功能都會泄露本地 DNS。
network.proxy.socks_remote_dns 為 true
我用的 firefox 最新版 每次搜索一個內(nèi)容后 搜索框上都會有個詢問 您是要訪問 XX 嗎? 請問這個怎么完全關(guān)閉,在設(shè)置找了半天也沒發(fā)現(xiàn)
macOS 10.15.3 Firefox 73.0.1 目前不管你是什么外觀,它的滾動條都是源生的,不會根據(jù)主題變化。這個功能什么時候加上去啊?
發(fā)送通知權(quán)限已設(shè)置為允許,但還是無法彈出通知,控制臺提示“通知權(quán)限只能從安全的上下文請求”,這種情況該怎么解決?另外,傳統(tǒng)版 Edge 可以彈出,Chrome 沒測試。
我的預(yù)期是,在網(wǎng)頁中點開的新標簽頁就在當前頁面的右邊打開,這個可以通過設(shè)置中的 browser.tabs.insertAfterCurrent 改為 true 來實現(xiàn),but,這樣設(shè)置之后, 點+號新建的標簽頁也是出現(xiàn)在當前標簽頁的右側(cè),而不是最右側(cè)。 另外還有個問題更蛋疼,我現(xiàn)在網(wǎng)頁中點開一個新標簽頁,如果當前打開的標簽頁中有相同網(wǎng)址的標簽頁(猜測的),這個新標簽頁會跳過去。。。
蛋疼的一逼呀,我是真的想支持一下 Firefox,但它就不能讓我像用 Chrome 那樣省點心嗎。。
Firefox 起一個 Youtube 開全屏,然后在另一個屏幕上將 Firefox 窗口變?yōu)榧せ?這個時候上方的系統(tǒng)菜單欄消失不見.很難受,有人知道如何解決嗎?
我們的需求是做一套響應(yīng)式的業(yè)務(wù)系統(tǒng),也可能不止一套,需要響應(yīng)式,還需要支持用 Cordova 打包起來跑在 iOS 和 Android 上面,簡單地調(diào)用通信錄什么的在電腦上也要支持常見的幾個瀏覽器,不用兼容很舊的瀏覽器 現(xiàn)在找了幾家: - https://material.angular.io/ - https://www.primefaces.org/primeng - https://vmware.github.io/clarity/ 看起來功能都好強大的,不知道哪個更適合我們的需求,大家有什么推薦沒
想寫一個 Flutter 圖表插件,苦于起名。
目前卡在 flutter create xxx -t=plugin 這一步。
求一個好聽易記的名字 ??
大家好,我們是字節(jié)跳動 Flutter 基礎(chǔ)架構(gòu)團隊,致力于為字節(jié)旗下全系產(chǎn)品提供高品質(zhì)的跨平臺技術(shù),目前公司內(nèi)已經(jīng)有 30+ 個業(yè)務(wù)在使用 Flutter 引擎技術(shù),包括但不限于頭條、火山、西瓜視頻等業(yè)務(wù)。
我們團隊負責(zé)整個公司 Flutter 的通用平臺建設(shè)和技術(shù)優(yōu)化的工作,可以理解為大家所熟悉的技術(shù)中臺,我們在優(yōu)化 Flutter 引擎性能與穩(wěn)定性、強化 Flutter 容器能力、豐富組件庫、改善研發(fā)體驗、探索多端一體化場景等各方向都投入大量人力,并且已經(jīng)取得不錯的成果。
字節(jié)跳動全球推出了多款有影響力的產(chǎn)品,包括抖音、今日頭條、西瓜視頻、TikTok 、TopBuzz 等,旗下全線產(chǎn)品總 MAU (月活躍用戶)超過 15 億,已覆蓋全球 150 個國家和地區(qū)、曾在 40 多個國家和地區(qū)排在應(yīng)用商店總榜前列。公司目前處于高速發(fā)展期,堅信 Flutter 技術(shù)能給公司更多的產(chǎn)品和開發(fā)者帶來價值。當然,如果不了解 Flutter,可以看看我的博客 http://gityuan.com/flutter/gityuan.com/flutter,帶來初窺 Flutter 的技術(shù)魅力。
Flutter 基礎(chǔ)架構(gòu)團隊大牛如云,技術(shù)氛圍濃厚,追求極致,和優(yōu)秀的人做有挑戰(zhàn)的事。Flutter 基礎(chǔ)架構(gòu)團隊歡迎你的加入,不需要有任何 Flutter 經(jīng)驗,只需要熟悉 Android/iOS/Web 等任一技術(shù)棧即可,簡歷至 [email?protected] 。
很高興掘金提供了這次跟大家一起交流的機會,大家可以問我們關(guān)于「 Flutter 技術(shù)」、「跨平臺技術(shù)」、「客戶端開發(fā)」、「職業(yè)發(fā)展」、「個人成長」相關(guān)方面的問題。我們會在 2020/7/27-2020/7/29 期間,挑選出有價值有意義的問題進行回答。
RTRTRTRT
現(xiàn)在每次調(diào)試 Flutter 上的 SQLite 都得導(dǎo)出到電腦上,然后再操作,搜了一下市場,Stetho 只能調(diào)試網(wǎng)絡(luò)。
找了一圈,發(fā)現(xiàn)一個 SQLHelper 的 AndroidStudio 插件,安裝上之后也沒找到在哪里使用。。。
請問你們是怎么調(diào)試的啊
前段時間公司決定使用 flutter 做移動端開發(fā),由于開發(fā)同學(xué)是前端轉(zhuǎn) flutter 對 android 不熟,所以有了讓我協(xié)助寫幾個 flutter 插件這樣的訴求。
請各位不吝賜教順手 star
百度人臉識別和活體檢測 flutter 插件
Google reCAPTCHA flutter 插件
配置讀取工具庫
TabBar 的當前索引假設(shè)為 2, 當我進入下一個頁面, 再退回來的時候, 顯示當前活躍的索引就變成了 0 了, 但實際還是 2, 因為左右滑的時候是跳到 1 或 3, 后來從網(wǎng)上看了下別人的一些 demo, 發(fā)現(xiàn)有些也存在這種問題?
cronStock() async { _stockCode = _stockCodeController.value.text; _growRate = _growRateController.value.text; _reduceRate = _reduceRateController.value.text; …… saveData(); refreshStock(true); await AndroidAlarmManager.periodic(const Duration(seconds: 30), periodicAlarmID, refreshStock, wakeup: true); } saveData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.reload(); try { print('0001 saveData: '+DateTime.now().toString()); prefs.setString('stock_prefix', stockPrefix); prefs.setString('stock_code', _stockCodeController.value.text); prefs.setDouble('grow_rate', double.parse(_growRateController.value.text)); prefs.setDouble('reduce_rate', double.parse(_reduceRateController.value.text)); } on FormatException catch(e) { } catch (e) { // No specified type, handles all } } …… refreshStock([bool isManual = false]) { print('0002 refreshStock afterTimer: '+DateTime.now().toString()); Future stockShared = getShared(); stockShared.then((List shared) { print('isManual: $isManual'); print('shareData: $shared'); }); } Future getShared() async { print('getShared'); SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.reload(); return [ prefs.getString('stock_prefix'), prefs.getString('stock_name'), prefs.getString('stock_code'), prefs.getDouble('grow_rate'), prefs.getDouble('reduce_rate'), // 當前值 prefs.getString('stock_price'), prefs.getDouble('stock_rate'), prefs.getInt('rate_color'), ]; }
AndroidAlarmManager 插件用到了 Isolate 環(huán)境,在 Isolate 環(huán)境下 refreshStock 函數(shù)中的 getShared()拿到的 SharedPreferences 數(shù)和上面 saveData()存儲的數(shù)據(jù)不同步,要多次間隔調(diào)用的后才會同步到。
原文地址: https://blog.codemagic.io/releasing-your-flutter-desktop-application/
原文作者: https://medium.com/@rody.davis.jr
發(fā)布時間:2020 年 6 月 16 日
所以你建立了你的第一個 Flutter 應(yīng)用,并在 iOS 設(shè)備的 AppStore 和 Android 設(shè)備的 Google Play 上發(fā)布。然后,你想接觸更多的受眾和目標網(wǎng)絡(luò),所以你用靜態(tài)主機發(fā)布了它。但你仍然想要更多。如果你想要移動設(shè)備的性能,但又想要 web 的響應(yīng)速度,那么桌面版就是答案。
目前的選擇
對于桌面,你有幾個選擇來發(fā)布--但它們都有一定的權(quán)衡。在這里,我們將討論為什么你可能想選擇一個而不是另一個。
Electron
你可能以前聽說過,因為目前 MacOS 和 Windows 上的大部分第三方應(yīng)用都是用它發(fā)布的。Electron 是一個由 chromium 驅(qū)動的瀏覽器,它使用 Node.js 將網(wǎng)絡(luò)瀏覽器與文件系統(tǒng)粘合在一起。你可以得到為網(wǎng)絡(luò)開發(fā)的好處,但又有桌面的靈活性。它使用大量的 JS 來做到這一點,所以你失去了 AOT (提前編譯) ,這意味著沒有剪枝,優(yōu)化或一般性能。
Flutter 在移動端工作得很好的原因是由于發(fā)布構(gòu)建的 AOT 。你可以通過在根目錄下添加一個 manifest 文件和一些額外的模板腳本,用 Electron 發(fā)布一個 Flutter 應(yīng)用程序。您打包您的 Flutter Web 應(yīng)用程序,就像部署到靜態(tài)托管時一樣。如果您想使用 FLUTTER_WEB_USE_SKIA 標志,您可以從 Web 版本中獲得更好的性能。
單機版
無論你是用 Electron 、桌面嵌入還是自定義嵌入器來構(gòu)建你的 Flutter 桌面應(yīng)用,你都需要一種方法來向世界發(fā)布應(yīng)用。如果你用 Codemagic 、Github Actions 或手動創(chuàng)建一個發(fā)布構(gòu)建,你就把構(gòu)建上傳到 Amazon S3 或類似的 CDN,并把鏈接提供給客戶。這種方法很好,因為你不必等待審查過程,也不必處理每個平臺的非常具體的規(guī)則。然后,你可以把這個鏈接放在你的 Flutter Web 應(yīng)用或 PWA 清單上,只要用戶覺得合適,就可以提供原生體驗。
這種方法的一個主要缺點是你如何處理更新。您需要使用一個庫或自定義構(gòu)建的解決方案,用于在后臺下載和安裝更新或通知用戶新的更新。你給用戶的步驟越多,他們完成所有步驟的可能性就越小。你以用戶為代價換取了靈活性和可用性。
有一些不錯的工具,比如 MacOS 和 Windows 的 Sparkle,它為你每天使用的很多應(yīng)用程序提供了動力。每當你看到一個彈出窗口說一個更新已經(jīng)準備好安裝時,很有可能是在使用這個庫。該庫通過托管的 RSS 源運行,你可以通過解析來獲取發(fā)布說明、版本和安裝鏈接。該應(yīng)用程序?qū)L試在后臺為你自動安裝它們,并在你下次啟動時重新加載。
官方商店
現(xiàn)在,你可能會推遲的選擇是發(fā)布到官方商店。在應(yīng)用商店之外,可能有合法的理由,因為你可能沒有遵循所有的指導(dǎo)方針,有一個自定義的部署和發(fā)布后臺,企業(yè)應(yīng)用只用于內(nèi)部或有限的使用,或者你只是想在你的網(wǎng)站上有一個鏈接來下載應(yīng)用。許多應(yīng)用程序甚至在商店中提供應(yīng)用程序,但也有一個在線版本,可能是測試頻道或特殊構(gòu)建。我發(fā)現(xiàn)這是一個很好的方法,因為例如 Mac AppStore 仍然沒有像 iOS 那樣為 MacOS 應(yīng)用提供 TestFlight 。
部署到商店可能具有挑戰(zhàn)性,但我相信最終是值得的,因為你獲得了安全性和自動更新。他們還將處理付款和退款。在未來的文章中,我將會介紹向 Mac AppStore 發(fā)布 Flutter 應(yīng)用程序,就像我在 iPadOS 和 MacOS 上發(fā)布新的 Widget Studio 一樣。順便說一下,Widget Studio 也可以作為一個 PWA 。我建議在 MacOS 上,你應(yīng)該只包括你正在積極使用的權(quán)限,如果你想分享 Mac 和 iPad 應(yīng)用的購買,你需要有相同的捆綁 ID 。你不會用 Catalyst,而是用當前的桌面嵌入來做這件事。
結(jié)束語
這是一個激動人心的時刻,以一種原生的方式將移動應(yīng)用帶到桌面,這在以前是不可能的。Flutter 很厲害,可以讓你針對 MacOS 、Windows 和 Linux,現(xiàn)在由你來決定如何發(fā)布。Codemagic 支持 Mac 和 Linux ,這比 2 個復(fù)選框還要簡單。如果你有任何問題,請告訴我,我期待著看到你的 Flutter 桌面應(yīng)用程序!
Rody Davis Jr 是一名專業(yè)的全棧開發(fā)者,在企業(yè)和個人應(yīng)用方面都有豐富的經(jīng)驗。他使用最新的框架為 App Store 、Google Play 、Web 和桌面創(chuàng)建應(yīng)用程序。Rody 熱愛 Flutter 、Web 和所有有創(chuàng)意的東西,并在 Medium 上寫 Flutter 文章。他希望通過他的應(yīng)用接觸到盡可能多的人,并展示最新科技的可能性。
通過 www.DeepL.com/Translator (免費版)翻譯
需要定時運行一個函數(shù)獲取 API 數(shù)據(jù),用的 Timer.periodic 做間隔執(zhí)行,但是測試發(fā)現(xiàn)正式包手機息屏后 Timer.periodic 就暫停了,真機調(diào)試模式下是沒有問題的。后來換 android_alarm_manager 插件實現(xiàn)間隔調(diào)用還是有同樣的問題。
我這邊沿襲了 web 的笨辦法,在一個 webservice.dart 下面維護所有的接口 url,然后每一個功能模塊 import 一次,各位有啥更先進的辦法嗎,比如作為一個環(huán)境變量來維護,剛上手對 dart 特性還不太熟悉,先謝謝了
副標題:Ubuntu 團隊已經(jīng)為所有 Linux 發(fā)行版上的 Flutter 應(yīng)用程序制作了一個新的基于 GTK+的主機。 原文地址: https://medium.com/flutter/announcing-flutter-linux-alpha-with-canonical-19eb824590a9
原文作者: https://medium.com/@csells
發(fā)布時間:2020 年 7 月 8 日
作者:Chris Sells (Google) & Ken VanDine (Canonical)
Google 對 Flutter 的目標一直是提供一個可移植的工具包,用于構(gòu)建以原生速度運行的漂亮 UI,無論你的目標是哪個平臺。為了驗證這一能力,我們首先關(guān)注移動平臺 Android 和 iOS,在這兩個平臺上,我們已經(jīng)看到有超過 8 萬個快速、漂亮的 Flutter 應(yīng)用發(fā)布到 Google Play 。
為了在這一成功的基礎(chǔ)上再接再厲,一年多來,我們一直在將我們的關(guān)注點擴展到包括桌面級體驗,包括網(wǎng)絡(luò)和桌面操作系統(tǒng):macOS 、Windows 和 Linux 。這項工作包括對引擎進行大量重構(gòu),以支持桌面風(fēng)格的鼠標和鍵盤輸入,以及可調(diào)整大小的頂層窗口。它還包括新的 UI 功能,能很好地適應(yīng)桌面,如 Material Density 支持和 NavigationRail ,以及在 Dart:FFI 中的實驗和對系統(tǒng)菜單欄和標準對話框的訪問,與底層桌面操作系統(tǒng)深度集成的實驗。所有這些工作都是為了確保 Flutter 除了適合移動風(fēng)格的體驗外,還能處理全功能、全尺寸的桌面應(yīng)用。
長期以來,我們的愿景是讓 Flutter 為平臺提供動力。我們已經(jīng)看到這一點在谷歌通過 Assistant 等產(chǎn)品體現(xiàn)出來,所以現(xiàn)在我們很高興看到其他人利用 Flutter 為更多平臺提供動力。今天,我們很高興與世界上最流行的桌面 Linux 發(fā)行版 Ubuntu 的發(fā)行商 Canonical 一起,共同宣布 Flutter 的 Linux alpha 的可用性。
為什么 Linux 要用 Flutter ?
去年,當 Google 宣布用 Flutter 支持桌面級應(yīng)用時,Canonical 看到了一個令人興奮的機會,使包括 Ubuntu 在內(nèi)的 Linux 發(fā)行版成為對 Flutter 應(yīng)用開發(fā)者有吸引力的目標平臺。Flutter 的原生跨平臺故事正在迅速發(fā)展,Canonical 希望成為先鋒。通過在 Flutter 中實現(xiàn)對桌面 Linux 的支持,Canonical 讓應(yīng)用開發(fā)者可以非常容易地通過 Linux 的應(yīng)用商店 Snap Store 為 Linux 用戶發(fā)布應(yīng)用。通過使 Linux 成為一流的 Flutter 平臺,Canonical 正在邀請應(yīng)用開發(fā)者向數(shù)百萬 Linux 用戶發(fā)布他們的應(yīng)用,并擴大向他們提供高質(zhì)量的應(yīng)用。
關(guān)于 Flutter 的一些事情讓 Canonical 感到興奮。 快速增長的應(yīng)用開發(fā)者生態(tài)系統(tǒng) 多平臺支持 高度優(yōu)化的本地應(yīng)用 現(xiàn)代 UI 框架,支持聲明式、反應(yīng)式和可組合的 widget 。 使用 Visual Studio Code 、Android Studio 和 IntelliJ 的豐富開發(fā)平臺。
谷歌最初宣布宣布支持 Flutter 桌面,首先是支持 macOS 的 alpha 版本,并計劃支持 Linux 和 Windows 。Canonical 對 Flutter 進行了大量投資,專門成立了一個開發(fā)者團隊,與谷歌的開發(fā)者一起為大多數(shù) Linux 發(fā)行版帶來最佳的 Flutter 體驗。Canonical 將繼續(xù)與谷歌合作,進一步完善對 Linux 的支持,并保持與其他支持平臺的功能對等。
Flokk 。證明 Flutter 已經(jīng)為桌面做好了準備。
為了證明 Flutter 已經(jīng)為桌面做好了準備,我們與 gskinner 的設(shè)計師和開發(fā)人員合作,創(chuàng)建了一個創(chuàng)新的、漂亮的 Flutter 桌面應(yīng)用。Flokk 是一個現(xiàn)實世界的應(yīng)用,它可以使用現(xiàn)實世界的數(shù)據(jù),特別是你的谷歌聯(lián)系人列表。
https://youtu.be/cTFJcq7UTRY
除了能夠管理你的聯(lián)系人,包括搜索聯(lián)系人、添加新的聯(lián)系人和編輯現(xiàn)有的聯(lián)系人,Flokk 還可以讓你將 GitHub 和 Twitter 的手柄信息與你的聯(lián)系人關(guān)聯(lián)起來。
Flokk 通訊錄應(yīng)用是用 Flutter 打造的,針對的是桌面。
GitHub 和 Twitter 通知的顯示,將你的聯(lián)系人變成了你自己的個人社交網(wǎng)絡(luò)。如果你在 Flokk Contacts 中沒有看到你喜歡的社交網(wǎng)絡(luò),那么好消息是 Flokk 是 完全開源 的,所以你可以提交 PR 來添加你的收藏夾。
除了在社交領(lǐng)域的創(chuàng)新,Flokk 還使用 Flutter 功能,讓人看起來感覺很好。僅舉一例,深色主題不僅可以切換顏色,而且在切換時還會有動畫變化。
Flutter 使 Flokk 能夠利用流暢的動畫、高性能的滾動和簡單的主題。
Flokk Contacts 應(yīng)用背后的創(chuàng)意團隊是由 Grant Skinner 領(lǐng)導(dǎo)的,他以卓越的設(shè)計和創(chuàng)新用戶體驗的實現(xiàn)而聞名。對于在 Linux 上與 Flutter 的合作,Grant 這樣說。 "構(gòu)建 Flokk Contacts 應(yīng)用是一件輕而易舉的事情! 我們能夠?qū)⑽覀冎霸?Flutter 方面的所有專業(yè)知識應(yīng)用到 Linux 上,幾乎沒有任何調(diào)整,應(yīng)用程序運行得非常好。與 Canonical 團隊合作是一次美妙的經(jīng)歷;他們熱情、投入,并且熱衷于讓 Flutter 不僅適用于 Linux,而且適用于每個平臺。這是一個了不起的項目,我很高興能夠用 Flutter 瞄準另一個主要的操作系統(tǒng)。" - Grant Skinner
如果你想在 Linux 機器上使用 Flokk 應(yīng)用,你可以在 GitHub 上下載最新版本?;蛘?如果你正在運行 snapd,你可以從 Snap Store 下載 Flokk 應(yīng)用。
在 Linux 上輕松安裝 Flutter
現(xiàn)在你已經(jīng)看到了 Flutter 對于桌面級應(yīng)用的工作效果,尤其是在 Linux 上,你會想讓它在自己的 Linux 機器上運行。為了使這一點盡可能的簡單,我們很高興在 Snap Store 中提供 Flutter SDK for Linux 作為 Snap 。Flutter SDK snap 提供了在您最喜歡的 Linux 發(fā)行版上開發(fā) Flutter 應(yīng)用程序所需的一切。不需要安裝一堆開發(fā)依賴;只需安裝 Flutter SDK snap 和您最喜歡的 IDE,您就擁有了為 Linux 創(chuàng)建、構(gòu)建和發(fā)布應(yīng)用程序所需的一切。
例如,如果你想開始為 Linux 開發(fā) Flutter 應(yīng)用程序,而你選擇的 IDE 是 Visual Studio Code,這就是你在 Linux 終端需要做的一切。 $ snap install --classic flutter $ snap install --classic code $ code --install-extension dart-code.flutter 。
如果你也想使用 Linux 開發(fā)移動應(yīng)用,你可以通過安裝 Android SDK 或 Android Studio (其中包括 Android SDK )來實現(xiàn)。有關(guān) Flutter SDK 作為快件的更多信息,請參閱 https://snapcraft.io/flutter 。
Flutter for Linux 桌面
一旦在 Linux 機器上安裝了 Flutter SDK,要構(gòu)建桌面應(yīng)用就需要升級到 Flutter dev 或 master 通道。然后啟用 Linux 桌面支持。 啟用 Linux 桌面支持: $ flutter channel dev $ flutter upgrade $ flutter config --enable-linux-desktop
現(xiàn)在,當你創(chuàng)建一個新的 Flutter 項目時,你會得到一個 linux 子目錄,讓你在 Linux 桌面上運行應(yīng)用程序。 $ flutter create counter $ cd counter $ flutter run -d linux
著名的 Flutter Counter 應(yīng)用也能在 Linux 上運行得很好。
你將得到的是一個用 Flutter 構(gòu)建的、運行在最新穩(wěn)定版 GTK+上的閃亮的新 Linux 應(yīng)用。如果你有一個現(xiàn)有的 Flutter 項目,你想在啟用 Linux 后添加 Linux 支持,你可以像這樣添加 linux 子目錄。 $ cd my_flutter_app $ flutter create .
這將創(chuàng)建 linux 子目錄與 Runner 項目,你需要在 Linux 桌面上構(gòu)建和運行你的 Flutter 應(yīng)用程序。
從 Flutter 訪問 Linux 中的本地代碼
除了通過編寫 Dart 創(chuàng)建 Flutter 小部件來支持桌面外,你的 Linux 桌面應(yīng)用程序還可以使用 平臺通道 或 C/C++的 Dart 外函數(shù)接口 來訪問所有的原生 Linux ?;蛘?如果你想重用已有的代碼,你可以在 pub.dev ,Dart 和 Flutter 的包管理器網(wǎng)站上找到這些代碼。在 pub.dev 上,你會發(fā)現(xiàn)大部分的包都是純 Dart 的,其中大部分在 Linux 應(yīng)用中工作得很好。有些包,被稱為插件,其中有針對一個或多個平臺的本地代碼。作為此次發(fā)布的一部分,我們在 pub.dev 上發(fā)布了三個使用 Linux 本地功能的插件。 url_launcher :在提供的 URL 上啟動默認瀏覽器。 shared_preferences : 在應(yīng)用程序會話之間共享的用戶偏好。 path_provider :特殊用途目錄的路徑信息,如下載、圖片等。
這些插件中的每一個都可以供你在你的應(yīng)用程序中使用,以及作為如何從你的 Flutter 代碼中原生地訪問 Linux 的一個例子,比如 url_launcher 的 Linux 實現(xiàn) 。
部署到 Snap Store
要將你的 Flutter 應(yīng)用部署到 Snap Store,你首先需要安裝 Snapcraft,這個工具你將用來構(gòu)建和發(fā)布你的應(yīng)用作為一個快照。 $ sudo snap install snapcraft --classic
要驅(qū)動 Snapcraft 工具,你需要在你的應(yīng)用程序的項目目錄下創(chuàng)建一個 snapcraft.yaml 文件。作為一個例子,這是 Flokk 的 snapcraft.yaml 文件。 name: flokk-contacts version: 1.0.1 summary: Flokk Contacts description: A fresh and modern Google Contacts manager that integrates with GitHub and Twitter. confinement: strict base: core18 grade: stable apps: flokk-contacts: command: flokk-contacts extensions: [flutter-master] plugs: - network parts: flokk-contacts: source: . plugin: flutter flutter-target: lib/main.dart # 應(yīng)用程序的主入口點文件。
在你的 snapcraft.yaml 文件所在的目錄下,你現(xiàn)在可以運行 snapcraft 來構(gòu)建你的應(yīng)用程序的 snap 。 $ snapcraft
如果一切順利,這將在你當前的工作目錄下生成一個文件,比如 flokk-contacts_1.0.1_amd64.snap 。 一旦你在 Snap Store 中 設(shè)置了發(fā)布賬戶 ,你就可以發(fā)布你的 snap 了。 $ snapcraft 登錄 $ snapcraft register flokk-contacts. $ snapcraft upload flokk-contacts_1.0.1_amd64.snap --release edge
該命令將把應(yīng)用程序上傳到 Snap Store,并嘗試將其發(fā)布到 邊緣通道 中。一旦您的應(yīng)用程序發(fā)布到邊緣通道,就可以通過 Snap Store 桌面客戶端或使用命令行進行安裝。 $ snap install --edge flokk-contacts
有關(guān)構(gòu)建你的第一個快照并在 Snap Store 中發(fā)布的更多細節(jié),請參見 https://snapcraft.io/first-snap#flutter,獲取指導(dǎo)教程。
Flutter Linux 桌面樣本
Flokk 應(yīng)用是一個針對 Linux 桌面的現(xiàn)實世界 Flutter 應(yīng)用的優(yōu)秀例子。如果想了解更簡單的示例,你可以看看 照片搜索應(yīng)用 ,它也是專門為展示桌面功能而打造的。
嘗試 Linux 上的照片搜索示例
照片搜索是一款簡單的在線照片搜索應(yīng)用,它使用多個插件來接入原生平臺功能,支持 macOS 和 Linux 。
如果想了解一個 Linux 桌面應(yīng)用的例子,并有一步一步的說明,我推薦 《編寫一個 Flutter 桌面應(yīng)用》 codelab ,它指導(dǎo)你使用 OAuth 和 GraphQL 在 Flutter 中構(gòu)建一個 GitHub 客戶端。
Flutter 的 GitHub codelab 客戶端應(yīng)用在行動中。
對于一個更全面的應(yīng)用,可以鍛煉 Flutter 更多的表面積,以及提供幾個小程序,我推薦 Flutter Gallery ,它是去年重新設(shè)計的,支持桌面以及手機。如果你想看看它的運行情況,你也可以在 Snap Store 上查看。
Flutter Gallary 樣品可在 Snap 商店購買
又有一款桌面應(yīng)用展現(xiàn)了 Flutter 有趣的一面,它是由 Thorsten Lorenz 打造的一款名為 batufo 的多人游戲。這款游戲以美麗的背景為背景,讓來自世界各地的玩家實時對戰(zhàn)。
在多個 Flutter 平臺上實時播放
Thorsten 一直在構(gòu)建這個游戲,以支持多個 Flutter 平臺,包括 Linux,macOS,Android 和 iOS 。如果你想看看他是如何做到的,并跟隨未來的更新,他將他的編碼課程以 視頻 的形式提供, 代碼也可以在 GitHub 上獲得 。如果要從 Linux 上安裝游戲,你可以從 Snap Store 上把它拉下來。
概要
通過這個 alpha 版本以及 Google 和 Canonical 之間的緊密合作,Linux 開發(fā)者可以為他們選擇的操作系統(tǒng)獲得 Flutter 支持。通過 快照安裝 Flutter SDK 。使用 Visual Studio Code 或 Android Studio 在 Linux 上構(gòu)建和測試你的桌面應(yīng)用。將您的應(yīng)用部署到 Snap Store 。最新的細節(jié),請看 flutter.dev 上的桌面頁面 。最重要的是, 一定要提供反饋 ,這樣我們才能繼續(xù)讓 Flutter 在 Linux 上做到最好,就像我們努力為每個支持的 Flutter 平臺做的那樣。
來自 Canonical 團隊的 Flutter for Linux 是我們的夢想向前邁出的一大步,讓 Flutter 成為構(gòu)建應(yīng)用的最佳方式,無論你的目標是哪個平臺。針對桌面平臺,使得 Flutter 引擎對谷歌本身無法直接支持的長尾設(shè)備的適應(yīng)性更強,但我們計劃繼續(xù)為這些設(shè)備建立合作伙伴關(guān)系,并啟用生態(tài)系統(tǒng)。
只要有設(shè)備需要快速、漂亮的應(yīng)用,那就是我們希望 Flutter 出現(xiàn)的地方。
通過 www.DeepL.com/Translator (免費版)翻譯
前言
前幾天寫了一個 Fluter 插件 tcard ,用來實現(xiàn)類似于探探卡片的布局。效果如下,本文講解如何使用 Stack 控件實現(xiàn)這個布局。
在線查看
初識 Stack
Stack 是一個有多子項的控件,它會將自己的子項相對于自身邊緣進行定位,后面的子項會覆蓋前面的子項。通常用來實現(xiàn)將一個控件覆蓋于另一個控件之上的布局,比如在一張圖片上顯示一些文字。子項的默認位置在 Stack 左上角,也可以用 Align 或者 Positioned 控件分別進行定位。 Stack( children:
[ Container( width: 100, height: 100, color: Colors.red, ), Container( width: 90, height: 90, color: Colors.green, ), Container( width: 80, height: 80, color: Colors.blue, ), ], )
Stack (Flutter Widget of the Week)
布局思路
要使用 Stack 實現(xiàn)這個卡片布局的大致思路如下 首先需要前,中,后三個子控件,使用 Align 控件定位在容器中。 需要一個手勢監(jiān)聽器 GestureDetector 監(jiān)聽手指滑動。 監(jiān)聽手指在屏幕上滑動同時更新最前面卡片的位置。 判斷移動的橫軸距離進行卡片位置變換動畫或者卡片回彈動畫。 如果運行了卡片位置變換動畫在動畫結(jié)束后更新卡片的索引值。
卡片布局 創(chuàng)建 Stack 容器以及前,中,后三個子控件 class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { // 前面的卡片,使用 Align 定位 Widget _frontCard() { return Align( child: Container( color: Colors.blue, ), ); } // 中間的卡片,使用 Align 定位 Widget _middleCard() { return Align( child: Container( color: Colors.red, ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard() { return Align( child: Container( color: Colors.green, ), ); } @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: Stack( children: [ // 后面的子項會顯示在上面,所以前面的卡片放在最后 _backCard(), _middleCard(), _frontCard(), ], ), ), ), ), ); } } 對子控件分別定位并設(shè)置其尺寸
定位需要設(shè)置 Align 控件的 alignment 屬性,傳入一個 Alignment(x, y) 進行設(shè)置。設(shè)置尺寸需要使用 LayoutBuilder 獲取當前父容器的尺寸,然后根據(jù)容器尺寸進行計算。 class _MyAppState extends State { // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, -0.5), // 使用 SizedBox 確定卡片尺寸 child: SizedBox.fromSize( // 計算卡片尺寸,相對于父容器 size: Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9), child: Container( color: Colors.blue, ), ), ); } // 中間的卡片,使用 Align 定位 Widget _middleCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, 0.0), child: SizedBox.fromSize( // 計算卡片尺寸,相對于父容器 size: Size(constraints.maxWidth * 0.85, constraints.maxHeight * 0.9), child: Container( color: Colors.red, ), ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard(BoxConstraints constraints) { return Align( alignment: Alignment(0.0, 0.5), child: SizedBox.fromSize( // 計算卡片尺寸,相對于父容器 size: Size(constraints.maxWidth * 0.8, constraints.maxHeight * .9), child: Container( color: Colors.green, ), ), ); } @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: LayoutBuilder( builder: (context, constraints) { // 使用 LayoutBuilder 獲取容器的尺寸,傳個子項計算卡片尺寸 return Stack( children: [ // 后面的子項會顯示在上面,所以前面的卡片放在最后 _backCard(constraints), _middleCard(constraints), _frontCard(constraints), ], ); }, ), ), ), ), ); } } 更新最前面卡片位置
向 Stack 容器添加一個 GestureDetector ,手指在屏幕上移動時更新最前面卡片的位置。 class _MyAppState extends State { // 保存最前面卡片的定位 Alignment _frontCardAlignment = Alignment(0.0, -0.5); // 保存最前面卡片的旋轉(zhuǎn)角度 double _frontCardRotation = 0.0; // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { return Align( alignment: _frontCardAlignment, // 使用 Transform.rotate 旋轉(zhuǎn)卡片 child: Transform.rotate( angle: (pi / 180.0) * _frontCardRotation, // 使用 SizedBox 確定卡片尺寸 child: SizedBox.fromSize( size: Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9), child: Container( color: Colors.blue, ), ), ), ); } // 省略...... @override Widget build(BuildContext context) { return MaterialApp( title: 'TCards demo', debugShowCheckedModeBanner: false, home: Scaffold( body: Center( child: SizedBox( width: 300, height: 400, child: LayoutBuilder( builder: (context, constraints) { // 使用 LayoutBuilder 獲取容器的尺寸,傳個子項計算卡片尺寸 Size size = MediaQuery.of(context).size; double speed = 10.0; return Stack( children: [ // 后面的子項會顯示在上面,所以前面的卡片放在最后 _backCard(constraints), _middleCard(constraints), _frontCard(constraints), // 使用一個占滿父元素的 GestureDetector 監(jiān)聽手指移動 SizedBox.expand( child: GestureDetector( onPanDown: (DragDownDetails details) {}, onPanUpdate: (DragUpdateDetails details) { // 手指移動就更新最前面卡片的 alignment 屬性 _frontCardAlignment += Alignment( details.delta.dx / (size.width / 2) * speed, details.delta.dy / (size.height / 2) * speed, ); // 設(shè)置最前面卡片的旋轉(zhuǎn)角度 _frontCardRotation = _frontCardAlignment.x; // setState 更新界面 setState(() {}); }, onPanEnd: (DragEndDetails details) {}, ), ), ], ); }, ), ), ), ), ); } }
卡片動畫
這個布局有三種動畫,最前面卡片移開的動畫;后面兩張卡片位置和尺寸變化的動畫;最前面卡片回到原位的動畫。 判斷卡片橫軸移動距離
在手指離開屏幕時判斷卡片橫軸的移動距離,如果最前面的卡片橫軸移動距離超過限制就運行換位動畫,否則運行回彈動畫。 // 改變位置的動畫 void _runChangeOrderAnimation() {} // 卡片回彈的動畫 void _runReboundAnimation(Offset pixelsPerSecond, Size size) {} // 省略... // 卡片橫軸距離限制 final double limit = 10.0; SizedBox.expand( child: GestureDetector( // 省略... onPanEnd: (DragEndDetails details) { // 如果最前面的卡片橫軸移動距離超過限制就運行換位動畫,否則運行回彈動畫 if (_frontCardAlignment.x > limit || _frontCardAlignment.x < -limit) { _runChangeOrderAnimation(); } else { _runReboundAnimation( details.velocity.pixelsPerSecond, size, ); } }, ), ), 卡片回彈動畫
首先實現(xiàn)卡片回彈的動畫,使用 AnimationController 控制動畫,在 initState 初始化動畫控制器。創(chuàng)建一個 AlignmentTween 設(shè)置動畫運動值,起始值是卡片當前位置,最終值是卡片的默認位置。然后將一個彈簧模擬 SpringSimulation 傳遞給動畫控制器,讓動畫模擬運行。 class _MyAppState extends State with TickerProviderStateMixin { // 省略... // 卡片回彈動畫 Animation _reboundAnimation; // 卡片回彈動畫控制器 AnimationController _reboundController; // 省略... // 卡片回彈的動畫 void _runReboundAnimation(Offset pixelsPerSecond, Size size) { // 創(chuàng)建動畫值 _reboundAnimation = _reboundController.drive( AlignmentTween( // 起始值是卡片當前位置,最終值是卡片的默認位置 begin: _frontCardAlignment, end: Alignment(0.0, -0.5), ), ); // 計算卡片運動速度 final double unitsPerSecondX = pixelsPerSecond.dx / size.width; final double unitsPerSecondY = pixelsPerSecond.dy / size.height; final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); final unitVelocity = unitsPerSecond.distance; // 創(chuàng)建彈簧模擬的定義 const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1); // 創(chuàng)建彈簧模擬 final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); // 根據(jù)給定的模擬運行動畫 _reboundController.animateWith(simulation); // 重置旋轉(zhuǎn)值 _frontCardRotation = 0.0; setState(() {}); } @override void initState() { super.initState(); // 初始化回彈的動畫控制器 _reboundController = AnimationController(vsync: this) ..addListener(() { setState(() { // 動畫運行時更新最前面卡片的 alignment 屬性 _frontCardAlignment = _reboundAnimation.value; }); }); } // 省略... } 卡片換位動畫
卡片換位動畫就是將最前面的卡片移除可視區(qū),將中間的卡片移動到最前面,將最后的卡片移動到中間,然后新建一個最后面的卡片。在卡片更換位置的同時需要改變卡片的尺寸,位置動畫和尺寸動畫同時進行。首先定義每個卡片運動時的動畫值 /// 卡片尺寸 class CardSizes { static Size front(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.9, constraints.maxHeight * 0.9); } static Size middle(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.85, constraints.maxHeight * 0.9); } static Size back(BoxConstraints constraints) { return Size(constraints.maxWidth * 0.8, constraints.maxHeight * .9); } } /// 卡片位置 class CardAlignments { static Alignment front = Alignment(0.0, -0.5); static Alignment middle = Alignment(0.0, 0.0); static Alignment back = Alignment(0.0, 0.5); } /// 卡片運動動畫 class CardAnimations { /// 最前面卡片的消失動畫值 static Animation frontCardDisappearAnimation( AnimationController parent, Alignment beginAlignment, ) { return AlignmentTween( begin: beginAlignment, end: Alignment( beginAlignment.x > 0 ? beginAlignment.x + 30.0 : beginAlignment.x - 30.0, 0.0, ), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.0, 0.5, curve: Curves.easeIn), ), ); } /// 中間卡片位置變換動畫值 static Animation middleCardAlignmentAnimation( AnimationController parent, ) { return AlignmentTween( begin: CardAlignments.middle, end: CardAlignments.front, ).animate( CurvedAnimation( parent: parent, curve: Interval(0.2, 0.5, curve: Curves.easeIn), ), ); } /// 中間卡片尺寸變換動畫值 static Animation middleCardSizeAnimation( AnimationController parent, BoxConstraints constraints, ) { return SizeTween( begin: CardSizes.middle(constraints), end: CardSizes.front(constraints), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.2, 0.5, curve: Curves.easeIn), ), ); } /// 最后面卡片位置變換動畫值 static Animation backCardAlignmentAnimation( AnimationController parent, ) { return AlignmentTween( begin: CardAlignments.back, end: CardAlignments.middle, ).animate( CurvedAnimation( parent: parent, curve: Interval(0.4, 0.7, curve: Curves.easeIn), ), ); } /// 最后面卡片尺寸變換動畫值 static Animation backCardSizeAnimation( AnimationController parent, BoxConstraints constraints, ) { return SizeTween( begin: CardSizes.back(constraints), end: CardSizes.middle(constraints), ).animate( CurvedAnimation( parent: parent, curve: Interval(0.4, 0.7, curve: Curves.easeIn), ), ); } }
使用一個 AnimationController 控制動畫運行,動畫運行時在卡片上應(yīng)用以上的動畫值,否則使用卡片默認的位置和尺寸。 class _MyAppState extends State with TickerProviderStateMixin { // 省略... // 卡片位置變換動畫控制器 AnimationController _cardChangeController; // 前面的卡片,使用 Align 定位 Widget _frontCard(BoxConstraints constraints) { // 判斷動畫是否在運行 bool forward = _cardChangeController.status == AnimationStatus.forward; // 使用 Transform.rotate 旋轉(zhuǎn)卡片 Widget rotate = Transform.rotate( angle: (pi / 180.0) * _frontCardRotation, // 使用 SizedBox 確定卡片尺寸 child: SizedBox.fromSize( size: CardSizes.front(constraints), child: Container( color: Colors.blue, ), ), ); // 在動畫運行時使用動畫值 if (forward) { return Align( alignment: CardAnimations.frontCardDisappearAnimation( _cardChangeController, _frontCardAlignment, ).value, child: rotate, ); } // 否則使用默認值 return Align( alignment: _frontCardAlignment, child: rotate, ); } // 中間的卡片,使用 Align 定位 Widget _middleCard(BoxConstraints constraints) { // 判斷動畫是否在運行 bool forward = _cardChangeController.status == AnimationStatus.forward; Widget child = Container(color: Colors.red); // 在動畫運行時使用動畫值 if (forward) { return Align( alignment: CardAnimations.middleCardAlignmentAnimation( _cardChangeController, ).value, child: SizedBox.fromSize( size: CardAnimations.middleCardSizeAnimation( _cardChangeController, constraints, ).value, child: child, ), ); } // 否則使用默認值 return Align( alignment: CardAlignments.middle, child: SizedBox.fromSize( size: CardSizes.middle(constraints), child: child, ), ); } // 后面的卡片,使用 Align 定位 Widget _backCard(BoxConstraints constraints) { // 判斷動畫是否在運行 bool forward = _cardChangeController.status == AnimationStatus.forward; Widget child = Container(color: Colors.green); // 在動畫運行時使用動畫值 if (forward) { return Align( alignment: CardAnimations.backCardAlignmentAnimation( _cardChangeController, ).value, child: SizedBox.fromSize( size: CardAnimations.backCardSizeAnimation( _cardChangeController, constraints, ).value, child: child, ), ); } // 否則使用默認值 return Align( alignment: CardAlignments.back, child: SizedBox.fromSize( size: CardSizes.back(constraints), child: child, ), ); } // 改變位置的動畫 void _runChangeOrderAnimation() { _cardChangeController.reset(); _cardChangeController.forward(); } // 省略... @override void initState() { super.initState(); // 省略... // 初始化卡片換位動畫控制器 _cardChangeController = AnimationController( duration: Duration(milliseconds: 1000), vsync: this, ) ..addListener(() => setState(() {})) ..addStatusListener((status) { if (status == AnimationStatus.completed) { // 動畫運行結(jié)束后重置位置和旋轉(zhuǎn) _frontCardRotation = 0.0; _frontCardAlignment = CardAlignments.front; setState(() {}); } }); } // 省略... }
數(shù)據(jù)更新
主題內(nèi)容長度不能超過 20000 個字符。。。超長限制,全文地址在此 用 Flutter 實現(xiàn)探探卡片布局 鑒于 Flutter 高性能渲染和跨平臺的優(yōu)勢,閃點清單在移動端 APP 上,使用了完整的 Flutter 框架來開發(fā)。既然是完整 APP,架構(gòu)搭建完全不受歷史 Native APP 的影響,沒有歷史包袱的沉淀,設(shè)計也能更靈活和健壯。
國際化語言的支持,是很多 APP 都有的一個強需求,APP 無論大小,只要還不想放棄國外的客戶,一般就需要支持國際化。
官方支持
Flutter 官方方案提供了國際化的基礎(chǔ)支持,如 Flutter 內(nèi)置組件的國際化、語言代理、Widget 使用語言包、語言設(shè)置回調(diào)等,并支持自定義第三方類來擴展,可以參考 Flutter 國際化文檔。 官方支持代碼示例: class DemoLocalizations { DemoLocalizations(this.locale); final Locale locale; static DemoLocalizations of(BuildContext context) { return Localizations.of(context, DemoLocalizations); } static Map> _localizedValues = { 'en': { 'title': 'Hello World', }, 'es': { 'title': 'Hola Mundo', }, }; String get title { return _localizedValues[locale.languageCode]['title']; } }
官方方案的缺陷
官方的支持有幾個缺陷: 依賴于 BuildContext 對象,在非 Widget 中調(diào)用時,需要層層傳遞 BuildContext 對象,或存儲全局 BuildContext 對象。 在 MaterialApp 初始化前無法使用國際化(原因也是依賴于 BuildContext 對象)。 語言包定義推薦使用 Map 方式,無法利用靜態(tài)語言的優(yōu)勢(語法提示、錯誤檢查等);而為語言包每個屬性自定義類和類字段,成本較高、使用和更新靈活性差。
i18n 介紹
鑒于 Flutter 官方支持的缺陷,我們調(diào)研了很多第三方庫,最終發(fā)現(xiàn)了 i18n,并在此基礎(chǔ)上、結(jié)合 Flutter 官方支持和自身封裝,實現(xiàn)了更靈活易用的方案。
基礎(chǔ)使用
i18n 使用 yaml 格式來定義語言包,同時提供構(gòu)建腳本一鍵生成 Dart 語言包 Class 。如下: lib/messages.i18n.yaml button: save: Save load: Load users: welcome(String name): "Hello $name!" logout: Logout
該配置會生成幾個 Class:Messages 、ButtonMessages 、UserMessages,生成后的 Dart 文件使用方式如下: Messages m = Messages(); debugPrint(m.users.logout); debugPrint(m.users.welcome('World'));
生成的 Dart 文件預(yù)覽(開發(fā)時無需關(guān)心): class Messages { const Messages(); ButtonMessages get button => ButtonExampleMessages(this); UsersMessages get users => UsersExampleMessages(this); } class ButtonMessages { final Messages _parent; const ButtonMessages(this._parent); String get save => "Save"; String get load => "Load"; } class UsersMessages { final Messages _parent; const UsersMessages(this._parent); String get logout => "Logout"; String welcome(String name) => "Hello $name!"; }
進階功能
下面講解一些進階用法。
函數(shù)定義
i18n 支持函數(shù)定義,并支持傳參,如上述的 welcome 函數(shù): debugPrint(m.users.welcome('World'));
參數(shù)定義基本沒有限制,可以隨意定義參數(shù)個數(shù)和類型。
內(nèi)置函數(shù)
i18n 支持了一些內(nèi)置函數(shù),用于做不同語言解析的體驗優(yōu)化,如:plural 、cardinal 、ordinal 。具體規(guī)則和使用,可以參考這里: http://cldr.unicode.org/index/cldr-spec/plural-rules
使用 Dart 字符串模板
Dart 字符串模板是非常強大的,而在 i18n 中,你可以使用字符串模板(這點非常贊),如: count(int cnt): "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}."
前置編譯
i18n 依然依賴了 Dart 官方提供的 builder_runner 工具,來從 yaml 文件生成 Dart 文件,使用方式: flutter pub run build_runner build 。
語言包使用
前置編譯后,每個語言包會生成 N 個 Class (語言包的每一個分類或組合會生成一個 Class 文件),然后會生成一個根 Class,我們可以直接使用根 Class (當然也可以使用任何一個分類層級的 Class )。
比如兩個語言包文件: AppMessages.i18n.yaml 和 AppMessages_en.i18n.yaml (未加語言后綴的,會認為是默認語言包,因此 AppMessages.i18n.yaml 是默認語言包),會生成 2 個根 Dart Class: class AppMessages 和 class AppMessages_en extends AppMessages 。
AppMessages_en 自動繼承自 AppMessages ,因此我們可以直接使用 AppMessages 類型來存儲語言包,并在語言切換時重新為其實例化對應(yīng)的子類: AppMessages appMessages = new AppMessages(); resetLocalLang(String localeName) { switch (localeName) { case 'en': appMessages = AppMessages_en(); break; case 'zh': default: appMessages = AppMessages(); break; } }
然后你可以在任意地方使用語言包: debugPrint('Load Button: ${appMessages.button.load}'); FlatButton( child: Text(appMessages.button.save), onPressed: () { /// 干點什么 }, )
Flutter 集成
集成到 Flutter,依然要依賴于官方的支持,在 MaterialApp 中設(shè)置和監(jiān)聽本地語言包: @override Widget build(BuildContext context) { return MaterialApp( localeResolutionCallback: (Locale locale, Iterable supportedLocales) { /// Local changed }, localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: ['zh', 'en'] .map((loc) => new Locale(loc)) .toList(growable: false), /// ... ); }
結(jié)尾
國際化支持,是一個移動端 APP 框架層的基礎(chǔ)能力,設(shè)計原則應(yīng)該是使用無感知、靈活易擴展;但維護成本是難免有增加的,比如每次改文案要所有語言包同時更改。
講到這里,還并沒有完成基礎(chǔ)框架的搭建,后面我們會講解更多的 Flutter 架構(gòu)設(shè)計內(nèi)容,比如:通知、分享、UI 設(shè)計等等。
持續(xù)分享閃點清單在 Flutter 上的開發(fā)經(jīng)驗。 閃點清單 ,一款懸浮清單軟件:
https://www.ithome.com/0/480/365.htm 目前依然在使用 chrome,習(xí)慣了不想換。 第八條有說支持 Netflix 4K,現(xiàn)在新版也支持了嗎,有沒有人測試過?
輸入一段正常的網(wǎng)址,經(jīng)常給我跳到這個頁面:https://cn.bing.com/?scope=web&FORM=ANNNB1 已經(jīng)被煩死了,這個問題大家有解決辦法嗎?
edge 版本是 80.0.361.66(當前最新版),在 chrome 商店里點擊添加到 chrome 會報錯"There was a problem adding the item to Chrome. Please refresh the page and try again.",我記得之前都是可以正常安裝的,代理用的是 switchyomega,雖然個人認為不是代理的原因,但是想看看有沒有用路由端代理或者在國外的同學(xué)試試能不能復(fù)現(xiàn).
版本號:81.0.416.20開全局 都沒法同步 一直提示:無法連接到同步服務(wù)器。正在重試…
微軟在經(jīng)過了一年多的測試之后推出了一款基于 Chromium 內(nèi)核的全新 Edge 瀏覽器,所以有人要換瀏覽器嗎?
在使用 chrome 時,chrome 會自動把網(wǎng)頁中的搜索引擎加入本地的搜索引擎列表中,之后輸入關(guān)鍵詞按 tab 鍵即可切換引擎。 在 edge 中同樣也有此功能,edge 的搜索設(shè)置中提到 要在此處查看更多搜索引擎,請打開一個新的標簽頁,轉(zhuǎn)到要添加的搜索引擎,然后搜索一些內(nèi)容。
但是實測,edge 并不會自動添加搜索引擎,測試過 zhihu、bilibili、都沒有效果。不知道是 bug 還是我的設(shè)置有沖突,想問有沒有遇到同樣問題的朋友?現(xiàn)在阻止我遷移到 edge 的阻力就剩這個了。
想嘗試 angular , 但是感覺 angular 的模板 "不夠 typescript" , 例如官網(wǎng)上的一段 template :
Products
{{ product.name }}
單純看這個模板, 完全看不出來 product 的類型, 同時 typescript 和 eslint 也沒能力管控這個模板中的變量。相對的來說, 在 react 的 tsx 中, typescript 和 eslint 是能夠?qū)ψ兞窟M行充分的靜態(tài)分析的。
無意引戰(zhàn), 確實希望試試 angular , 可是, 是我使用的姿勢不對嗎?
https://material.angular.io/guide/theming
Material 主題可以自定義,主要是默認主題顏色太激進了,有沒有好看的主題包推薦?
有誰搶先體驗了一下新特性嗎?來分享一下唄。
還有,啥時候出 Angular 板塊?不帶 JS 的。
一、項目介紹 運用 angular+angular-cli+angular-router+ngrx/store+rxjs+webpack+node+wcPop 等技術(shù)實現(xiàn)開發(fā)的仿微信 angular 版聊天室 angular-chatroom 實例項目,實現(xiàn)了下拉刷新、聊天消息右鍵菜單、發(fā)送消息、表情(動圖),圖片、視頻預(yù)覽,紅包打賞等功能。
二、技術(shù)實現(xiàn) MVVM 框架:angular8.0 / @angular/cli 狀態(tài)管理:@ngrx/store / rxjs 地址路由:@angular/router 彈窗組件:wcPop 打包工具:webpack 2.0 環(huán)境配置:node.js + cnpm 圖片預(yù)覽:previewImage 輪播滑動:swiper
{ "name": "angular-chatroom", "dependencies": { "@angular/animations": "~8.0.1", "@angular/common": "~8.0.1", "@angular/compiler": "~8.0.1", "@angular/core": "~8.0.1", "@angular/forms": "~8.0.1", "@angular/platform-browser": "~8.0.1", "@angular/platform-browser-dynamic": "~8.0.1", "@angular/router": "~8.0.1", "rxjs": "~6.4.0", }, "devDependencies": { "@angular-devkit/build-angular": "~0.800.0", "@angular/cli": "~8.0.3", "@angular/compiler-cli": "~8.0.1", "@angular/language-service": "~8.0.1", "@ngrx/store": "^8.0.1", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "@types/swiper": "^4.4.3", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", "jquery": "^2.2.3", "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "swiper": "^4.5.0", } } /* * angular 主模塊配置 */ import { BrowserModule } from '@angular/platform-browser' import { NgModule } from '@angular/core' import { FormsModule } from '@angular/forms' import { AppRoutingModule } from './app-routing.module' // 引入狀態(tài)管理 import { StoreModule } from '@ngrx/store' import { reducer } from '../ngrx' // 載入公共組件( component ) import { HeaderComponent } from '../components/header' import { TabBarComponent } from '../components/tabbar' import { XtnScroll } from '../components/xtnScroll/Scroll' import { NotFoundComponent } from '../components/404' // 載入頁面組件( view ) import { AppComponent } from './app.component' import { LoginComponent } from '../views/auth/login' import { RegisterComponent } from '../views/auth/register' import { IndexComponent } from '../views/index' import { ContactComponent } from '../views/contact' import { UinfoComponent } from '../views/contact/uinfo' import { UcenterComponent } from '../views/ucenter' import { GroupChatComponent } from '../views/chat/group-chat' import { GroupInfoComponent } from '../views/chat/group-info' import { SingleChatComponent } from '../views/chat/single-chat' @NgModule({ declarations: [ // 公共組件 HeaderComponent, TabBarComponent, XtnScroll, NotFoundComponent, // 頁面組件 AppComponent, LoginComponent, RegisterComponent, IndexComponent, ContactComponent, UinfoComponent, UcenterComponent, GroupChatComponent, GroupInfoComponent, SingleChatComponent, ], imports: [ BrowserModule, AppRoutingModule, FormsModule, StoreModule.forRoot(reducer) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } /* * angular 路由守衛(wèi)(驗證 token ) */ import { Router, CanActivate } from '@angular/router' declare var wcPop: any; export class Auth implements CanActivate{ constructor(private router: Router){} canActivate(){ let that = this // 驗證 token const token: boolean = window.sessionStorage.getItem('token') ? true : false if(!token){ // 未登錄授權(quán) /* wcPop({ content: '還未登錄授權(quán)!', anim: 'shake', style: 'background:#e03b30;color:#fff;', time: 2, end: function () { that.router.navigate(['/login']); } }); */ that.router.navigate(['/login']); } return token } } function surrounds() { setTimeout(function () { //chrome var sel = window.getSelection(); var anchorNode = sel.anchorNode; if (!anchorNode) return; if (sel.anchorNode === $(".J__wcEditor")[0] || (sel.anchorNode.nodeType === 3 && sel.anchorNode.parentNode === $(".J__wcEditor")[0])) { var range = sel.getRangeAt(0); var p = document.createElement("p"); range.surroundContents(p); range.selectNodeContents(p); range.insertNode(document.createElement("br")); //chrome sel.collapse(p, 0); (function clearBr() { var elems = [].slice.call($(".J__wcEditor")[0].children); for (var i = 0, len = elems.length; i < len; i++) { var el = elems[i]; if (el.tagName.toLowerCase() == "br") { $(".J__wcEditor")[0].removeChild(el); } } elems.length = 0; })(); } }, 10); } // 定義最后光標位置 var _lastRange = null, _sel = window.getSelection && window.getSelection(); var _rng = { getRange: function () { if (_sel && _sel.rangeCount > 0) { return _sel.getRangeAt(0); } }, addRange: function () { if (_lastRange) { _sel.removeAllRanges(); _sel.addRange(_lastRange); } } } // 消息處理 function isEmpty() { // var html = $editor.html(); var html = $(".J__wcEditor").html(); html = html.replace(/
/ig, "\r\n"); html = html.replace(/<[^img].*?>/ig, ""); html = html.replace(/ /ig, ""); return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""; }
歡迎大家一起交流學(xué)習(xí) Q:282310962 wx:xy190310
不考慮學(xué)習(xí)曲線,以項目健壯性和開發(fā)為優(yōu)先考慮因素,大型項目應(yīng)該用哪個呢?還有,Angular 和 AngularJS 是不同的,為什么沒有 Angular(Typescript) 的頻道呢?
github 地址: https://github.com/qianxiaoning/demo-angularJs1.7.5 歡迎大家 star 或者 fork 呀~ 目錄結(jié)構(gòu) src/ components/ 組件 config/ dict.js 一個全局變量的 run 方法 router.js 路由表 validation.js 'angular-validation'的 config 配置 controllers/ 控制器 data/ 數(shù)據(jù) mock 文件 directives/ 指令 filters/ 過濾器 image/ 圖片 pages/ 頁面 services/ 服務(wù)(公共方法) app.js 入口函數(shù) common.less 公共樣式文件 index.html html 模板文件
angularjs 1.5.11
adminLTE
路由切換之后會出現(xiàn)下面一部分空白,f12 或者刷新或者改變?yōu)g覽器窗口大小后就正常了,是瀏覽器卡了還是樣式崩了?
背景
網(wǎng)上很多類似的表單生成,包括 json schema 生成表單也有好幾款成熟的,但是問題都是不支持完全的表單 html 模板,而是提供一個模板組件,接收 json schema 參數(shù)去生成表單。
問題就是,我們的業(yè)務(wù)需求可能會變動,或者頁面需要加一些樣式,布局修改等,這種沒有表單源碼的表單生成,遇到不符合 UI 設(shè)計,或者后期需求變動頁面調(diào)整都是不利的。
表單生成+支持表單 html 頁面模板源碼,好處就很明顯,你既可以用表單組件生成頁面,也可以復(fù)制表單 HTML 原代碼出來進行二次開發(fā)。才真正釋放生產(chǎn)力。所以就有了這個工程: ngx-form-builder
功能實現(xiàn) 支持 json schema 生成表單,支持 bootstrap + ng-zorro-antd 兩種 UI 樣式組件。 支持源碼復(fù)制、下載。和生成的模板支持 StackBlitz 編輯器在線 demo 預(yù)覽編輯 支持 Yapi 接口文檔直接生成表單頁面(思路:接口文檔——>json schema ——>表單頁面)。
DEMO 截圖
Github 源碼
歡迎 PR,或者 issue 提出你們的使用場景
https://github.com/giscafer/ngx-form-builder
更新內(nèi)容 i18n 多語言支持
技術(shù)棧 Typescript Angular Material2 rxjs Graphql
相關(guān)鏈接
項目地址
DEMO
ng-notadd-mock-server
Quick start git clone https://github.com/notadd/ng-notadd.git cd ng-notadd npm install npm start # or use ng cli ng serve
Roadmap
1.0 [ ] 支持 Apollo-Graqphql [ ] 更加完整的 儀表盤頁面
1.1 [ ] json 生成表單
1.2 [ ] 手機端兼容 [ ] 漸進式應(yīng)用(PWA)
1.3 [ ] 更多組件支持
1.4 [ ] 基礎(chǔ)頁面(個人信息頁,登錄頁...) [ ] recaptcha 支持 (默認關(guān)閉)
1.5 [ ] excel 導(dǎo)入與導(dǎo)出 [ ] 選定行列導(dǎo)出 excel
1.6 [ ] 截圖生成 [ ] firebase (國內(nèi)無法使用) or 其他替代方案 支持
1.7 [ ] 可 DIY 儀表盤 [ ] json 生成簡單儀表盤
1.8 [ ] 支持 electron 構(gòu)建桌面應(yīng)用
2.0 [ ] 企業(yè)級自定義表單 [ ] 企業(yè)級表單系統(tǒng)
后續(xù) [ ] excel 在線編輯 [ ] word 在線編輯
一點說明
為了方便維護,ng-notadd 將剝離出 ng-material2 (擴展組件庫) 和 ng-noform 兩個項目
ng-notadd
基于 Angular7 Material2 的中后臺解決方案
技術(shù)棧 Typescript Angular Material2 rxjs Graphql
相關(guān)鏈接
項目地址
DEMO
ng-notadd-mock-server
Quick start git clone https://github.com/notadd/ng-notadd.git cd ng-notadd npm install npm start # or use ng cli ng serve
Roadmap
0.9 [ ] i18n 多語言支持
1.0 [ ] 支持 Apollo-Graqphql [ ] 更加完整的 儀表盤頁面
1.1 [ ] json 生成表單
1.2 [ ] 手機端兼容 [ ] 漸進式應(yīng)用(PWA)
1.3 [ ] 更多組件支持
1.4 [ ] 基礎(chǔ)頁面(個人信息頁,登錄頁...) [ ] recaptcha 支持 (默認關(guān)閉)
1.5 [ ] excel 導(dǎo)入與導(dǎo)出 [ ] 選定行列導(dǎo)出 excel
1.6 [ ] 截圖生成 [ ] firebase (國內(nèi)無法使用) or 其他替代方案 支持
1.7 [ ] 可 DIY 儀表盤 [ ] json 生成簡單儀表盤
1.8 [ ] 支持 electron 構(gòu)建桌面應(yīng)用
2.0 [ ] 企業(yè)級自定義表單 [ ] 企業(yè)級表單系統(tǒng)
后續(xù) [ ] excel 在線編輯 [ ] word 在線編輯