零基础学习到第一行代码第三版天气预报实战第一阶段:搜索城市数据,由于废弃,强行写完代码,有以下个问题
①在AS虚拟机运行,搜索框只能输入数字,有查询结果,无法切换英文或中文输入
②安装到手机运行,搜索框输入英文或中文无反应,依旧只能输入数字查询,
③强行写完代码有太多的?.操作,感觉不对劲。我有尝试把override fun onActivityCreated替换为override fun onAttach,直接启动不了,看文档好像要配合onViewCreate使用,这段代码我就不懂该怎么拆分了。
附上这段代码的原文
使用MVVM架构,logic包存放业务逻辑相关代码,含dao,model ,network3个子包;ui包含
palce,weather2个子包,分别对应2个主要界面
依赖库
dependencies {
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.fragment:fragment-ktx:1.5.7'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:name=".SunnyWeatherApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SunnyWeather"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
需要引用Context的全局方式和平台的领牌值
package com.sunnyweather.android
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
class SunnyWeatherApplication : Application() {
companion object {
const val Token = "MNqVXy86vYXurQrD"
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
context = applicationContext
}
}
logic/model定义数据模型,PlaceResponse.kt
package com.sunnyweather.android.logic.model
import android.location.Location
import com.google.gson.annotations.SerializedName
data class PlaceResponse(val status: String, val places: List<Place>)
data class Place(val name: String, val location: Location,
@SerializedName("formatted_address") val address: String)
data class Location(val lng: String, val lat: String)
logic/network下定义一个用于访问天气搜索的API接口,Repository.kt
package com.sunnyweather.android.logic.network
import com.sunnyweather.android.SunnyWeatherApplication
import com.sunnyweather.android.logic.model.PlaceResponse
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface PlaceService {
@GET("v2/place?token=${SunnyWeatherApplication.Token}&lang=zh_CN")
fun searchPlaces(@Query("query") query: String): Call<PlaceResponse>
}
logic/network下的Retrofit构建器, ServiceCreator.kt
ackage com.sunnyweather.android.logic.network
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceCreator {
private const val BASE_URL = "https://api.caiyunapp.com"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
inline fun <reified T> create(): T = create(T::class.java)
}
logic/network下定义统一的网络数据访问入口,对所有网络请求的API进行封装。AS更新了版本后,
enqueue(object : Callback<T>, retrofit2.Callback<T>)
override fun onResult(result: T)
原书本没有retrofit2.Callback和onRESULT这两项,不重写onResult, object会红线报错
##SunnyWeatherNetwork.kt
package com.sunnyweather.android.logic.network
import org.chromium.base.Callback
import retrofit2.Call
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
object SunnyWeatherNetwork {
private val placeService = ServiceCreator.create<PlaceService>()
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
private suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T>, retrofit2.Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(
RuntimeException("response body is null")
)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResult(result: T) {
TODO("Not yet implemented")
}
})
}
}
}
logic仓库层,每次发起网络请求获取最新的数据, Repository.kt
package com.sunnyweather.android.logic
import androidx.lifecycle.liveData
import com.sunnyweather.android.logic.model.Place
import com.sunnyweather.android.logic.network.SunnyWeatherNetwork
import kotlinx.coroutines.Dispatchers
object Repository {
fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
val result = try {
val placeResponse = SunnyWeatherNetwork.searchPlaces(query)
if (placeResponse.status == "ok") {
val places = placeResponse.places
Result.success(places)
} else {
Result.failure(RuntimeException("response status is ${placeResponse.status}"
))
}
} catch (e: Exception) {
Result.failure<List<Place>>(e)
}
emit(result)
}
}
ui/place包下定义ViewModel, PlaceViewModel.kt
package com.sunnyweather.android.ui.place
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import com.sunnyweather.android.logic.Repository
import com.sunnyweather.android.logic.model.Place
class PlaceViewModel : ViewModel() {
private val searchLiveData = MutableLiveData<String>()
val placeList = ArrayList<Place>()
val placeLiveData = searchLiveData.switchMap() {
query -> Repository.searchPlaces(query)
}
fun searchPlaces(query: String) {
searchLiveData.value = query
}
}
ui/place包下建recycler适配器, PlaceAdapter.kt
package com.sunnyweather.android.ui.place
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import com.sunnyweather.android.R
import com.sunnyweather.android.logic.model.Place
class PlaceAdapter(private val fragment: Fragment, private val placeList: List<Place>) :
RecyclerView.Adapter<PlaceAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val placeName: TextView = view.findViewById(R.id.placeName)
val placeAddress: TextView = view.findViewById(R.id.placeAddress)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.place_item,
parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val place = placeList[position]
holder.placeName.text = place.name
holder.placeAddress.text = place.address
}
override fun getItemCount() = placeList.size
}
ui/place包下fragment, 感觉有问题的就是这里的代码没写好
PlaceFragment.kt
package com.sunnyweather.android.ui.place
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sunnyweather.android.R
class PlaceFragment : Fragment() {
private val viewModel by lazy { ViewModelProvider(this)[PlaceViewModel::class.java] }
private lateinit var adapter: PlaceAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_place, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val recyclerView = activity?.findViewById<RecyclerView>(R.id.recyclerView)
val searchPlaceEdit = activity?.findViewById<EditText>(R.id.searchPlaceEdit)
val bgImageView = activity?.findViewById<ImageView>(R.id.bgImageView)
val layoutManager = LinearLayoutManager(activity)
if (recyclerView != null) {
recyclerView.layoutManager = layoutManager
}
adapter = PlaceAdapter(this, viewModel.placeList)
if (recyclerView != null) {
recyclerView.adapter = adapter
}
searchPlaceEdit?.addTextChangedListener { editable ->
val content = editable.toString()
if (content.isNotEmpty()) {
viewModel.searchPlaces(content)
} else {
recyclerView?.visibility = View.GONE
bgImageView?.visibility = View.VISIBLE
viewModel.placeList.clear()
adapter.notifyDataSetChanged()
}
}
viewModel.placeLiveData.observe(viewLifecycleOwner, Observer { result ->
val places = result.getOrNull()
if (places != null) {
recyclerView?.visibility = View.VISIBLE
bgImageView?.visibility = View.GONE
viewModel.placeList.clear()
viewModel.placeList.addAll(places)
adapter.notifyDataSetChanged()
} else {
Toast.makeText(activity, "未能查询到任何地点", Toast.LENGTH_SHORT).show()
result.exceptionOrNull()?.printStackTrace()
}
})
}
}
fragment_place.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:windowBackground">
<ImageView
android:id="@+id/bgImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:contentDescription="@string/imageview"
android:src="@drawable/bg_place" />
<FrameLayout
android:id="@+id/actionBarLayout"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/colorPrimary">
<EditText
android:id="@+id/searchPlaceEdit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:hint="@string/address"
android:background="@drawable/search_bg"
android:autofillHints="editText"
android:inputType="numberSigned" />
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/actionBarLayout"
android:visibility="gone"/>
</RelativeLayout>
recycler子项,place_item.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="130dp"
android:layout_margin="12dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="18dp"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/placeName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"/>
<TextView
android:id="@+id/placeAddress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/placeFragment"
android:name="com.sunnyweather.android.ui.place.PlaceFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
修改fragme布局res/values/themes.xml的原生ActionBar,
parent="Theme.MaterialComponents.Light.NoActionBar"