Android 写业务不用架构会怎么样?
作者:唐子玄
链接:
https://juejin.cn/post/7144737325698777096
本文由作者授权发布。
软件的首要技术使命是“管理复杂度” —— 《代码大全》
架构的目的在于“将复杂度分层”
物理层 数据链路成 网络层 传输层 应用层
技术选型
Navigation graph:一个带标签的 xml 文件,用于配置页面及其包含的动作。 NavHost:一个页面容器。 NavController:页面跳转控制器,用于发起动作。
https://developer.android.com/guide/navigation/navigation-getting-started?hl=zh-cn
界面框架
class TemplateSearchActivity : AppCompatActivity() {
companion object {
const val NAV_HOST_ID = "searchFragmentContainer"
}
private lateinit var etSearch: EditText
private lateinit var tvSearch: TextView
private lateinit var ivClear: ImageView
private lateinit var ivBack: ImageView
private lateinit var vInputBg: View
private val contentView by lazy(LazyThreadSafetyMode.NONE) {
LinearLayout {
layout_width = match_parent
layout_height = match_parent
orientation = vertical
background_color = "#0C0D14"
fitsSystemWindows = true// 搜索条
ConstraintLayout {
layout_width = match_parent
layout_height = wrap_content
// 返回按钮
ivBack = ImageView {
layout_id = "ivSearchBack"
layout_width = 7
layout_height = 14
scaleType = scale_fit_xy
start_toStartOf = parent_id
top_toTopOf = parent_id
margin_start = 22
margin_top = 11
src = R.drawable.search_back
onClick = { finish() }
}
// 搜索框背景
vInputBg = View {
layout_id = "vSearchBarBg"
layout_width = 0
layout_height = 36
start_toEndOf = "ivSearchBack"
align_vertical_to = "ivSearchBack"
end_toStartOf = ID_SEARCH
margin_start = 19.76
margin_end = 16
// 轻松定义圆角背景,省去新增一个 xml
shape = shape {
corner_radius = 54
solid_color = "#1AB8BCF1"
}
}
// 搜索框放大镜icon
ImageView {
layout_id = "ivSearchIcon"
layout_width = 16
layout_height = 16
scaleType = scale_fit_xy
start_toStartOf = "vSearchBarBg"
align_vertical_to = "vSearchBarBg"
margin_start = 16
src = R.drawable.template_search_icon
}
// 搜索框
etSearch = EditText {
layout_id = "etSearch"
layout_width = 0
layout_height = wrap_content
start_toEndOf = "ivSearchIcon"
end_toStartOf = ID_CLEAR_SEARCH
align_vertical_to = "vSearchBarBg"
margin_start = 7
margin_end = 12
textSize = 14f
textColor = "#F2F4FF"
imeOptions = EditorInfo.IME_ACTION_SEARCH
hint = "输入您想搜索的模板"
hint_color = "#686A72"
background = null
maxLines = 1
inputType = InputType.TYPE_CLASS_TEXT
}
// 搜索框尾部清空按钮
ivClear = ImageView {
layout_id = "ivClearSearch"
layout_width = 20
layout_height = 20
scaleType = scale_fit_xy
align_vertical_to = "vSearchBarBg"
end_toEndOf = "vSearchBarBg"
margin_end = 12
src = R.drawable.template_search_clear
// 搜索按钮
tvSearch = TextView {
layout_id = "tvSearch"
layout_width = wrap_content
layout_height = wrap_content
textSize = 14f
textColor = "#686A72"
text = "搜索"
gravity = gravity_center
align_vertical_to = "ivSearchBack"
end_toEndOf = parent_id
margin_end = 16
}
}
// 搜索体
FragmentContainerView {
layout_id = NAV_HOST_ID
layout_width = match_parent
layout_height = match_parent
NavHostFragment.create(R.navigation.search_navigation).also {
supportFragmentManager.beginTransaction()
.replace(NAV_HOST_ID.toLayoutId(), it)
.setPrimaryNavigationFragment(it)
.commit()
}
}
}
}override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
initView()
}private fun initView() {
// 绘制界面初始状态
tvSearch?.apply {
isEnabled = false
textColor = "#484951"
}
ivClear?.visibility = gone
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
}
https://github.com/wisdomtl/Layout_DSL
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/search_navigation"
app:startDestination="@id/SearchHistoryFragment">
<!--联想页-->
<fragment
android:id="@+id/SearchHintFragment"
android:name="com.bilibili.studio.search.template.fragment.SearchHintFragment"
android:label="search_hint_fragment">
<!--跳转结果页-->
<action
android:id="@+id/action_to_result"
app:destination="@id/SearchResultFragment" />
<!--跳转历史页-->
<action
android:id="@+id/action_to_history"
app:popUpTo="@id/SearchHistoryFragment" />
</fragment>
<!--历史页-->
<fragment
android:id="@+id/SearchHistoryFragment"
android:name="com.bilibili.studio.search.template.fragment.SearchHistoryFragment"
android:label="search_history_fragment">
<!--跳转结果页-->
<action
android:id="@+id/action_to_result"
app:destination="@id/SearchResultFragment" />
<!--跳转联想页-->
<action
android:id="@+id/action_to_hint"
app:destination="@id/SearchHintFragment" />
</fragment>
<!--结果页-->
<fragment
android:id="@+id/SearchResultFragment"
android:name="com.bilibili.studio.search.template.fragment.SearchResultFragment"
android:label="search_result_fragment">
<!--跳转历史页-->
<action
android:id="@+id/action_to_history"
app:popUpTo="@id/SearchHistoryFragment" />
<!--跳转联想页-->
<action
android:id="@+id/action_to_hint"
app:destination="@id/SearchHintFragment" />
</fragment>
</navigation>
findNavController(NAV_HOST_ID.toLayoutId()).navigate(
R.id.action_history_to_result, // 预定义在 xml 中的 action
bundleOf("keywords" to event.keyword) // 携带跳转参数
)3 支离破碎的刷新
class TemplateSearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
initView()
}private fun initView() {
// 绘制界面初始状态
tvSearch.apply {
isEnabled = false
textColor = "#484951"
}
ivClear.visibility = gone
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
}
class TemplateSearchActivity : AppCompatActivity() {
private fun initView() {
// 初始化时立刻被执行
tvSearch.apply {
isEnabled = false
textColor = "#484951"
}
ivClear.visibility = gone
// 监听输入框字符变化
etSearch.addTextChangedListener(object :TextWatcher{
override fun beforeTextChanged(s: CharSequence?,s: Int,c: Int,after: Int){}
// 将来的某个时间点被执行的逻辑
override fun onTextChanged(char: CharSequence?, s: Int, b: Int, c: Int) {
val input = char?.toString() ?: ""
// 显示 X,并高亮搜索
if(input.isNotEmpty()) {
ivClear.visibility = visible
tvSearch.apply {
textColor = "#F2F4FF"
isEnabled = true
}
}
// 隐藏 X,并置灰搜索
else {
ivClear.visibility = gone
tvSearch.apply {
textColor = "#484951"
isEnabled = false
}
}
}override fun afterTextChanged(s: Editable?) {}
})KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
}
4 名不副实的子程序
class TemplateSearchActivity : AppCompatActivity() {
private fun initView() {
tvSearch.apply {
isEnabled = false
textColor = "#484951"
}
ivClear.visibility = gone
etSearch.addTextChangedListener(object :TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}override fun onTextChanged(char: CharSequence?, start: Int, before: Int, count: Int) {
val input = char?.toString() ?: ""
if(input.isNotEmpty()) {
ivClear.visibility = visible
tvSearch.apply {
textColor = "#F2F4FF"
isEnabled = true
}
}else {
ivClear.visibility = gone
tvSearch.apply {
textColor = "#484951"
isEnabled = false
}
}
}override fun afterTextChanged(s: Editable?) {
}
})
// 监听键盘搜索按钮
etSearch.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val input = etSearch.text.toString() ?: ""
if(input.isNotEmpty()) { searchAndHideKeyboard() }
true
} else false
}
// 监听搜索条搜索按钮
tvSearch.setOnClickListener {
searchAndHideKeyboard()
}
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
// 跳转到搜索页 + 拉长搜索条 + 隐藏搜索按钮 + 隐藏键盘
private fun searchAndHideKeyboard() {
vInputBg.end_toEndOf = parent_id // 拉长搜索框(与父亲右边对齐)
runCatching {
findNavController(NAV_HOST_ID.toLayoutId()).navigate(
R.id.action_history_to_result,
bundleOf("keywords" to etSearch?.text.toString())
)
}
tvSearch.visibility = gone
KeyboardUtils.hideSoftInput(etSearch)
}
}
子程序应该有单一且明确的目的。—— 《代码大全》
1. 搜索框绘制效果变化
2. 输入法的显示状态变化
3. Fragment 的切换。以这样的视角做抽象就能提高代码的复用度,详细的实现细节会在后续篇章展开。
5 剪不断理还乱的耦合
class TemplateSearchActivity : AppCompatActivity() {
private fun initView() {
tvSearch?.apply {
isEnabled = false
textColor = "#484951"
}
ivClear?.visibility = gone
etSearch?.addTextChangedListener(object :TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}override fun onTextChanged(char: CharSequence?, start: Int, before: Int, count: Int) {
val input = char?.toString() ?: ""
if(input.isNotEmpty()) {
ivClear?.visibility = visible
tvSearch?.apply {
textColor = "#F2F4FF"
isEnabled = true
}
}else {
ivClear?.visibility = gone
tvSearch?.apply {
textColor = "#484951"
isEnabled = false
}
}
}override fun afterTextChanged(s: Editable?) {
}
})
etSearch?.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val input = etSearch?.text?.toString() ?: ""
if(input.isNotEmpty()) {
searchAndHideKeyboard()
}
true
} else false
}
tvSearch?.setOnClickListener {
searchAndHideKeyboard()
}
// 监听清空按钮
ivClear?.setOnClickListener {
etSearch?.text = null
etSearch?.requestFocus()
// 弹键盘
KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
// 回到历史页
backToHistory()
}
// 监听搜索框触摸事件并回到历史页
etSearch?.setOnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
backToHistory()
}
false
}KeyboardUtils.showSoftInputWithDelay(etSearch, 300)
}
// 监听系统返回并回退到历史页
override fun onBackPressed() {
super.onBackPressed()
backToHistory()
}private fun backToHistory() {
runCatching {
findNavController(NAV_HOST_ID.toLayoutId()).currentDestination?.takeIf { it.id == R.id.SearchResultFragment }?.let {
// 还原搜索框和搜索按钮
tvSearch?.visibility = visible
vInputBg?.end_toStartOf = "tvSearch"
}
// 弹出结果页回到历史页
findNavController(NAV_HOST_ID.toLayoutId()).navigate(R.id.action_to_history)
}
}
}
6 总结
The View observes the Model for changes. —— Trygve Reenskaug
如果有人在阅读你的代码时不得不搜索整个应用程序以便找到所需的信息,那么就应该重新组织你的代码了。——《代码大全》