Restful API服务

231

我想创建一个服务来调用基于web的REST API。

我希望在应用初始化时启动该服务,然后能够请求URL并返回结果。同时,我想能够显示进度窗口或类似的东西。

目前我已经创建了一个使用IDL的服务,但是我读过某个地方说,你只需要跨应用程序通信时才需要使用它,所以我认为这些可以去掉,但不知道如何在没有它的情况下进行回调。另外,当我执行post(Config.getURL("login"), values)方法时,应用似乎会暂停一段时间(这很奇怪——我认为服务的想法是它在不同的线程上运行!)

目前我有一个服务,其中包含post和get http方法,还有几个AIDL文件(用于双向通信),一个ServiceManager处理启动、停止、绑定等等服务,并根据需要动态创建特定代码的Handler进行回调。

我不希望有人给我完整的代码库来工作,但一些指针将不胜感激。

完整代码:

public class RestfulAPIService extends Service  {

final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();

public void onStart(Intent intent, int startId) {
    super.onStart(intent, startId);
}
public IBinder onBind(Intent intent) {
    return binder;
}
public void onCreate() {
    super.onCreate();
}
public void onDestroy() {
    super.onDestroy();
    mCallbacks.kill();
}
private final IRestfulService.Stub binder = new IRestfulService.Stub() {
    public void doLogin(String username, String password) {

        Message msg = new Message();
        Bundle data = new Bundle();
        HashMap<String, String> values = new HashMap<String, String>();
        values.put("username", username);
        values.put("password", password);
        String result = post(Config.getURL("login"), values);
        data.putString("response", result);
        msg.setData(data);
        msg.what = Config.ACTION_LOGIN;
        mHandler.sendMessage(msg);
    }

    public void registerCallback(IRemoteServiceCallback cb) {
        if (cb != null)
            mCallbacks.register(cb);
    }
};

private final Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {

        // Broadcast to all clients the new value.
        final int N = mCallbacks.beginBroadcast();
        for (int i = 0; i < N; i++) {
            try {
                switch (msg.what) {
                case Config.ACTION_LOGIN:
                    mCallbacks.getBroadcastItem(i).userLogIn( msg.getData().getString("response"));
                    break;
                default:
                    super.handleMessage(msg);
                    return;

                }
            } catch (RemoteException e) {
            }
        }
        mCallbacks.finishBroadcast();
    }
    public String post(String url, HashMap<String, String> namePairs) {...}
    public String get(String url) {...}
};

几个 AIDL 文件:

package com.something.android

oneway interface IRemoteServiceCallback {
    void userLogIn(String result);
}
and
package com.something.android
import com.something.android.IRemoteServiceCallback;

interface IRestfulService {
    void doLogin(in String username, in String password);
    void registerCallback(IRemoteServiceCallback cb);
}

以及服务管理器:

public class ServiceManager {

    final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();
    public IRestfulService restfulService;
    private RestfulServiceConnection conn;
    private boolean started = false;
    private Context context;

    public ServiceManager(Context context) {
        this.context = context;
    }

    public void startService() {
        if (started) {
            Toast.makeText(context, "Service already started", Toast.LENGTH_SHORT).show();
        } else {
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.startService(i);
            started = true;
        }
    }

    public void stopService() {
        if (!started) {
            Toast.makeText(context, "Service not yet started", Toast.LENGTH_SHORT).show();
        } else {
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.stopService(i);
            started = false;
        }
    }

    public void bindService() {
        if (conn == null) {
            conn = new RestfulServiceConnection();
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.bindService(i, conn, Context.BIND_AUTO_CREATE);
        } else {
            Toast.makeText(context, "Cannot bind - service already bound", Toast.LENGTH_SHORT).show();
        }
    }

    protected void destroy() {
        releaseService();
    }

    private void releaseService() {
        if (conn != null) {
            context.unbindService(conn);
            conn = null;
            Log.d(LOG_TAG, "unbindService()");
        } else {
            Toast.makeText(context, "Cannot unbind - service not bound", Toast.LENGTH_SHORT).show();
        }
    }

    class RestfulServiceConnection implements ServiceConnection {
        public void onServiceConnected(ComponentName className, IBinder boundService) {
            restfulService = IRestfulService.Stub.asInterface((IBinder) boundService);
            try {
            restfulService.registerCallback(mCallback);
            } catch (RemoteException e) {}
        }

        public void onServiceDisconnected(ComponentName className) {
            restfulService = null;
        }
    };

    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        public void userLogIn(String result) throws RemoteException {
            mHandler.sendMessage(mHandler.obtainMessage(Config.ACTION_LOGIN, result));

        }
    };

    private Handler mHandler;

    public void setHandler(Handler handler) {
        mHandler = handler;
    }
}

服务初始化和绑定:

// this I'm calling on app onCreate
servicemanager = new ServiceManager(this);
servicemanager.startService();
servicemanager.bindService();
application = (ApplicationState)this.getApplication();
application.setServiceManager(servicemanager);

服务函数调用:

// this lot i'm calling as required - in this example for login
progressDialog = new ProgressDialog(Login.this);
progressDialog.setMessage("Logging you in...");
progressDialog.show();

application = (ApplicationState) getApplication();
servicemanager = application.getServiceManager();
servicemanager.setHandler(mHandler);

try {
    servicemanager.restfulService.doLogin(args[0], args[1]);
} catch (RemoteException e) {
    e.printStackTrace();
}

...later in the same file...

Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {

        switch (msg.what) {
        case Config.ACTION_LOGIN:

            if (progressDialog.isShowing()) {
                progressDialog.dismiss();
            }

            try {
                ...process login results...
                }
            } catch (JSONException e) {
                Log.e("JSON", "There was an error parsing the JSON", e);
            }
            break;
        default:
            super.handleMessage(msg);
        }

    }

};

5
这对于学习Android REST客户端实现的人可能非常有帮助。Dobjanschi的演讲已转录成PDF格式:https://drive.google.com/file/d/0B2dn_3573C3RdlVpU2JBWXdSb3c/edit?usp=sharing - Kay Zed
由于许多人推荐Virgil Dobjanschi的演示,并且IO 2010的链接现在已经失效,因此这里提供YT视频的直接链接:http://www.youtube.com/watch?v=xHXn3Kg2IQE。 - Jose_GD
11个回答

286
如果您的服务将成为应用程序的一部分,则会使其变得比需要的复杂得多。由于您有一个简单的用例,即从RESTful Web服务获取一些数据,因此应该研究ResultReceiverIntentService
当您想执行某些操作时,此服务+ ResultReceiver模式通过使用startService()启动或绑定到服务。您可以通过Intent中的extras指定要执行的操作并传递您的ResultReceiver(活动)。
在服务中,您实现onHandleIntent以执行在Intent中指定的操作。当操作完成时,您使用传入的ResultReceiver向Activity send消息,此时将调用onReceiveResult
例如,您想从Web服务中提取一些数据。
  1. 创建意图并调用startService方法。
  2. 服务中的操作开始并向活动发送一条消息,表示它已经启动了。
  3. 活动处理此消息并显示进度。
  4. 服务完成操作并将一些数据发送回您的活动。
  5. 您的活动处理数据并将其放入列表视图中。
  6. 服务向您发送一条消息,表示它已完成,并将自身停止。
  7. 活动收到完成消息并隐藏进度对话框。

我知道您提到不希望有代码,但开源Google I/O 2010应用程序就是以我描述的这种方式使用服务的。

更新后添加示例代码:

活动。

public class HomeActivity extends Activity implements MyResultReceiver.Receiver {

    public MyResultReceiver mReceiver;

    public void onCreate(Bundle savedInstanceState) {
        mReceiver = new MyResultReceiver(new Handler());
        mReceiver.setReceiver(this);
        ...
        final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, QueryService.class);
        intent.putExtra("receiver", mReceiver);
        intent.putExtra("command", "query");
        startService(intent);
    }

    public void onPause() {
        mReceiver.setReceiver(null); // clear receiver so no leaks.
    }

    public void onReceiveResult(int resultCode, Bundle resultData) {
        switch (resultCode) {
        case RUNNING:
            //show progress
            break;
        case FINISHED:
            List results = resultData.getParcelableList("results");
            // do something interesting
            // hide progress
            break;
        case ERROR:
            // handle the error;
            break;
    }
}

服务:

public class QueryService extends IntentService {
    protected void onHandleIntent(Intent intent) {
        final ResultReceiver receiver = intent.getParcelableExtra("receiver");
        String command = intent.getStringExtra("command");
        Bundle b = new Bundle();
        if(command.equals("query") {
            receiver.send(STATUS_RUNNING, Bundle.EMPTY);
            try {
                // get some data or something           
                b.putParcelableArrayList("results", results);
                receiver.send(STATUS_FINISHED, b)
            } catch(Exception e) {
                b.putString(Intent.EXTRA_TEXT, e.toString());
                receiver.send(STATUS_ERROR, b);
            }    
        }
    }
}

ResultReceiver扩展 - 编辑后实现MyResultReceiver.Receiver

public class MyResultReceiver implements ResultReceiver {
    private Receiver mReceiver;

    public MyResultReceiver(Handler handler) {
        super(handler);
    }

    public void setReceiver(Receiver receiver) {
        mReceiver = receiver;
    }

    public interface Receiver {
        public void onReceiveResult(int resultCode, Bundle resultData);
    }

    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        if (mReceiver != null) {
            mReceiver.onReceiveResult(resultCode, resultData);
        }
    }
}

33
答案的一个小补充:在onPause方法中执行mReceiver.setReceiver(null);后,你应该在onResume方法中执行mReceiver.setReceiver(this);。否则,如果您的活动在没有重新创建的情况下恢复,则可能无法接收事件。 - Vincent Mimoun-Prat
7
文档中不是说您不必调用stopSelf,因为IntentService会代替您执行该操作吗? - Mikael Ohlson
2
@MikaelOhlson 正确,如果您子类化了IntentService,则不应调用stopSelf,因为这样做会丢失对同一IntentService的任何挂起请求。 - quietmint
1
IntentService会在任务完成后自行终止,因此this.stopSelf()是不必要的。 - Euporie
1
如果请求必须完成,AsyncTask将不足够-如果应用程序被杀死(例如低内存并且用户接听电话),则请求不一定会完成。如果请求是从服务中运行的,则在应用程序移至后台时,进程不太可能被终止。服务进程比后台进程具有更高的优先级。对于拉取用于UI显示的数据,请求是否不完整并不重要,但是如果您要从应用程序发布更新到服务器,则会因为在应用程序被杀死时可能会丢失数据。 - snafu109
显示剩余24条评论

18

开发Android REST客户端应用程序 对我来说是一份非常棒的资源。演讲者没有展示任何代码,而是涉及了设计考虑和技术,讲解如何在Android中构建坚固的Rest Api。如果你喜欢播客或不喜欢播客,我推荐至少听一次这个内容,但就我个人而言,我已经听了4到5遍,并且可能还会再听一次。

开发Android REST客户端应用程序
作者:Virgil Dobjanschi
描述:

本课程将介绍在Android平台上开发RESTful应用程序的架构考虑因素。它着重于设计模式、平台集成和针对Android平台的性能问题。

在我的API第一个版本中我真的没考虑到那么多问题,所以我必须进行重构。


4
这包含了你在刚开始时从未考虑过的种种考虑因素。 - Thomas Ahle
是的,我的第一次尝试开发Rest客户端几乎完全符合他所描述的不应该做的事情(幸运的是,在我观看这个视频之前,我意识到其中很多都是错误的)。我对此有点儿好笑。 - Terrance
我已经看过这个视频不止一次,并正在实现第二种模式。我的问题是,我需要在复杂的数据库模型中使用事务来更新本地数据,以便从服务器获取新鲜数据,但ContentProvider接口没有提供这样的方法。你有什么建议吗,Terrance? - Flávio Faria
2
请注意,Dobjanschi关于HttpClient的评论已经过时。请参见https://dev59.com/FGkw5IYBdhLWcg3w8O-o#15524143。 - Donal Lafferty
是的,现在更倾向于使用 HttpURLConnection。此外,随着 Android 6.0 的发布,官方已经删除了对 Apache HTTP 客户端的支持(officially been removed)。 - RonR

16

当我点击post(Config.getURL("login"), values)时,应用程序似乎会暂停一段时间(看起来很奇怪 - 我认为服务的想法是在不同的线程上运行!)

不,你必须自己创建一个线程,本地(Local)服务默认在UI线程上运行。


11

6

5

我想指出一下一个独立的类,它包含所有功能。

http://github.com/StlTenny/RestService

它以非阻塞方式执行请求,并在易于实现的处理程序中返回结果。甚至还带有一个示例实现。


4
请注意,Robby Pond的解决方案存在缺陷:这样只允许一次调用一个api,因为IntentService只能处理一个intent。通常您想同时执行多个api调用。如果您要这样做,必须扩展Service而不是IntentService,并创建自己的线程。

1
您仍然可以通过将webservice api调用委托给IntentService派生类的成员变量中呈现的执行器线程服务来对IntentService进行多次调用。 - Viren

4
假设我想在事件 - 按钮的 onItemClicked() 上启动服务。在这种情况下,接收机制将不起作用,因为:-
a) 我从 onItemClicked() 将接收器(作为 Intent extra)传递给了服务
b) 活动移到后台。 在 onPause() 中,我将 ResultReceiver 中的接收器引用设置为 null,以避免泄漏活动。
c) 活动被销毁。
d) 活动重新创建。 然而,在此时,服务将无法回调到活动,因为该接收器引用已丢失。
在这种情况下,有限广播或 PendingIntent 机制似乎更有用- 参见Notify activity from service

1
你所说的有问题。当 Activity 转到后台时,它并不会被销毁,因此接收器仍然存在,Activity 上下文也存在。 - DArkO
@DArkO 当Activity被暂停或停止时,它可能会在Android系统低内存情况下被杀死。请参考Activity生命周期 - jk7

2

当我调用post(Config.getURL("login"), values)时,应用程序似乎会暂停一段时间(看起来很奇怪 - 我以为服务的想法是它在不同的线程上运行!)

在这种情况下,最好使用asynctask,它在不同的线程上运行,并在完成后将结果返回到ui线程。


1
罗比提供了很好的答案,但我可以看到你还在寻找更多信息。我以一种简单但错误的方式实现了REST API调用。直到我观看了这个Google I/O video,我才明白我错在哪里。它并不像组合一个带有HttpUrlConnection get/put调用的AsyncTask那么简单。

链接已失效。这是更新后的链接 - Google I/O 2010 - Android REST客户端应用程序 - zim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接