Dart SDK version is 3.7.0
1
dependencies:
flutter:
sdk: flutter
permission_handler: ^11.0.1 # 权限管理
flutter_contacts: ^1.1.9+2
call_log: ^5.0.5
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
2 contact_and_calls_page.dart
import '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;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_requestPermissions();
}
@override
void 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)}秒';
}
@override
Widget 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 {
@override
Widget 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.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
// Java 8 配置
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// 启用 Core Library Desugaring
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
// 匹配 Java 版本
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
defaultConfig {
applicationId = "com.example.contactlist"
minSdk = 21 // 确保 minSdk 不低于 21
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
// 启用 MultiDex
multiDexEnabled = 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 = "../.."
}