无名之辈的Android之路

Android实战:美食项目(四)

2021-09-14  本文已影响0人  搬码人

导入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。

Flow的引入 image.png

这里还需做一下优化,当第一次从网络获取数据后就保存数据,下一次访问相同食谱信息时就优先从本地数据库中获取,使获取数据的速度更快。详情代码见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

接受传递过来的参数

参数定义 传递参数

详情页面搭建

detail_fragment.xml
enable属性:可以取消控键的点击视觉效果 enable

实现滑动控键效果

如下图,点击Detail将显示美食的详细信息,点击Ingredient将展示制作该美食所需要的食材。同时上方的按钮会随着两个板块之间的交换而滑动。
实现思路:最底层是一个Chip控键(这个控键的好处就是本身就具有圆形左右边框,不需要自己创建shape来实现),Detial和Ingredient两个TextView平分底层宽度,中间有个移动的Chip,然后我们就只需要在DetailFragment中设置点击事件以及动画就可以实现上述效果。

image.png

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。

registerOnPagerCallback

6、如果想设置下方Button跟着viewPager一起滑动的话可以使用registerOnPagerChangedCallback里面的onPageScrolled方法
这里提供一下思路->我们需要两个数据:按钮板块的宽度,viewPager的宽度。需要进行百分比的转换:上方按钮移动百分比距离下方viewPager跟着移动百分比的距离。使用positionOffset传值。

image.png

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.png

SummaryFragment
由于我们这里需要的数据是固定的,不用更改所以直接传递参数即可。如果需要数据的实时更改使用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
    }

}

未完待续
项目完整代码

上一篇下一篇

猜你喜欢

热点阅读