Dart SDK version is 3.7.01
dependencies:flutter:sdk: flutterpermission_handler: ^11.0.1 # 權限管理flutter_contacts: ^1.1.9+2call_log: ^5.0.5cupertino_icons: ^1.0.8dev_dependencies:flutter_test:sdk: flutterflutter_lints: ^5.0.0
2 contact_and_calls_page.dartimport 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:call_log/call_log.dart';class ContactAndCallsPage extends StatefulWidget {@override_ContactAndCallsPageState createState() => _ContactAndCallsPageState(); }class _ContactAndCallsPageState extends State<ContactAndCallsPage>with SingleTickerProviderStateMixin {List<Contact> _contacts = [];Iterable<CallLogEntry> _callLogs = [];bool _isLoading = true;String _errorMessage = '';late TabController _tabController;@overridevoid initState() {super.initState();_tabController = TabController(length: 2, vsync: this);_requestPermissions();}@overridevoid dispose() {_tabController.dispose();super.dispose();}// 請求權限Future<void> _requestPermissions() async {setState(() {_isLoading = true;_errorMessage = '';});try {// Android 特定權限處理var contactStatus = await Permission.contacts.request();var phoneStatus = await Permission.phone.request();// 檢查是否獲得權限if (contactStatus.isGranted || phoneStatus.isGranted) {await _fetchContacts();await _fetchCallLogs();} else {setState(() {_errorMessage = '需要通訊錄和通話記錄權限以正常使用功能';});_showPermissionDeniedDialog();}} catch (e) {setState(() {_errorMessage = '權限請求發生錯誤: $e';});print('權限請求錯誤: $e');} finally {setState(() {_isLoading = false;});}}// 顯示權限被拒絕的對話框void _showPermissionDeniedDialog() {showDialog(context: context,builder: (context) => AlertDialog(title: Text('權限受限'),content: Text(_errorMessage),actions: [TextButton(onPressed: () {Navigator.of(context).pop();openAppSettings(); // 打開應用設置},child: Text('打開設置'),),TextButton(onPressed: () => Navigator.of(context).pop(),child: Text('取消'),),],),);}// 獲取通訊錄聯系人Future<void> _fetchContacts() async {try {// 多重權限檢查bool permission = await FlutterContacts.requestPermission(readonly: true);if (permission) {// 添加詳細配置獲取聯系人final contacts = await FlutterContacts.getContacts(withProperties: true, // 獲取聯系人屬性withPhoto: false, // 不獲取照片以提高性能sorted: true, // 按名稱排序);// 過濾掉沒有電話號碼的聯系人final filteredContacts = contacts.where((contact) =>contact.phones.isNotEmpty).toList();setState(() {_contacts = filteredContacts;});print('獲取到聯系人數量: ${_contacts.length}');} else {setState(() {_errorMessage = '未獲得通訊錄權限';});}} catch (e) {print('獲取通訊錄錯誤: $e');setState(() {_errorMessage = '獲取通訊錄失敗: $e';});}}// 獲取通話記錄Future<void> _fetchCallLogs() async {try {// 獲取通話記錄Iterable<CallLogEntry> callLogs = await CallLog.get();// 過濾最近30天的通話記錄final now = DateTime.now();final thirtyDaysAgo = now.subtract(Duration(days: 30));setState(() {_callLogs = callLogs.where((log) {final logTime = DateTime.fromMillisecondsSinceEpoch(log.timestamp ?? 0);return logTime.isAfter(thirtyDaysAgo);}).take(100) // 限制最多100條記錄.toList();});print('獲取到通話記錄數量: ${_callLogs.length}');} catch (e) {print('獲取通話記錄錯誤: $e');setState(() {_errorMessage = '獲取通話記錄失敗: $e';});}}// 轉換通話類型String _getCallType(CallType? callType) {switch (callType) {case CallType.incoming:return '呼入';case CallType.outgoing:return '呼出';case CallType.missed:return '未接';default:return '未知';}}// 格式化通話記錄時間String _formatCallLogTime(int? timestamp) {if (timestamp == null) return '未知時間';final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);return '${dateTime.year}-${_twoDigits(dateTime.month)}-${_twoDigits(dateTime.day)} ''${_twoDigits(dateTime.hour)}:${_twoDigits(dateTime.minute)}:${_twoDigits(dateTime.second)}';}// 補充兩位數方法String _twoDigits(int n) {return n.toString().padLeft(2, '0');}// 格式化通話時長String _formatCallDuration(int? duration) {if (duration == null || duration == 0) return '未接通';final minutes = duration ~/ 60;final seconds = duration % 60;return '$minutes分${_twoDigits(seconds)}秒';}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('通訊錄和通話記錄'),bottom: TabBar(controller: _tabController,tabs: [Tab(icon: Icon(Icons.contacts), text: '通訊錄'),Tab(icon: Icon(Icons.call), text: '通話記錄'),],),actions: [IconButton(icon: Icon(Icons.refresh),onPressed: _requestPermissions, // 直接調用權限請求方法),],),body: _isLoading? Center(child: CircularProgressIndicator()): _errorMessage.isNotEmpty? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Icon(Icons.error_outline, color: Colors.red, size: 100),SizedBox(height: 20),Text(_errorMessage,style: TextStyle(color: Colors.red),textAlign: TextAlign.center,),SizedBox(height: 20),ElevatedButton(onPressed: _requestPermissions,child: Text('重新獲取權限'),)],),): TabBarView(controller: _tabController,children: [_buildContactsList(),_buildCallLogsList(),],),);}// 構建通訊錄列表Widget _buildContactsList() {if (_contacts.isEmpty) {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Icon(Icons.contacts, size: 100, color: Colors.grey),SizedBox(height: 20),Text('暫無聯系人',style: TextStyle(fontSize: 18, color: Colors.grey),),],),);}return ListView.builder(itemCount: _contacts.length,itemBuilder: (context, index) {Contact contact = _contacts[index];return ListTile(leading: CircleAvatar(backgroundColor: Colors.primaries[index % Colors.primaries.length],child: Text(contact.name.first.isNotEmpty ? contact.name.first[0] : '?',style: TextStyle(fontWeight: FontWeight.bold,color: Colors.white,),),),title: Text(contact.name.first.isNotEmpty ? contact.name.first : '未命名',style: TextStyle(fontWeight: FontWeight.bold),),subtitle: Text(contact.phones.isNotEmpty? contact.phones.first.number: '無電話號碼',style: TextStyle(color: Colors.grey[600]),),);},);}// 構建通話記錄列表Widget _buildCallLogsList() {if (_callLogs.isEmpty) {return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Icon(Icons.call, size: 100, color: Colors.grey),SizedBox(height: 20),Text('暫無通話記錄',style: TextStyle(fontSize: 18, color: Colors.grey),),],),);}return ListView.builder(itemCount: _callLogs.length,itemBuilder: (context, index) {CallLogEntry callLog = _callLogs.elementAt(index);return ListTile(leading: Icon(callLog.callType == CallType.missed? Icons.call_missed: (callLog.callType == CallType.incoming? Icons.call_received: Icons.call_made),color: callLog.callType == CallType.missed? Colors.red: Colors.green,),title: Text(callLog.number ?? '未知號碼'),subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Text('時間: ${_formatCallLogTime(callLog.timestamp)}'),Text('類型: ${_getCallType(callLog.callType)}'),Text('通話時長: ${_formatCallDuration(callLog.duration)}'),],),);},);} }
3 main.dart
import 'package:flutter/material.dart'; import 'contact_and_calls_page.dart';void main() {runApp(MyApp()); }class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: '通訊錄和通話記錄',theme: ThemeData(primarySwatch: Colors.blue,visualDensity: VisualDensity.adaptivePlatformDensity,),home: ContactAndCallsPage(),);} }
4? ?降級java sdk到1.8 ---? ?build.gradle.kts
plugins {id("com.android.application")id("kotlin-android")id("dev.flutter.flutter-gradle-plugin") }android {namespace = "com.example.contactlist"compileSdk = flutter.compileSdkVersionndkVersion = flutter.ndkVersioncompileOptions {// Java 8 配置sourceCompatibility = JavaVersion.VERSION_1_8targetCompatibility = JavaVersion.VERSION_1_8// 啟用 Core Library DesugaringisCoreLibraryDesugaringEnabled = true}kotlinOptions {// 匹配 Java 版本jvmTarget = JavaVersion.VERSION_1_8.toString()}defaultConfig {applicationId = "com.example.contactlist"minSdk = 21 // 確保 minSdk 不低于 21targetSdk = flutter.targetSdkVersionversionCode = flutter.versionCodeversionName = flutter.versionName// 啟用 MultiDexmultiDexEnabled = true}buildTypes {release {signingConfig = signingConfigs.getByName("debug")}} }dependencies {// 升級到 2.1.4 或更高版本coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")// MultiDex 支持implementation("androidx.multidex:multidex:2.0.1") }flutter {source = "../.." }