如何用Android Volley库向Django 2.0服务器上传图片?
嘿,你已经把Django后端的核心模块(模型、序列化器、视图、URL)都搭好了,接下来用Volley实现图片上传其实很清晰,我来给你拆解具体步骤:
用Volley实现Android到Django的图片上传方案
Android端实现(Volley自定义Multipart请求)
Volley默认的Request不支持multipart/form-data(文件上传的标准格式),所以我们需要先自定义一个MultipartRequest类来同时处理表单参数和图片文件:
import com.android.volley.AuthFailureError; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.HttpHeaderParser; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Map; public class MultipartRequest extends Request<String> { private final Response.Listener<String> mListener; private final Map<String, String> mParams; private final Map<String, byte[]> mFileParams; private final String mBoundary = "apiclient-" + System.currentTimeMillis(); private final String mTwoHyphens = "--"; private final String mLineEnd = "\r\n"; public MultipartRequest(String url, Map<String, String> params, Map<String, byte[]> fileParams, Response.Listener<String> listener, Response.ErrorListener errorListener) { super(Method.POST, url, errorListener); this.mListener = listener; this.mParams = params; this.mFileParams = fileParams; } @Override public String getBodyContentType() { return "multipart/form-data; boundary=" + mBoundary; } @Override public byte[] getBody() throws AuthFailureError { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); try { // 添加表单参数(发送方、接收方手机号) if (mParams != null && !mParams.isEmpty()) { for (Map.Entry<String, String> entry : mParams.entrySet()) { buildTextPart(dos, entry.getKey(), entry.getValue()); } } // 添加图片文件 if (mFileParams != null && !mFileParams.isEmpty()) { for (Map.Entry<String, byte[]> entry : mFileParams.entrySet()) { buildFilePart(dos, entry.getKey(), entry.getValue()); } } // 结束请求标记 dos.writeBytes(mTwoHyphens + mBoundary + mTwoHyphens + mLineEnd); return bos.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } private void buildTextPart(DataOutputStream dos, String paramName, String paramValue) throws IOException { dos.writeBytes(mTwoHyphens + mBoundary + mLineEnd); dos.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"" + mLineEnd); dos.writeBytes(mLineEnd); dos.writeBytes(paramValue + mLineEnd); } private void buildFilePart(DataOutputStream dos, String paramName, byte[] fileBytes) throws IOException { dos.writeBytes(mTwoHyphens + mBoundary + mLineEnd); dos.writeBytes("Content-Disposition: form-data; name=\"" + paramName + "\"; filename=\"uploaded_image.jpg\"" + mLineEnd); dos.writeBytes("Content-Type: image/jpeg" + mLineEnd); // 根据实际图片类型调整(比如png) dos.writeBytes(mLineEnd); ByteArrayInputStream bis = new ByteArrayInputStream(fileBytes); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = bis.read(buffer)) != -1) { dos.write(buffer, 0, bytesRead); } dos.writeBytes(mLineEnd); } @Override protected Response<String> parseNetworkResponse(NetworkResponse response) { String responseString = ""; if (response != null) { try { responseString = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return Response.success(responseString, HttpHeaderParser.parseCacheHeaders(response)); } @Override protected void deliverResponse(String response) { mListener.onResponse(response); } }
调用自定义请求上传图片
假设你已经从相册/相机获取到了图片的Uri,接下来把图片转成字节流,搭配表单参数发送请求:
// 1. 准备表单参数(发送方、接收方手机号) Map<String, String> formParams = new HashMap<>(); formParams.put("sender_phone", "138xxxx1234"); // 替换为实际发送方手机号 formParams.put("receiver_phone", "139xxxx5678"); // 替换为实际接收方手机号 // 2. 将图片Uri转为字节数组(工具方法) Uri imageUri = ...; // 你的图片Uri byte[] imageBytes = null; try { imageBytes = getImageBytesFromUri(imageUri); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "读取图片失败", Toast.LENGTH_SHORT).show(); return; } Map<String, byte[]> fileParams = new HashMap<>(); fileParams.put("image", imageBytes); // 这个key要和Django视图接收的字段名一致 // 3. 发送Volley请求 String uploadApiUrl = "http://你的服务器IP:端口/api/upload-image/"; // 替换为你的实际API地址 MultipartRequest uploadRequest = new MultipartRequest( uploadApiUrl, formParams, fileParams, response -> { // 上传成功回调 Log.d("UploadSuccess", "服务器返回:" + response); Toast.makeText(this, "图片上传完成", Toast.LENGTH_SHORT).show(); }, error -> { // 上传失败回调 Log.e("UploadError", "错误信息:" + error.getMessage()); Toast.makeText(this, "上传失败,请检查网络", Toast.LENGTH_SHORT).show(); } ); // 添加到Volley请求队列 Volley.newRequestQueue(this).add(uploadRequest);
图片Uri转字节数组的工具方法
private byte[] getImageBytesFromUri(Uri uri) throws IOException { InputStream inputStream = getContentResolver().openInputStream(uri); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } inputStream.close(); return outputStream.toByteArray(); }
Django端配合调整
确保你的视图能正确处理multipart/form-data请求,接收图片并保存到指定目录,同时创建Exchange记录:
from django.http import JsonResponse from .models import Exchange from .serializers import ExchangeSerializer import os from django.conf import settings def upload_image(request): if request.method == 'POST': # 获取表单参数和图片文件 sender_phone = request.POST.get('sender_phone') receiver_phone = request.POST.get('receiver_phone') image_file = request.FILES.get('image') # 参数校验 if not all([sender_phone, receiver_phone, image_file]): return JsonResponse({'error': '缺少必要参数'}, status=400) # 保存图片到服务器指定目录(比如media/images下) save_dir = os.path.join(settings.MEDIA_ROOT, 'images') os.makedirs(save_dir, exist_ok=True) relative_path = os.path.join('images', image_file.name) full_save_path = os.path.join(save_dir, image_file.name) with open(full_save_path, 'wb+') as destination: for chunk in image_file.chunks(): destination.write(chunk) # 创建Exchange记录 exchange = Exchange.objects.create( sender_phone=sender_phone, receiver_phone=receiver_phone, img=relative_path # 存储相对路径,方便后续访问 ) serializer = ExchangeSerializer(exchange) return JsonResponse(serializer.data, status=201) return JsonResponse({'error': '仅支持POST请求'}, status=405)
另外要确保Django配置了媒体文件路径:
# settings.py MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/'
并在主URL中添加媒体文件访问路由(开发阶段使用):
# urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # 你的其他路由 path('api/upload-image/', views.upload_image, name='upload-image'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
关键注意事项
- 权限配置:在AndroidManifest.xml中添加必要权限,Android 6.0+还需动态申请存储权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 旧版本Android --> - 图片压缩:大尺寸图片会导致上传缓慢甚至失败,建议上传前对图片进行压缩(缩小分辨率或降低质量)。
- 服务器地址:Android模拟器访问本地Django服务器要用
10.0.2.2代替localhost,真机需确保和服务器在同一局域网或服务器有公网IP。
内容的提问来源于stack exchange,提问作者mohamadrezach




