首页 > 解决方案 > 如何在 Android Widget (SDK 29+) 中运行 http 请求并获得响应

问题描述

我正在构建一个简单的信息显示小部件。所有离线的东西都可以正常工作。如何处理异步 Web 请求。(我可以在正常活动中处理一个罚款)

我正在尝试使用基于另一个 stackoverflow 答案的处理程序,但小部件挂起而没有任何明显的错误。

我已经尽可能地简化了我的代码以使其更容易发布,并且它使用一个任何人都可以使用的简单比特币价格指数作为一个简单的测试场景(我的应用程序会更复杂)

显现:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="co.uk.kicktechnic.lcdclockwidget">

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.Light"><!--android:theme="@style/Theme.LCDClockWidget"-->

        <activity
            android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidgetGrantSettingsActivity"
            android:label="@string/title_activity_initial_grant_settings">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidget">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/l_c_d_clock_widget_info" />
        </receiver>

        <activity android:name="uk.co.kicktechnic.lcdclockwidget.LCDClockWidgetConfigureActivity">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
            </intent-filter>
        </activity>
    </application>

</manifest>

代码:

public class LCDClockWidget extends AppWidgetProvider {
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
        //views.setTextViewText(R.id.appwidget_sun, String.valueOf(appWidgetId)); //original - show widget ID

        Intent intentUpdate = new Intent(context, LCDClockWidget.class);
        intentUpdate.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);

        views.setTextViewText(R.id.appwidget_moon, "set in updateAppWidget");
        
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }


    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);

            loadBTC(context, appWidgetManager, appWidgetId);

            views.setTextViewText(R.id.appwidget_moon, "set in onUpdate");
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
    
    static void loadBTC(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
        try {
            Request request = new Request.Builder()
                    .url("https://api.coindesk.com/v1/bpi/currentprice.json")
                    .build();
            OkHttpClient okHttpClient = new OkHttpClient();
            okHttpClient.newCall(request).enqueue(new Callback() {

                @Override
                public void onFailure(Call call, IOException e) {
                    Toast.makeText(context, "Error during BPI loading : "
                            + e.getMessage(), Toast.LENGTH_SHORT).show();
                }

                @Override
                public void onResponse(Call call, Response response)
                        throws IOException {
                    final String body = response.body().string();

                    //original code - works in a normal activity
                    //runOnUiThread(new Runnable() {
                    //    @Override
                    //    public void run() {
                    //        parseBpiResponse(body, context);
                    //    }
                    //});

                    //based on a stackoverflow answer
                    Handler mHandler = new Handler();
                    mHandler.post(new Runnable(){
                        public void run() {
                            parseBpiResponse(body, context);
                        }
                    });
                }
            });
        } catch (Exception e) {
            //Toast.makeText(context, "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }

    static void parseBpiResponse(String body, Context context) {
        try {
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);

            StringBuilder builder = new StringBuilder();

            JSONObject jsonObject = new JSONObject(body);
            //JSONObject timeObject = jsonObject.getJSONObject("time");
            //builder.append(timeObject.getString("updated")).append("\n\n");

            JSONObject bpiObject = jsonObject.getJSONObject("bpi");
            JSONObject usdObject = bpiObject.getJSONObject("USD");
            builder.append("$ ").append(usdObject.getString("rate")).append("\n");

            JSONObject gbpObject = bpiObject.getJSONObject("GBP");
            builder.append("£ ").append(gbpObject.getString("rate")).append("\n");

            JSONObject euroObject = bpiObject.getJSONObject("EUR");
            builder.append("€ ").append(euroObject.getString("rate")).append("\n");

            views.setTextViewText(R.id.appwidget_btc, builder.toString());
        } catch (Exception e) {
        }
    }
}

编辑1:

遵循建议并使用文章:https ://medium.com/@sambhaji2134/jobintentservice-android-example-7f58bd2720bf

我已经添加了这个(并暂时删除了任何网络/BTC 内容)

@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    for (int appWidgetId : appWidgetIds) {
        updateAppWidget(context, appWidgetManager, appWidgetId);
        Toast.makeText(context, "Widget has been updated! ", Toast.LENGTH_SHORT).show();

        mServiceResultReceiver = new BTCServiceResultReceiver(new Handler());
        mServiceResultReceiver.setReceiver((BTCServiceResultReceiver.Receiver) this);
        showDataFromBackground(context, mServiceResultReceiver);
    }
}

public void showData(String data, Context context) {
    //mTextView.setText(String.format("%s\n%s", mTextView.getText(), data));

    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.l_c_d_clock_widget);
    views.setTextViewText(R.id.appwidget_btc, data);
}

public void onReceiveResult(int resultCode, Bundle resultData, Context context) {
    final int SHOW_RESULT = 123;

    switch (resultCode) {
        case SHOW_RESULT:
            if (resultData != null) {
                showData(resultData.getString("data"), context);
            }
            break;
    }
}

根据文章,我将 ServiceResultReciever 和 JobIntentService 破解在一起:

public class BTCServiceResultReceiver extends ResultReceiver {
private Receiver mReceiver;

/**
 * Create a new ResultReceive to receive results.  Your
 * {@link #onReceiveResult} method will be called from the thread running
 * <var>handler</var> if given, or from an arbitrary thread if null.
 *
 * @param handler the handler object
 */

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

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

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

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


public class BTCJobIntentService extends JobIntentService {
private static final String TAG = JobService.class.getSimpleName();
public static final String RECEIVER = "receiver";
public static final int SHOW_RESULT = 123;
/**
 * Result receiver object to send results
 */
private ResultReceiver mResultReceiver;
/**
 * Unique job ID for this service.
 */
static final int DOWNLOAD_JOB_ID = 1000;
/**
 * Actions download
 */
private static final String ACTION_DOWNLOAD = "action.DOWNLOAD_DATA";

static void enqueueWork(Context context, BTCServiceResultReceiver workerResultReceiver) {
    Intent intent = new Intent(context, JobService.class);
    intent.putExtra(RECEIVER, workerResultReceiver);
    intent.setAction(ACTION_DOWNLOAD);
    enqueueWork(context, JobService.class, DOWNLOAD_JOB_ID, intent);
}

@Override
protected void onHandleWork(@NonNull Intent intent) {
    // We have received work to do.  The system or framework is already
    // holding a wake lock for us at this point, so we can just go.
    Log.i("SimpleJobIntentService", "Executing work: " + intent);
    String label = intent.getStringExtra("label");
    if (label == null) {
        label = intent.toString();
    }
    toast("Executing: " + label);
    for (int i = 0; i < 5; i++) {
        Log.i("SimpleJobIntentService", "Running service " + (i + 1) + "/5 @ " + SystemClock.elapsedRealtime());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
    }
    Log.i("SimpleJobIntentService", "Completed service @ " + SystemClock.elapsedRealtime());
}

@Override
public void onDestroy() {
    super.onDestroy();
    toast("All work complete");
}

@SuppressWarnings("deprecation")
final Handler mHandler = new Handler();

// Helper for showing tests
void toast(final CharSequence text) {
    mHandler.post(new Runnable() {
        @Override public void run() {
            Toast.makeText(BTCJobIntentService.this, text, Toast.LENGTH_SHORT).show();
        }
    });
}

标签: javaandroidandroid-widget

解决方案


您正在尝试在后台线程中完成工作,但无法保证您的工作会在进程终止之前完成。

更重要的是,您views在通话parseBpiResponse()后会及时更新。updateAppWidget()您启动网络 I/O 但随后立即执行updateAppWidget(),因此您的更改views发生得太晚了。

至少,您也需要updateAppWidget()加入parseBpiResponse()

更好的做法是onUpdate()将所有这些逻辑移到一个JobIntentService.


推荐阅读