您当前的位置: 首页 > 慢生活 > 程序人生 网站首页程序人生
Flutter for Android 开发者(flutter项目运行到安卓模拟器)
发布时间:2026-07-20 00:30:05编辑:雪饮阅读()
-
官方文档
- Android SDK 构建工具
- Android SDK 命令行工具
- 安卓模拟器
- Android SDK 平台工具
- CMake
- NDK(并肩作战)
https://docs.flutter.dev/platform-integration/android/setup
那么这个我们稍后说,先把上篇中没有完成的关键词屏蔽时候的搜索接口完善
我后端用的是thinkphp8,默认的\app\controller\index.php控制器:
<?php
namespace app\controller;
use app\BaseController;
use GuzzleHttp\Client;
use think\Db;
class Index extends BaseController
{
public function dataSync()
{
// 调试:确认方法被调用
//echo "Method called\n";
//请求参数
$requestParams=$this->request->post();
if(!$requestParams){
return json(['code' =>'php500' , 'msg' => '参数为空']);
}
// echo "Params received\n";
//请求地址
$apiUrl="https://search-server.6wlua.com/Search/Social/Query";
//请求头 x-bundle-info值为 "1, com.xingba.web.online"
$authorization=$this->request->header('authorization');
if(!$authorization){
return json(['code' =>'php500' , 'msg' => '缺少请求头authorization']);
}
$client = new Client(['timeout' => 30]);
//echo "Client created\n";
try {
// echo "Making request...\n";
$response = $client->request('POST', $apiUrl, [
'headers' => [
'x-bundle-info' => '1, com.xingba.web.online',
'Authorization' => $authorization,
],
'json' => $requestParams,
'verify' => false,
]);
// echo "Request completed\n";
$result = $response->getBody()->getContents();
// echo "Got result: " . strlen($result) . " bytes\n";
if($result){
$result=json_decode($result,true);
if(!isset($result["msg"])){
return json(['code' =>'php500' , 'msg' => '三方接口异常msg']);
}
if(!isset($result["msg"][0])){
return json(['code' =>'php500' , 'msg' => '三方接口异常msg[0]','origin_response_data'=>$result]);
}
if(!isset($result["msg"][0]["list"])){
return json(['code' =>'php500' , 'msg' => '三方接口异常msg[0].list','origin_response_data'=>$result]);
}
$list=$result["msg"][0]["list"];
if($list){
$typeList=[];
foreach($list as $item){
$title=$item["title"];
$list_item=json_encode($item);
$origin_id=$item["id"];
$origin_type=$item["type"];
$originIdCount=\think\facade\Db::name("third_list_data")
->where("origin_id",$origin_id)
->where("origin_type",$origin_type)
->count();
if($originIdCount>0){
$updateData=[
"title"=>$title,
"list_item"=>$list_item,
];
\think\facade\Db::name("third_list_data")->where("origin_id",$origin_id)->update($updateData);
}
if($originIdCount==0){
\think\facade\Db::name("third_list_data")->insert([
"title"=>$title,
"list_item"=>$list_item,
"origin_id"=>$origin_id,
"origin_type"=>$origin_type
]);
}
$typeList[]=$origin_type;
$typeList=array_unique($typeList);
$result["msg"][0]["list"][0]["typeList"]=$typeList;
}
}
return json($result);
}
return json(['code' => 'php200', 'raw' => $result]);
} catch (\Exception $e) {
//echo "Exception caught: " . $e->getMessage() . "\n";
return json(['code' => 'php500', 'msg' => $e->getMessage()]);
}
}
public function dataSyncAfterDataList()
{
$requestParams=$this->request->post();
if(!$requestParams){
return json(['code' =>'php500' , 'msg' => '参数为空']);
}
if(!isset($requestParams[0]["blocked_keyword_list"])){
return json(['code' =>'php500' , 'msg' => '缺少关键参数blocked_keyword_list']);
}
if(!$requestParams[0]["blocked_keyword_list"]){
return json(['code' =>'php500' , 'msg' => 'blocked_keyword_list为空']);
}
if($requestParams[0]["or"][0]["field"]!="title"){
return json(['code' =>'php500' , 'msg' => 'or[0].field必须为title']);
}
if($requestParams[0]["or"][0]["op"]!='$like'){
return json(['code' =>'php500' , 'msg' => 'or[0].op必须为$like']);
}
$title=mb_substr($requestParams[0]["or"][0]["value"], 1);
if(!$title){
return json(['code' =>'php500' , 'msg' => '缺少关键参数title']);
}
//页码
$page=$requestParams[0]["page"];
if(!$page){
return json(['code' =>'php500' , 'msg' => '缺少关键参数page']);
}
//每页数量
$limit=$requestParams[0]["limit"];
if(!$limit){
return json(['code' =>'php500' , 'msg' => '缺少关键参数limit']);
}
$idList=[];
foreach($requestParams[0]["blocked_keyword_list"] as $blocked_keyword){
$currentIdList=\think\facade\Db::name("third_list_data")->where("title","like","%".$blocked_keyword."%")->column("id");
if(!$currentIdList){
$currentIdList=[-1];
}
$idList=array_merge($idList,$currentIdList);
$idList=array_unique($idList);
}
if(!$idList){
$idList=[-1];
}
$paginatorData = \think\facade\Db::name("third_list_data")
->where("id","not in",$idList)
->where("title","like","%".$title."%")
->paginate([
'list_rows' => $limit,
'page' => $page,
]);
$paginatorDataArray=$paginatorData->toArray();
$responseData["status"]="SUCCESS";
$responseData["code"]=200;
$list=$paginatorDataArray["data"];
foreach($list as &$item){
$list_item=json_decode($item["list_item"],true);
$item=$list_item;
}
$msg0=[
"count"=>$paginatorDataArray["total"],
"list"=>$list,
];
$responseData["msg"]=[$msg0];
return json($responseData);
}
}
然后之前的flutter中的search_result_table.dart需要修改为先预先判断若有屏蔽关键词,则先同步数据到我们自己的数据库,然后再自定义搜索,那么修改后的search_result_table.dart:
import 'dart:convert' show jsonEncode, jsonDecode;
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'blocked_keywords_button.dart';
class SearchResultTable extends StatefulWidget {
const SearchResultTable({super.key});
@override
State<SearchResultTable> createState() => _SearchResultTableState();
}
class _SearchResultTableState extends State<SearchResultTable> {
final TextEditingController _apiController = TextEditingController(
text: "https://search-server.xoczl.net/Search/Social/Query",
);
final TextEditingController _keywordController = TextEditingController(
text: "搭讪",
);
final TextEditingController authorizationController = TextEditingController(
text:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoi5ri45a6iLTEiLCJpZCI6IjI0MDAwMDAwMDAyIiwiaXAiOiI1Mi44OC4xNjkuMjUzIiwidHlwZSI6MywicGFja2FnZV9pZCI6MSwibGV2ZWwiOjEsImlzcyI6IkdVRVNUIiwic3ViIjoic29jaWFsLXNlcnZlciIsImV4cCI6MTc4NDg4NTEzNywibmJmIjoxNzg0MjgwMzM3LCJpYXQiOjE3ODQyODAzMzcsImp0aSI6ImUwMTdmOWI2MjlmNDRjMmU5YTMxMDlkOWZkNWYzMDEyIn0.28x7hLxpeI0k90xfQDiwr72fkz0Vzwztrr9QomEaPZU",
);
List<dynamic> _searchResults = [];
int _resultCount = 0;
final Map<int, Uint8List> _imageDataUris = {};
List<String> _blockedKeywords = [];
String? _syncMessage;
void _onBlockedKeywordsChanged(List<String> keywords) {
_blockedKeywords = keywords;
}
@override
void dispose() {
_apiController.dispose();
_keywordController.dispose();
authorizationController.dispose();
super.dispose();
}
Widget _buildGridCell({
required Widget child,
bool rightBorder = false,
bool bottomBorder = false,
bool topBorder = false,
}) {
return Container(
decoration: BoxDecoration(
border: Border(
right: rightBorder ? const BorderSide() : BorderSide.none,
bottom: bottomBorder ? const BorderSide() : BorderSide.none,
top: topBorder ? const BorderSide() : BorderSide.none,
),
),
child: child,
);
}
Widget _buildHeaderCell(String text) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)),
),
);
}
//数据同步后的请求
Future<void> dataSyncAfterDataList(
List<dynamic> requestBody,
String auth,
List<String> blockedKeywords,
) async {
try {
requestBody[0]['blocked_keyword_list'] = blockedKeywords;
final response = await http.post(
Uri.parse("http://localhost:8082/index/dataSyncAfterDataList"),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
debugPrint('请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
final count = msg[0]['count'] as int;
final list = msg[0]['list'] as List<dynamic>;
setState(() {
_resultCount = count;
_searchResults = list;
_imageDataUris.clear();
});
_fetchImagesForResults(list, "null", auth);
} else {
debugPrint('请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
}
} catch (e) {
debugPrint('请求异常: $e');
}
}
//原请求
Future<void> originRequest(
String apiUrl,
List<dynamic> requestBody,
String auth,
) async {
try {
final response = await http.post(
Uri.parse(apiUrl),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
debugPrint('请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
if (msg is! List || msg.isEmpty) {
if (msg is Map && msg['message'] != null) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(msg['message'].toString())));
}
return;
}
final count = msg[0]['count'] as int;
final list = msg[0]['list'] as List<dynamic>;
setState(() {
_resultCount = count;
_searchResults = list;
_imageDataUris.clear();
});
_fetchImagesForResults(list, apiUrl, auth);
} else {
debugPrint('请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
}
} catch (e) {
debugPrint('请求异常: $e');
}
}
//同步数据请求(按页循环同步,直到没有更多数据)
Future<bool> asyncRequest(
List<dynamic> requestBody,
String auth,
int? totalPage,
) async {
try {
var info = '正在同步第${requestBody[0]['page']}页数据';
if (totalPage != null) {
info += ',预计共${totalPage}页';
}
setState(() {
_syncMessage = info;
});
final response = await http.post(
Uri.parse("http://localhost:8082/index/dataSync"),
headers: {
'authorization': auth,
'Content-Type': 'application/json',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'X-Bundle-Info': '1, com.xingba.web.online',
},
body: jsonEncode(requestBody),
);
if (response.statusCode == 200) {
debugPrint('同步请求成功, 响应: ${response.body}');
final responseBody = jsonDecode(response.body);
final msg = responseBody['msg'];
final list = msg[0]['list'] as List<dynamic>;
totalPage =
((msg[0]['count'] as num) / (requestBody[0]['limit'] as num))
.ceil();
if (list.isNotEmpty) {
// 还有数据,页码+1,继续同步下一页
requestBody[0]['page'] = (requestBody[0]['page'] as int) + 1;
await asyncRequest(requestBody, auth, totalPage);
}
// 当前页无数据,同步完成
return true;
} else {
debugPrint('同步请求失败, 状态码: ${response.statusCode}, 响应: ${response.body}');
return false;
}
} catch (e) {
debugPrint('同步请求异常: $e');
return false;
}
}
//前置请求
Future<void> preRequest(
List<dynamic> requestBody,
String apiUrl,
String auth,
List<String> blockedKeywords,
) async {
if (blockedKeywords.isNotEmpty) {
// 有屏蔽关键词:先按页同步数据到本地数据库
bool syncComplete = await asyncRequest(requestBody, auth, null);
if (syncComplete) {
setState(() {
_syncMessage = null;
});
// 同步完成后,调用筛选接口
await dataSyncAfterDataList(requestBody, auth, blockedKeywords);
}
} else {
// 无屏蔽关键词:直接请求原始API
await originRequest(apiUrl, requestBody, auth);
}
}
Future<void> _fetchImagesForResults(
List<dynamic> list,
String apiUrl,
String auth,
) async {
for (int i = 0; i < list.length; i++) {
final item = list[i];
final covers = item['covers'];
if (covers != null && covers is List && covers.isNotEmpty) {
final resource = covers[0]['resource'];
final file = covers[0]['file'];
if (file == null) {
continue;
}
if (file != null) {
final hash = file['hash']?.toString() ?? '';
if (hash.isNotEmpty) {
if (hash != "2bf933b436b05c58") {
// continue;
}
}
}
if (resource != null) {
final path = resource['path']?.toString() ?? '';
final name = resource['name']?.toString() ?? '';
final rawStorageType = resource['storage_type'];
final int storage_type = rawStorageType is int
? rawStorageType
: int.tryParse(rawStorageType?.toString() ?? '') ?? 0;
if (path.isNotEmpty && name.isNotEmpty) {
final baseUrl = storage_type == 4
? 'https://photos.7ujx6.com'
: 'https://media-server.ehgyz.com';
final imageUrl = '$baseUrl$path/$name';
debugPrint('图片[$i] 请求URL: $imageUrl');
try {
final imageResponse = await http.get(
Uri.parse(imageUrl),
headers: {
'referer': 'https://xn--10jong-2o7io6t.30gnao.space/',
'origin': 'https://xn--10jong-2o7io6t.30gnao.space',
'x-bundle-info': '1, com.xingba.web.online',
'authorization': auth,
},
);
if (imageResponse.statusCode == 200) {
final contentType = imageResponse.headers['content-type'] ?? '';
final imageBytes = imageResponse.bodyBytes;
debugPrint(
'图片[$i] content-type: $contentType, 大小: ${imageBytes.length} bytes',
);
if (!contentType.startsWith('image/')) {
// 服务器返回的不是图片,打印前200字符看看实际返回了什么
final bodyText = String.fromCharCodes(
imageBytes.length > 200
? imageBytes.sublist(0, 200)
: imageBytes,
);
debugPrint('图片[$i] 非图片响应: $bodyText');
return;
}
// 验证图片数据是否有效(检查文件头)
if (imageBytes.length < 32) {
debugPrint('图片[$i] 数据太小: ${imageBytes.length} bytes');
return;
}
// 去除前16字节和后16字节(与JS端解密逻辑一致)
final trimmedBytes = imageBytes.sublist(
16,
imageBytes.length - 16,
);
debugPrint('图片[$i] 裁剪后大小: ${trimmedBytes.length} bytes');
if (mounted) {
setState(() {
_imageDataUris[i] = trimmedBytes;
});
}
} else {
debugPrint('图片[$i] 请求失败, 状态码: ${imageResponse.statusCode}');
}
} catch (e) {
debugPrint('图片[$i]请求失败: $e');
}
}
}
}
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(border: Border.all()),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _apiController,
decoration: const InputDecoration(
hintText: '请输入api地址',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _keywordController,
decoration: const InputDecoration(
hintText: '请输入关键词',
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: authorizationController,
decoration: const InputDecoration(
hintText: '请输入authorization',
border: OutlineInputBorder(),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
//搜索
ElevatedButton(
onPressed: () async {
if (_apiController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请输入api地址')));
return;
}
if (_keywordController.text.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('请输入关键词')));
return;
}
debugPrint('屏蔽关键词列表: $_blockedKeywords');
/*
发起http的post请求
请求头需要包含authorization
请求头content-type的值为application/json
请求的json对象格式为:
var jsonObject=[{"name":"social.post","limit":100,"page":1,"or":[{"field":"title","op":"$like","value":"%kasumi"},{"field":"tags","op":"$in","value":["kasumi"]}],"and":[{"field":"categories_id","op":"$nin","value":["137","257","347","264","764","931","97","101","365","534","115","361","335","389","336","425","367","456","930","85","2","382","78","252","154","1108","917","416","344","1107","337","74","99","281","93","230","454","166","175","397","452","414","285","928","766","1009","288","113","322","49","1106","426","81","758","332","104","88","364","1011","333","57","240","695","86","396","677","41","265","959","102","56","358","221","301","929","242","187","224","789","340","349","55","231","402","105","169","461","346","50","182","210","676","48","292","95","413","131","334","35","348","310","455","43","246","77","401","330","293","355","84","161","23","312","1012","359","771","87","124","163","395","1214","123","927","926","388","39","934","202","274","453","214","286","932","375","405","212","404","38","7","46","40","363","268","1110","75","350","262","218","394","92","37","247","128","675","933"]},{"field":"status","op":"$in","value":[1,9]}],"sort":[{"field":"create_time","order":"DESC"}]}];
这个json对象中仅仅用输入的关键词替换这个对象中的kasumi
*/
final keyword = _keywordController.text;
final apiUrl = _apiController.text;
final auth = authorizationController.text;
final requestBody = [
{
"name": "social.post",
"limit": 10,
"page": 1,
"or": [
{
"field": "title",
"op": "\$like",
"value": "%$keyword",
},
{
"field": "tags",
"op": "\$in",
"value": [keyword],
},
],
"and": [
{
"field": "categories_id",
"op": "\$nin",
"value": [
"137",
"257",
"347",
"264",
"764",
"931",
"97",
"101",
"365",
"534",
"115",
"361",
"335",
"389",
"336",
"425",
"367",
"456",
"930",
"85",
"2",
"382",
"78",
"252",
"154",
"1108",
"917",
"416",
"344",
"1107",
"337",
"74",
"99",
"281",
"93",
"230",
"454",
"166",
"175",
"397",
"452",
"414",
"285",
"928",
"766",
"1009",
"288",
"113",
"322",
"49",
"1106",
"426",
"81",
"758",
"332",
"104",
"88",
"364",
"1011",
"333",
"57",
"240",
"695",
"86",
"396",
"677",
"41",
"265",
"959",
"102",
"56",
"358",
"221",
"301",
"929",
"242",
"187",
"224",
"789",
"340",
"349",
"55",
"231",
"402",
"105",
"169",
"461",
"346",
"50",
"182",
"210",
"676",
"48",
"292",
"95",
"413",
"131",
"334",
"35",
"348",
"310",
"455",
"43",
"246",
"77",
"401",
"330",
"293",
"355",
"84",
"161",
"23",
"312",
"1012",
"359",
"771",
"87",
"124",
"163",
"395",
"1214",
"123",
"927",
"926",
"388",
"39",
"934",
"202",
"274",
"453",
"214",
"286",
"932",
"375",
"405",
"212",
"404",
"38",
"7",
"46",
"40",
"363",
"268",
"1110",
"75",
"350",
"262",
"218",
"394",
"92",
"37",
"247",
"128",
"675",
"933",
],
},
{
"field": "status",
"op": "\$in",
"value": [1, 9],
},
],
"sort": [
{"field": "create_time", "order": "DESC"},
],
},
];
await preRequest(
requestBody,
apiUrl,
auth,
_blockedKeywords,
);
debugPrint(
'搜索按钮被点击, api: ${_apiController.text}, 关键词: ${_keywordController.text}',
);
},
child: const Text('搜索'),
),
const SizedBox(width: 8),
BlockedKeywordsButton(onChanged: _onBlockedKeywordsChanged),
],
),
),
if (_syncMessage != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
),
const SizedBox(width: 8),
Text(
_syncMessage!,
style: const TextStyle(color: Colors.blue),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
const Text(
'搜索结果',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
],
),
),
const Divider(),
Row(
children: [
Expanded(
flex: 2,
child: _buildGridCell(
child: _buildHeaderCell('标题'),
rightBorder: true,
bottomBorder: true,
topBorder: true,
),
),
Expanded(
flex: 2,
child: _buildGridCell(
child: _buildHeaderCell('图片'),
rightBorder: true,
bottomBorder: true,
topBorder: true,
),
),
Expanded(
child: _buildGridCell(
child: _buildHeaderCell('操作'),
bottomBorder: true,
topBorder: true,
),
),
],
),
Flexible(
child: ListView.builder(
itemCount: _searchResults.length,
itemBuilder: (context, index) {
final item = _searchResults[index];
final title = item['title']?.toString() ?? '';
final imageBytes = _imageDataUris[index];
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 2,
child: _buildGridCell(
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(title),
),
),
rightBorder: true,
bottomBorder: true,
),
),
Expanded(
flex: 2,
child: _buildGridCell(
child: Center(
child: Padding(
padding: const EdgeInsets.all(4.0),
child: imageBytes != null
? Image.memory(
imageBytes,
fit: BoxFit.contain,
height: 80,
)
: const Text('加载中...'),
),
),
rightBorder: true,
bottomBorder: true,
),
),
Expanded(
child: _buildGridCell(
child: Center(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: const Size(60, 32),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
debugPrint('详情按钮被点击');
},
child: const Text('详情'),
),
),
bottomBorder: true,
),
),
],
),
);
},
),
),
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'共$_resultCount条记录,共${(_resultCount / 100).ceil()}页',
),
const SizedBox(width: 16),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
debugPrint('下一页按钮被点击');
},
child: const Text('下一页'),
),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
minimumSize: Size.zero,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
debugPrint('上一页按钮被点击');
},
child: const Text('上一页'),
),
],
),
),
),
],
),
],
),
);
}
}
接下回到正题,下载Android studio
https://developer.android.google.cn/studio?hl=zh-cn
我是在这个中文网下载的,其实更推荐英文官网下载,我这里因为某些原因就直接从这里下载了。
最终下载得到
android-studio-quail2-windows.exe
安装过程你最好翻墙,安装过程若遇到这个问题

可以先忽略
然后就是经过漫长的安装

找到sdk管理器,在New Project和Open下方的More Actions按钮点开后的下拉里面

确认第一个API级别为36的条目已被选中。
我这里默认选中的是36.1
那我选择36
然后就是有了两个选中的,然后点击apply就开始下载了。
那我接着又把36.1给去除勾选了。
切换到SDK工具标签页。
请确认以下SDK工具已被选中:

对比下我就是NDK (Side by side),Android SDK Command-line Tools (latest),CMake这三个没有安装
那么对应勾选安装即可。
同意安卓授权
flutter doctor --android-licenses
C:\Users\Administrator>flutter doctor --android-licenses
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Unable to locate Android SDK.
创建虚拟设备

有个这个提示的时候在右侧开启
Windows Hypervisor Platform is not enabled.

我这里是开启并重启Enable and Restart Now
重启后继续建立虚拟设备
然后还是有提示

所以我准备全部开启hyper-v
结果问题依旧
右键开始菜单 → Windows PowerShell (管理员)
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -All -NoRestart
Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform -All -NoRestart
无效
在powerShell中管理员执行
bcdedit /set hypervisorlaunchtype auto
以及
bcdedit /set vsmlaunchtype auto
立刻重启电脑,重启后重新打开管理员 PowerShell 输入
bcdedit,确认两项都是 auto。终于解决
继续创建虚拟设备,那我创建phone的pixel6吧


图形加速选择硬件提升渲染性能(Graphics acceleration)

再次经过漫长的下载镜像
然后试试运行模拟器。
验证你的设置
C:\Users\Administrator>flutter doctor
┌─────────────────────────────────────────────────────────┐
│ A new version of Flutter is available! │
│ │
│ To update to the latest version, run "flutter upgrade". │
└─────────────────────────────────────────────────────────┘
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.44.4, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN)
[√] Windows Version (11 专业版 64-bit, 25H2, 2009)
[X] Android toolchain - develop for Android devices
X Unable to locate Android SDK.
Install Android Studio from: https://developer.android.com/studio/index.html
On first launch it will assist you in installing the Android SDK components.
(or visit https://flutter.dev/to/windows-android-setup for detailed instructions).
If the Android SDK has been installed to a custom location, please use
`flutter config --android-sdk` to update to that location.
[X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe)
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[√] Visual Studio - develop Windows apps (Visual Studio Community 2026 18.7.3)
[√] Connected device (2 available)
[√] Network resources
! Doctor found issues in 2 categories.
这里可以看到sdk没有识别到“X Unable to locate Android SDK.”
C:\Users\Administrator>flutter config --android-sdk "D:\softwareInstall\androidSdk"
配置安卓sdk路径后再次执行
C:\Users\Administrator>flutter config --android-sdk "D:\softwareInstall\androidSdk"
Setting "android-sdk" value to "D:\softwareInstall\androidSdk".
You may need to restart any open editors for them to read new settings.
C:\Users\Administrator>flutter doctor
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.44.4, on Microsoft Windows [版本 10.0.26200.8875], locale zh-CN)
[√] Windows Version (11 专业版 64-bit, 25H2, 2009)
[!] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe)
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[√] Visual Studio - develop Windows apps (Visual Studio Community 2026 18.7.3)
[√] Connected device (3 available)
[√] Network resources
! Doctor found issues in 2 categories.
这次的提示就很简单,按提示运行
flutter doctor --android-licenses
然后按提示一一路按y同意即可
然后检查模拟器,可以看到已经有模拟器了
C:\Users\Administrator>flutter emulators && flutter devices
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
1 available emulator:
Id • Name • Manufacturer • Platform
Pixel_6 • Pixel 6 • Google • android
To run an emulator, run 'flutter emulators --launch <emulator id>'.
To create a new emulator, run 'flutter emulators --create [--name xyz]'.
You can find more information on managing emulators at the links below:
https://developer.android.com/studio/run/managing-avds
https://developer.android.com/studio/command-line/avdmanager
Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this source!
Found 3 connected devices:
sdk gphone16k x86 64 (mobile) • emulator-5554 • android-x64 • Android 17 (API 37) (emulator)
Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26200.8875]
Edge (web) • edge • web-javascript • Microsoft Edge 150.0.4078.65
Run "flutter emulators" to list and start any available device emulators.
If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for
connected devices with the "--device-timeout" flag. Visit https://flutter.dev/setup/ for troubleshooting tips.
这里可以看到有3个模拟器。
那么接下来我们把flutter运行到安卓
先在调试这里运行选择dart && flutter
然后就出现了我们刚才创建的模拟器pixel6

选择这个pixel6进行运行,选择之后并没有运行,要在“运行”=》“启动调试”,才会以pixel6的配置进行运行。
接下来控制台可能会报错各种,基本都是因为翻墙问题,那我这里给几个关键点
第一个是多看日志,尤其是这个日志,日志路径如:
C:\Users\Administrator\.gradle\daemon\9.1.0\daemon-32892.out.log
这里daemon-后面的是进程id,应该是gradle进程id
每次启动的时候进程id会变化的。
第二个就是我发现我一直卡在cmake上面,通过进程id的日志分析到是卡在cmake安装的。
那么我这里让ai给我查到的我的版本的cmake下载地址
https://dl.google.com/android/repository/cmake-3.22.1-windows.zip
然后我手动下载后解压放到路径如
D:\softwareInstall\androidSdk\cmake\3.22.1
也就是你的android的sdk路径
另外就是为了用国内镜像解决翻墙问题的几个文件
例如建立文件D:\flutterDemo\flutterDemo1\flutter_application_1\android\app\ build.gradle.kts:
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.flutter_application_1"
compileSdk = flutter.compileSdkVersion
ndkVersion = "30.0.15729638"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.flutter_application_1"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}
例如建立文件D:\flutterDemo\flutterDemo1\flutter_application_1\android\gradle\wrapper\ gradle-wrapper.properties:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-all.zip
例如建立文件D:\flutterDemo\flutterDemo1\flutter_application_1\android\ build.gradle.kts:
allprojects {
repositories {
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
maven { url = uri("https://maven.aliyun.com/repository/public") }
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
例如建立文件D:\flutterDemo\flutterDemo1\flutter_application_1\android\ settings.gradle.kts:
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/central") }
maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
maven { url = uri("https://maven.aliyun.com/repository/public") }
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
}
include(":app")
以及建立环境变量(最好是系统级别而非用户级别)
变量名FLUTTER_STORAGE_BASE_URL,变量值:https://storage.flutter-io.cn
关键字词:flutter,android,安卓,运行,开发,项目,模拟
上一篇:微信语音转换mp3实现
下一篇:返回列表