7 Lập trình desktop app với Electron | Blog | manhhomienbienthuy mới nhất

Lâu lắm mới lại lập trình 🤣 Thật tình cờ là vừa trở lại lập trình thì gặp ngay công nghệ tiên tiến mới. Nói thế thôi chứ thực ra là cũ người mới ta. Electron ( trước kia gọi là Atom Shell ) đã Open từ rất lâu rồi, và những ứng dụng sử dụng công nghệ tiên tiến này cũng được sử dụng thoáng đãng ( ví dụ như tôi vẫn hằng ngày sử dụng Visual Studio Code và Microsoft Teams ) .

Bối cảnh

Dòng đời xô đẩy thế nào tôi lại được giao một dự án Bất Động Sản Proof of Concept ( PoC ). Và nhu yếu là cần giao một mẫu sản phẩm mẫu mà người mua hoàn toàn có thể thuận tiện thực thi và kiểm tra hoạt động giải trí. Trong trường hợp này không gì tốt hơn một ứng dụng chạy ngay trên máy tính, không nhu yếu setup hay thông số kỹ thuật gì nhiều .Ý tưởng bắt đầu là sẽ viết ứng dụng bằng Java, sau đó đóng gói, người mua chỉ cần cài JRE là hoàn toàn có thể chạy được. Tuy nhiên, tôi không có kinh nghiệm tay nghề gì về Java ngoại trừ chút kỹ năng và kiến thức học từ thời sinh viên. Vừa học vừa làm thì cũng được thôi nhưng dự án Bất Động Sản PoC thì không có nhiều thời hạn thế. Với kinh nghiệm tay nghề làm web nhiều năm thì lúc này Electron là một lựa chọn tốt. Với lựa chọn này thì bắt buộc phải biết NodeJS .

Tuy kinh nghiệm với JavaScript của tôi chỉ toàn là phía client, chưa bao giờ làm lập trình NodeJS, tuy nhiên nó cũng là JavaScript cả. Hơn nữa, ứng dụng không có yêu cầu cao về hiệu suất, nó chỉ cần chạy đúng logic là được. Suy đi tính lại thì Electron là lựa chọn hợp lý nhất.

Bạn đang đọc: Lập trình desktop app với Electron | Blog | manhhomienbienthuy

Nói qua một chút ít về Electron, thì những ứng dụng viết bằng Electron không khác một trình duyệt web là mấy. Khi bật ứng dụng lên cũng có nghĩa là mở trình duyệt lên ( Electron có sẵn Chromium ở trong, tuy nhiên JavaScript engine của nó hơi khác một chút ít ) và tải một trang HTML. Việc của lập trình viên là sử dụng HTML, CSS, JS để tạo giao diện cho ứng dụng. Khi người dùng click những nút, thì lúc này có 2 giải pháp giải quyết và xử lý :

  1. Xử lý như một trang web bình thường, tức là sẽ submit form, gọi Ajax hay tương tự để gửi lên server, nhận kết quả và hiển thị cho người dùng. Với cách làm này, thì các framework như React hay VueJS sẽ là lựa chọn tuyệt vời cho phía client để có một ứng dụng thật “ngầu”. Về phía server, có thể sử dụng NodeJS hay bất cứ một ngôn ngữ lập trình nào khác, để lập trình web, quá trình truyền nhận dữ liệu tất cả thông qua API. Nhiều trang web được thiết kế sẵn theo hướng này có thể build app desktop rất nhanh, chỉ cần bỏ HTML, CSS, JS vào Electron build là xong, thế là vừa có web, vừa có app luôn.
  2. Ứng dụng Electron có 2 tiến trình, tiến trình chính (main) và renderer. Renderer là tiến trình mà bật Chromium và hiển thị HTML cho người dùng. Tuy là bật trình duyệt web và hiển thị, tuy nhiên tiến trình này có thể tiến hành một số xử lý trực tiếp bằng code NodeJS luôn (ví dụ có thể sử dụng fs để làm việc với files) chứ không bị giới hạn trong môi trường trình duyệt. Một vài xử lý yêu cầu phải chạy ở tiến trình main (ví dụ bật dialog), thì từ renderer có thể invoke một event để tiến trình chính thực hiện, kết quả sẽ được trả về cho renderer để hiển thị.

Với nhu yếu một ứng dụng đơn thuần, ít cần thiết lập hay thông số kỹ thuật thì tôi quyết định hành động sẽ viết ứng dụng theo hướng thứ 2 .

Lập trình ứng dụng đơn giản

Trong nội dung bài viết này, tôi sẽ trình diễn cách lập trình một ứng dụng đơn thuần. Ý tưởng là nhập vào một thư mục và rename tổng thể file trong thư mục đó thành 1 tên ngẫu nhiên có độ dài cố định và thắt chặt ( cũng tuỳ theo input ) .

Cài đặt và cấu hình

Trước hết, Electron chạy trên NodeJS nên yêu cầu máy tính phải cài sẵn NodeJS và npm. Với những người lập trình web thì những công cụ này không còn gì xa lạ nữa, nên trong bài viết này tôi không đi sâu vào chi tiết việc cài đặt này.

Sau khi đã có NodeJS thì việc tiếp theo là khởi tạo một project mới bằng lệnh và nhập những thông tin thiết yếu

USDnpm init

Câu lệnh sẽ khởi tạo 1 project NodeJS mới với file package.json chứa các thông tin mà chúng ta đã nhập vào. Bước tiếp theo là cài đặt electron để sử dụng

USDnpm install -D electron

Có thể nhiều người thắc mắc tại sao lại có -D trong câu lệnh trên. Nguyên nhân là bởi vì mục đích cuối cùng của project không phải là 1 package NodeJS. Mục đích cuối cùng là 1 ứng dụng chạy trên desktop, nên electron chỉ là devDependencies chứ không phải dependencies. Ngoài ra thì việc này có liên quan đến việc build app ở dưới.

Như đã nói ở trên, một ứng dụng Electron thực ra là mở một trình duyệt rồi tải trang HTML, cho nên vì thế tôi cần thiết lập thêm những công cụ để build CSS, JS, v.v… Với ví dụ này tôi sử dụng TailwindCSS nên cần setup thêm những package sau :

USDnpm install -D postcss postcss-cli autoprefixer tailwindcss

Việc thông số kỹ thuật những công cụ này cũng không khó, tôi trọn vẹn làm theo hướng dẫn là được .

Viết ứng dụng

Về cơ bản ứng dụng sẽ gồm có 1 file HTML ( đặt tên tuỳ ý ), những file CSS, JS đi kèm với file HTML đó và 1 file NodeJS để để khởi động Electron bật ứng dụng. Cấu trúc thư mục của ứng dụng sẽ tựa như như dưới đây :

├── static
|   ├── src
|   |   ├── app.css // sẽ  build bằng tailwindcss thành /static/app.css
|   ├── app.js
├── index.html
├── main.js
└── package.json

Nội dung của file index.html như sau (màn hình có 2 trường input và 1 button)

html

    
    Simple app with Electron<p class="p"><!--</p-->title>
    <meta name=" viewport " content=" width = device-width, initial-scale = 1 ">
    <link rel=" stylesheet " href="https://mix166.vn/viet-ung-dung-desktop-bang-javascript-1656872617/static%20/%20app.css">

    </p><div class="
flex flex-col
justify-center
items-center
gap-4
w-screen
h-screen
">
        <div class=" flex flex-row gap-4 justify-center items-center ">
            <label class=" text-right font-lg w-32 " for=" thư mục ">
                Select input
            <p class="p"><!--</p-->label>
            </p><div class=" flex flex-row items-center w-96 ">
                <input type=" text " class="
outline-none
flex-grow
border
border-gray-300
rounded-l
h-12
p-4
" name=" thư mục " id=" thư mục " disabled>
                <button class="
outline-none
bg-gray-200
hover : bg-gray-400
border-t
border-r
border-b
border-gray-300
h-12
px-4
- ml-1
rounded-r
" id=" folder-btn ">
                    <svg xmlns=" http://www.w3.org/2000/svg " class=" h-6 w-6 inline-block " fill=" none " viewbox=" 0 0 24 24 " stroke=" currentColor ">
                        <path stroke-linecap=" round " stroke-linejoin=" round " stroke-width=" 2 " d=" M5 19 a2 2 0 01-2-2 V7a2 2 0 012 - 2 h4l2 2 h4a2 2 0 012 2 v1M5 19 h14a2 2 0 002 - 2 v - 5 a2 2 0 00-2-2 H9a2 2 0 00-2 2 v5a2 2 0 01-2 2 z ">
                    <p class="p"><!--</p-->svg>
                    Choose folder
                </p><p class="p"><!--</p-->button>
            </p><p class="p"><!--</p-->div>
        </p><p class="p"><!--</p-->div>
        </p><div class=" flex flex-row gap-4 justify-center items-center ">
            <label class=" text-right font-lg w-32 " for=" length ">
                Filename length
            <p class="p"><!--</p-->label>
            <input type=" number " class="
border border-gray-200
focus : border-blue-500
outline-none
w-96
rounded-l
h-12
rounded
p-4
" value=" 16 " name=" length " id=" length ">
        </p><p class="p"><!--</p-->div>

        </p><div class=" ml-36 w-96 text-left ">
            <button class="
outline-none
bg-blue-500
hover : bg-blue-700
px-8
py-2
text-white
uppercase
" id=" action-btn ">
                <svg xmlns=" http://www.w3.org/2000/svg " class=" h-6 w-6 inline-block " fill=" none " viewbox=" 0 0 24 24 " stroke=" currentColor ">
                    <path stroke-linecap=" round " stroke-linejoin=" round " stroke-width=" 2 " d=" M15 15 l - 2 5L9 9 l11 4-5 2 zm0 0 l5 5M7. 188 2.239 l. 777 2.897 M5. 136 7.965 l - 2.898 -. 777M13. 95 4.05 l - 2.122 2.122 m - 5.657 5.656 l - 2.12 2.122 ">
                <p class="p"><!--</p-->svg>
                Run
            </p><p class="p"><!--</p-->button>
        </p><p class="p"><!--</p-->div>

        
    <p class="p"><!--div>
<p class="p"><!--html>
</script></p>
<p class="my-4">File <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">static/src/app.css</code> thì hoàn toàn sử dụng TailwindCSS nên nội dung đơn giản như sau:</p>
<pre class="text-sm"><code>@tailwindbase;
@tailwindcomponents;
@tailwindutilities;
</code></pre>
<p class="my-4">Ngoài ra thì file <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">static/app.js</code> hiện tại đang trống, chúng ta sẽ bổ sung thêm các xử lý cần thiết sau.</p>
<p class="my-4">Cuối cùng là file <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">main.js</code>, chúng ta cần khởi tạo app mới, tải trang HTML và hiển thị cho người dùng. Tất cả thao tác đó sử dụng code như sau:</p>
<pre class="text-sm"><code>const{app,BrowserWindow}=require(" electron ") ;

letmainWin;

/ * *
* Hàm dùng để khởi tạo Window
* /
constcreateWindow=( )=>{
/ / Tạo Window mới với
mainWin=newBrowserWindow( {
width:800,
height:650,
icon:" static / icon.jpeg ",
webPreferences:{
nodeIntegration:true,
contextIsolation:false,
} ,
} ) ;

/ / Không cần menu
mainWin.removeMenu( ) ;

/ / Tải file html và hiển thị
mainWin.loadFile(". / index.html ") ;

/ / mainWin. webContents. openDevTools ( ) ;
} ;

/ / Sau khi khởi động thì mở Window
app.whenReady( ) .then(createWindow) ;

/ / Xử lý sau khi Window được đóng
app.on(" window-all-closed ",( )=>{
app.quit( ) ;
} ) ;

/ / Xử lý khi app ở trạng thái active, ví dụ click vào icon
app.on(" activate ",( )=>{
/ / Mở window mới khi không có window nào
if(BrowserWindow.getAllWindows( ) .length= = =0){
createWindow( ) ;
}
} ) ;
</code></pre>
<p>Sau khi đã hoàn thành xong những file trên, giờ đây tất cả chúng ta hoàn toàn có thể khởi động app và xem thành quả</p>
<pre class="text-sm"><code>USDnpx electron .
</code></pre>
<p class="my-4"><img fifu-featured="1" loading="lazy" alt="" title="" class="w-auto h-auto mx-auto" height="270" src="https://i.imgur.com/oXjqcV3.png" width="360" title="7 Lập trình desktop app với Electron | Blog | manhhomienbienthuy mới nhất 1"></p>
<h3 class="group font-semibold mt-8 mb-4 pb-2 border-b border-gray-100 dark:border-gray-700 text-2xl text-gray-900 dark:text-gray-100" id="tuong-tac"><span id="Tuong_tac">Tương tác</span></h3>
<p>Mặc dù ứng dụng đã trông rất ngầu, nhưng hiện tại nó chưa hoạt động giải trí. Bởi những với những button tất cả chúng ta chưa giải quyết và xử lý gì. Công việc tiếp theo chính là viết thêm giải quyết và xử lý cho những button này .</p>
<h4 class="group font-semibold mt-8 mb-4 pb-2 border-b border-gray-100 dark:border-gray-700 text-xl text-gray-900 dark:text-gray-100" id="select-folder"><span id="Select_folder">Select folder</span></h4>
<p>Để hoàn toàn có thể input một thư mục, tất cả chúng ta cần sử dụng đến dialog. Tuy nhiên là dialog chỉ hoàn toàn có thể bật từ main process mà không hề bật từ renderer được. Nói qua một chút ít thì để thao tác với dialog thì tôi đã phải tìm kiếm, chạy thử không biết bao nhiêu lần. Nguyên nhân là những hướng dẫn trên mạng hầu hết đã lỗi thời, khi mà từ renderer hoàn toàn có thể gọi remote và bật dialog được luôn .</p>
<blockquote class="my-4 py-1 px-4 text-gray-400 bg-gray-100 border-l-4 border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-300">
<p class="my-4 mt-2 mb-2">Có 1 cách khác, đó là sử dụng HTML input file và thêm thuộc tính <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">webkitdirectory</code>. Tuy nhiên cách làm này không hoàn toàn giống với dialog. Sử dụng cách này thì mỗi lần chọn folder, thực chất là chọn input tất cả file trong folder đó.</p>
</blockquote>
<p class="my-4">Ở <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">app.js</code> là file sẽ được thực thi ở renderer, thì chúng ta cần <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">invoke</code> một event để main process xử lý, sau đó nhận kết quả và hiển thị cho người dùng, như kiểu dưới đây:</p>
<pre class="text-sm"><code>const{ipcRenderer}=require(" electron ") ;


document.querySelector(" # folder-btn ") .addEventListener(" click ",( )=>{
ipcRenderer
.invoke(" select-folder ")
.then( (data)=>{
if(!data.canceled){
document.querySelector(" # folder ") .value=data<p class="p">.</p><div style="margin-bottom:15px;margin-top:15px;"><p style="padding: 20px; background: #eaf0ff;">Xem thêm: Giáo án Tập viết - Tiết 4: C - Chia ngọt sẻ bùi </p></div>filePaths[0] ;
}
} )
.catch( (err)=>{
console.log(err)
} ) ;
} ) ;
</code></pre>
<p>Lưu ý một chút ít là dù Electron thực ra là mở một trình duyệt và hiển thị HTML nhưng nó có đôi chút độc lạ so với trình duyệt thường thì. Ở phía client, tất cả chúng ta hoàn toàn có thể thực thi 1 số ít code NodeJS mà thông thường trình duyệt không tương hỗ. Tuy nhiên với ứng dụng trong bài thì điều đó chưa thiết yếu .Ở main process, mọi việc đơn thuần là bật dialog để chọn thư mục rồi trả về cho renderer là được .</p>
<pre class="text-sm"><code>const{ipcMain,dialog}=require(" electron ") ;

ipcMain.handle(" select-folder ",async( )=>{
constpathObj=awaitdialog.showOpenDialog(mainWin,{
properties:[" openDirectory "] ,
} ) ;
returnpathObj;
} ) ;
</code></pre>
<p>Kết quả là tất cả chúng ta có một dialog để chọn như dưới đây</p>
<p class="my-4"><img loading="lazy" alt="dialog" class="w-auto h-auto mx-auto" height="270" src="https://i.imgur.com/qBu5SSw.png" width="360" title="7 Lập trình desktop app với Electron | Blog | manhhomienbienthuy mới nhất 2"></p>
<h4 class="group font-semibold mt-8 mb-4 pb-2 border-b border-gray-100 dark:border-gray-700 text-xl text-gray-900 dark:text-gray-100" id="submit"><span id="Submit">Submit</span></h4>
<p>Tương tự như ở trên, so với button submit, thì ở phía renderer chỉ đơn thuần là invoke một sự kiện và truyền tài liệu cho main process .</p>
<pre class="text-sm"><code>document.querySelector(" # action-btn ") .addEventListener(" click ",( )=>{
constthư mục=document.querySelector(" # folder ") .value;
constlength=parseInt(document.querySelector(" # length ") .value) ;

ipcRenderer.invoke(" rename ",{thư mục:thư mục,length:length} ) ;
} ) ;
</code></pre>
<p>Toàn bộ giải quyết và xử lý sẽ triển khai ở main process :</p>
<pre class="text-sm"><code>constfs=require(" fs ") ;
constpath=require(" path ") ;

ipcMain.handle(" rename ",(_,data)=>{
constrandomName=(length)=>{
varresult=" ";
varcharacters=
" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ";
varcharactersLength=characters.length;
for(vari=0;i<length result math.floor returnresult fs.readdir m>{
files.forEach( (file)=>{
oldPath=path.resolve(data.thư mục,file) ;
ext=path.extname(file) ;
newPath=path.resolve(data.thư mục,randomName(data.length)+ext) ;
fs.rename(oldPath,newPath,(err)=>{
if(err){
console.log(err) ;
}else{
console.log(" Successfully renamed the file "+file) ;
}
} ) ;
} ) ;
} ) ;

dialog.showMessageBoxSync(mainWin,{
message:" Successfully renamed the all files in "+data.thư mục,
type:" info ",
} ) ;
} ) ;
</length></code></pre>
<p class="my-4">Chỉ có 1 điểm tôi muốn nói thêm là ở xử lý trên, tôi tiếp tục sử dụng dialog để thông báo cho người dùng về việc xử lý đã hoàn thành. Có người nhiều sẽ nghĩ sao không dùng <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">alert</code> cho nhanh. Câu trả lời <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">alert</code> vẫn hoạt động (như trình duyệt bình thường) nhưng sau đó thì cửa sổ app sẽ bị treo (chỗ này thì khác trình duyệt rồi 😂). Nguyên nhân là do <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">alert</code> hay kể cả <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">prompt</code>, <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">confirm</code> sẽ block thread đang chạy và như tôi thấy thì sau đó thread sẽ không chạy tiếp nữa (trừ khi click ra đâu đó rồi quay lại) (xem thêm ở đây).</p>
<p>Vì vậy toàn bộ những giải quyết và xử lý kiểu như vậy đều nên sử dụng dialog ở main process thay vì giải quyết và xử lý ở renderer. Kết quả là tất cả chúng ta đã rename thành công xuất sắc hàng loạt file trong 1 thư mục như dưới đây :</p>
<p class="my-4"><img loading="lazy" alt="renamed" class="w-auto h-auto mx-auto" height="270" src="https://i.imgur.com/MToKMtj.png" width="360" title="7 Lập trình desktop app với Electron | Blog | manhhomienbienthuy mới nhất 3"></p>
<h3 class="group font-semibold mt-8 mb-4 pb-2 border-b border-gray-100 dark:border-gray-700 text-2xl text-gray-900 dark:text-gray-100" id="build-app"><span id="Build_app">Build app</span></h3>
<p>Sau khi đã lập trình xong toàn bộ những yếu tố ở trên, chỉ còn 1 bước sau cuối nữa là triển khai xong. Đó là build app hoàn hảo để hoàn toàn có thể chạy ở bất kể đâu ( vì để nguyên như trên thì nhu yếu máy phải thiết lập NodeJS và những package ) .Có nhiều công cụ khác nhau được cho phép thao tác này, tuy nhiên tôi sử dụng electron-builder. Cài đặt package này như sau :</p>
<pre class="text-sm"><code>USDnpm install -D electron-builder
</code></pre>
<p class="my-4">Sau đó thì tạo một file JS (tên gì cũng được, tôi đặt tên là <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">build-app.js</code>) với nội dung là sử dụng electron-builder để build app như sau:</p>
<pre class="text-sm"><code>constbuilder=require(" electron-builder ") ;

builder.build( {
config:{
appId:" electron.rename ",
productName:" SampleApp ",
win:{
target:{
target:" zip ",
arch:" x64 ",
} ,
} ,
} ,
} ) ;
</code></pre>
<p>Trên đây chỉ là thông số kỹ thuật đơn thuần, có rất nhiều thông số kỹ thuật khác nhau, mời những bạn tìm hiểu thêm thêm ở đây. Sau khi thông số kỹ thuật xong thì thực thi file để build app là xong :</p>
<pre class="text-sm"><code>USDnode build-app.js
</code></pre>
<p class="my-4">Chờ mấy phút để app build xong (tuỳ cấu hình máy mạnh hay yếu). Kết quả sẽ được ghi ở thư mục <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">dist</code> bao gồm 1 file zip, 1 thư mục <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">win-unpack</code> (nội dung giống hệt file zip) và một số file debug. Trong thư mục sẽ có các tất cả các file cần thiết để chạy app nên app có thể chạy ở bất kỳ máy nào (dùng x64, x86 thì phải sửa lại cấu hình trên một chút để build). Giờ đây chúng ta chỉ cần click file <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">SampleApp.exe</code> là app sẽ chạy.</p>
<div style="margin-bottom:15px;margin-top:15px;">
<p style="padding: 20px; background: #eaf0ff;">Xem thêm: Giáo án Tập viết - Tiết 4: C - Chia ngọt sẻ bùi</p>
</div>
<p class="my-4">Một lưu ý nhỏ là click file exe để chạy app thì tất cả các câu <code class="bg-gray-100 p-0.5 dark:bg-gray-900 text-sm">console.log</code> ở main process sẽ chẳng được ghi ra ở đâu cả (hoặc được ghi ra đâu đó mà tôi không biết). Muốn xem những thông tin này (để debug chẳng hạn) thì cần phải gọi file exe từ console (PowerShell, cmd hoặc bash, v.v...).</p>
<h2 class="group font-semibold mt-8 mb-4 pb-2 border-b border-gray-100 dark:border-gray-700 text-3xl text-gray-900 dark:text-gray-100" id="nhan-xet"><span id="Nhan_xet">Nhận xét</span></h2>
<p>Electron thực sự là một giải pháp tốt cho những người chỉ có kinh nghiệm tay nghề làm web như tôi hoàn toàn có thể lập trình ứng dụng cho desktop. Tuy nhiên, cá thể tôi cho rằng đây chỉ là một giải pháp tình thế mà thôi. Tuy ứng dụng chạy khá nhanh, hiệu suất ổn nhưng dung tích của nó lại là yếu tố. Ứng dụng đơn thuần như trong bài mà build xong file zip cũng có dung tích gần 80MB, còn file unzip là gần 200MB, đó là những số lượng khá lớn cho một ứng không có gì là phức tạp .Tất nhiên là với sự tăng trưởng của công nghệ tiên tiến bán dẫn, việc file có dung tích hơi lớn một chút ít lúc bấy giờ trọn vẹn không phải là yếu tố gì quá to tát. Nhưng với những ai theo đuổi sự tuyệt vời và hoàn hảo nhất thì có lẽ rằng một giải pháp native hơn sẽ là tốt hơn .</p>
</path></svg></button></div>
<div style="margin-bottom:15px;margin-top:15px;">
<p style="padding: 20px; background: #eaf0ff;">Source: https://mix166.vn <br /> Category: Ứng Dụng Mới </p>
</div>
<p>                            </label></div>
</path></svg></button></div>
<div class="related-posts clearfix">
<div class="widget-title">Bài viết liên quan</div>
<div class="slider-post pad5">
<div class="col-md-3">
<ul class="h-b-1-1 h-b-1-2">
<li><span style="background-image:url(https://mix166.vn/wp-content/uploads/nhung-ung-dung-thanh-toan-truc-tuyen-tot-va-duoc-nhieu-nguoi-su-dung-nhat5-300x200.jpg)"></span></li>
<li>Những ứng dụng thanh toán trực tuyến tốt và được nhiều người sử dụng nhất</li>
</ul></div>
<div class="col-md-3">
<ul class="h-b-1-1 h-b-1-2">
<li><span style="background-image:url(https://mix166.vn/wp-content/uploads/nhung-ung-dung-nghe-nhac-tren-di-dong-pho-bien-nhat-hien-nay-300x169.jpg)"></span></li>
<li>Top ứng dụng nghe nhạc tốt nhất trên IOS</li>
</ul></div>
<div class="col-md-3">
<ul class="h-b-1-1 h-b-1-2">
<li><span style="background-image:url(https://mix166.vn/wp-content/uploads/1651267986_43_Top-ung-dung-To-Do-List-tot-nhat-giup-ban-lam.png)"></span></li>
<li>Những ứng dụng tốt nên có trên điện thoại mỗi người</li>
</ul></div>
<div class="col-md-3">
<ul class="h-b-1-1 h-b-1-2">
<li><span style="background-image:url(https://mix166.vn/wp-content/uploads/articlewriting1-300x187.jpg)"></span></li>
<li>12 ứng dụng ghi chú tốt nhất cho Android năm 2019 – Phần 1</li>
</ul></div>
</p></div>
</p></div>
<p>                    </label></div>
</p></div>
<p>