概述

App Widget是应用程序窗口小部件(Widget),是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。

Widget布局

appwidget-provider标签

这个东西是用来定义桌面widget的大小,初始状态等等信息的,它的位置应该放在res/xml文件夹下,具体的xml参数如下:

android:minWidth : 最小宽度
android:minHeight : 最小高度
android:updatePeriodMillis : 更新widget的时间间隔(ms)
android:previewImage : 预览图片
android:resizeMode : widget可以被拉伸的方向。horizontal表示可以水平拉伸,vertical表示可以竖直拉伸
android:widgetCategory : widget可以被显示的位置home_screen表示可以将widget添加到桌面,keyguard表示widget可以被添加到锁屏界面
android:initialLayout : 加载到桌面时对应的布局文件
android:initialKeyguardLayout : 加载到锁屏界面时对应的布局文件

我自己的配置文件如下:res/xml/my_appwidget_info.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"android:initialLayout="@layout/my_appwidget"android:minHeight="60dp"android:minWidth="180dp"android:previewImage="@mipmap/preview"android:resizeMode="horizontal|vertical"android:widgetCategory="home_screen|keyguard">
</appwidget-provider>

布局文件:res/layout/my_appwidget.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#33000000"android:orientation="vertical"><TextView
        android:id="@+id/song_name"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="5dip"android:gravity="center_horizontal"android:textSize="16sp"android:text="song name"/><LinearLayout
        android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:gravity="center_horizontal"android:orientation="horizontal" ><ImageView
            android:id="@+id/prev_song"android:layout_width="40dp"android:layout_height="40dp"android:layout_marginRight="8dip"android:background="@mipmap/car_musiccard_up" /><ImageView
            android:id="@+id/play_pause"android:layout_width="40dp"android:layout_height="40dp"android:layout_marginRight="8dip"android:src="@mipmap/car_musiccard_play" /><ImageView
            android:id="@+id/next_song"android:layout_width="40dp"android:layout_height="40dp"android:background="@mipmap/car_musiccard_down" /></LinearLayout>
</LinearLayout>

注意:在构造Widget布局时,App Widget支持的布局和控件非常有限,有如下几个:

//App Widget支持的布局:
FrameLayout、LinearLayout、RelativeLayout、GridLayout
//App Widget支持的控件:
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper

除此之外的所有控件(包括自定义控件)都无法显示,无法显示时,添加出来的widget会显示“加载布局出错”。

AppWidgetProvider类

上面我们通过appwidget-provider标签就可以得到初始化的布局,视图等,但我们的widget要实时更新怎么办,要响应用户操作怎么办,这就需要额外的类来辅助处理了,这个类就是AppWidgetProvider。

由于AppWidgetProvider要接收到当前widget的状态(是否被添加,是否被删除等),所以要接收通知,必然是继承自BroadcastReceiver。

AppWidgetProvider中的广播处理函数如下:(根据不同的使用情况,重写不同的函数)

  • onUpdate():
    onUpdate()在主线程中执行,如果处理需要花费时间多于10秒,处理应在service中完成。3种情况下会调用onUpdate():
    (1)在时间间隔到时调用,时间间隔在widget定义的android:updatePeriodMillis中设置;
    (2)用户拖拽到主页,widget实例生成。
    (3)机器重启,实例在主页上显示

  • onDeleted(Context, int[]):
    当 widget 被删除时被触发。

  • onEnabled(Context):
    当第1个 widget 的实例被创建时触发。也就是说,如果用户对同一个 widget 增加了两次(两个实例),那么onEnabled()只会在第一次增加widget时触发。

  • onDisabled(Context):
    当最后1个 widget 的实例被删除时触发。

  • onReceive(Context, Intent):
    在接收到广播时调用。

我们可以先写个简单的Provider类,后面根据需求慢慢丰富:

public class MyAppWidgetProvider extends AppWidgetProvider {// widget更新时调用@Overridepublic void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);super.onUpdate(context, appWidgetManager, appWidgetIds);}// widget被删除时调用@Overridepublic void onDeleted(Context context, int[] appWidgetIds) {Log.d("hx", "onDeleted: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);super.onDeleted(context, appWidgetIds);}// 最后一个widget被删除时调用@Overridepublic void onDisabled(Context context) {Log.d("hx", "onDisabled");super.onDisabled(context);}// 第一个widget被创建时调用@Overridepublic void onEnabled(Context context) {Log.d("hx", "onEnabled");super.onEnabled(context);}// 接收广播的回调函数@Overridepublic void onReceive(Context context, Intent intent) {Log.d("hx", "onReceive");super.onReceive(context, intent);}
}

这里不是继承接口,这几个函数并不需要全部重写,要用到哪个就重写哪个。

注册MyAppWidgetProvider

至此我们的Widget还不能添加到桌面,还需要最后一步。前面我们讲到AppWidgetProvider派生自BroadcastReciver,所以要提前注册,BroadcastReciver的注册有两种方法,静态注册和动态注册,因为这里要接收来自系统的消息,而且在程序启动时就开始自动监听,所以需要静态注册。

        <!-- 声明widget对应的AppWidgetProvider --><receiver android:name=".MyAppWidgetProvider" ><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter><meta-data android:name="android.appwidget.provider"android:resource="@xml/my_appwidget_info" /></receiver>

(1)接收的action定义为:”android.appwidget.action.APPWIDGET_UPDATE”这表明接收系统发来的有关这个app的所有widget的消息(主要是增加、删除)。
(2)<.meta-data> 指定了 AppWidgetProviderInfo 对应的资源文件:
android:name – 指定metadata名,指定为android.appwidget.provider表示这个data中的数据是AppWidgetProviderInfo 类型的
android:resource – 指定 AppWidgetProviderInfo 对应的资源路径。即,xml/my_appwidget_info.xml。

现在可以把Widget添加到桌面啦,效果图如下:

Widget 交互

上面我们实现了Widget的界面,但此时Widget还不能更新内容和响应用户操作,接下来看看Widget的交互。

发送广播与按钮事件绑定

因为appwidget运行的进程和我们创建的应用不在一个进程中,所以我们也就不能像平常引用控件那样来获得控件的实例。这时候,我们就要靠RemoteViews,直译成中文应该是远程视图, 也就是说通过这个东西我们能够获得不在同一进程中的对象。

先看一下Demo代码,后面慢慢分析:

    @Overridepublic void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {       /**构造RemoteViews实例*/ RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);//添加点击事件remoteViews.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));remoteViews.setOnClickPendingIntent(R.id.play_pause, getPendingIntent(context, R.id.play_pause));remoteViews.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));// 更新AppwidgetappWidgetManager.updateAppWidget(appWidgetIds, remoteViews);super.onUpdate(context, appWidgetManager, appWidgetIds);}private PendingIntent getPendingIntent(Context context,int resID){Intent intent = new Intent();//注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProvider,使用Action的方式接收不到intent.setClass(context, MyAppWidgetProvider.class);intent.setData(Uri.parse("hx:" + resID));PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent, PendingIntent.FLAG_UPDATE_CURRENT);return pendingIntent;}

首先创建了一个remoteViews实例,然后将其中的按钮与点击事件绑定,后面的参数是一个PendingIntent。下面看看PendingIntent又是个什么。

intent意思是意图,pending表示即将发生或来临的事情。
PendingIntent这个类用于处理即将发生的事情。比如在通知Notification中用于跳转页面,但不是马上跳转。所以我们可以将它理解成一个封装成消息的intent。即这个intent并不是立即start,而是像消息一样被发送出去,等接收方接到以后,再分析里面的内容。

可以看到,PendingIntent是Intent的封装,在构造PendingIntent前,需要先构造一个Intent(注意这里是显式intent,直接将这个广播发送给MyAppWidgetProvider),并可以利用Intent的属性传进去action,Extra等,同样,在接收时,对方依然是接收Intent,而不是接收PaddingIntent。这个问题,我们后面可以看到。

PendingIntent.getBroadcast(context, 0, intent, 0);

指从系统中获取一个用于可以发送BroadcastReceiver广播的PendingIntent对象。所以流程就是我们点击控件,就会发出一个广播,广播携带了Intent,接收方(onReceive)通过Intent判断我们点击的是哪个控件,再作出相应的处理。

这里有一点需要注意:
我们intent传递数据使用的是setData

intent.setData(Uri.parse("hx:" + resID));

那么能不能用putExtra呢?

intent.putExtra("id", resID);

实践发现这种方式取到的id值永远只有一个,即使点击不同的控件也不会变。究其原因是因为没有创建新的PendingIntent,仍然是复用的前一个。如果要创建两个不同的PendingIntent,而不要系统替换前一个,不要仅仅在PutExtra()中包含不同的内容,因为在Extra的不同,并不会用来识别两个不同的PendingInent,要看两个PendingIntent是否相同,可以利用filterEquals (Intent other)来判断两个Intent是否相同,即除了Extra域的任何不同都会标识为两个不同的Intent。所以用setData就标识了两个不同的Intent。还有另外一种方式,使用不同的RequestCode也可以构建新的PendingIntent。

总结一下,两种方式:

  • 利用filterEquals (Intent other)里的那几个域的不同来构造不同的Intent
  • 在构建PendingIntent时使用不同的RequestCode

创建remoteViews实例后,还有一句:

appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);

就是利用updateAppWidget()将构造的RemoteView更新指定的widget界面。注意这里有个appWidgetIds,这个参数是通过OnUpdate()传过来的,它是一个int数组,里面存储了用户所创建的所有widget的ID值。更新的时候也是通过widget的ID值,一个个更新的。

接收广播

由于我们在创建广播的Intent时,使用的显示Intent,所以我们的广播不需要注册就会发到这们这个类(MyAppWidgetProvider.java)里面。

在接收到广播后,我们先判断Intent中是不是包含data,因为我们在发送广播时放data中塞了数据(控件的ID),所以只要data中有数据就可以判定是用户点击控件发来的广播。然后同样利用RemoteView修改textView的文字,代码如下:

@Overridepublic void onReceive(Context context, Intent intent) {Uri data = intent.getData();int resID = -1;if(data != null) {resID = Integer.parseInt(data.getSchemeSpecificPart());}/**通过远程对象设置文字*/RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.my_appwidget);switch (resID) {case R.id.prev_song:remoteViews.setTextViewText(R.id.song_name,"prev_song click");break;case R.id.play_pause:remoteViews.setTextViewText(R.id.song_name,"play_pause click");break;case R.id.next_song:remoteViews.setTextViewText(R.id.song_name,"next_song click");break;}// 获得appwidget管理实例,用于管理appwidget以便进行更新操作AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);// 相当于获得所有本程序创建的appwidgetComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);// 更新appwidgetappWidgetManager.updateAppWidget(componentName, remoteViews);super.onReceive(context, intent);}

其实RemoteView中的操作控件的方法非常有限,可以看这里:Android App Widget中如何调用RemoteView中的函数。

两个地方需要注意:

(1)在OnUpdate中,我们更新界面是通过传过来的widget的id数组来更新所有widget的。而这里是通过获取ComponentName来更新的。其实这里还有另一种实现方式,即我们可以把OnUpdate中传过来的appWidgetIds保存起来,在这里同样使用OnUpdate中的appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);来更新。但我更推荐这里利用ComponentName的这种方式来更新,因为当我们的应用程序进程被杀掉后再起来的时候,会赋予新的appWidgetIds,使用ComponentName这种更新方式还是可以继续响应的,而利用保存appWidgetIds的方式是不会响应的。

(2)在实际项目中,大家可能会想到复用remoteView,即如果已经创建了就不再重新加载layout,而是重新绑定控件及数据,千万不要这样!!!
如果你在绑定数据时涉及图片等大数据,remoteView不会每次清理,所以如果每次都使用同一个remoteView进行传输会因为溢出而绐终无响应! 你看着每次动作都通过updateAppWidget传送出去了,但界面死活就是没响应;而且重装应用程序也不会有任何反应,只能重启手机才会重新有反应,也是醉了。
主要原因在于:Binder data size limit is 512k,由于传输到appWidget进程中的Binder最大数据量是512K,并且RemoteView也不会每次清理, 所以如果每次都使用同一个RemoteView进行传输会因为溢出而报错.所以必须每次重新建一个RemoteView来传输!!!!!!

话不多说,看效果:

实战(音乐播放器)

首先需要一个播放音乐的Service,这里叫做MusicManageService:

public class MusicManageService extends Service {private MediaPlayer mPlayer;private int mIndex = 4;// 从中间开始放private int[] mArrayList = new int[9];public static String ACTION_CONTROL_PLAY = "action_control_play";public static String KEY_USR_ACTION = "key_usr_action";public static final int ACTION_PRE = 0, ACTION_PLAY_PAUSE = 1, ACTION_NEXT = 2;private boolean mPlayState = false;//接收MyAppWidgetProvider发过来的广播,控制播放器播放private BroadcastReceiver receiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {String action  = intent.getAction();if (ACTION_CONTROL_PLAY.equals(action)) {int widget_action = intent.getIntExtra(KEY_USR_ACTION, -1);switch (widget_action){case ACTION_PRE:playPrev(context);Log.d("hx","action_prev");break;case ACTION_PLAY_PAUSE:if (mPlayState) {pause(context);Log.d("hx","action_pause");}else{play(context);Log.d("hx","action_play");}break;case ACTION_NEXT:playNext(context);Log.d("hx","action_next");break;default:break;}}}};@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(ACTION_CONTROL_PLAY);registerReceiver(receiver, intentFilter);initList();mediaPlayerStart();}private void mediaPlayerStart(){mPlayer = new MediaPlayer();mPlayer = MediaPlayer.create(getApplicationContext(), mArrayList[mIndex]);mPlayer.start();mPlayState = true;updateUI(getApplicationContext(), MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);}private void initList() {mArrayList[0] = R.raw.dui_ni_ai_bu_wan;mArrayList[1] = R.raw.fei_yu;mArrayList[2] = R.raw.gu_xiang_de_yun;mArrayList[3] = R.raw.hen_ai_hen_ai_ni;mArrayList[4] = R.raw.new_day;mArrayList[5] = R.raw.shi_jian_li_de_hua;mArrayList[6] = R.raw.ye_gui_ren;mArrayList[7] = R.raw.yesterday_once_more;mArrayList[8] = R.raw.zai_lu_shang;}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {return super.onStartCommand(intent, flags, startId);}@Overridepublic void onDestroy() {super.onDestroy();mPlayer.stop();unregisterReceiver(receiver);}/*** 播放下一首* @param context*/public void playNext(Context context) {if (++mIndex > 8) {mIndex = 0;}mPlayState = true;playSong(context);updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY,mIndex);}/*** 播放上一首** @param context*/public void playPrev(Context context) {if (--mIndex < 0) {mIndex = 8;}mPlayState = true;playSong(context);updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);}/*** 继续播放*/public void play(Context context) {mPlayState = true;mPlayer.start();updateUI(context, MyAppWidgetProvider.UPDATE_UI_PLAY, mIndex);}/*** 暂停播放** @param context*/public void pause(Context context) {mPlayState = false;mPlayer.pause();updateUI(context, MyAppWidgetProvider.UPDATE_UI_PAUSE, mIndex);}/*** 播放指定的歌曲** @param context*/private void playSong(Context context) {AssetFileDescriptor afd = context.getResources().openRawResourceFd(mArrayList[mIndex]);try {mPlayer.reset();mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getDeclaredLength());mPlayer.prepare();mPlayer.start();afd.close();} catch (Exception e) {Log.e("hx","Unable to play audio queue do to exception: "+ e.getMessage(), e);}}//发送广播到MyAppWidgetProvider用来改变widget的显示private void updateUI(Context context, int state, int songId) {Intent actionIntent = new Intent(MyAppWidgetProvider.ACTION_UPDATE_UI);actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_PLAY_BTN, state);actionIntent.putExtra(MyAppWidgetProvider.KEY_UI_TEXT, songId);context.sendBroadcast(actionIntent);}
}

Service写完之后记得在Manifest中注册,还有Service需要发广播给MyAppWidgetProvider通知更新UI,所以MyAppWidgetProvider除了响应系统的Action之外,还需要加上自己的Action:

        <!-- 声明widget对应的AppWidgetProvider --><receiver android:name=".MyAppWidgetProvider" ><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /><action android:name="action_update_ui" /></intent-filter><meta-data android:name="android.appwidget.provider"android:resource="@xml/my_appwidget_info" /></receiver><!-- 播放音乐service--><service android:name=".MusicManageService"/>

然后就是重写MyAppWidgetProvider文件:

public class MyAppWidgetProvider extends AppWidgetProvider {private boolean mStop = true;public static String ACTION_UPDATE_UI = "action_update_ui";  //Actionpublic static String KEY_UI_PLAY_BTN = "ui_play_btn_key"; //putExtra中传送当前播放状态的keypublic static String KEY_UI_TEXT = "ui_text_key"; //putExtra中传送TextView的keypublic static final int UPDATE_UI_PLAY = 1, UPDATE_UI_PAUSE =2;//当前歌曲的播放状态// 更新所有的 widgetprivate void updateRemoteViews(Context context,AppWidgetManager appWidgetManager, String songName, Boolean play_pause) {RemoteViews remoteView = new RemoteViews(context.getPackageName(),R.layout.my_appwidget);//将按钮与点击事件绑定remoteView.setOnClickPendingIntent(R.id.play_pause,getPendingIntent(context, R.id.play_pause));remoteView.setOnClickPendingIntent(R.id.prev_song, getPendingIntent(context, R.id.prev_song));remoteView.setOnClickPendingIntent(R.id.next_song, getPendingIntent(context, R.id.next_song));//设置内容if (!songName.equals("")) {remoteView.setTextViewText(R.id.song_name, songName);}//设定按钮图片if (play_pause) {remoteView.setImageViewResource(R.id.play_pause, R.mipmap.car_musiccard_pause);}else {remoteView.setImageViewResource(R.id.play_pause, R.mipmap.car_musiccard_play);}// 相当于获得所有本程序创建的appwidgetComponentName componentName = new ComponentName(context, MyAppWidgetProvider.class);appWidgetManager.updateAppWidget(componentName, remoteView);}@Overridepublic void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {Log.d("hx", "onUpdate: appWidgetIds.length=" + appWidgetIds.length + " appWidgetIds[0]=" + appWidgetIds[0]);updateRemoteViews(context, appWidgetManager, "", false);}private PendingIntent getPendingIntent(Context context,int resID){Intent intent = new Intent();//注意这个intent构造的是显式intent,直接将这个广播发送给MyAppWidgetProviderintent.setClass(context, MyAppWidgetProvider.class);intent.addCategory(Intent.CATEGORY_ALTERNATIVE);intent.setData(Uri.parse("hx:" + resID));PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,intent,PendingIntent.FLAG_UPDATE_CURRENT);return pendingIntent;}// 接收广播的回调函数@Overridepublic void onReceive(Context context, Intent intent) {Log.d("hx", "onReceive");String action = intent.getAction();Log.d("hx", "action:"+action);if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) { //手势操作后系统发来的广播Uri data = intent.getData();int resID = Integer.parseInt(data.getSchemeSpecificPart());switch (resID) {case R.id.play_pause:controlPlay(context, MusicManageService.ACTION_PLAY_PAUSE);if(mStop) {Intent startIntent = new Intent(context, MusicManageService.class);context.startService(startIntent);mStop = false;}break;case R.id.prev_song:controlPlay(context, MusicManageService.ACTION_PRE);break;case R.id.next_song:controlPlay(context, MusicManageService.ACTION_NEXT);break;}} else if (ACTION_UPDATE_UI.equals(action)) { //MusicManageService发来的广播int play_pause =  intent.getIntExtra(KEY_UI_PLAY_BTN, -1);int songId = intent.getIntExtra(KEY_UI_TEXT, -1);switch (play_pause) {case UPDATE_UI_PLAY:updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, true);break;case UPDATE_UI_PAUSE:updateRemoteViews(context, AppWidgetManager.getInstance(context), "current sond id:" + songId, false);break;default:break;}}super.onReceive(context, intent);}//发送广播到MusicManageService控制播放private void controlPlay(Context context, int ACTION) {Intent actionIntent = new Intent(MusicManageService.ACTION_CONTROL_PLAY);actionIntent.putExtra(MusicManageService.KEY_USR_ACTION, ACTION);context.sendBroadcast(actionIntent);}
}

Demo下载地址

Android App Widget 开发相关推荐

  1. Android APP 快速开发教程(安卓)

    Android APP 快速开发教程(安卓) 前言 本篇博客从开发的角度来介绍如何开发一个Android App,需要说明一点是,这里只是提供一个如何开发一个app的思路,并不会介绍很多技术上的细节, ...

  2. Android app应用开发高级进阶系列专栏解读

    1.前言 在从事android app开发的几年里,最开始接触做android 都是从app开发开始做的,在做app的这几年中把积累下来的做的一些功能,都整理出来了作为自己的技术资料,在以后开发类似的 ...

  3. 谈谈Android App混合开发

    推酷 文章 站点 主题 公开课 活动 客户端 荐 周刊 登录 谈谈Android App混合开发 时间 2015-08-25 20:13:43bxbxbai 原文  http://bxbxbai.gi ...

  4. Android桌面组件App Widget开发三步走

    桌面组件App Widget是Android的实用功能,开发过程虽然不是很难,但是步骤不少,略有麻烦.为了方便以后再次使用的时候,快速上手,概括了下面的关键步骤.并且把项目打了包,方便以后的使用.新建 ...

  5. Android App Widget中如何调用RemoteView中的函数

    我们在开发App Widget时候,要创建一个RemoteView来呈现界面.但是会发现如果我们想要控制RemoteView中的view时候是无法使用findViewbyId来控制Child View ...

  6. 京东Android APP HarmonyOS 开发实践!

    以下文章来源于京东零售技术 ,作者侯伟浩 狄彩林 原文链接 京东鸿蒙版来了〜 背景 随着鸿蒙2.0的发布,华为部分手机用户迎来鸿蒙时代,京东作为华为鸿蒙OS的合作APP,首次投入鸿蒙应用商用版开发,目 ...

  7. Android—App—必备开发组件—调试工具篇—Stetho[配合OkHttp框架使用]

    一.First and Foremost : 测试同学,在测试Android-App时,所需要的其中一个重要的技能即判断页面数据错误后,能迅速定位是服务器接口问题,还是APP逻辑问题.此时就需要知道服 ...

  8. 分享篇 - 58同城基于Android APP Bundle开发的全新编译模式(编译速度提升70%)

    目录 1. Wafers 项目背景 2. 效果展示 3. 实现方案 4. 改造期间遇到的问题 5. 如何接入使用 6. 对比 Instant Run 和 Apply Changes 7. 总结 1. ...

  9. 【编程新实务】Lab4 系统登录/注册模块(Android app)的开发

    目录 Github仓库 前言 展示 安卓前端 安卓后端 服务器后端 补充: 总结 Github仓库 客户端+服务端 客户端开发环境:Android studio(API 21) 服务端开发环境:IDE ...

最新文章

  1. 考研计算机网课辅导,考研计算机网课辅导哪个好
  2. 哈希表数据结构_Java数据结构哈希表如何避免冲突
  3. 修炼一名软件工程师的职业水准
  4. 《微软:DirectShow开发指南》第三章 Programming DirectShow Applications
  5. Cron定时任务应用到Thinkphp – 贤生博客
  6. mysql1.0.17.0安装教程_mysql 8.0.17 安装配置图文教程
  7. GPRS网络继电器SAC07GSA评估套件使用心得
  8. h3c模拟器网络初级综合实验
  9. 队列——数据结构严蔚敏C语言版
  10. L1-084 拯救外星人
  11. 论文阅读-2017-Vidal-NEARP
  12. JQuery实现复选框CheckBox的全选、反选、提交操作
  13. DB_RECOVERY_FILE_DEST,LOG_ARCHIVE_DEST,LOG_ARCHIVE_DEST_N
  14. win7 32位安装oracle10g步骤
  15. 驱动 - 数码管显示数值
  16. bzoj1758+WC2010
  17. 从旧金山到瑞典的开发者的福利
  18. Solidworks工程图如何使用,替换图纸格式模板文件
  19. 数据结构中的elem,elemtype是什么
  20. 【渝粤题库】国家开放大学2021春2604城市轨道交通行车组织题目

热门文章

  1. JAVA使用Springboot+MP+VUE+Swagger前后端分离进行微信支付
  2. 安装并配置TrueNas存储平台
  3. HTML中图片无法显示的问题
  4. Google PR值
  5. 基于ArduinoUNO的LD3320语音识别+SYN6288语音合成的智能分类垃圾桶
  6. Qt - 窗口移动拉伸
  7. python 图片拼接成数字_用Python语言对任意图像进行m*n的均匀分块并拼接还原(思路非常清晰,步骤简单)...
  8. Python数据分析报告:北京市每月PM2.5的值和分析影响PM2.5
  9. TP5.1 支付宝app支付 (沙箱本地测试)
  10. SVG中以任意直线为对称轴的镜像变换及其矩阵