Hướng dẫn chi tiết cách xây dựng cấu trúc và code một mini project Bài tập lớn môn Lập trình ứng dụng mobile của sinh viên năm 3

0 0 0

Người đăng: Tờ Mờ Sáng học Lập trình

Theo Viblo Asia

Hôm trước tôi làm form khảo sát những chủ đề bài tập lớn các môn chuyên ngành mà anh em sinh viên bây giờ thường gặp phải, thì trong đó có 1 bạn sinh viên năm 3 cung cấp thông tin rất hữu ích về việc bản thân bạn ấy và những người bạn của bạn ấy gặp khá nhiều khó khăn khi làm Bài tập lớn môn Lập trình ứng dụng mobile, vì chưa biết các kỹ thuật để xây dựng cấu trúc cho 1 project demo ứng dụng mobile là như thế nào.

Bản thân tôi thời sinh viên cũng gặp rất nhiều khó khăn với môn học này khi không biết phải bắt đầu như thế nào, nên tôi hiểu những khó khăn mà anh em đang trải qua. Vậy nên tôi cũng lọ mọ code 1 mini project minh họa bài toán bạn ấy đang gặp phải.

Trong project này tôi áp dụng một kiến trúc rất phổ biến trong các dự án thực tế trên công ty, đó là: Clean Architecture. Bên cạnh đó thì trong phần UI của ứng dụng tôi cũng áp dụng MVVM pattern để xây dựng.

Đương nhiên là 1 mini project của 1 Bài tập lớn trên trường Đại học thì phạm vi và độ phức tạp sẽ không quá cao, phù hợp để anh em tham khảo và dễ dàng hiểu được.

Đây là bài viết hướng dẫn chi tiết để giúp quá trình tham khảo code của anh em được dễ hiểu hơn.


Bước 0: Xác định công nghệ và công cụ cần sử dụng

  • Ngôn ngữ lập trình và runtime: C# / .NET 8
  • Framework: .NET MAUI
  • IDE: Visual Studio Community 2022

Bước 1. Xác định mục tiêu

Ở trong form khảo sát thì bạn sinh viên năm 3 mô tả cũng tương đối rõ ràng rồi.

Mục tiêu của bạn ấy là xây dựng 1 ứng dụng mobile sử dụng C# và .NET MAUI để người dùng quản lý và thống kê chi tiêu cá nhân theo ngày, tháng, năm.

Yêu cầu của môn học


Bước 2. Liệt kê các chức năng chính

  • Thêm/sửa/xóa chi tiêu hàng ngày.
  • Xem thống kê chi tiêu theo ngày/tháng/năm.
  • Hiển thị biểu đồ cột hoặc biểu đồ tròn.
  • Thống kê các loại chi tiêu chiếm nhiều tiền từ bé đến lớn.

Bước 3. Thiết kế kiến trúc cơ bản của hệ thống

Ở đây chúng ta có thể áp dụng một kiến trúc rất phổ biến trong các dự án thực tế, đó là Clean Architecture (Các bạn có thể đọc thêm về kiến trúc này tại đây: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

Ứng dụng của này có thể chia thành 4 layer như sau:

  • Presentation Layer: Nói đơn giản thì phần này chính là phần chúng ta dùng các file .xaml.xaml.cs để code giao diện (UI) => Lát nữa khi tạo solution tôi sẽ đặt tên project này là ExpenseTracker.Maui
  • Application Layer: Phần này chứa các use case, hay nhiều người gọi là các service để xử lý business logic => Lát nữa khi tạo solution tôi sẽ đặt tên project này là ExpenseTracker.Applications (Lý do riêng project này đặt tên số nhiều chỉ là vì nếu đặt Application số ít nó bị trùng tên với 1 class trong namespace Microsoft.Maui.Controls, đặt trùng vẫn có thể fix lỗi bằng cách chỉ định cả namespace nữa)
  • Domain Layer: Phần này chúng ta sẽ tạo các file entity, interface, constant, enum, ... cần thiết cho ứng dụng> Layer này sẽ không phụ thuộc vào bất kỳ layer nào khác, mà các layer khác sẽ reference đến Domain Layer để có thể sử dụng được các entity của ứng dụng => Lát nữa khi tạo solution tôi sẽ đặt tên project này là ExpenseTracker.Domain
  • Infrastructure Layer: Phần này chúng ta sẽ tạo các file để kết nối với Database (ví dụ có thể sử dụng SQLite), các hàm trong file đó được implement dựa trên quy định của interface trong Domain Layer => Lát nữa khi tạo solution tôi sẽ đặt tên project này là ExpenseTracker.Infrastructure

Vậy sẽ có câu hỏi đặt ra là: Làm sao để tạo được các "phần" trên ở trong source code của mình?

=> Chúng ta cần phân biệt được khái niệm SolutionProject khi sử dụng Visual Studio.

Thông thường khi tạo 1 solution trong Visual Studio, anh em sẽ thấy nó mặc định chỉ có 1 project bên trong thôi, và các bạn sinh viên khi mới học sẽ thường code hết vào trong 1 project đó.

Nhưng khi làm dự án thực tế, để tách riêng các thành phần như UI, Business Logic, Data Access, ... thì sẽ tách nó ra thành nhiều project khác nhau. Mỗi project đảm nhiệm một chức năng. Cái này khi đọc các sách chuyên ngành các bạn sẽ thấy họ nhắc đến thuật ngữ gọi là Separation of Concerns (Tách biệt các mối quan tâm)

Và để có sự liên kết giữa các project thì tất cả những project đó sẽ nằm trong cùng 1 solution và có reference tới nhau.

Một solution có thể chứa nhiều project con ở bên trong

Từ ý tưởng đó, chúng ta sẽ tạo 4 project trong solution tương ứng với 4 thành phần tôi nói ở trên. Và để cho tổng quát thì solution tôi sẽ đặt tên là ExpenseTracker, thể hiện đây là solution dành cho dự án theo dõi chi tiêu.

Do đó khi mới tạo project anh em nhớ sửa tên solution như sau:

Tạo solution khác tên với project

Tuy nhiên lưu ý có 1 điều đặc biệt, đó là khi tạo project trong Visual Studio nó có sẵn nhiều template, anh em có thể chọn loại template cho từng project như tôi dưới đây:

  • Presentation Layer: Anh em chọn loại .NET MAUI App (cái này khi các bạn tạo solution lúc đầu đã phải chọn để có thể làm app .NET MAUI App rồi)

  • 3 project còn lại là Application Layer, Domain LayerInfrastructure Layer: Anh em chọn loại .NET MAUI Class Library

    3 project còn lại là Application Layer, Domain Layer và Infrastructure Layer: Anh em chọn loại .NET MAUI Class Library

Tiếp theo là cách để các bạn có thể tạo reference giữa các project:

  • Đầu tiên, nhấn chuột phải vào phần Dependencies của mỗi project, sau đó chọn Add Project Reference...

    Tạo reference giữa các project

  • Sau đó tick chọn project cần reference tới và nhấn OK

    Sau đó chọn project cần reference tới và nhấn OK

Các project cần reference tới nhau như sau:

  • ExpenseTracker.Domain sẽ không reference tới đâu cả.
  • ExpenseTracker.Applications sẽ reference tới ExpenseTracker.Domain.
  • ExpenseTracker.Infrastructure sẽ reference tới ExpenseTracker.Domain.
  • ExpenseTracker.Maui sẽ reference tới ExpenseTracker.Domain, ExpenseTracker.ApplicationsExpenseTracker.Infrastructure.

Bước 4. Thiết kế database

Sau khi tạo được bộ khung kiến trúc ứng dụng rồi thì chúng ta cần bắt tay vào thiết kế database để lưu trữ dữ liệu của app.

Ở đây để cho đơn giản thì chúng ta có thể sử dụng SQLite để lưu trữ cục bộ trên điện thoại luôn. Hiểu đơn giản thì nó giống như app Zola hay bị chửi vì lưu dữ liệu trên máy người dùng ấy. Nhược điểm đương nhiên là nặng máy vì tốn dung lượng bộ nhớ trên máy của người dùng và khi chuyển sang máy khác thì không thấy dữ liệu như khi dùng máy cũ nữa.

Nhưng ở ứng dụng trong bài tập lớn của sinh viên năm 3 thì mình tạm dùng SQLite cũng được. Sau này làm dự án thực tế thì các bạn có thể tìm hiểu cách tạo database server trên cloud thay vì lưu trong máy người dùng.

Với ứng dụng đơn giản này thì chỉ cần 1 bảng chính để lưu thông tin về mỗi lượt chi tiêu.

Tôi sẽ đặt tên bảng này là Expense (nghĩa là Chi tiêu). Và để cho đơn giản thì chúng ta sẽ dùng Entity Framework Core để làm việc với SQLite.

Khi đi làm thì anh em Dev sẽ thường đặt tên tiếng Anh thay vì tiếng Việt, tại vì mấy lý do sau:

  • Tiếng Anh là ngôn ngữ chung trong lập trình, hầu hết technical document của các ngôn ngữ lập trình, tài liệu hướng dẫn, code mẫu, thư viện, framework,... đều viết bằng tiếng Anh.

  • Nếu đặt tên hàm, tên biến bằng tiếng Việt có dấu thì khi code có thể sẽ bị lỗi font, lỗi encode, nhất là khi chạy trên hệ thống khác hoặc trên server.

  • Còn nếu dùng tiếng Việt không dấu thì đôi khi có những từ tiếng Việt lúc bỏ dấu đi sẽ khá khó hiểu đúng ý nghĩa, ví dụ:

    • lon nghĩa là lợn hay ***
    • ban nghĩa là bạn hay bàn
    • cam nghĩa là cấm hay cầm hay quả cam
    • ...

    => Vậy nên anh em cũng nên chịu khó học tiếng Anh và tập làm quen với việc đặt tên biến, tên hàm, ... bằng tiếng Anh, để sau này đi làm đỡ bỡ ngỡ.

Ví dụ ở đây tôi sẽ tạo 1 entity Expense (nghĩa là Chi tiêu)

Tên thuộc tính Kiểu dữ liệu (trong .NET) Mô tả
Id Guid Mã định danh cho mỗi bản ghi trong bảng
Date DateTime Ngày chi tiêu
Category Category (đây là 1 enum mình tự định nghĩa) Danh mục chi tiêu. Ví dụ: 0 là Mua sắm, 1 là Chi phí di chuyển, 2 là Ăn uống, ... Lý do nên lưu dữ liệu này ở dạng số là để sau này nếu anh em muốn bổ sung chức năng hiển thị đa ngôn ngữ (tiếng Việt, tiếng Anh, tiếng Trung, ...)
Amount decimal Số tiền chi tiêu
Description string? Ghi chú mô tả về khoản chi tiêu đó (tùy chọn người dùng có thể nhập ghi chú hoặc không)

Bước 5. Code project ExpenseTracker.Domain

Sau khi thiết kế DB rồi thì mình sẽ quay lại code để code project ExpenseTracker.Domain. Vì project này gồm các entity, interface, constant, enum, ... Đây là những thứ mà các project khác đều cần sử dụng tới nên chúng ta phải code project này trước.

Trong ứng dụng này tôi sẽ tạo:

  • 1 thư mục Entities, trong này có file entity là Expense.cs để code class Expense gồm các thuộc tính của 1 chi tiêu. Ví dụ:

    using System;
    using ExpenseTracker.Domain.Enums; namespace ExpenseTracker.Domain.Entities
    { public class Expense { /// <summary> /// Mã định danh cho mỗi bản ghi trong bảng /// </summary> public Guid Id { get; set; } /// <summary> /// Ngày chi tiêu /// </summary> public DateTime Date { get; set; } /// <summary> /// Danh mục chi tiêu /// </summary> public Category Category { get; set; } /// <summary> /// Số tiền chi tiêu /// </summary> public decimal Amount { get; set; } /// <summary> /// Ghi chú /// </summary> public string? Description { get; set; } /// <summary> /// Hàm khởi tạo /// </summary> public Expense() { Id = Guid.NewGuid(); Date = DateTime.Now; } }
    } 
  • 1 thư mục Enums, trong đó có file Category.cs để định nghĩa các danh mục chi tiêu. Ví dụ:

    using System; namespace ExpenseTracker.Domain.Enums
    { /// <summary> /// Định nghĩa danh mục chi tiêu /// </summary> public enum Category { /// <summary> /// Mua sắm  /// </summary> Shopping = 0, /// <summary> /// Chi phí di chuyển /// </summary> Transportation = 1, /// <summary> /// Ăn uống /// </summary> Dining = 2, /// <summary> /// Học tập /// </summary> Studying = 3, /// <summary> /// Giải trí /// </summary> Entertainment = 4, /// <summary> /// Sức khỏe /// </summary> Health = 5, /// <summary> ///Khác /// </summary> Other = 6 }
    }
    
  • 1 thư mục Interfaces, trong đó có file interface là IExpenseRepository.cs để định nghĩa các hàm mà sau này bên project ExpenseTracker.Infrastructure sẽ implement. Ví dụ:

    using ExpenseTracker.Domain.Entities;
    using ExpenseTracker.Domain.Enums; namespace ExpenseTracker.Domain.Interfaces
    { /// <summary> /// Interface định nghĩa các phương thức để thao tác với dữ liệu chi tiêu /// </summary> public interface IExpenseRepository { /// <summary> /// Lấy thông tin chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần tìm</param> /// <returns>Chi tiêu tương ứng với Id</returns> Task<Expense> GetByIdAsync(Guid id); /// <summary> /// Lấy danh sách tất cả các chi tiêu /// </summary> /// <returns>Danh sách tất cả chi tiêu</returns> Task<IEnumerable<Expense>> GetAllAsync(); /// <summary> /// Lấy danh sách chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Danh sách chi tiêu được sắp xếp theo ngày giảm dần</returns> Task<IEnumerable<Expense>> GetByDateRangeAsync(DateTime startDate, DateTime endDate); /// <summary> /// Lấy danh sách chi tiêu theo danh mục /// </summary> /// <param name="category">Danh mục cần tìm</param> /// <returns>Danh sách chi tiêu thuộc danh mục được chọn</returns> Task<IEnumerable<Expense>> GetByCategoryAsync(Category category); /// <summary> /// Thêm mới một chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần thêm</param> /// <returns>Chi tiêu đã được thêm vào database</returns> Task<Expense> AddAsync(Expense expense); /// <summary> /// Cập nhật thông tin chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần cập nhật</param> Task UpdateAsync(Expense expense); /// <summary> /// Xóa chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần xóa</param> Task DeleteAsync(Guid id); /// <summary> /// Tính tổng số tiền chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Tổng số tiền chi tiêu</returns> Task<decimal> GetTotalAmountByDateRangeAsync(DateTime startDate, DateTime endDate); /// <summary> /// Tính tổng chi tiêu theo từng danh mục trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Dictionary chứa tổng chi tiêu của mỗi danh mục</returns> Task<Dictionary<Category, decimal>> GetCategoryTotalsAsync(DateTime startDate, DateTime endDate); }
    }
    

Bước 6. Code phần kết nối và query DB (project ExpenseTracker.Infrastructure trong solution)

Tiếp đến là code project ExpenseTracker.Infrastructure để connect và query dữ liệu tới DB.

Trong project này tôi sẽ tạo:

  • 1 thư mục Data, trong đó có file ExpenseDbContext.cs để sử dụng EntityFrameworkCore để tự động mapping giữa entity và bảng trong SQLite. Ở đây tôi sẽ đánh index cho trường Date vì sau này làm phần thống kê sẽ phải query theo trường này nhiều.

    using ExpenseTracker.Domain.Entities;
    using Microsoft.EntityFrameworkCore; namespace ExpenseTracker.Infrastructure.Data
    { /// <summary> /// Lớp context để tương tác với cơ sở dữ liệu. Kế thừa từ DbContext của Entity Framework Core /// </summary> public class ExpenseDbContext : DbContext { /// <summary> /// DbSet đại diện cho bảng Expense trong cơ sở dữ liệu /// </summary> public DbSet<Expense> Expense { get; set; } /// <summary> /// Constructor nhận vào các tùy chọn kết nối database. Tự động tạo database nếu chưa tồn tại /// </summary> public ExpenseDbContext(DbContextOptions<ExpenseDbContext> options) : base(options) { Database.EnsureCreated(); } /// <summary> /// Hàm cấu hình model khi tạo database /// </summary> protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Tạo index cho cột Date để tối ưu tìm kiếm modelBuilder.Entity<Expense>().HasIndex(e => e.Date); // Cấu hình độ chính xác của cột Amount là 18 chữ số với 2 số thập phân modelBuilder.Entity<Expense>().Property(e => e.Amount).HasPrecision(18,2); } }
    } 
  • 1 thư mục Repositories, trong đó có file ExpenseRepository.cs để viết các hàm CRUD để query tới SQLite.

    using ExpenseTracker.Domain.Entities;
    using ExpenseTracker.Domain.Enums;
    using ExpenseTracker.Domain.Interfaces;
    using ExpenseTracker.Infrastructure.Data;
    using Microsoft.EntityFrameworkCore; namespace ExpenseTracker.Infrastructure.Repository
    { /// <summary> /// Repository xử lý các truy vấn dữ liệu liên quan đến chi tiêu /// </summary> public class ExpenseRepository(ExpenseDbContext context) : IExpenseRepository { private readonly ExpenseDbContext _context = context; /// <summary> /// Lấy thông tin chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần tìm</param> /// <returns>Chi tiêu tương ứng với Id</returns> public async Task<Expense> GetByIdAsync(Guid id) { return await _context.Expense.FindAsync(id); } /// <summary> /// Lấy danh sách tất cả các chi tiêu /// </summary> /// <returns>Danh sách chi tiêu</returns> public async Task<IEnumerable<Expense>> GetAllAsync() { return await _context.Expense.ToListAsync(); } /// <summary> /// Lấy danh sách chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Danh sách chi tiêu được sắp xếp theo ngày giảm dần</returns> public async Task<IEnumerable<Expense>> GetByDateRangeAsync(DateTime startDate, DateTime endDate) { return await _context.Expense .Where(e => e.Date >= startDate && e.Date <= endDate) .OrderByDescending(e => e.Date) .ToListAsync(); } /// <summary> /// Lấy danh sách chi tiêu theo danh mục /// </summary> /// <param name="category">Danh mục cần tìm</param> /// <returns>Danh sách chi tiêu thuộc danh mục được chọn</returns> public async Task<IEnumerable<Expense>> GetByCategoryAsync(Category category) { return await _context.Expense .Where(e => e.Category == category) .OrderByDescending(e => e.Date) .ToListAsync(); } /// <summary> /// Thêm mới một chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần thêm</param> /// <returns>Chi tiêu đã được thêm vào database</returns> public async Task<Expense> AddAsync(Expense expense) { await _context.Expense.AddAsync(expense); await _context.SaveChangesAsync(); return expense; } /// <summary> /// Cập nhật thông tin chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần cập nhật</param> public async Task UpdateAsync(Expense expense) { _context.Expense.Update(expense); await _context.SaveChangesAsync(); } /// <summary> /// Xóa chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần xóa</param> public async Task DeleteAsync(Guid id) { var expense = await GetByIdAsync(id); if (expense != null) { _context.Expense.Remove(expense); await _context.SaveChangesAsync(); } } /// <summary> /// Tính tổng số tiền chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Tổng số tiền chi tiêu</returns> public async Task<decimal> GetTotalAmountByDateRangeAsync(DateTime startDate, DateTime endDate) { return await _context.Expense .Where(e => e.Date >= startDate && e.Date <= endDate) .SumAsync(e => e.Amount); } /// <summary> /// Tính tổng chi tiêu theo từng danh mục trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Dictionary chứa tổng chi tiêu của mỗi danh mục</returns> public async Task<Dictionary<Category, decimal>> GetCategoryTotalsAsync(DateTime startDate, DateTime endDate) { return await _context.Expense .Where(e => e.Date >= startDate && e.Date <= endDate) .GroupBy(e => e.Category) .ToDictionaryAsync( g => g.Key, g => g.Sum(e => e.Amount) ); } }
    }
    

Lưu ý là class ExpenseRepository trong file ExpenseRepository.cs sẽ implement interface IExpenseRepository, interface này đã được định nghĩa trong project ExpenseTracker.Domain ở trên đó. Để sau này nếu muốn chuyển sang DB khác thì chúng ta chỉ cần tạo 1 class khác và cũng implement interface IExpenseRepository. Còn các service sẽ không bị thay đổi logic. Ngoài ra nó cũng giúp chúng ta dễ dàng mock test khi viết unit test. Anh em có thể tìm hiểu thêm về Dependency Injection (https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection)

Tiếp đến chúng ta cần chạy 2 câu lệnh trong terminal để thực hiện migration.

Vì Entity Framework Core sử dụng Code First, nghĩa là chúng ta sẽ code class entity trong C# trước. Sau đó chạy lệnh migration thì Entity Framework Core sẽ tự tạo ra cấu trúc cơ sở dữ liệu tương ứng.

LƯU Ý:

  • Database sẽ không tự cập nhật cấu trúc khi bạn sửa class entity trong code C# (ví dụ thêm/sửa property). Do đó, khi đó chúng ta cũng cần thực hiện câu lệnh migration để:
    • Tạo ra file mô tả thay đổi (migrating step).
    • Áp dụng thay đổi đó vào DB.
  • Điều kiện cần để chạy Migration:
    • Project đã cài Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design và provider tương ứng (ví dụ: Microsoft.EntityFrameworkCore.Sqlite)
    • Đã cấu hình DbContextOnModelCreating
    • DbContext đã được đăng ký trong DI container

Để thực hiện migration lần đầu tiên, chúng ta sẽ:

  • Cài đặt dotnet-ef tools trước (nếu chưa cài):

    dotnet tool install --global dotnet-ef
    
  • Sau đó chạy lệnh migration để tạo cấu trúc database:

    dotnet ef migrations add <Tên Migration> --project
    

    Ví dụ:

    dotnet ef migrations add InitialCreate --project
    
  • Áp dụng Migration vào DB (Lệnh này sẽ chạy tất cả migration chưa áp dụng và cập nhật DB):

    dotnet ef database update
    

Bước 7. Code project ExpenseTracker.Applications

Ở project này tôi sẽ tạo:

  • 1 thư mục Interfaces, trong đó code 1 file IExpenseService.cs để định nghĩa các hàm mà file ExpenseService.cs sẽ implement. Ví dụ:

    using ExpenseTracker.Domain.Entities; namespace ExpenseTracker.Applications.Interfaces
    { /// <summary> /// Interface định nghĩa các business logic liên quan đến quản lý chi tiêu /// </summary> public interface IExpenseService { /// <summary> /// Lấy thông tin chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần tìm</param> /// <returns>Chi tiêu tương ứng với Id</returns> Task<Expense> GetExpenseByIdAsync(Guid id); /// <summary> /// Lấy danh sách chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Danh sách chi tiêu được sắp xếp theo ngày giảm dần</returns> Task<IEnumerable<Expense>> GetExpensesAsync(DateTime startDate, DateTime endDate); /// <summary> /// Thêm mới một chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần thêm</param> /// <returns>Chi tiêu đã được thêm vào database</returns> /// <exception cref="ArgumentNullException">Thrown khi expense null</exception> Task<Expense> AddExpenseAsync(Expense expense); /// <summary> /// Cập nhật thông tin chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần cập nhật</param> /// <returns>Chi tiêu đã được cập nhật</returns> /// <exception cref="ArgumentNullException">Thrown khi expense null</exception> Task<Expense> UpdateExpenseAsync(Expense expense); /// <summary> /// Xóa chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần xóa</param> /// <returns>Task hoàn thành</returns> Task DeleteExpenseAsync(Guid id); /// <summary> /// Tính tổng số tiền chi tiêu /// </summary> /// <param name="expenses">Danh sách chi tiêu cần tính tổng</param> /// <returns>Tổng số tiền chi tiêu</returns> Task<decimal> CalculateTotalAmountAsync(IEnumerable<Expense> expenses); }
    } 
  • 1 thư mục Services, trong đó code 1 file ExpenseService.cs

    using ExpenseTracker.Applications.Interfaces;
    using ExpenseTracker.Domain.Entities;
    using ExpenseTracker.Domain.Interfaces; namespace ExpenseTracker.Applications.Services
    { /// <summary> /// Service xử lý các business logic liên quan đến quản lý chi tiêu /// </summary> public class ExpenseService : IExpenseService { private readonly IExpenseRepository _expenseRepository; /// <summary> /// Khởi tạo service với repository được inject /// </summary> public ExpenseService(IExpenseRepository expenseRepository) { _expenseRepository = expenseRepository; } /// <summary> /// Lấy thông tin chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần tìm</param> /// <returns>Chi tiêu tương ứng với Id</returns> public async Task<Expense> GetExpenseByIdAsync(Guid id) { return await _expenseRepository.GetByIdAsync(id); } /// <summary> /// Lấy danh sách chi tiêu trong khoảng thời gian /// </summary> /// <param name="startDate">Ngày bắt đầu</param> /// <param name="endDate">Ngày kết thúc</param> /// <returns>Danh sách chi tiêu được sắp xếp theo ngày giảm dần</returns> public async Task<IEnumerable<Expense>> GetExpensesAsync(DateTime startDate, DateTime endDate) { return await _expenseRepository.GetByDateRangeAsync(startDate, endDate); } /// <summary> /// Thêm mới một chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần thêm</param> /// <returns>Chi tiêu đã được thêm vào database</returns> /// <exception cref="ArgumentNullException">Thrown khi expense null</exception> public async Task<Expense> AddExpenseAsync(Expense expense) { ArgumentNullException.ThrowIfNull(expense); await _expenseRepository.AddAsync(expense); return expense; } /// <summary> /// Cập nhật thông tin chi tiêu /// </summary> /// <param name="expense">Chi tiêu cần cập nhật</param> /// <returns>Chi tiêu đã được cập nhật</returns> /// <exception cref="ArgumentNullException">Thrown khi expense null</exception> public async Task<Expense> UpdateExpenseAsync(Expense expense) { ArgumentNullException.ThrowIfNull(expense); await _expenseRepository.UpdateAsync(expense); return expense; } /// <summary> /// Xóa chi tiêu theo Id /// </summary> /// <param name="id">Id của chi tiêu cần xóa</param> /// <returns>Task hoàn thành</returns> public async Task DeleteExpenseAsync(Guid id) { await _expenseRepository.DeleteAsync(id); } /// <summary> /// Tính tổng số tiền chi tiêu /// </summary> /// <param name="expenses">Danh sách chi tiêu cần tính tổng</param> /// <returns>Tổng số tiền chi tiêu</returns> public Task<decimal> CalculateTotalAmountAsync(IEnumerable<Expense> expenses) { return Task.FromResult(expenses.Sum(e => e.Amount)); } }
    }
    

Vì yêu cầu của bài tập lớn này rất đơn giản, nên các bạn cũng sẽ thấy các hàm trong ExpenseService này cũng rất đơn giản, hầu hết chỉ là gọi đến hàm trong repository và trả về kết quả thôi.

Trong các dự án thực tế sau này thì có thể sẽ phức tạp hơn.


Bước 8. Code giao diện (Pretation Layer)

Các dự án thực tế thường sẽ có vị trí Designer đảm nhiệm việc thiết kế UI. Nhưng anh em sinh viên hoặc làm dự án cá nhân không có designer trợ giúp thì các bạn có thể tham khảo thiết kế trên một số trang web như Pinterest, Dribbble, ...

Tôi cũng không biết nhiều về design, nên anh em thấy UI tôi code xấu thì có thể tự xây dựng theo mắt nhìn của mình sao cho đẹp nhé.

Các màn hình trong app

Tôi chỉ có lời khuyên là anh em nên tách ra mỗi màn hình trong app sẽ code thành 1 file .xaml

Ví dụ:

  • Có file ExpenseListPage.xaml để dùng làm giao diện của trang chính, khi bắt đầu vào ứng dụng thì sẽ hiển thị trang này.
  • File AddEditExpensePage.xaml để code giao diện khi thêm hoặc sửa 1 chi tiêu.
  • File StatisticsPage.xaml để code giao diện thống kê, hiển thị các biểu đồ cột, biểu đồ tròn.

Các file .xaml này tương ứng với phần View trong mô hình MVVM (Model-View-ViewModel)

Khi tạo file .xaml trong project .NET MAUI trên Visual Studio thì sẽ tự động có kèm thêm 1 file .xaml.cs, file này được gọi là code-behind file. Đây là file code C# chứa logic liên quan đến trang đó, ví dụ như gán ViewModel, xử lý sự kiện, xử lý điều hướng, ...

Ngoài những file .xaml và file code-behind (.xaml.cs) trên, thì chúng ta cũng cần thêm các file C# để xử lý ViewModel trong mô hình MVVM (Model-View-ViewModel).

Một số điểm giúp các bạn phân biệt nên code gì trong file code-behind (.xaml.cs), nên code gì trong file C# ViewModel (chỉ có .cs)

Điểm so sánh .xaml.cs ViewModel.cs
Biết về giao diện (UI)? ✅ Có thể truy cập Button, Label, ... ❌ Không nên biết giao diện
Có xử lý sự kiện UI không? ✅ Có thể xử lý sự kiện (Clicked, TextChanged) ✅ Có, nhưng thông qua ICommand, không gắn trực tiếp với UI
Có sử dụng dữ liệu không? ✅ Có, nhưng chủ yếu gán BindingContext ✅ Chính là nơi xử lý dữ liệu
Dễ unit test không? ❌ Khó test vì gắn chặt UI ✅ Dễ test vì không phụ thuộc giao diện

View và ViewModel

Về vấn đề quản lý string và Localization (chức năng mà có thể thay đổi ngôn ngữ ứng dụng tiếng Anh, tiếng Việt, ...)

localization

Mặc dù trong bài tập lớn này không có yêu cầu làm localization, nhưng tôi vẫn chủ động đưa hết các string vào chung 1 file resource, chứ không fix cứng các string đó trong code.

Resource strings

Cái này là rút kinh nghiệm từ bài học đau đớn khi tôi mới đi làm lúc ra trường.

Khi ấy tôi được giao vào một dự án mới. Mọi thứ được build từ đầu. Team bọn tôi thì toàn anh em mới ra trường chưa có kinh nghiệm gì.

Bọn tôi gần như chưa bao giờ nghĩ đến việc làm localization, nên khi code chỗ nào trên UI cần string là bọn tôi code ở đó luôn.

Đến khi dự án có khách hàng nước ngoài muốn dùng thử, sếp yêu cầu bọn tôi phải bổ sung chức năng chuyển đổi tiếng Anh, tiếng Việt và có thể sau này sẽ thêm một số ngôn ngữ khác nữa.

Vậy là lúc đó chúng tôi mới nhận ra vấn đề. Bắt đầu biết đến localization và phải sửa code rất nhiều, đi tìm từng ngóc ngách đang fix cứng string trong code để chỉnh sửa thì mới áp dụng được localization.

Anh em có thể tìm hiểu thêm về localization trong .NET MAUI ở đây: https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/localization?view=net-maui-8.0

Phần biểu đồ cũng hơi đặc biệt với các anh em sinh viên chưa động đến chức năng này bao giờ, nên tôi cũng có suggest thêm là:

  • Anh em nên dùng Biểu đồ cột để thống kê số tiền chi theo từng ngày hoặc từng tháng.
  • Còn biểu đồ tròn thì để thống kê theo tỉ lệ % chi tiêu theo danh mục (mua sắm, chi phí di chuyển, ăn uống, ...)
  • Anh em có thể sử dụng thư viện Syncfusion.Maui.Charts. Ưu điểm của thư viện này là dễ tích hợp và giao diện đẹp mắt. Nhược điểm là sẽ bị hiển thị thông báo license. Nếu không thích thì anh em có thể mày mò thư viện khác như Microcharts

Sử dụng thư viện Syncfusion mà không trả phí thì lần đầu vào tab thống kê sẽ hiển thị thông báo như thế này. Nhấn OK xong thì không hiện nữa


Trên đây là quá trình tôi lọ mọ code project demo cho bài tập lớn môn Lập trình ứng dụng mobile của một bạn sinh viên năm 3.

Tôi thì code web nhiều hơn là mobile. Nên tôi hi vọng các anh em chuyên làm Mobile Dev nếu có đọc được bài viết này thì cũng góp ý thêm cho tôi học tập, cũng như chia sẻ để các bạn sinh viên sau này muốn theo mảng mobile sẽ có những góc nhìn chính xác hơn nữa.

Anh em nào cần source code thì comment địa chỉ email dưới bài viết rồi tôi gửi nhé. Tôi đang refactor code cho đỡ bẩn cái xong thì mới đẩy lên GitHub. Lúc nào xong thì tôi sẽ gửi mail báo anh em.

Đang trong kỳ MayFest nên nếu bài viết này hữu ích với anh em thì nhờ anh em ủng hộ bằng cách upvote, comment và bookmark bài viết này giúp tôi nhé.

Cảm ơn anh em rất nhiều!

Bình luận

Bài viết tương tự

- vừa được xem lúc

2 Cách để Triển Khai MVVM Trong Dự Án IOS

MVVM không nhất thiết phải bind cùng RxSwift, nhưng nó sẽ tốt hơn, vậy tại sao . MVVM Cùng Swift. Để thực hiện hai cách ràng buộc mà không phụ thuộc, chúng ta cần tạo Observable của riêng chúng ta. Đây là đoạn code :.

0 0 86

- vừa được xem lúc

2 Ways to Execute MVVM iOS

Đối với việc phát triển ứng dụng dành cho thiết bị di động, MVVM là kiến trúc hiện đại. Nó thực hiện phân tách mối quan tâm tốt hơn để làm cho mã sạch hơn.

0 0 28

- vừa được xem lúc

Một chút về MVC, MVP và MVVM

MVC, MVP, và MVVM là 3 mô hình thông dụng khi phát triển phần mềm. Trong bài viết này, mình sẽ giới thiệu với các bạn 3 mô hình Model View Controller (MVC), Model View Presenter (MVP) và Model View Vi

0 0 92

- vừa được xem lúc

Khởi tạo ViewModel sao cho hợp thời đại

Bài viết này tôi sẽ sử dụng Kotlin để khởi tạo ViewModel và AndroidViewModel. Nếu bạn chưa biết Delegation trong Kotlin thì hãy đọc bài viết này trước nhé.

0 0 70

- vừa được xem lúc

Mô hình MVVM và cách triển khai trong ứng dụng Android

Xin chào các bạn trong bài viết này, mình sẽ hướng dẫn các bạn tìm hiểu và cách triển khai Mô hình kiến trúc MVVM trong Ứng dụng Android, không khó khăn lắm đâu cùng theo dõi nha . 1>Định Nghĩa.

0 0 365

- vừa được xem lúc

BLoC Hay MVVM + GetX – Đâu là “Chân Lý” cho phát triển dự án bằng Flutter?

Trước khi đi vào tìm kiếm và so sánh giữa các Kiến trúc khi triển khai trên Flutter – Dart để xem Kiến trúc nào sẽ phù hợp, tối ưu, thuận tiện, dễ triển khai … hơn thì mình xin phép kể về hành trình v

0 0 71