作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Tomasz Czura的头像

Tomasz Czura

Tomasz (MCS)是一个Android向导和团队领导. 他最喜欢的项目是做一个酒店娱乐系统的应用程序和后端.

Expertise

Previously At

Welltok
Share

你更喜欢什么:给一个架构糟糕但运行良好的应用添加新功能, 或者修复良好架构中的错误, but buggy, Android应用程序? Personally, I would definitely choose the second option. 添加新特性, even a simple one, 在应用程序中会变得非常费力吗, considering all the dependencies from everything in every class. 我记得我的一个Android项目, 一个项目经理让我添加一个小功能——比如下载数据并在新屏幕上显示它. It was an app written by one of my colleagues who found a new job. The feature should not take more than a half of a working day. 我非常乐观……

After seven hours of investigation into how the app works, 有哪些模块, 以及它们如何相互交流, 我对这个特性做了一些试验实现. It was hell. 数据模型中的一个小变化就会导致登录屏幕的大变化. 添加网络请求需要更改几乎所有屏幕的实现 GodOnlyKnowsWhatThisClassDoes class. 当将数据保存到数据库或整个应用程序崩溃时,按钮颜色的改变会导致奇怪的行为. 第二天过了一半, 我告诉了项目经理, “我们有两种方法来实现这一功能. First, 我可以再花三天的时间,最后将以一种非常肮脏的方式实现它, 每个新功能或bug修复的实现时间将呈指数级增长. 或者,我可以重写应用程序. 这将花费我两到三周的时间,但我们将为未来的应用程序更改节省时间.”“幸运的是,他同意了第二个选择. 如果我曾经怀疑为什么一个应用程序(即使是一个非常小的应用程序)中好的软件架构是重要的, 这个应用程序完全消除了它们. 但是我们应该使用哪种Android架构模式来避免这些问题呢?

在本文中,我将向您展示一个Android应用程序中的简洁架构示例. 然而,这种模式的主要思想可以适用于任何平台和语言. Good architecture should be independent of details like platform, language, database system, input, or output.

Example App

我们将创建一个简单的Android应用程序来注册我们的位置与以下功能:

  • 用户可以创建一个有名称的帐户.
  • 用户可以编辑帐号名.
  • 用户可以删除该帐号.
  • 用户可以选择激活的帐号.
  • 用户可以保存位置.
  • 用户可以看到用户的位置列表.
  • 用户可以看到用户列表.

Clean Architecture

The layers are the main core of a clean architecture. 在我们的应用程序中,我们将使用三个层:表示层、域层和模型层. 每个层应该是分开的,不需要知道其他层. 它应该存在于自己的世界中,最多共享一个小接口进行通信.

层职责:

  • Domain: 包含应用程序的业务规则. It should provide use cases which reflect the features of our app.
  • Presentation: 向用户显示数据,并收集必要的数据,如用户名. 这是一种输入/输出.
  • Model: 为我们的应用提供数据. 它负责从外部来源获取数据并将其保存到数据库中, cloud server, etc.

哪些层应该知道其他层? The simplest way to get the answer is thinking about changes. 让我们以表示层为例——我们将向用户展示一些东西. 如果我们在表示上做了一些改变,我们是否也应该在模型层上做一些改变? 假设我们有一个“User”屏幕,其中包含用户的姓名和最后位置. 如果我们想显示用户最近的两个位置,而不是只有一个, 我们的模式不应该受到影响. 所以,我们有了第一个原则: The presentation layer does not know about the model layer.

相反,模型层应该知道表示层? 不,因为如果我们改变,e.g., 从数据库到网络的数据源, 它不应该改变UI中的任何东西(如果你想在这里添加一个加载器——是的), but we can also have a UI loader when using a database). 所以这两层是完全分开的. Great!

那么领域层呢? 它是最重要的一个,因为它包含了所有主要的业务逻辑. 在将数据传递给模型层或呈现给用户之前,我们希望在这里处理数据. 它应该独立于任何其他层——它不知道关于数据库的任何事情, the network, 或者用户界面. 由于这是核心,其他层将只与这一层通信. Why do we want to have this completely independent? 业务规则的变化可能比UI设计或数据库或网络存储中的某些东西要少. We will communicate with this layer via some provided interfaces. It does not use any concrete model or UI implementation. 这些都是细节,记住细节会改变. 一个好的体系结构不受细节的约束.

现在说的理论够多了. Let’s start coding! 本文围绕代码展开,因此为了更好地理解,您应该从 GitHub 看看里面是什么. 这里创建了三个Git标签- architecture_v1, architecture_v2, and architecture_v3, 哪些与文章的部分相对应.

App Technology

In the app, I use Kotlin and Dagger 2 for dependency injection. Kotlin和Dagger 2在这里都不是必需的,但它使事情变得容易得多. You might be surprised that I do not use RxJava (nor RxKotlin), 但我觉得在这里用不上, 我不喜欢仅仅因为某个图书馆在最上面,有人说它是必须的,就使用它. 正如我所说的,语言和库是细节,所以您可以使用您想要的. 还使用了一些Android单元测试库:JUnit、robolelectric和Mockito.

Domain

在我们的Android应用程序架构设计中最重要的一层是域层. 让我们从它开始. 这是我们的业务逻辑和与其他层通信的接口所在的位置. 主要的核心是 UseCases, which reflect what the user can do with our app. 让我们为它们准备一个抽象:

abstract class UseCase {

    private var job: Deferred>? = null

    abstract suspend fun run(params: Params): OneOf

    fun execute(params: Params, onResult: (OneOf) -> Unit) {
        job?.cancel()
        job = async(CommonPool) {run(params)}
        launch(UI) {
            val result = job!!.await()
            onResult(result)
        }
    }

    打开fun cancel() {
        job?.cancel()
    }

    打开类NoParams
}

我决定在这里使用Kotlin的协程. Each UseCase 必须实现一个运行方法来提供数据. 此方法在后台线程上调用, 在收到结果之后, 它是在UI线程上传递的. 返回类型为 OneOf-我们可以用数据返回错误或成功:

sealed class OneOf {
    data class Error(val error: E) : OneOf()
    data class Success(val data: S) : OneOf()

    val isSuccess get() = this is Success
    val isError get() = this is Error

    fun  error(error: E) = Error(error)
    fun  success(data: S) = Success(data)

    fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any =
            when (this) {
                is Error -> onError(error)
                is Success -> onSuccess(data)
            }
}

领域层需要自己的实体,因此下一步是定义它们. 我们现在有两个实体: User and UserLocation:

数据类User(var id: Int)? = null, val name: String, var isActive: Boolean = false)

数据类UserLocation(var id: Int)? = null, val纬度:双,val经度:双,val时间:长,val userId: Int)

既然我们知道要返回什么数据,就必须声明数据提供程序的接口. These will be IUsersRepository and ILocationsRepository. 它们必须在模型层中实现:

接口IUsersRepository {
    fun setActiveUser(userId: Int): OneOf
    fun getActiveUser(): OneOf
    fun createUser(user: User): OneOf
    fun removeUser(userId: Int): OneOf
    fun editUser(user: User): OneOf
    fun users(): OneOf>
}

接口ILocationsRepository {
    fun locations(userId: Int): OneOf>
    fun addLocation(location: UserLocation): OneOf
}

这组操作应该足以为应用程序提供必要的数据. At this stage, 我们不决定如何存储数据——这是一个我们希望独立的细节. For now, our domain layer doesn’t even know that it’s on Android. 我们会尽量保持这种状态. 我稍后会解释).

最后(或者几乎是最后)一步是为我们的 UseCaseS,它将被表示数据使用. 它们都非常简单(就像我们的应用程序和数据一样简单)——它们的操作仅限于从存储库调用适当的方法, e.g.:

class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase, UserIdParams>() {
    override suspend fun run(params: UserIdParams): OneOf> = repository.locations(params.userId)
}

The Repository 抽象使我们 UseCases 非常容易测试-我们不需要关心网络或数据库. 它可以被以任何方式嘲笑, so our unit tests will test actual use cases and not other, unrelated classes. 这将使我们的单元测试简单而快速:

@RunWith (MockitoJUnitRunner::类)
类GetLocationsTests {
    private lateinit var getLocations: getLocations
    private val locations = listOf(UserLocation(1,1 .).0, 1.0, 1L, 1))

    @Mock
    private lateinit var locationsRepository: ILocationsRepository

    @Before
    fun setUp() {
        getLocations = getLocations (locationsRepository)
    }

    @Test
    fun '应该调用getLocations locations ' () {
        runBlocking {getLocations.运行(UserIdParams (1)}
        验证(locationsRepository,乘以(1)).locations(1)
    }

    @Test
    fun '应该返回从locationsRepository获取的位置' (){
        给定{locationsRepository.locations(1) }.willReturn(OneOf.Success(locations))
        val returnedLocations = runBlocking {getLocations.运行(UserIdParams (1)}
        返回位置应该等于其中之一.Success(locations)
    }
}

现在,域层已经完成.

Model

作为Android开发人员,您可能会选择Room,这是用于存储数据的新Android库. 但是,让我们想象一下,项目经理问您是否可以推迟关于数据库的决定,因为管理层正试图在Room和Room之间做出决定, Realm, and some new, 超高速存储库. 我们需要一些数据来开始使用UI,所以我们现在只把它保存在内存中:

类MemoryLocationsRepository @Inject构造器():ILocationsRepository {
    private val locations = mutableListOf()

    override fun locations(userId: Int): OneOf> = OneOf.Success(locations.filter { it.userId == userId })

    override fun addLocation(location: UserLocation): OneOf {
        val addedLocation =位置.copy(id = locations.size + 1)
        locations.add(addedLocation)
        return OneOf.成功(addedLocation)
    }
}

Presentation

两年前,我写了一篇 article about MVP as a very good app structure for Android. When Google announced the great Architecture Components, which made Android应用程序 development far easier, MVP is no longer needed and can be replaced by MVVM; however, 这种模式中的一些想法仍然非常有用,比如关于哑视图的想法. 它们应该只关心显示数据. To achieve this, we will make use of ViewModel and LiveData.

我们的应用程序的设计是非常简单的一个活动与底部导航, 其中两个菜单项显示 locations fragment or the users fragment. In these views we use ViewModels, which in turn use UseCaseS从域层,保持通信整洁和简单. 例如,这里是 LocationsViewModel:

类LocationsViewModel @Inject构造函数(private val getLocations: getLocations,
                                             private val saveLocation: SaveLocation) : BaseViewModel() {
    var locations = MutableLiveData>()

    loadLocations(userId: Int) {
        getLocations.执行(UserIdParams(userId)){它.oneOf(::handleError,::handleLocationsChange)}
    }

    fun saveLocation(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        saveLocation.执行(UserLocationParams(位置)){
            it.oneOf(::handleError) { location -> handleLocationSave(location, onSaved) }
        }
    }

    private fun handleLocationSave(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        val currentLocations =位置.value?.toMutableList() ?: mutableListOf()
        currentLocations.add(location)
        this.locations.value = currentLocations
        onSaved(location)
    }

    private fun handleLocationsChange(locations: List) {
        this.locations.value = locations
    }
}

对那些不熟悉viewmodels的人做一点解释—我们的数据存储在位置变量中. 当我们从 getLocations 用例时,它们被传递给 LiveData value. 此更改将通知观察者,以便他们可以做出反应并更新他们的数据. 我们为片段中的数据添加了一个观察者:

类LocationsFragment: BaseFragment() {

...

    initLocationsViewModel() {
       locationsViewModel = ViewModelProviders.of(activity!!viewModelFactory) [LocationsViewModel::类.java]
       locationsViewModel.locations.observe(this, Observer> { showLocations(it ?: emptyList()) })
       locationsViewModel.error.observe(this, Observer { handleError(it) })
    }

    private fun showLocations(locations: List) {
        locationsAdapter.地点=地点
    }

    私有fun handleError(错误:失败?) {
        toast(R.string.user_fetch_error).show()
    }

}

每次位置变化, 我们只是将新数据传递给分配给回收视图的适配器——这就是在回收视图中显示数据的正常Android流程.

因为我们在视图中使用ViewModel, 它们的行为也很容易测试——我们可以只模拟ViewModels而不关心数据源, network, or other factors:

@RunWith (RobolectricTestRunner::类)
@Config(application = TestRegistryRobolectricApplication::class)
类LocationsFragmentTests {

    private var usersViewModel = mock(UsersViewModel::class.java)
    private var locationsViewModel = mock(LocationsViewModel::class.java)

    lateinit var fragment: LocationsFragment

    @Before
    fun setUp() {
        UsersViewModelMock.intializeMock (usersViewModel)
        LocationsViewModelMock.intializeMock (locationsViewModel)

        fragment = LocationsFragment()
        fragment.viewModelFactory = ViewModelUtils.createFactoryForViewModels(usersViewModel, locationsViewModel)
        startFragment(片段)
    }


    @Test
    “getActiveUser on start”(){
        Mockito.验证(usersViewModel).getActiveUser()
    }

    @Test
    Fun '应该从活跃用户加载位置' (){
        usersViewModel.activeUserId.value = 1
        Mockito.验证(locationsViewModel).loadLocations(1)
    }

    @Test
    Fun '应该显示位置' (){
        val date =日期(1362919080000)//10-03-2013 13:38

        locationsViewModel.locations.value = listOf(UserLocation(1,1 .).0, 2.0, date.time, 1))

        val recyclerView = fragment.find(R.id.locationsRecyclerView)
        recyclerView.measure(100, 100)
        recyclerView.布局(0,0,100,100)
        val adapter = recyclerView.适配器作为LocationsListAdapter
        adapter.itemCount应该是1
        val viewHolder = recyclerView.findViewHolderForAdapterPosition(0) as LocationsListAdapter.LocationViewHolder
        viewHolder.latitude.文本“应该等于”“迟:1”.0"
        viewHolder.longitude.文本'应该等于' "Lng: 2.0"
        viewHolder.locationDate.文本'应该等于' "10-03-2013 13:38"
    }
}

您可能会注意到,表示层也被划分为具有清晰边界的较小层. Views like activities, fragments, ViewHolders, etc. 只负责显示数据. 它们只知道ViewModel层,并且只使用ViewModel层来获取或发送用户和位置. It is a ViewModel which communicates with the domain. ViewModel实现对于视图和useccase对于域是一样的. 换句话说,干净的架构就像一个洋葱——它有层,层也可以有层.

依赖注入

We have created all the classes for our architecture, 但还有一件事要做——我们需要一些东西把所有东西连接在一起. The presentation, domain, 模型层保持干净, 但我们需要一个模块,它将是最脏的一个,它将通过这些知识了解所有的事情, 它可以连接我们的图层. 实现它的最佳方法是使用一种常见的设计模式(SOLID中定义的干净代码原则之一)——依赖注入, 它为我们创建合适的对象,并将它们注入所需的依赖. 我在这里使用了Dagger 2(在项目中期,我将版本更改为2).16,它有更少的样板),但你可以使用任何你喜欢的机制. 最近,我玩了一下Koin库,我觉得它也值得一试. 我想在这里使用它,但是在测试时,我有很多嘲弄ViewModels的问题. 我希望我能尽快找到解决问题的办法——如果可以的话, 我可以在使用Koin和Dagger 2时展示这个应用程序的不同之处.

你可以在GitHub上用标签architecture_v1检查这个阶段的应用程序.

Changes

We finished our layers, tested the app—everything is working! 除了一件事——我们仍然需要知道我们的PM想要使用什么数据库. 假设他们来找你说管理层同意使用房间, but they still want to have a possibility to use the newest, 未来的超高速库, 所以你需要时刻牢记潜在的变化. Also, 其中一个利益相关者询问是否可以将数据存储在云中,并想知道这种更改的成本. So, 现在是时候检查我们的架构是否良好,以及我们是否可以在不改变表示层或域层的情况下更改数据存储系统.

Change 1

使用Room的第一件事是为数据库定义实体. 我们已经有了一些: User and UserLocation. 我们要做的就是添加注释,比如 @Entity and @PrimaryKey, and then we can use it in our model layer with a database. Great! 这是打破我们想要保持的所有架构规则的好方法. 实际上,域实体不能通过这种方式转换为数据库实体. 想象一下,我们也想从网络上下载数据. 我们可以使用更多的类来处理网络响应——转换我们的简单实体,使它们与数据库和网络一起工作. 这是通往未来灾难的最短路径(并哭喊:“到底是谁写的这段代码?”). 我们需要为我们使用的每种数据存储类型单独的实体类. 它不会花费太多,所以让我们正确地定义Room实体:

@Entity
数据类UserEntity(
        @PrimaryKey(autoGenerate = true) var id: Long?,
        @ColumnInfo(name = "name") var name: String,
        @ColumnInfo(name = "isActive") var isActive: Boolean = false
)

@Entity(foreignKeys = [
    ForeignKey(entity = UserEntity::class)
            parentColumns = ["id"],
            childColumns = ["userId"],
            onDelete = CASCADE)
])
数据类UserLocationEntity(
    @PrimaryKey(autoGenerate = true) var id: Long?,
    @ColumnInfo(name = "latitude") var latitude: Double,
    @ColumnInfo(name = "longitude") var longitude: Double,
    @ColumnInfo(name = "time") var时间:长,
    @ColumnInfo(name = "userId") var userId: Long
)

As you can see, 它们几乎与领域实体相同, 所以合并它们是很有诱惑力的. 这只是一个意外——数据越复杂,相似度就越小.

接下来,我们必须实现 UserDAO and the UserLocationsDAO, our AppDatabase的实现 IUsersRepository and ILocationsRepository. 这里有个小问题ILocationsRepository should return a UserLocation, but it receives a UserLocationEntity from the database. The same is for User-related classes. 在相反的方向,我们经过 UserLocation 当数据库需要 UserLocationEntity. 要解决这个问题,我们需要 Mappers 在我们的域和数据实体之间. I used one of my favorite Kotlin features—extensions. 我创建了一个名为 Mapper.kt, 并将所有用于类之间映射的方法放在那里(当然, it’s in the model layer—the domain doesn’t need it):

fun User.toEntity() = UserEntity(id .?.toLong(), name, isActive)
fun UserEntity.toUser() = User(this ..id?.toInt(), name, isActive)
fun UserLocation.toEntity() = UserLocationEntity(id .?.toLong(),纬度,经度,时间,userId.toLong())
乐趣UserLocationEntity.toUserLocation() = UserLocation(id .?.toInt(),纬度,经度,时间,userId.toInt())

The little lie I mentioned before is about domain entities. 我写道,他们对Android一无所知,但这并不完全正确. I added @Parcelize annotation to the User entity and extend Parcelable there, making it possible to pass the entity to a fragment. 对于更复杂的结构, we should provide the view layer’s own data classes, and create mappers like between domain and data models. Adding Parcelable 我敢冒的一个小风险,我知道,如果有的话 User 我将为表示创建单独的数据类并将其删除 Parcelable 从领域层.

最后要做的是修改我们的依赖注入模块,以提供新创建的依赖注入 Repository 实现,而不是之前的 MemoryRepository. 在我们构建并运行应用程序之后,我们可以去PM显示带有房间数据库的工作应用程序. 我们也可以通知PM,增加网络不会花费太多时间, and that we are open to any storage library the management wants. 您可以检查哪些文件被更改了—只更改了模型层中的文件. 我们的建筑非常整洁! Every next storage type can be built in the same way, 只需扩展我们的存储库并提供适当的实现. 当然,我们也可能需要多个数据源,比如数据库和网络. What then? Nothing to it, 我们只需要创建三个存储库实现—一个用于网络, 一个用于数据库, and a main one, where the correct data source would be selected (e.g.(如果我们有网络,从网络加载,如果没有,从数据库加载).

你可以在GitHub上用标签architecture_v2查看这个阶段的应用程序.

So, 一天快结束了——你坐在电脑前,端着一杯咖啡, 该应用程序已准备好发送到Google Play, 突然,项目经理来问你:“你能添加一个功能,可以从GPS中保存用户的当前位置吗??”

Change 2

一切都在变,尤其是软件. This is why we need clean code and clean architecture. 然而,如果我们不假思索地编码,即使是最干净的东西也可能是脏的. 实现从GPS获取位置时的第一个想法是在活动中添加所有位置感知代码, run it in our SaveLocationDialogFragment and create a new UserLocation 有相应的数据. 这可能是最快的方法. 但是,如果我们疯狂的PM来找我们,要求我们将获取位置从GPS更改为其他提供商(例如GPS),该怎么办.g.比如蓝牙或网络)? 这些变化很快就会失去控制. 我们怎样才能干净利落地做到这一点呢?

用户位置是数据. 获取位置是 UseCase-所以我认为我们的领域和模型层也应该涉及到这里. 这样,我们就多了一个 UseCase to implement—GetCurrentLocation. We also need something that will provide a location for us—an ILocationProvider 接口,使 UseCase 独立于GPS传感器等细节:

接口ILocationProvider {
    fun getLocation(): OneOf
    fun cancel()    
}

class GetCurrentLocation @Inject constructor(private val locationProvider: ILocationProvider) : UseCase() {
    override suspend fun run(params: NoParams): OneOf =
            locationProvider.getLocation()

    重载fun cancel() {
        super.cancel()
        locationProvider.cancel()
    }
}

You can see that we have one additional method here—cancel. This is because we need a way to cancel GPS location updates. Our Provider implementation, defined in the model layer, goes here:

类GPSLocationProvider构造函数(var activity: activity): ILocationProvider {

    private var locationManager: locationManager? = null
    private var locationListener: GPSLocationListener? = null

    override fun getLocation(): OneOf = runBlocking {
        val grantedResult = getLocationPermissions()

        if (grantedResult.isError) {
            val error = (granteresult =.Error).error
            OneOf.Error(error)
        } else {
            getLocationFromGPS ()
        }
    }

    private suspend fun getLocationPermissions(): OneOf = suspendCoroutine {
        Dexter.withActivity(活动)
                .withPermission(清单.permission.ACCESS_FINE_LOCATION)
                .withListener (PermissionsCallback (it))
                .check()
    }

    private suspend fun getLocationFromGPS (): OneOf = suspendCoroutine {
        locationListener?.unsubscribe()
        locationManager =活动.getSystemService(上下文.LOCATION_SERVICE)作为LocationManager
        locationManager?.let { manager ->
            locationListener = GPSLocationListener(manager, it)
            launch(UI) {
                manager.requestLocationUpdates (LocationManager.Network_provider, 01,0.0 f, locationListener)
            }
        }
    }

    重载fun cancel() {
        locationListener?.unsubscribe()
        locationListener = null
        locationManager = null
    }
}

This provider is prepared to work with Kotlin coroutines. 如果你还记得的话 UseCaseS的run方法是在后台线程上调用的,所以我们必须确保正确地标记我们的线程. As you can see, 我们必须在这里传递一个活动——当我们不再需要侦听器时,从它们取消更新和注销是至关重要的,以避免内存泄漏. 因为它实现了 ILocationProvider, we can easily modify it in the future to some other provider. 我们还可以很容易地测试当前位置的处理(自动或手动), 即使没有启用手机中的GPS,我们所要做的就是替换这个实现,返回一个随机构建的位置. To make it work, we have to add the newly created UseCase to the LocationsViewModel. ViewModel,反过来,必须有一个新方法, getCurrentLocation,它将实际调用用例. 只需要对UI进行一些小的更改,就可以调用它并在dagger中注册GPSProvider——瞧, 我们的应用程序完成了!

Summary

我试图向你展示我们如何开发一个易于维护的安卓应用程序, test, and change. 如果有新人加入你的工作,它也应该很容易理解, 他们在理解数据流或结构方面应该没有问题. 如果他们知道架构是干净的, 他们可以确保UI的更改不会影响模型中的任何内容, and adding a new feature will not take more than predicted. 但这并不是旅程的终点. 即使我们有一个结构良好的应用程序, 它很容易被混乱的代码更改破坏,“只是一会儿”, just to work.“记住——目前还没有代码”.“每个违反我们规则的代码都可以保存在代码库中,并可能成为未来的来源, bigger breaks. 如果你在一周后看到这些代码, 看起来像是有人在代码中实现了一些强依赖, to resolve it, you’ll have to dig through many other parts of the app. 良好的代码体系结构不仅在项目开始时是一个挑战,而且在Android应用程序生命周期的任何阶段都是一个挑战. 每次有东西要改变时,都应该考虑并检查代码. 为了记住这一点,你可以,例如,打印并悬挂你的Android架构图. 你也可以通过把层分成三个Gradle模块来强制层的独立性, 域模块不知道其他模块,表示模块和模型模块不相互使用. 但即便如此,我们还是意识到,应用程序代码中的混乱会在我们最意想不到的时候报复我们.

关于总博客的进一步阅读:

了解基本知识

  • Dagger在Android中的用途是什么?

    Dagger是一个依赖注入库,它使用代码生成和注释. 它用于管理依赖关系,这使得代码更容易测试和维护.

  • Kotlin适合Android吗?

    谷歌在2017年I/O大会上宣布正式支持Kotlin, 因为您无法在该平台上利用最新的Java特性, 现代Kotlin语言非常适合Android.

  • What are the key components of Android architecture?

    This year, in I/O 2018, 谷歌宣布了新的Android架构组件,如LiveData, ViewModel, and Room. 创建它们是为了简化SQLite操作, 简化数据更改的处理, 并帮助开发人员处理生命周期.

聘请Toptal这方面的专家.
Hire Now
Tomasz Czura的头像
Tomasz Czura

Located in Kraków, Poland

Member since February 6, 2016

About the author

Tomasz (MCS)是一个Android向导和团队领导. 他最喜欢的项目是做一个酒店娱乐系统的应用程序和后端.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Welltok

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.