使用Retrofit和Node.JS实现Android图像上传
在本教程中,我们将创建一个Android应用程序,该应用程序使用Retrofit MultiPart Request将图像上传到本地服务器。
我们将首先使用Node JS创建一个简单的服务器。
让我们开始吧。
设置Node.JS服务器
让我们建立一个简单的Node JS localhost服务器,其中我们可以上传文件。
首先,为节点js服务器创建一个单独的目录。
我们将其命名为jdserver
在目录内,我们将使用npm
安装以下软件包。
确保已安装node
和npm
。
mkdir jdserver cd jdserver npm install express npm install multer
Multer:是图像上传库。
它处理从请求中获取" formdata"。
Express是一种流行的Web框架。
让我们创建一个子文件夹"上载",通过执行" mkdir uploads"来包含上载的图像。
在jdserver
目录中,我们将创建一个multipart.js
文件,其中包含服务器设置和上传图像的代码:
var express = require("express"); var app = express(); var multer, storage, path, crypto; multer = require('multer') path = require('path'); crypto = require('crypto'); var form = "<!DOCTYPE HTML><html><body>" + "<form method='post' action='/upload' enctype='multipart/form-data'>" + "<input type='file' name='upload'" + "<input type='submit' </form>" + "</body></html>"; app.get('/', function (req, res){ res.writeHead(200, {'Content-Type': 'text/html' }); res.end(form); }); //Include the node file module var fs = require('fs'); storage = multer.diskStorage({ destination: './uploads/', filename: function(req, file, cb) { return crypto.pseudoRandomBytes(16, function(err, raw) { if (err) { return cb(err); } return cb(null, "" + (raw.toString('hex')) + (path.extname(file.originalname))); }); } }); //Post files app.post( "/upload", multer({ storage: storage }).single('upload'), function(req, res) { console.log(req.file); console.log(req.body); res.redirect("/uploads/" + req.file.filename); console.log(req.file.filename); return res.status(200).end(); }); app.get('/uploads/:upload', function (req, res){ file = req.params.upload; console.log(req.params.upload); var img = fs.readFileSync(__dirname + "/uploads/" + file); res.writeHead(200, {'Content-Type': 'image/png' }); res.end(img, 'binary'); }); app.listen(3000);
使用加密软件包更改上传文件名,并将其存储在上载目录中。
服务器将在本地主机上的3000端口上运行。
为了启动服务器,请执行以下操作:终端上jdserver目录中的node multipart.js。
当您在网络浏览器(127.0.0.1:3000)中打开本地主机时,应该看到以下内容:
因此,我们能够将图像上传到服务器。
它被保存在" uploads"目录中,如下所示:
为了测试POST是否有效,我们可以在终端中安装httpie。
一种方法是使用自制程序。
brew install httpie
要测试用于多部分图像上传的POST,我们可以在终端上运行以下命令:
http -f POST 127.0.0.1:3000/upload name='upload' [email protected]
如果存在于终端的当前目录中,则将上传wallpaper.png
。
这将登录到终端:
现在,所有操作都在服务器端进行了设置,让我们在Android应用程序中实施相同的操作。
改造多部分上传
通过网络呼叫发送图像不同于发送文本/纯文本或者x-www-urlencoded请求,它们本质上分别是文本/键值对。
为了发送图像,我们需要创建一个MultiPartRequest。
在Retrofit中,我们需要使用MultipartBody.Part和RequestBody来上传图像。
MultiPartBody.Part用于传递文件,而RequestBody用于传递纯文本。
为了使用MultiPart
,我们需要在@ Multipart
的基础上加注POST请求。
在以下部分中,我们将使用MultiPart将图像上传到本地托管的Node JS服务器。
项目结构
build.gradle包括以下依赖项:
implementation 'com.android.support:design:28.0.0' implementation 'com.squareup.retrofit2:retrofit:2.4.0'
AndroidManifest.xml如下所示:
我们已经添加了相关权限,自Android Nougat开始,需要FileProvider来获取图像路径。
代码
下面给出了activity_main.xml布局的代码:
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android" xmlns:app="https://schemas.android.com/apk/res-auto" xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <RelativeLayout android:id="@+id/content_main" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <TextView android:id="@+id/textView" android:layout_centerHorizontal="true" android:textAppearance="@style/TextAppearance.AppCompat.Display1" android:layout_width="wrap_content" android:layout_height="wrap_content" <ImageView android:id="@+id/imageView" android:layout_width="250dp" android:layout_height="250dp" android:layout_centerInParent="true" android:adjustViewBounds="true" android:scaleType="centerCrop" </RelativeLayout> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" app:srcCompat="@android:drawable/ic_menu_camera" <android.support.design.widget.FloatingActionButton android:id="@+id/fabUpload" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:layout_margin="16dp" app:srcCompat="@android:drawable/ic_menu_upload" </android.support.design.widget.CoordinatorLayout>
ApiService.java
package com.theitroad.androiduploadimageretrofitnodejs; import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.Part; interface ApiService { @Multipart @POST("/upload") Call<ResponseBody> postImage(@Part MultipartBody.Part image, @Part("upload") RequestBody name); }
在" @Part"批注名称中,我们必须指定与先前js文件中定义的密钥相同的密钥,即" upload"与File一起。
MainActivity.java
package com.theitroad.androiduploadimageretrofitnodejs; import android.annotation.TargetApi; import android.app.Activity; import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Parcelable; import android.provider.MediaStore; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.RequestBody; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import static android.Manifest.permission.CAMERA; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class MainActivity extends AppCompatActivity implements View.OnClickListener { ApiService apiService; Uri picUri; private ArrayList<String> permissionsToRequest; private ArrayList<String> permissionsRejected = new ArrayList<>(); private ArrayList<String> permissions = new ArrayList<>(); private final static int ALL_PERMISSIONS_RESULT = 107; private final static int IMAGE_RESULT = 200; FloatingActionButton fabCamera, fabUpload; Bitmap mBitmap; TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fabCamera = findViewById(R.id.fab); fabUpload = findViewById(R.id.fabUpload); textView = findViewById(R.id.textView); fabCamera.setOnClickListener(this); fabUpload.setOnClickListener(this); askPermissions(); initRetrofitClient(); } private void askPermissions() { permissions.add(CAMERA); permissions.add(WRITE_EXTERNAL_STORAGE); permissions.add(READ_EXTERNAL_STORAGE); permissionsToRequest = findUnAskedPermissions(permissions); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (permissionsToRequest.size() > 0) requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT); } } private void initRetrofitClient() { OkHttpClient client = new OkHttpClient.Builder().build(); apiService = new Retrofit.Builder().baseUrl("https://192.168.88.65:3000").client(client).build().create(ApiService.class); } public Intent getPickImageChooserIntent() { Uri outputFileUri = getCaptureImageOutputUri(); List<Intent> allIntents = new ArrayList<>(); PackageManager packageManager = getPackageManager(); Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); List<ResolveInfo> listCam = packageManager.queryIntentActivities(captureIntent, 0); for (ResolveInfo res : listCam) { Intent intent = new Intent(captureIntent); intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); intent.setPackage(res.activityInfo.packageName); if (outputFileUri != null) { intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); } allIntents.add(intent); } Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT); galleryIntent.setType("image/*"); List<ResolveInfo> listGallery = packageManager.queryIntentActivities(galleryIntent, 0); for (ResolveInfo res : listGallery) { Intent intent = new Intent(galleryIntent); intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name)); intent.setPackage(res.activityInfo.packageName); allIntents.add(intent); } Intent mainIntent = allIntents.get(allIntents.size() - 1); for (Intent intent : allIntents) { if (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity")) { mainIntent = intent; break; } } allIntents.remove(mainIntent); Intent chooserIntent = Intent.createChooser(mainIntent, "Select source"); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()])); return chooserIntent; } private Uri getCaptureImageOutputUri() { Uri outputFileUri = null; File getImage = getExternalFilesDir(""); if (getImage != null) { outputFileUri = Uri.fromFile(new File(getImage.getPath(), "profile.png")); } return outputFileUri; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { ImageView imageView = findViewById(R.id.imageView); if (requestCode == IMAGE_RESULT) { String filePath = getImageFilePath(data); if (filePath != null) { mBitmap = BitmapFactory.decodeFile(filePath); imageView.setImageBitmap(mBitmap); } } } } private String getImageFromFilePath(Intent data) { boolean isCamera = data == null || data.getData() == null; if (isCamera) return getCaptureImageOutputUri().getPath(); else return getPathFromURI(data.getData()); } public String getImageFilePath(Intent data) { return getImageFromFilePath(data); } private String getPathFromURI(Uri contentUri) { String[] proj = {MediaStore.Audio.Media.DATA}; Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable("pic_uri", picUri); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); picUri = savedInstanceState.getParcelable("pic_uri"); } private ArrayList<String> findUnAskedPermissions(ArrayList<String> wanted) { ArrayList<String> result = new ArrayList<String>(); for (String perm : wanted) { if (!hasPermission(perm)) { result.add(perm); } } return result; } private boolean hasPermission(String permission) { if (canMakeSmores()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED); } } return true; } private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) { new AlertDialog.Builder(this) .setMessage(message) .setPositiveButton("OK", okListener) .setNegativeButton("Cancel", null) .create() .show(); } private boolean canMakeSmores() { return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1); } @TargetApi(Build.VERSION_CODES.M) @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch (requestCode) { case ALL_PERMISSIONS_RESULT: for (String perms : permissionsToRequest) { if (!hasPermission(perms)) { permissionsRejected.add(perms); } } if (permissionsRejected.size() > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) { showMessageOKCancel("These permissions are mandatory for the application. Please allow access.", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT); } }); return; } } } break; } } private void multipartImageUpload() { try { File filesDir = getApplicationContext().getFilesDir(); File file = new File(filesDir, "image" + ".png"); ByteArrayOutputStream bos = new ByteArrayOutputStream(); mBitmap.compress(Bitmap.CompressFormat.PNG, 0, bos); byte[] bitmapdata = bos.toByteArray(); FileOutputStream fos = new FileOutputStream(file); fos.write(bitmapdata); fos.flush(); fos.close(); RequestBody reqFile = RequestBody.create(MediaType.parse("image/*"), file); MultipartBody.Part body = MultipartBody.Part.createFormData("upload", file.getName(), reqFile); RequestBody name = RequestBody.create(MediaType.parse("text/plain"), "upload"); Call<ResponseBody> req = apiService.postImage(body, name); req.enqueue(new Callback<ResponseBody>() { @Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { if (response.code() == 200) { textView.setText("Uploaded Successfully!"); textView.setTextColor(Color.BLUE); } Toast.makeText(getApplicationContext(), response.code() + " ", Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<ResponseBody> call, Throwable t) { textView.setText("Uploaded Failed!"); textView.setTextColor(Color.RED); Toast.makeText(getApplicationContext(), "Request failed", Toast.LENGTH_SHORT).show(); t.printStackTrace(); } }); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } @Override public void onClick(View view) { switch (view.getId()) { case R.id.fab: startActivityForResult(getPickImageChooserIntent(), IMAGE_RESULT); break; case R.id.fabUpload: if (mBitmap != null) multipartImageUpload(); else { Toast.makeText(getApplicationContext(), "Bitmap is null. Try again", Toast.LENGTH_SHORT).show(); } break; } } }
在上面的代码中,我们需要先获得运行时权限。
从摄像机/图库图像检索到的图像位图最终在多部分改造请求中传递到服务器。
如果您在通过USB连接的手机上运行应用程序,则基本网址与系统的IP地址相同。
在上面的代码中,基本URL是我当前的IP。
运行项目时,需要将其更改为您自己的。
确保服务器已启动并正在运行!