Skip to content

🔥 Eyepertizer 基于 Kotlin 语言仿写「开眼 Eyepetizer」的一个短视频 Android 客户端项目

Notifications You must be signed in to change notification settings

YoungTr/Eyepertizer

Repository files navigation

Eyepertizer

0 简介

仿照Eyepetizer,基于 Kotlin 语言仿写「开眼 Eyepertizer」的一个短视频 Android 客户端项目,主要对一些新的技术、主流框架学习加深印象。

效果图

app_02.png app_03.png app_04.png
71.jpeg 73.jpeg 74.jpeg
75.jpeg 76.jpeg 77.jpeg

1、MVP

MVP 将应用程序分为三个基本组成部分:

  1. Model:它负责处理应用程序的数据部分。
  2. View:它负责在屏幕上用特定的数据铺设视图。
  3. Presenter:它是连接模型和视图的桥梁。它还充当View的指导者。

MVP为上述组件制定了一些基本规则,如下所列:

  1. View 的唯一责任是按照 Presenter 的指示绘制用户界面。它是应用程序的一个哑巴部分。
  2. View 将所有的用户交互委托给它的 Presenter 。
  3. View 从不与 Model 直接通信。
  4. Presenter 负责将 View 的要求委托给 Model,并指示 View 对特定事件进行操作。
  5. Model 负责从服务器、数据库和文件系统获取数据。

序言

  1. Activity, Fragment, and a CustomView 作为应用程序的 View 部分。
  2. 每个 View 都有一个一对一关系的 Presenter。
  3. View 通过一个接口与它的 Presenter 进行通信,反之亦然。
  4. Model 被分成几个部分。ApiHelper, PreferenceHelper, DatabaseHelper, 和 FileHelper。这些都是DataManager的助手,它实质上是绑定了所有的 Model 部分。
  5. Presenter 通过一个接口与 DataManager 进行通信。
  6. DataManager 只在被要求时提供服务。
  7. Presenter 不能访问 Android 的任何 apis

架构图

  • View:它是应用的一部分,渲染用户界面并接受来自用户的交互。Activity,Fragment 和 CustomView 构成这一部分。
  • MvpView:它是一个接口,由 View 实现。它通过被暴露的方法和 Presenter 进行通信。
  • Presenter:它是 View 的决策对应物,是一个纯 Java 类,不能访问 Android 的 API。它接收 View 传递过来的用户交互,然后根据业务逻辑进行决策,最后指示 View 执行特定的动作。它还与 DataManager 进行通信,以获得执行业务逻辑所需的任何数据。
  • MvpPresenter:它是一个接口,由 Presenter 实现。它暴露方法与 View 进行通信。
  • AppDbHelper:数据库管理和应用中所有与数据库有关的数据处理都是在这一部分完成。
  • DbHelper:它是一个由 AppDbHelper 实现的接口,包含了给应用程序调用的方法。该层对 DbHelper 的任何具体实现进行解耦,使 AppDbHelper 成为即插即用的单元。
  • AppPreferenceHelper:与 AppDbHelper 类型,可以从 SharedPreferences 中读写数据。
  • PreferenceHelper:与 DbHelper 接口类似,由 AppPreferenceHelper 实现。
  • AppApiHelper:它负责管理网络 API 调用和 API 数据处理。
  • ApiHelper:它是一个与DbHelper一样的接口,但由AppApiHelper实现。
  • DataManager:它是一个由 AppDataManager 实现的接口。它包含所有数据处理操作的方法。理想情况下,它委托所有帮助者类提供的服务。为此,DataManager 接口扩展了DbHelper、PreferenceHelper 和 ApiHelper 接口。
  • AppDataManager:它是应用程序中任何数据相关操作的一个联系点。DbHelper, PreferenceHelper, 和 ApiHelper 只对 DataManager 起作用。它将所有特定的操作委托给任何 Helper。

Interactor & Repository

1、对于大型项目,DataManager 和 DbHelper 往往会变得非常大。所有需要对他们进行分散化,DataManager 可以分散化为 Interactor,DbHelper 可以分散化为 Repositories。

  • Interactor:每个 Presenter 有自己的 Interactor 实例,代替单例的 DataManager。Interactor 的职责与 DataManager 相同,它只关注自己所需的数据访问和调用。
  • Repository:DbHelper 被分解为 Repositories。例如,用户表的所有数据库查询都是通过 UserRepository 而不是 DbHelper 进行的。UserRepository ,与 DbHelper 类似,通过 UserDao 进行所有相关的查询。一个重要的区别是,Respository 是按需实例化的,每个 Interactor 会得到一个新的 Repository 示例,而不是单例的 DbHelper。

2、MVVM

MVVM架构是一个 Model-View-ViewModel 架构,它消除了每个组件之间的紧密耦合。最重要的是,在这个架构中,子代没有对父代的直接引用,他们只有通过观察物的引用。

  • Model:负责 Android 应用程序的数据和业务逻辑,包括本都和远程数据源、模型类、资源库等。
  • View:由 UI 代码(Activity,Fragment),XML 组成。它向 ViewModel 发送用户动作,当并不直接获得响应。必须订阅 ViewModel 才能获得相应。
  • ViewModel:它是 View 和 Model(业务逻辑)之间的一座桥梁,不会对 View 的直接引用,ViewModel 不知道正在与哪个 View 交互。它与 Model 交互,并暴露出可以被 View 观察到的可观察对象。

工程结构

MVVM Architecture - Android Tutorial for Beginners - Step by Step Guide

3、Dagger2

为什么需要依赖注入?

1、依赖注入建立在控制反转的概念之上,一个类应该从外部获得它的依赖关系,任何类不应该直接实例化另一个类,应该从配置类中获取实例。

2、如果一个类通过 new 操作符创建了另一个类的实例,那么这个类就不能独立于该类使用和测试,这被称为硬依赖(hard dependency)。

3、从类外提供依赖关系,增加了重复使用类的可能性,并且能够独立于其他类进行测试。

注入方式

  1. Constructor Injection:注入方法参数
  2. Field Injection:注入成员变量(不能是 private)
  3. Method Injection:注入方法参数
  • Dependency provider:用 @Module 注解的类负责提供可以被注入的对象。这些类定义了用 @Provides 注解的方法。从这些方法返回的对象可用于依赖性注入。
  • Dependency consumer@Inject 注解用来定义一个依赖关系。
  • Connecting consumer and producer:一个 @Component 注释的接口定义了对象(modules)的 provider 和表达依赖关系的对象之间的连接。这种连接类是由 Dagger 生成的。

Qualifier & Scop

1、@Qualifier 注解用于限定依赖关系。例如,一个类可以同时需要 Application Context 和 Activity Context。这两个对象都是 Context 类型的。Dagger 想要知道提供哪个变量,就需要明确指定他的标识符。因此,@Qualifier 被用来区分相同类型但具有不同实例的对象。

2、@Scope 同样用于自定义注解,我能可以通过@Scope自定义的注解来限定注解作用域,实现局部的单例;

Component

依赖关系(组件依赖)

AppComponent 持有一个全局的 Context 对象:

@Component(modules = [AppModule::class])
interface AppComponent {
    fun inject(context: Context)
    fun context(): Context
}
@Module
class AppModule(private val context: Context) {
    @Provides
    fun provideContext(): Context = context
}

ActivityComponent 需要使用 AppComponent 中的 Context 对象:

// 声明了依赖关系
@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {
    fun inject(activity: Activity)
}
@Module
class ActivityModule {
    @Provides
    fun provideSp(context: Context) = context.getSharedPreferences("Cooker", Context.MODE_PRIVATE)
}

依赖注入:

val appComponent = DaggerAppComponent.builder()
    .appModule(AppModule(this))
    .build()
appComponent.inject(this)

val activityComponent = DaggerActivityComponent.builder().appComponent(appComponent)
    .build()
activityComponent.inject(this)

ActivityComponent 声明依赖了 AppComponent,AppComponent 拥有 AppModule 中有可以提供 context 的 Provides,因此 ActivityModule 可以从 AppModule 拿到 context。

包含关系(组件继承)

声明继承需要如下几步:

  1. 子 Component 用 @SubComponent 注解;
  2. 子 Component 声明一个 Builder 来告诉父 Component 如何创建自己;
  3. 父 Component 对应的 Module 用 subcomponent 属性指明拥有哪些子 Component;
  4. 父 Component 声明一个抽象方法来获取子 Component 的 Builder。
// 子 Component 用 @SubComponent 注解
@Subcomponent(modules = [ActivityModule::class])
interface ActivityComponent {

    fun inject(activity: MainActivity)

    // 声明一个 Builder 来告诉父 Component 如何创建自己
    @Subcomponent.Builder
    interface Builder {
        fun build(): ActivityComponent
    }
}
// 父 Component 对应的 Module 用 subcomponents 属性指定拥有哪些子 Component
@Module(subcomponents = [ActivityComponent::class])
class AppModule(private val context: Context) {
    @Provides
    fun provideContext(): Context = context
}
@Component(modules = [AppModule::class])
interface AppComponent {
    fun inject(context: Context)

    //    fun context(): Context 不再需要

    // 父 Component 声明一个抽象方法来获取子 Component 的 Builder
    fun activityComponent(): ActivityComponent.Builder
}

依赖注入:

val appComponent = DaggerAppComponent.builder()
    .appModule(AppModule(this))
    .build()
    appComponent.inject(this)

val activityComponent = appComponent.activityComponent()
    .build()
activityComponent.inject(this)

dagger.android 扩展

  1. 在 AppComponent 中安装 AndroidInjectionModule,确保包含四大组件和 Fragment 的注入器类型
@Singleton
@Component(modules = [AndroidInjectionModule::class, ActivityBindModule::class, AppModule::class])
interface AppComponent {
    fun inject(application: EyepertizerApplication)
}
  1. 在一个抽象 Module 中添加一个使用 @ContributesAndroidInjector 注解标记的返回具体的 Activity 类型的抽象方法
@Module
abstract class ActivityBindModule {

    @ContributesAndroidInjector
    abstract fun splashActivityInjector(): SplashActivity

    @ContributesAndroidInjector
    abstract fun mainActivityInjector(): MainActivity
}
  1. Application 类实现 HasAndroidInjector 接口,并且注入一个 DispatchingAndroidInjector 类型的依赖作为 activityInjector() 方法的返回值。
class EyepertizerApplication : Application(), HasAndroidInjector {

    @Inject
    lateinit var dispatchingActivityInjector: DispatchingAndroidInjector<Any>


    companion object {
        @SuppressLint("StaticFieldLeak")
        lateinit var context: Application
    }

    override fun onCreate() {
        super.onCreate()
        context = this
        val component = DaggerAppComponent.builder().appModule(AppModule(this)).build()
        component.inject(this)

    }

    override fun androidInjector(): AndroidInjector<Any> = dispatchingActivityInjector
}
  1. 在 onCreate)() 方法中,在 super.onCreate() 之前调用 AndroidInjection.inject(this)
abstract class BaseActivity : AppCompatActivity(), MvpView {

    override fun onCreate(savedInstanceState: Bundle?) {
        performDI()
        super.onCreate(savedInstanceState)
    }

    private fun performDI() {
        AndroidInjection.inject(this)
    }
}

https://johnnyshieh.me/posts/dagger-subcomponent/

https://juejin.cn/post/6844904201219211272

4、coroutines

什么是协程

Coroutines = Co + Routines

Co means cooperation and Routines means functions.

Coroutines 的确切定义:一个以更高性能和更简单的方式管理并发的框架,它的轻量级线程是写在实际线程框架之上的,通过利用函数的合作性质来获得最大的好处。

为什么需要协程

解决回调嵌套

suspend fun fetchAndShowUser() {
  val user = fetchUser()	// fetch on IO thread
  showUser(user)					// back on UI thread
}
suspend fun fetchUser(): User {
  return GlobalScope.async(Dispatchs.IO) {
    // make network call
    // return user
  }.await()
}
  • Dispathers:Dispatchers帮助coroutines决定要在哪个线程上完成工作。IODefaultMain
  • suspend:suspend 方法可以启动、暂停和恢复。

suspend 函数只允许从一个coroutine或另一个suspend函数中调用。

在 kotlin 中有两个函数可以启动协程:

  • launch{}
  • async{}

launch{} 不返回任何东西,而 async{} 返回一个 Deferred 的实例,它有一个 await() 函数来返回 coroutine 的结果,就像我们在Java中的future,我们做future.get()来获取结果。

launch

suspend fun fetchUserAndSaveInDatabase() {
  // fetch user from network
  // save user in database
  // and do not returen anything
}
GlobalScope.launch(Dispatchers.Main) {
  fetchUserAndSaveInDatabase()	// do on IO thread
}

async

suspend fun fetchFirstUser(): User {
  // make network call
  // return user
}

suspend fun fetchSecondUser(): User {
  // make network call
  // return user
}
GlobalScope.launch(Dispatchers.Main) {
  val userOne = async(Dispatchers.IO) { fetchFirstUser() }
  val userTwo = asynce(Dispatcher.IO) { fetchSecondUser() }
  showUsers(userOne.await(), userTwo.await()) // back on UI thread
}

withContext 另一种写异步的方式,不必写 await()

suspend fun fetchUser(): User {
  return withContext(Dispatchers.IO) {
    // make network call
    // return user
  }
}
GlobalScope.launch(Dispatchers.Main) {
    val userOne = withContext(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = withContext(Dispatchers.IO) { fetchSecondUser() }
    showUsers(userOne, userTwo) // back on UI thread
}

当使用withContext时,它将串行而不是并行。

Kotlin Coroutines中的作用域

一旦 Activity 被销毁,后台任务就应该被取消。

class MainActivity : AppCompatActivity(), CoroutineScope {

    lateinit var job: Job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // create the job
        setContentView(R.layout.activity_main)

        launch {

        }
    }

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.IO + job


    override fun onDestroy() {
        job.cancel()    // cancel the job
        super.onDestroy()
    }
}

当需要全局范围,也就是我们的应用程序范围,而不是 Activity 范围时,我们可以使用GlobalScope,如下所示。

GlobalScope.launch(Dispatchers.Main) {
    val userOne = async(Dispatchers.IO) { fetchFirstUser() }
    val userTwo = async(Dispatchers.IO) { fetchSecondUser() }
}

Kotlin Coroutines中的异常处理

launch

try-catch

GlobalScope.launch(Dispatchers.Main) {
    try {
        fetchUserAndSaveInDatabase() // do on IO thread and back to UI Thread
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

handler

val handler = CoroutineExceptionHandler { _, exception ->
    Log.d(TAG, "$exception handled !")
}
GlobalScope.launch(Dispatchers.Main + handler) {
    fetchUserAndSaveInDatabase() // do on IO thread and back to UI Thread
}

async

val deferredUser = GlobalScope.async {
    fetchUser()
}
try {
    val user = deferredUser.await()
} catch (exception: Exception) {
    Log.d(TAG, "$exception handled !")
}

Mastering Kotlin Coroutines In Android - Step By Step Guide

Android开发中异常处理

launch {
  try {
    val users = getUsers()
    val moreUesrs = getMoreUsers()
  } catch(exception: Exception) {
    println("$exception handled !")
  }
}

如果其中一个网络调用失败,它将直接转到 catch

假设想为失败的网络调用返回一个空列表,但是继续其他网络调用的响应。可以在各个网络调用中加入try-catch块,如下所示:

launch {
  try {
    val user = try {
      getUsers()
    } catch (e: Exception) {
      emptyList<User>()
    }
    val moreUsers = try {
      getMoreUsers()
    } catch (e: Exception) {
      emptyList<User>()
    }
  } catch (exception: Exception) {
    println("$exception handled !")
  }
}

并行网络调用:

launch {
    try {
        val usersDeferred = async {  getUsers() }
        val moreUsersDeferred = async { getMoreUsers() }
        val users = usersDeferred.await()
        val moreUsers = moreUsersDeferred.await()
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

在这里,我们将面临一个问题,如果任何网络错误出现,应用程序将崩溃!,它不会进入 catch

为了解决这个问题,我们必须像下面那样使用 coroutineScope

launch {
    try {
        coroutineScope {
            val usersDeferred = async {  getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = usersDeferred.await()
            val moreUsers = moreUsersDeferred.await()
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

但是,假设我们想为失败的网络调用返回一个空列表,并继续其他网络调用的响应。我们将不得不使用supervisorScope,并像下面那样为各个网络调用添加try-catch块。

launch {
    try {
        supervisorScope {
            val usersDeferred = async { getUsers() }
            val moreUsersDeferred = async { getMoreUsers() }
            val users = try {
                usersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
            val moreUsers = try {
                moreUsersDeferred.await()
            } catch (e: Exception) {
                emptyList<User>()
            }
        }
    } catch (exception: Exception) {
        Log.d(TAG, "$exception handled !")
    }
}

结论

  • 在不使用 async 的情况下,我们可以使用 try-catchCoroutineExceptionHandler
  • 在使用 asynce 时,除了 try-catch 之外,还可以使用 coroutineScopesupervisorScope
  • 对于 async ,当你想在一个或一些任务失败时继续执行其他任务时,除了顶层的 try-catch 之外,还需要使用 supervisorScope 和每个任务的单独 try-catch
  • 对于 async,当你不想在任何任务失败后继续执行其他任务时,可以使用 coroutineScope 和顶级的 try-catch

主要的区别是,只要它的任何一个子任务失败,coroutineScope 就会取消。如果我们想在一个任务失败时继续执行其他任务,我们就用supervisorScope。SupervisorScope不会在其中一个失败时取消其他子任务。

4、ROOM

优势

  • 对查询进行编译时验证
  • 减少模板代码
  • 易于理解和使用
  • 与RxJava、LiveData和Kotlin Coroutines轻松集成

ROOM 组成部分

  • Database:包含数据库持有者,并作为底层连接到应用程序持久化、关系型数据的主要访问点。
  • Entity:代表数据库中的一个表。
  • DAO:包含用于访问数据库的方法

Database

  • 继承 RoomDatabase 的抽象类
  • 注解中包括与数据库相关实例的 entities 列表
  • 包含一个抽象方法,该方法没有参数,并返回带有 @Dao 注释的类
  • 可以调用 Room.databaseBuilder()Room.inMemoryDatabaseBuilder() 来获得一个 Database 实例
@Database(entities = [SearchHistory::class], version = 1)
abstract class SearchDatabase : RoomDatabase() {

    abstract fun searchDao(): SearchDao
}

Entity

一个实体代表数据库中的一个表。这个类用 @Entity 注解。该类中的数据成员代表表内的列。

@Entity
data class SearchHistory(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "value") val value: String
)
  • Entity 的所有字段都必须是 public 的或者有 getter 和 setter 方法
  • Entity 类应该有一个空的构造方法或一个带有所有字段的参数化构造函数,Room 也可以使用部分的构造函数
  • 每个实例必须只是有一个主键,可以使用 @PrimaryKey 注释来定义单个字段的主键,或者使用 primaryKeys 属性定义多个字段
@Entity(primaryKeys = arrayOf("id", "value"))
  • 默认情况下,Room 使用类的名称作为数据库的表。@Entity 注解的 tableName 可以定义表的名字,@ColumnInfo 注解的 name 属性定义列的名称。
@Entity(tableName = "users")
  • 如果不想持久化某些字段,可以使用 @Ignore 注解
@Ignore val picture: Bitmap?
  • 可以使用 @Entity 注解的 indices 属性来为实体添加索引。另外,可以通过将@Index 注解的 unique 属性设置为 true 来创建唯一的索引。
@Entity(indices = arrayOf(Index(value = ["last_name", "address"])))@Entity(indices = arrayOf(Index(value = ["first_name", "last_name"], unique = true)))

Data Access Object (DAO)

DAO 提供了一个访问数据库的 API。这是一个被 @Dao 注解的接口。这个接口中的所有方法都用于从数据库中获取数据或对数据库进行修改。这些方法被注解为 @Query, @Insert, @Delete。

@Dao
interface SearchDao {

    @Query("SELECT * FROM search_history")
    fun getAll(): List<SearchHistory>

    @Query("SELECT * FROM search_history WHERE id IN (:searchIds)")
    fun loadAllByIds(searchIds: IntArray): List<SearchHistory>

    @Insert
    fun insertAll(vararg searches: SearchHistory)

    @Delete
    fun delete(search: SearchHistory)
}

Introduction to Room Persistent Library in Android

Data Access Objects - DAO in Room

Todo

THANKS

https://github.com/MindorksOpenSource
https://github.com/VIPyinzhiwei/Eyepetizer