前景提要
年前到现在,一直在处理工作上的事情,需求一个接一个的来临,在加上私事,导致自己一直没有时间写文章,偷懒,懈怠了。趁着现在好不容易完成工作上的需求,review code,总结一下自己这半年来遇到的一些问题。
近期回顾
日期格式
通常,获取本地时间并将其转换成年-月-日 时:分:秒的格式时,采用的代码如下:
val yyyyMMddFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val now = System.currentTimeMillis()
println(yyyyMMddFormat.format(now))
打印输出为
2024-08-08 08:08:08
一般情况下,这么写都是没问题的,只是有一次,不小心把“yyyy-MM-dd HH:mm:ss”写成了"yyyy-MM-dd hh:mm:ss",时间的大写H写成小写h,导致获取到的结果出现差错,于是就思考起这几个的不同区别。
YYYY和yyyy
yyyy表示公历年份,而YYYY表示周数年份,ISO-8601 标准中定义的一种表示方法,周数年份表示方法在一年结束的时候可能不同于实际年份。例如,2020 年的最后几天可能被视为 2021 年的第一周的一部分。
假设当前时间是2024年12月31日08时08分08秒,根据公历年份和周数年份计算,两者得到的时间是不一样的,代码如下:
val strDate = "2024-12-31 08:08:08"
val yyyyMMddFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val yyyyMMddFormat1 = SimpleDateFormat("YYYY-MM-dd HH:mm:ss", Locale.getDefault())
val nowDate = yyyyMMddFormat.parse(strDate)
val calendar = Calendar.getInstance()
calendar.time = nowDate
val now = calendar.timeInMillis
println(yyyyMMddFormat.format(now))
println(yyyyMMddFormat1.format(now))
打印输出的结果如下:
2024-12-31 08:08:08
2025-12-31 08:08:08
一般年初或者年末的时间,才会有区别,其他到时候,周数年份和公历年份是一样的。
MM和mm
在标准日期格式中,大写字母M指代月份,小写字母m指代分钟,假设当前时间为2024-06-07 08:09:10,此时用yyyy-MM-dd HH:mm:ss和yyyy-mm-dd HH:mm:ss得到的时间如下:
2024-06-07 08:09:10
2024-09-07 08:09:10
其中,第一行打印出来的是正常无误的时间,而第二行打印出来的月份是错误的,而这个09-07,其中09是指当前的分钟,也就是说,yyyy-mm-dd HH:mm:ss表示年分日时分秒。
DD和dd
均表示日期,不过表示的日期有所不同,
小写d表示月份中的日期(day of the month),取值范围01~31,
大写D表示一年中的第几天(day of the year),取值范围001~365(闰年时366)
假设当前时间是2024-06-07 08:09:10,那么,根据代码打印输出,结果如下:
2024-06-07 08:09:10
2024-06-159 08:09:10
07是六月份的第七天,而159是2024年的第159天。
HH和hh
这个就是十二小时制和二十四小时制的区别了,HH显示的是二十四小时,而hh显示的是十二小时。
假设当前时间是2024-06-07 18:09:10,代码如下,
val strDate = "2024-06-07 18:09:10"
val yyyyMMddFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
val yyyyMMddFormat1 = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
val nowDate = yyyyMMddFormat.parse(strDate)
val calendar = Calendar.getInstance()
calendar.time = nowDate
val now = calendar.timeInMillis
println(yyyyMMddFormat.format(now))
println(yyyyMMddFormat1.format(now))
打印输出如下:
2024-06-07 18:09:10
2024-06-07 06:09:10
那,如何用十二小时区分上下午呢,加一个字符就可以了,通过"yyyy-MM-dd hh:mm:ss a"格式转换过来的时间,就带上上下午的标志了
输出结果如下:
2024-06-07 06:09:10 下午
SS和ss
大写S通常指代毫秒,而小写的s指代秒。 代码如下:
val strDate = "2024-06-07 18:09:10.123"
val yyyyMMddFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
val yyyyMMddFormat1 = SimpleDateFormat("yyyy-MM-dd HH:mm:SSS", Locale.getDefault())
val nowDate = yyyyMMddFormat.parse(strDate)
val calendar = Calendar.getInstance()
calendar.time = nowDate
val now = calendar.timeInMillis
println(yyyyMMddFormat.format(now))
println(yyyyMMddFormat1.format(now))
打印输出如下:
2024-06-07 18:09:10.123
2024-06-07 18:09:123
不过一般情况下,不会要求精确到毫秒的。
Google Play 保护机制
Android应用想要在海外使用,有两种方式,一种是上架Google Play Store,另一种则是apk下载安装。
这两种方式,都绕不过Google Play Protect--海外Android大多数都自带Google三件套。Google Play Store有自己的审核机制(审核时会对aab包进行Google Play Protect扫描审核),而在Google Play Store之外,安装apk会进行Google Play 保护机制拦截扫描。
Google Play Store
在Google Play上架应用有很多需要注意的点,而Google Play保护机制只是其中之一。其他的包括权限的申请,App隐私协议等。这些都会影响到App是否能够成功通过审核。
举一个简单的例子,Google隐私政策规定,金融向的App,READ_SMS、READ_EXTERNAL_STORAGE和READ_MEDIA_IMAGES这三个权限,是禁止获取的,如果在Manifest.xml文件中静态申明了这三个权限,在提审阶段会被扫描出来,提审失败。
Google play store上架部分就不多涉及了,按着要求来,避免账号关联。
Apk安装
海外的用户,除了通过Google Play Store下载应用,还可以通过对应的网页下载、下载链接下载或者apk包来安装应用。
一旦Google Play 保护机制启动,安装Apk时,会进行扫描,扫描后会对用户进行提示,然后让用户决定是否安装。但如果App申请了敏感权限,有可能会导致Google Play 保护机制屏蔽apk,轻则隐藏安装按钮,重则直接不允许安装apk。
根据Google Play 保护机制里介绍, 此时,Google play把apk认定为潜在有害应用或恶意软件,对apk的安装进行拦截。
下面是Google对恶意软件的描述:
可能有害?
在描述恶意应用时,“可能”一词的含糊不清,让人感到困惑。**Google Play 保护机制会移除被标记为“可能有害”的应用,因为应用确实包含恶意行为,而我们只是不确定应用是否有害。这里之所以使用“可能”一词,是因为恶意应用的运作方式因各种变量而异,因此对一部 Android 设备有害的应用可能不会给另一部 Android 设备带来风险。例如,搭载最新版 Android 的设备不会受到有害应用的影响,这类应用会使用已废弃的 API 执行恶意行为,但搭载 Android 早期版本的设备可能会面临风险。移动账单欺诈会给连接到服务运营商的设备带来风险,但仅连接到 Wi-Fi 的设备不受这些应用的影响。 如果应用明显会给部分或所有 Android 设备和用户带来风险,就会被标记为 PHA。
在对恶意软件描述时,Google也只是使用了"可能"。
解决办法
当初遇到这个问题的时候也是猝不及防的,然后查询文件资料,询问同做过海外项目的一些大神,发现几乎是同一时间内,金融类非上架的Apk都被拦截了。跟同事排查整理了一下,大致确定,是因为申请了短信权限才导致App被拦截无法安装的。
那么,此时有三种方法。
1、去除短信权限。
去除短信权限后,经过测试,发现能够正常安装。不过这会影响到用户数据的收集(毕竟是金融类App,希望尽可能收集用户的信息,降低金融风险)。
2、混淆加固
使用第三方加固方案,能够正常安装应用。这个和领导提了,不过领导有自己的考虑。
3、关闭Google Play 保护机制
可以引导用户关闭Google Play 保护机制的检测,不过,考虑到操作步骤,遂放弃,不要想着去教育指导用户。
4、隐藏方法:投诉
第二天带有短信权限的包又可以重新安装了,听同行的人说,可能是投诉多了,Google关闭了对于短信的检测。
需要注意的是,因为当时是因为确定了READ_SMS权限才导致这个问题的,所以处理起来会容易点。
MMKV可能导致部分机型闪退
MMKV是Android端比较好用的的一款存储工具,它的快速读写能力优于Android自带的SharedPreferences,有很多文章对比了MMKV和SharedPreferences的优缺点。
也正因此,在存储数据的时候,选择采用MMKV而不是SharePreferences。
MMKV使用起来和SharePreferences差不多,引入依赖和初始化。
开发过程中,能够正常使用,只是没想到,正式包在个别机型上居然报初始化错误。找了一圈没有解决方案,最后只好把MMKV替换成原来的ShapePreferences。
应用自动更新弹窗
Google Play Store上架的应用当然不能加入自更新弹窗,而且也不能加入热更新,这样被查出来,账号成本不划算,所以,这个自更新功能,是在自由渠道的Apk中加入的。
最初以为,这个自更新弹窗和普通弹窗没什么区别的,实际开发才发现,有很多需要注意的点。
首先,更新弹窗有强制更新和非强制更新之分。
非强制弹窗
非强制弹窗,效果和普通弹窗差不多,可取消,或者可以点击外部取消弹窗
setCanceledOnTouchOutside(true)//点击外部取消弹窗
setCancelable(true)//弹窗可取消
需要注意的是,点击下载按钮有两种方案实现下载,一种是静默安装,这种方式体验比较好,不过要考虑apk下载完成后如何安装,可以直接调用安装代码,也可以通过消息的方式(点击消息安装应用)。如果是通过消息告知安装包下载情况的话,需要注意的是,如果app退出的话,部分手机会下载失败。如果此时重新打开app,再次下载的话,要处理未下载完的老包和新包的冲突。、
如果新包和老包是同一个包还好,支持断点续传,继续下载就可以了。但如果新包和老包并非同一个包,那么要先把老包删除了,再下载新包。而且还需要注意包的命名,安装包安装成功后是否删除这些注意事项。
强制弹窗
强制弹窗就没有静默安装的方式了。在用户未完成安装之前,用户是不允许使用App的,此时,强制弹窗不允许取消,设置如下:
setCanceledOnTouchOutside(false)
setCancelable(false)
同时,如果点击取消按钮,则直接退出App,这么做,是为了让用户能够体验最新的功能(其实大部分还是因为老版本出现bug)。
还有,还需要判断一下,该版本的安装包是否已经安装了,如果没有安装,则下载,如果安装了,则直接点击安装。
还有个体验的优化,一般情况下,更新弹窗都是出现在首次打开App,后面根据体验做了优化,如果是强制更新,则要求用户更新后才可使用,而非强制更新,则在用户点击后退退出app的时候,弹窗提示用户是否更新。
WebView加载Rtc视频时出现的权限请求问题
其实这个问题,比较粗心和搞笑。
H5请求客户端权限的时候,有两种方案,一种是根据jsBridge交互,另一种则是通过onPermissionRequest方法,JsBridge交互容易,最后可以通过js方法把交互结果传过去,而onPermissionRequest,是webView自己请求权限的方法,
override fun onPermissionRequest(request: PermissionRequest?) {
super.onPermissionRequest(request)
request?.let {
it.grant(it.resources)
}
}
request?.let {it.grant(it.resources)}使用这个来申请相机权限。
报错:Either grant() or deny() has been already called.
具体点,setWebChromeClient的onPermissionRequest中调用request.grant(request.getResources()),即请求获取android.webkit.resource.VIDEO_CAPTURE,因为h5需要请求相机进行录制视频,所以请求了这个权限,但是提示报错Either grant() or deny() has been already called.
查了下问题,说是因为重复调用了
request.grant(request.resources)或者request.deny()。
review一遍代码,还是不解,设置了标记,仍然提示这个错误。 在chatGpt上查找了半天,翻来覆去都是提示说重复调用request.grant() 或者request.deny()。
在经过半小时的努力后,终于发现盲点。
super.onPermissionRequest(request)
点进源码发现:
/**
* Notify the host application that web content is requesting permission to
* access the specified resources and the permission currently isn't granted
* or denied. The host application must invoke {@link PermissionRequest#grant(String[])}
* or {@link PermissionRequest#deny()}.
*
* If this method isn't overridden, the permission is denied.
*
* @param request the PermissionRequest from current web content.
*/
public void onPermissionRequest(PermissionRequest request) {
request.deny();
}
原来按着原来的逻辑,先调用了一次request.deny(),然后request.grant(),怪不得会提示这个错误。
把super.onPermissionRequest(request)注释了,重新运行,success!
一般情况下,super父类的方法都是空方法,所以下意识地会忽略这串代码。
横竖屏切换
一般情况下,Android应用都是适配竖屏模式的,因为大多数应用,没有横屏使用的场景,一般有横竖屏切换需求的,一般啊,是视频播放或者拍照类App。竖屏选择频道,横屏观看,体验更佳。
而在横竖屏切换时,Activity的生命周期会被重新调用。
运行Activity时生命周期如下: onCreate->onStart->onResume
竖屏切换横屏时生命周期如下: onPause->onStop->onSaveInstanceState->onDestroy->onCreate->onStart->onRestoreInstanceState->onResume
其中,onSaveInstanceState和onRestoreInstanceState不算生命周期,不过可以用来存储数据。
同样,切换回来后,也是这么一遍的生命周期。
screenOrientation
在AndroidManifest.xml配置清单文件中,Activity下添加标签android:screenOrientation="landscape"或android:screenOrientation="portrait",控制该Activity在设备上的显示方向,那么Activity不进行横竖屏切换。
Activity创建时生命周期如下: onCreate->onStart->onResume
把设备从垂直方向旋转90°,此时Activity的生命周期没有变化,且显示仍然是竖屏(portrait)。
或者,原来Activity在设备上是横屏显示,那么,物理操作设备旋转90°,此时Activity显示仍然是横屏(landscape),且生命周期没有变化。
configChanges
那,如果需求需要支持横竖屏切换,且也能做到生命周期不变化,怎么做呢?
这就要用到configChanges标签了。
configChanges是一个常用于Activity的属性,用于指定当设备配置发生变化时(例如旋转屏幕、键盘可用性变化、语言切换等),Activity如何处理这些变化。默认情况下,当设备配置发生变化时,系统会销毁并重建当前的
Activity。但是通过在AndroidManifest.xml中的Activity元素中指定configChanges属性,可以让Activity自行处理这些变化,而不需要重新创建。
configChange的常见值如下:
-
orientation:设备方向变化(例如从竖屏变为横屏)。 -
screenSize:屏幕尺寸变化。 -
keyboardHidden:键盘的可见性变化(例如硬件键盘被收起)。 -
screenLayout:屏幕布局变化。 -
uiMode:用户界面模式变化(例如日间模式和夜间模式之间的切换)。 -
density:屏幕密度变化。 -
smallestScreenSize:最小屏幕尺寸变化。 -
layoutDirection:布局方向变化(例如从左到右与从右到左)。
为该Activity赋值该值 android:configChanges="orientation",重新测试。
启动Activity时的生命周期: onCreate->onStart->onResume
横竖屏切换时: onPause->onStop->onSaveInstanceState->onDestroy->onCreate->onStart->onRestoreInstanceState->onResume
理论上来说,此时,横竖屏切换了,只是触发了orientation,为什么Activity还会重建?
查了下资料,原来,在API12以上的版本中,横竖屏切换时,不仅会触发orientation,还会触发screenSize的变化。那么,重新设置
android:configChanges="orientation|screenSize"
测试发现,此时,横竖屏切换不会重建Activity。
用处
有什么用处呢?让设备横竖屏切换又如何呢?
第一,UI就是按着竖屏来设计的,如果横屏显示,会影响用户体验。
第二,如果重建Activity,在Activity运行中的数据就没办法保存。当然,前面提到的两个方法onSaveInstanceState和onRestoreInstanceState,这两个可以用来存取数据,不过比较麻烦。
第三,当Activity拉起相机时,相机是可以横屏拍照的,横竖屏切换,会导致存储的数据都是。
openCamera.setOnClickListener {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (intent.resolveActivity(packageManager) != null) {
val photoFile =
File("${cacheDir.absolutePath + File.separator}${System.currentTimeMillis()}.png")
photoPath = photoFile.absolutePath
val photoUri: Uri = FileProvider.getUriForFile(
this,
"${packageName}.provider",
photoFile
)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
cameraLauncher.launch(intent)
}
}
下面是一段打开相机拍照的代码,其中photoFile就是即将拍照的相片文件。如果拍照时横竖屏切换,文件地址就会丢失。
至于其他的场景,目前开发过程中没有遇见。
fillViewport
fillViewport 是 Android 中 ScrollView 和 HorizontalScrollView 的一个属性,用于指定当内容视图(ScrollView 或 HorizontalScrollView 的子视图)小于滚动视图的可见区域时,是否将内容视图扩展以填充整个滚动视图的可见区域。
设置 fillViewport="true" 可以使得内容视图填充整个 ScrollView,即使内容视图的尺寸不足以填充整个滚动区域。
用到这个是因为遇到一个需求,ScrollView中的布局,其中一个要充满整个屏幕,其他内容下拉出现。
在这里做一个记录。
Notification
消息,先下一个结论吧,Android端的消息,远不如IOS的好用。
Android端的消息,随着进程被杀死,进程被回收,消息的送达率就会降低。
当然,如果App进入白名单,可以常驻后台,或者,接入厂商通道(不过厂商通道有时候也有接收不到的情况),那,消息就可以和IOS的消息一样,作用很大。
普通消息
发送消息,直接上代码:
val channelId = 1000
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelIdString = channelId.toString()
val builder: Notification.Builder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
channelIdString,
"topic",
NotificationManager.IMPORTANCE_HIGH
)
nm.createNotificationChannel(channel)
Notification.Builder(this, channel.id)
} else {
Notification.Builder(this)
}
builder.setContentTitle("Title")
.setContentText("This is a test message")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_logo)
.setPriority(Notification.PRIORITY_MAX)
.setAutoCancel(true)
val notification = builder.build()
nm.notify(channelId, notification)
这里,先根据channelId创建一个消息通道,然后Notification.Builder创建消息,NotificationManager发送消息。
如果想要更新该消息内容,那也容易。
只要channelId不变,更改builder.setContentText()里面的内容或者更新自定义View,然后调用代码nm.notify(channelId, notification)就能实现该消息的更新了,这个常用于后台更新、下载、播放音乐。
但当调用上面代码时,channelId自增或者变化,就相当于发送了一条新消息。
增添事件
然后为消息增加点击效果。
val channelId = 1000
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channelIdString = channelId.toString()
val builder: Notification.Builder =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
channelIdString,
"topic",
NotificationManager.IMPORTANCE_HIGH
)
nm.createNotificationChannel(channel)
Notification.Builder(this, channel.id)
} else {
Notification.Builder(this)
}
val intent = Intent(this, MainActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
builder.setContentTitle("Title")
.setContentText("This is a test message")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_logo)
.setPriority(Notification.PRIORITY_MAX)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
val notification = builder.build()
nm.notify(channelId, notification)
pendingIntent是实现点击的效果的,打开MainActivity。当然,也可以通过广播的形式实现点击效果。
val intent = Intent(this, MainActivity::class.java)
intent.putExtra("notification", "action_jump_home")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
也可以通过这种方式实现点击效果,打开MainActivity,并在onNewIntent中捕获到intent传来的内容。
大文本内容
而如果消息的文本内容比较大,有两种方式,一种是自定义View,另一种如下:
val style = NotificationCompat.BigTextStyle().bigText(msg.message)
builder.setStyle(style)
自定义View
消息也是可以使用自定义View的,自定义的界面RemoteViews。
自定义RemoteViews的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="150dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_logo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:src="@mipmap/ic_logo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintBottom_toBottomOf="@+id/iv_logo"
app:layout_constraintStart_toEndOf="@+id/iv_logo"
app:layout_constraintTop_toTopOf="@+id/iv_logo" />
</LinearLayout>
<ProgressBar
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginHorizontal="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_logo" />
</LinearLayout>
这是自定义RemoteViews的布局,logo,文本内容和一个progressBar进度条。
代码设置:
val remoteView = RemoteViews(packageName, R.layout.layout_notification)
remoteView.setTextViewText(R.id.tv_content, "this is a test message $num")
remoteView.setProgressBar(R.id.progress, 100, num, false)
...省略代码...
builder.setContent(remoteView)
num是进度条的多少,然后在builder中设置contnet,这样就能实现自定义View的效果。
不过有一点需要注意的是,这个RemoteViews目前不支持ConstrainLayout。
RemoteViews常用于更新应用组件,比如AppWidget(桌面组件)、通知等,只支持有限的布局和视图组件。
RemoteViews 支持以下几种布局:
-
LinearLayout
- 常用于简单的水平或垂直排列。
- 适用属性:
orientation、gravity、padding、layout_weight等。
-
RelativeLayout
- 允许视图相对于父布局或其他视图进行定位。
- 适用属性:
layout_alignParent*、layout_center*、layout_to*、layout_above、layout_below等。
-
FrameLayout
- 适合简单的视图堆叠。
- 适用属性:
layout_gravity等。
-
GridLayout
- 支持在网格中排列视图,适合更复杂的布局。
- 适用属性:
layout_row、layout_column、layout_rowSpan、layout_columnSpan等。
RemoteViews 支持的视图组件
RemoteViews 支持的视图组件包括:
- TextView (
TextView,Button,Chronometer,DigitalClock) - ImageView (
ImageView) - ProgressBar (
ProgressBar) - ListView (
ListView,GridView,StackView,AdapterViewFlipper) - 其他视图 (
ViewFlipper,FrameLayout,LinearLayout,RelativeLayout,GridLayout)
注意事项
RemoteViews不支持自定义视图或大多数复杂视图。- 你只能使用
RemoteViews提供的特定方法(如setTextViewText、setImageViewResource等)来更新这些视图的内容。 - 对于复杂的布局,建议使用组合方式,例如使用嵌套的
LinearLayout或RelativeLayout以模拟更复杂的布局。
有感而发
Android项目做的多了,越发觉得,基础性的工作,简单容易,可替代性太高了。
这半年以来,开发了四五个项目,业务需求大同小异,就是界面和一些项目逻辑是不一样的,工作的大部分内容也都是围绕界面来的,而大部分的bug呢,也都是因为在平时开发过程中不细心,ui交互没有从用户的角度出发。
而在我看来,原生Android开发, 能自定义View,就能满足大部分的工作业务需求了。而一些特殊的场景,视频、人脸、地图之类的,都有可支持的第三方开发应用商。而第三方应用商,人家就是专业做这个的,普通的Android开发,真比不过。
而纯原生Android开发,随着大前端,或者跨端的发展,原生开发将会越加艰难,或者说,基础简单的工作,越发艰难了。要么专精一块,有人视频开发,有人地图开发,要么深耕底层framework。这都是需要花大量时间的。
最后
感谢你看到最后,最后说一两点~
1.如果你有不同的看法,欢迎你在文章下面进行评论留言。
2.如果文章对你有帮助,或者你认可的话,请随手点个赞,支持一下。
我是个技术小白,欢迎私信交流。
(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除,同样,转载请提前告知)