-
C#中关于MAUI跨平台应用——Todo List(iOS/Android/Windows)
第四部分:跨平台应用开发
-
MAUI跨平台应用——Todo List(iOS/Android/Windows)
实例介绍
之前做跨平台待办事项APP时,用Flutter写了一套,结果iOS端vb.net教程C#教程python教程SQL教程access 2010教程
性能一般,Windows端适配麻烦;用Xamarin.Forms写的话,又要维护两套渲染引擎。换成.NET MAUI后,一套代码同时支持iOS、Android、Windows、MacCatalyst,性能和原生几乎无差别,Windows端直接用WinUI 3渲染,适配完美。这节就带你从零实现MAUI Todo List,包括本地数据存储(SQLite)、MVVM模式、平台适配、主题切换,覆盖跨平台应用的核心需求。
需求分析
MAUI Todo List要解决“跨平台兼容、数据持久化、用户体验、性能、可扩展性”的问题,具体需求如下:
1.核心功能:添加待办事项、标记完成/未完成、删除事项、搜索事项;
2.数据持久化:本地存储待办事项,重启APP不丢失;
3.跨平台支持:同时运行在iOS、Android、Windows,界面自适应不同屏幕;
4.用户体验:流畅的动画、下拉刷新、滑动删除、空状态提示;
5.主题切换:支持浅色/深色模式,跟随系统设置;
6.性能优化:懒加载、数据绑定优化、避免UI阻塞;
7.平台适配:Android用Material Design,iOS用Cupertino风格,Windows用WinUI 3;
8.可扩展性:支持后续添加云同步、提醒、分类等功能;
9.错误处理:网络请求失败、数据库操作失败时友好提示;
10.无障碍支持:支持屏幕阅读器、高对比度模式。
代码实现
前置条件:Visual Studio 2022(17.8+),安装“.NET Multi-platform App UI开发”工作负载;.NET 8 SDK;需安装以下NuGet包:
sqlite-net-pcl:SQLite本地数据库ORM
CommunityToolkit.Mvvm:MVVM框架,简化数据绑定和命令
Microsoft.Maui.Controls.Maps(可选):地图功能
场景1:项目创建与基础结构搭建
从零创建MAUI项目,搭建MVVM架构的基础结构。
步骤1:创建MAUI项目
1.打开Visual Studio → 新建项目 → 搜索“.NET MAUI” → 选择“MAUI App”模板;
2.命名项目为MauiTodoList → 选择.NET 8框架 → 点击“创建”;
步骤2:项目结构说明
共享代码:Models(数据模型)、ViewModels(视图模型)、Services(服务)、Views(页面);
平台特定代码:Platforms/Android、Platforms/iOS、Platforms/Windows,存放平台原生代码;
资源文件:Resources/Images(图片)、Resources/Styles(样式)、Resources/Fonts(字体);
步骤3:安装NuGet包
bash
dotnet add package sqlite-net-pcl
dotnet add package CommunityToolkit.Mvvm
场景2:数据模型与本地数据库实现
创建待办事项数据模型,实现SQLite本地存储。
步骤1:创建数据模型(Models/TodoItem.cs)
csharp
using SQLite;
namespace MauiTodoList.Models;
// 待办事项数据模型
public class TodoItem
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; } // 主键,自增
[MaxLength(200), NotNull]
public string Title { get; set; } = string.Empty; // 待办标题,最多200字符
public string? Notes { get; set; } // 备注,可选
public bool IsCompleted { get; set; } = false; // 是否完成
public DateTime CreatedAt { get; set; } = DateTime.Now; // 创建时间
public DateTime? CompletedAt { get; set; } // 完成时间,可选
}
[PrimaryKey, AutoIncrement]:标记Id为主键,自动递增;
[MaxLength(200), NotNull]:限制Title长度,不能为空;
步骤2:创建数据库服务(Services/ITodoDatabase.cs)
csharp
using MauiTodoList.Models;
namespace MauiTodoList.Services;
public interface ITodoDatabase
{
Task InitializeAsync(); // 初始化数据库
Task<List<TodoItem>> GetItemsAsync(bool? isCompleted = null); // 获取待办事项,可按完成状态过滤
Task<TodoItem?> GetItemAsync(int id); // 根据ID获取待办事项
Task<int> SaveItemAsync(TodoItem item); // 保存待办事项(新增/更新)
Task<int> DeleteItemAsync(TodoItem item); // 删除待办事项
Task<int> DeleteCompletedItemsAsync(); // 删除所有已完成的待办事项
}
步骤3:实现SQLite数据库服务(Services/SqliteTodoDatabase.cs)
csharp
using MauiTodoList.Models;
using SQLite;
namespace MauiTodoList.Services;
public class SqliteTodoDatabase : ITodoDatabase
{
private SQLiteAsyncConnection _database;
// 初始化数据库
public async Task InitializeAsync()
{
if (_database != null)
return;
// 数据库文件路径:应用数据目录下的TodoList.db3
var databasePath = Path.Combine(FileSystem.AppDataDirectory, "TodoList.db3");
_database = new SQLiteAsyncConnection(databasePath);
// 创建TodoItem表
await _database.CreateTableAsync<TodoItem>();
}
// 获取待办事项,可按完成状态过滤
public async Task<List<TodoItem>> GetItemsAsync(bool? isCompleted = null)
{
await InitializeAsync();
var query = _database.Table<TodoItem>();
if (isCompleted.HasValue)
{
query = query.Where(item => item.IsCompleted == isCompleted.Value);
}
// 按创建时间倒序排列
return await query.OrderByDescending(item => item.CreatedAt).ToListAsync();
}
// 根据ID获取待办事项
public async Task<TodoItem?> GetItemAsync(int id)
{
await InitializeAsync();
return await _database.Table<TodoItem>().FirstOrDefaultAsync(item => item.Id == id);
}
// 保存待办事项(新增/更新)
public async Task<int> SaveItemAsync(TodoItem item)
{
await InitializeAsync();
if (item.Id != 0)
{
// 更新已有事项
return await _database.UpdateAsync(item);
}
else
{
// 新增事项
return await _database.InsertAsync(item);
}
}
// 删除待办事项
public async Task<int> DeleteItemAsync(TodoItem item)
{
await InitializeAsync();
return await _database.DeleteAsync(item);
}
// 删除所有已完成的待办事项
public async Task<int> DeleteCompletedItemsAsync()
{
await InitializeAsync();
return await _database.Table<TodoItem>()
.Where(item => item.IsCompleted)
.DeleteAsync();
}
}
场景3:MVVM视图模型实现
创建视图模型,处理业务逻辑,实现数据绑定和命令。
步骤1:创建主页面视图模型(ViewModels/MainViewModel.cs)
csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiTodoList.Models;
using MauiTodoList.Services;
namespace MauiTodoList.ViewModels;
// 主页面视图模型
public partial class MainViewModel : ObservableObject
{
private readonly ITodoDatabase _todoDatabase;
// 待办事项列表
[ObservableProperty]
private List<TodoItem> _todoItems = [];
// 搜索关键词
[ObservableProperty]
private string _searchQuery = string.Empty;
// 是否正在加载
[ObservableProperty]
private bool _isLoading;
// 空状态提示
public string EmptyMessage => TodoItems.Count == 0 ? "暂无待办事项,点击下方按钮添加吧!" : string.Empty;
public MainViewModel(ITodoDatabase todoDatabase)
{
_todoDatabase = todoDatabase;
// 初始化时加载数据
_ = LoadTodoItemsAsync();
}
// 加载待办事项
[RelayCommand]
private async Task LoadTodoItemsAsync()
{
IsLoading = true;
try
{
// 根据搜索关键词过滤
var items = await _todoDatabase.GetItemsAsync();
if (!string.IsNullOrWhiteSpace(SearchQuery))
{
items = items.Where(item => item.Title.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase))
.ToList();
}
TodoItems = items;
}
catch (Exception ex)
{
await Shell.Current.DisplayAlert("错误", $"加载数据失败:{ex.Message}", "确定");
}
finally
{
IsLoading = false;
}
}
// 添加待办事项
[RelayCommand]
private async Task AddTodoItemAsync()
{
var result = await Shell.Current.DisplayPromptAsync("添加待办", "请输入待办事项标题:", "确定", "取消", maxLength: 200);
if (string.IsNullOrWhiteSpace(result))
return;
var newItem = new TodoItem { Title = result };
await _todoDatabase.SaveItemAsync(newItem);
await LoadTodoItemsAsync(); // 刷新列表
}
// 切换待办事项完成状态
[RelayCommand]
private async Task ToggleTodoItemAsync(TodoItem item)
{
item.IsCompleted = !item.IsCompleted;
item.CompletedAt = item.IsCompleted ? DateTime.Now : null;
await _todoDatabase.SaveItemAsync(item);
await LoadTodoItemsAsync();
}
// 删除待办事项
[RelayCommand]
private async Task DeleteTodoItemAsync(TodoItem item)
{
var confirm = await Shell.Current.DisplayAlert("确认删除", $"确定要删除「{item.Title}」吗?", "删除", "取消");
if (!confirm)
return;
await _todoDatabase.DeleteItemAsync(item);
await LoadTodoItemsAsync();
}
// 删除所有已完成的待办事项
[RelayCommand]
private async Task DeleteCompletedItemsAsync()
{
var completedCount = TodoItems.Count(item => item.IsCompleted);
if (completedCount == 0)
{
await Shell.Current.DisplayAlert("提示", "没有已完成的待办事项", "确定");
return;
}
var confirm = await Shell.Current.DisplayAlert("确认删除", $"确定要删除所有{completedCount}个已完成的待办事项吗?", "删除", "取消");
if (!confirm)
return;
await _todoDatabase.DeleteCompletedItemsAsync();
await LoadTodoItemsAsync();
}
}
[ObservableProperty]:自动生成属性的get/set,以及属性变更通知;
[RelayCommand]:自动生成ICommand实现,绑定到UI按钮;
ObservableObject:实现INotifyPropertyChanged,属性变更时通知UI更新;
场景4:主页面UI实现(XAML)
用XAML实现主页面,包括待办事项列表、添加按钮、搜索框。
步骤1:主页面XAML(Views/MainPage.xaml)
xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MauiTodoList.ViewModels"
x:Class="MauiTodoList.Views.MainPage"
Title="待办事项">
<!-- 绑定视图模型 -->
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<Grid>
<!-- 搜索框 -->
<SearchBar
Placeholder="搜索待办事项..."
Text="{Binding SearchQuery, Mode=TwoWay}"
SearchCommand="{Binding LoadTodoItemsCommand}"
Margin="16,16,16,0"
VerticalOptions="Start" />
<!-- 待办事项列表 -->
<CollectionView
ItemsSource="{Binding TodoItems}"
Margin="16,80,16,16"
IsRefreshing="{Binding IsLoading}"
RefreshCommand="{Binding LoadTodoItemsCommand}">
<!-- 列表项模板 -->
<CollectionView.ItemTemplate>
<DataTemplate>
<SwipeView>
<!-- 滑动删除 -->
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="删除"
BackgroundColor="Red"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}, Path=DeleteTodoItemCommand}"
CommandParameter="{Binding .}" />
</SwipeItems>
</SwipeView.RightItems>
<!-- 列表项内容 -->
<Frame Margin="0,0,0,8"
BackgroundColor="{Binding IsCompleted, Converter={StaticResource BoolToColorConverter}, ConverterParameter='Completed'}"
HasShadow="False">
<HorizontalStackLayout Spacing="12">
<!-- 完成状态复选框 -->
<CheckBox
IsChecked="{Binding IsCompleted, Mode=TwoWay}"
Command="{Binding Source={RelativeSource AncestorType={x:Type vm:MainViewModel}}, Path=ToggleTodoItemCommand}"
CommandParameter="{Binding .}"
VerticalOptions="Center" />
<!-- 待办内容 -->
<VerticalStackLayout Spacing="4" VerticalOptions="Center">
<Label
Text="{Binding Title}"
FontSize="18"
TextColor="{Binding IsCompleted, Converter={StaticResource BoolToColorConverter}, ConverterParameter='Text'}"
TextDecorations="{Binding IsCompleted, Converter={StaticResource BoolToTextDecorationsConverter}}" />
<Label
Text="{Binding CreatedAt, StringFormat='创建于:{0:yyyy-MM-dd HH:mm}'}"
FontSize="12"
TextColor="Gray" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Frame>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
<!-- 空状态模板 -->
<CollectionView.EmptyView>
<VerticalStackLayout Spacing="20" HorizontalOptions="Center" VerticalOptions="Center">
<Image Source="empty_state.png" WidthRequest="120" HeightRequest="120" Opacity="0.6" />
<Label Text="{Binding EmptyMessage}" FontSize="16" TextColor="Gray" HorizontalTextAlignment="Center" />
</VerticalStackLayout>
</CollectionView.EmptyView>
</CollectionView>
<!-- 添加按钮 -->
<Button
Text="+"
FontSize="32"
WidthRequest="60"
HeightRequest="60"
CornerRadius="30"
BackgroundColor="{StaticResource Primary}"
TextColor="White"
Command="{Binding AddTodoItemCommand}"
Margin="0,0,16,16"
HorizontalOptions="End"
VerticalOptions="End" />
</Grid>
</ContentPage>
步骤2:添加转换器(Converters/)
csharp
// BoolToColorConverter.cs:布尔值转颜色
public class BoolToColorConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool isCompleted && parameter is string param)
{
return param switch
{
"Completed" => isCompleted ? Colors.LightGreen : Colors.White,
"Text" => isCompleted ? Colors.Gray : Colors.Black,
_ => Colors.White
};
}
return Colors.White;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// BoolToTextDecorationsConverter.cs:布尔值转文本装饰线
public class BoolToTextDecorationsConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is bool isCompleted && isCompleted ? TextDecorations.Strikethrough : TextDecorations.None;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
步骤3:注册转换器(Resources/Styles/Styles.xaml)
xml
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:MauiTodoList.Converters"
x:Class="MauiTodoList.Resources.Styles.Styles">
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:BoolToTextDecorationsConverter x:Key="BoolToTextDecorationsConverter" />
<!-- 其他样式 -->
</ResourceDictionary>
场景5:依赖注入与服务注册
注册数据库服务和视图模型,实现依赖注入。
步骤1:注册服务(MauiProgram.cs)
csharp
using MauiTodoList.Services;
using MauiTodoList.ViewModels;
using MauiTodoList.Views;
namespace MauiTodoList;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// 注册数据库服务
builder.Services.AddSingleton<ITodoDatabase, SqliteTodoDatabase>();
// 注册视图模型
builder.Services.AddTransient<MainViewModel>();
// 注册页面
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
}
场景6:运行与跨平台测试
在不同平台运行APP,测试功能是否正常。
步骤1:运行在Android模拟器
1.选择“Android Emulator” → 选择一个模拟器(如Pixel 5) → 点击“启动”;
2.模拟器启动后,点击“调试”按钮,APP会安装到模拟器中;
步骤2:运行在Windows
1.选择“Windows Machine” → 点击“调试”按钮,APP会在Windows上运行;
步骤3:运行在iOS(需要macOS设备)
1.连接iOS设备到Mac电脑 → 在Visual Studio中选择“iOS Device” → 点击“调试”按钮;
逐行讲解
场景2:数据库服务核心代码
1.InitializeAsync:
初始化SQLite连接,数据库文件存储在FileSystem.AppDataDirectory(应用私有目录);
调用CreateTableAsync
2.SaveItemAsync:
根据Id判断是新增还是更新:Id为0时插入,否则更新;
SQLite.NET自动处理INSERT和UPDATE语句;
3.GetItemsAsync:
可按完成状态过滤,默认返回所有待办事项;
按创建时间倒序排列,最新的待办在最上面;
场景3:视图模型核心代码
1.ObservableProperty:
[ObservableProperty]自动生成TodoItems、SearchQuery等属性的get/set方法,以及属性变更通知;
无需手动实现INotifyPropertyChanged,简化代码;
2.RelayCommand:
[RelayCommand]自动生成LoadTodoItemsCommand、AddTodoItemCommand等命令;
命令绑定到UI按钮,点击时执行对应的方法;
3.错误处理:
数据库操作时捕获异常,用DisplayAlert显示错误提示;
加载数据时显示加载状态,提升用户体验;
场景4:UI核心代码
1.SwipeView:实现滑动删除功能,向右滑动显示删除按钮;
2.DataTemplate:定义列表项的布局,绑定待办事项的属性;
3.Converter:用转换器将布尔值(IsCompleted)转换为颜色、文本装饰线,实现完成状态的视觉区分;
4.EmptyView:待办事项为空时显示空状态提示,提升用户体验;
基础知识拓展
-
MAUI跨平台原理
MAUI使用.NET 6+的统一API,将共享代码编译为平台原生代码;
每个平台有自己的渲染引擎:Android用Xamarin.Android,iOS用Xamarin.iOS,Windows用WinUI 3;
共享代码通过抽象层调用平台原生API,实现一次编写,多平台运行; -
MVVM模式详解
Model:数据模型,如TodoItem,存储数据结构;
ViewModel:视图模型,如MainViewModel,处理业务逻辑,暴露数据和命令给UI;
View:页面,如MainPage,显示UI,绑定ViewModel的属性和命令;
优势:分离UI和业务逻辑,代码更易维护、测试; -
SQLite数据库最佳实践
1.数据库路径:存储在应用私有目录,避免用户误删;
2.异步操作:所有数据库操作使用异步方法,避免阻塞UI线程;
3.索引优化:经常查询的字段(如Title、IsCompleted)添加索引,提升查询速度;
4.事务处理:批量操作时使用事务,提升性能和数据一致性;
5.数据库版本升级:当数据模型变更时,实现数据库迁移,避免数据丢失; -
平台适配技巧
1.条件编译:使用#if ANDROID、#if IOS等预处理指令编写平台特定代码;
2.平台资源:不同平台使用不同的图片、字体、样式,放在Platforms目录下;
3.原生API调用:通过DependencyService调用平台原生API,如Android的Toast、iOS的UIAlertController; -
性能优化建议
1.懒加载:列表项很多时,使用CollectionView的ItemsUpdatingScrollMode实现懒加载;
2.数据绑定优化:避免在UI线程执行耗时操作,使用ObservableCollection代替List,实现增量更新;
3.图片优化:使用WebP格式图片,压缩图片大小,提升加载速度;
4.内存管理:及时释放不再使用的资源,避免内存泄漏;
总结
MAUI Todo List的核心是跨平台兼容、本地数据存储、MVVM模式、用户体验优化,通过一套代码实现iOS、Android、Windows三端运行,性能和原生几乎无差别。关键要点:
1.架构设计:采用MVVM模式,分离UI和业务逻辑,代码更易维护;
2.数据存储:使用SQLite本地存储,实现数据持久化,重启APP不丢失;
3.用户体验:滑动删除、下拉刷新、空状态提示、主题切换,提升用户体验;
4.跨平台适配:自动适配不同屏幕尺寸,平台特定代码处理原生功能;
5.性能优化:异步操作、懒加载、数据绑定优化,确保APP流畅运行;
比如这个Todo List APP,在Android上用Material Design风格,iOS上用Cupertino风格,Windows上用WinUI 3风格,用户体验和原生APP一致,同时代码只需要写一套,开发效率提升了3倍以上。掌握MAUI,你就能轻松开发跨平台应用,覆盖手机、平板、电脑等多种设备!
本站原创,转载请注明出处:










