导入Room缓存数据
当有网络的时候从网络数据库中获取,无网络的情况下直接从本地数据库中获取。
创建本地数据库的数据表RecipeEntity
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
@Entity(tableName = "foodRecipeTable")
class RecipeEntity(
@PrimaryKey(autoGenerate = true)
val id:Int,
val type:String,
val recipe: FoodRecipe
)
app还存在收藏喜欢的食谱的功能
创建收藏的数据表
@Entity(tableName = "favorite_table")
data class FavoriteEntity(
@PrimaryKey(autoGenerate = true)
val id:Int,
val recipe: Result
)
创建接口Dao
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
@Dao
interface RecipeDao {
//插入数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipeEntity: RecipeEntity)
//查询数据
@Query("select * from foodRecipeTable where type=:type")
fun getRecipes(type:String):Flow<List<RecipeEntity>>
//更新数据
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateRecipe(recipeEntity: RecipeEntity)
/**------------favorite----------*/
//查询所有收藏的食谱
@Query("select * from favorite_table")
fun getAllFavorites():Flow<List<FavoriteEntity>>
//插入收藏的食谱
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFavorite(favoriteEntity: FavoriteEntity)
//删除收藏
@Delete
suspend fun deleteFavorite(favoriteEntity: FavoriteEntity)
}
使用单例设计模式创建数据库对象
@TypeConverters(RecipeTypeConverter::class)
@Database(entities = [RecipeEntity::class,FavoriteEntity::class],
version = 1,
exportSchema = false)
abstract class RecipeDatabase :RoomDatabase(){
abstract fun getRecipeDao():RecipeDao
companion object{
@Volatile
private var instance:RecipeDatabase?=null
fun getInstance(context: Context):RecipeDatabase{
if (instance!=null){
return instance!!
}
synchronized(this){
if (instance == null){
instance = Room.databaseBuilder(context,RecipeDatabase::class.java,
"food_recipe.db").build()
}
return instance!!
}
}
}
}
提供本地数据库入口
class LocalRepository(context: Context){
private val recipeDao = RecipeDatabase.getInstance(context).getRecipeDao()
//插入数据
suspend fun insertRecipe(recipeEntity: RecipeEntity){
recipeDao.insertRecipe(recipeEntity)
}
//查询数据
fun getRecipes(type:String): Flow<List<RecipeEntity>>{
return recipeDao.getRecipes(type)
}
//更新数据
suspend fun updateRecipe(recipeEntity: RecipeEntity){
recipeDao.updateRecipe(recipeEntity)
}
//查询所有收藏的食谱
fun getAllFavorites():Flow<List<FavoriteEntity>>{
return recipeDao.getAllFavorites()
}
//插入收藏的食谱
suspend fun insertFavorite(favoriteEntity: FavoriteEntity){
recipeDao.insertFavorite(favoriteEntity)
}
//删除收藏
suspend fun deleteFavorite(favoriteEntity: FavoriteEntity){
recipeDao.deleteFavorite(favoriteEntity)
}
}
创建类型转换器 ->因为本地数据库只能识别常规类型的数据,所以当我们存储非常规类型数据时需要将其转换为数据库能识别的数据,当我们取出数据显示时就将其转换为我们需要的数据类型。
FoodRecipe类型
class RecipeTypeConverter {
//foodRecipe -> String
@TypeConverter
fun foodRecipeToString(recipe:FoodRecipe):String{
return Gson().toJson(recipe)
}
//String -> foodRecipe
@TypeConverter
fun stringToFoodRecipe(string: String):FoodRecipe{
return Gson().fromJson(string,FoodRecipe::class.java)
}
@TypeConverter
fun resultToString(recipe:Result):String{
return Gson().toJson(recipe)
}
@TypeConverter
fun stringToResult(str:String):Result{
return Gson().fromJson(str,Result::class.java)
}
}
Flow
Flow的作用与LiveData几乎一样
Flow默认是受阻塞的(suspend)
Flow的优点:当Flow中有数据时就会通过emit()把数据发射出去,接受数据使用collect。
这里还需做一下优化,当第一次从网络获取数据后就保存数据,下一次访问相同食谱信息时就优先从本地数据库中获取,使获取数据的速度更快。详情代码见https://www.jianshu.com/p/0e2cd186c7c4
进入详情页面
将每个页面的与bottomNavigationView关联
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHost = supportFragmentManager.findFragmentById(R.id.fragmentContainerView)
as NavHostFragment
val navController = navHost.navController
binding.bottomNavigationView.setupWithNavController(navController)
}
}
创建详情页DetailFragment以及另外两个详情页中的板块
image.png
创建页面切换动画并添加到navigation中
切换动画
image.png
给每一个Item添加点击事件
添加点击事件在navigation中detailfragment添加argument ->参数
image.png接受传递过来的参数
参数定义 传递参数详情页面搭建
enable属性:可以取消控键的点击视觉效果 enable
实现滑动控键效果
如下图,点击Detail将显示美食的详细信息,点击Ingredient将展示制作该美食所需要的食材。同时上方的按钮会随着两个板块之间的交换而滑动。
实现思路:最底层是一个Chip控键(这个控键的好处就是本身就具有圆形左右边框,不需要自己创建shape来实现),Detial和Ingredient两个TextView平分底层宽度,中间有个移动的Chip,然后我们就只需要在DetailFragment中设置点击事件以及动画就可以实现上述效果。
ViewPager2的使用
1、创建两个Fragment ->SummerFragment、IngredientFragment
2、在详情页面的xml文件中拖入viewPager2的控件、并进行布局
3、创建viewPager的Adapter:需要两个重要变量 ->FragmentManager、Lifecycle,除此之外还需要一个属性变量放入如之前创建的Fragment
class ViewPagerAdapter(
private val fragments:List<Fragment>,
fm:FragmentManager,
lifecycle:Lifecycle
):FragmentStateAdapter(fm,lifecycle) {
override fun getItemCount(): Int {
return fragments.size
}
override fun createFragment(position: Int): Fragment {
return fragments[position]
}
}
4、设置viewPager显示页面 -> currentItem
currentItem
5、当滑动下方viewPager的时候,上方的Button也需要移动->需要监听事件
因为我们这里只有两个viewPager,所以使用if判断position即可,有很多viewPager的话可以使用when。
6、如果想设置下方Button跟着viewPager一起滑动的话可以使用registerOnPagerChangedCallback里面的onPageScrolled方法
这里提供一下思路->我们需要两个数据:按钮板块的宽度,viewPager的宽度。需要进行百分比的转换:上方按钮移动百分比距离下方viewPager跟着移动百分比的距离。使用positionOffset传值。
DetailFragment中含有的方法简介
indicatorAnim:滑动动画
selectDetail:点击Detail发生的变化,包括动画以及viewPager 的变化
selectIngredient:点击Ingredient发生的变化,包括动画以及viewPager 的变化
initEvent:每个按钮的点击事件
viewPager.registerOnPageChangeCallback:对viewPager滑动操作时,对应的变化,也就是上方按钮跟着滑动。
initViewPager:对ViewPager进行配置
class DetailFragment:Fragment() {
private lateinit var binding:FragmentDetailBinding
private val recipeArgs:DetailFragmentArgs by navArgs()
private val favoriteViewModel:FavoriteViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDetailBinding.inflate(inflater)
binding.detailBtn.isSelected = true
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recipe = recipeArgs.recipe
binding.executePendingBindings()
initEvent()
initViewPager()
favoriteViewModel.readFavorites()
favoriteViewModel.favoriteRecipes.observe(viewLifecycleOwner) {
it.forEach {entity->
if (entity.recipe == recipeArgs.recipe){
binding.collectBtn.isSelected = true
return@forEach
}
}
}
}
private fun indicatorAnim(value:Float){
binding.indicatorView.animate()
.translationX(value)
.setDuration(300)
.start()
}
private fun initViewPager(){
val fragments = listOf(SummerFragment(recipeArgs.recipe.summary),IngredientFragment(recipeArgs.recipe.extendedIngredients))
binding.viewPager.adapter = ViewPagerAdapter(fragments,requireActivity().supportFragmentManager,lifecycle)
}
private fun initEvent(){
binding.backBtn.setOnClickListener{
requireActivity().onBackPressed()
}
binding.detailBtn.setOnClickListener {
selectDetail()
binding.viewPager.currentItem = 0
}
binding.ingredientBtn.setOnClickListener {
selectIngredient()
binding.viewPager.currentItem = 1
}
binding.collectBtn.setOnClickListener {
if (binding.collectBtn.isSelected){
//从数据库收藏表中删除这个食谱
favoriteViewModel.favoriteRecipes.value?.forEach{entity->
if (entity.recipe == recipeArgs.recipe){
favoriteViewModel.deleteFavorite(entity)
binding.collectBtn.isSelected = false
}
}
}else{
//插入数据表中
favoriteViewModel.insertFavorite(recipeArgs.recipe)
binding.collectBtn.isSelected = true
}
}
binding.viewPager.registerOnPageChangeCallback(object:ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
if (position==0){
selectDetail()
}else{
selectIngredient()
}
}
})
}
private fun selectDetail(){
if (!binding.detailBtn.isSelected){
binding.detailBtn.isSelected = true
binding.ingredientBtn.isSelected = false
indicatorAnim(0f)
}
}
private fun selectIngredient(){
if (!binding.ingredientBtn.isSelected){
binding.ingredientBtn.isSelected = true
binding.detailBtn.isSelected = false
val space = binding.ingredientBtn.x - binding.detailBtn.x
indicatorAnim(space)
}
}
}
summary板块的搭建
外层容器直接使用LinearLayout也可
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frameLayout3"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.recipe.detail.SummerFragment" >
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/summaryTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="summary" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
如果想关闭ScrollView旁边纵向的的滚动条可配置这个属性,如果你的ScrollView是横向显示的同理。
image.pngSummaryFragment
由于我们这里需要的数据是固定的,不用更改所以直接传递参数即可。如果需要数据的实时更改使用ViewModel即可。
但是我们不能直接将从网络获取的数据显示到summaryTextView.text上,因为我们从网络上获取的数据还带有html语言的字符,这时就需要一个解析工具Jsoup。
Jsoup的依赖
implementation 'org.jsoup:jsoup:1.13.1'
class SummerFragment(private val summary:String) : Fragment() {
private lateinit var binding:FragmentSummerBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FragmentSummerBinding.inflate(inflater)
binding.summaryTextView.text = Jsoup.parse(summary).text()
return binding.root
}
}
网友评论