Me and my colleague are having a debate as to where would be the right place to map our entity objects or remote dto objects to plain simple domain objects.
Our structure looks like this.
source(includes dao) > repo(includes source) > usecase(includes repo)
My colleague thinks that mapping to domain should be done inside the source so that the domain object could be passed on to the next layers as so
class SomeSourceImpl(private val dao: Dao) : SomeSource {
override fun get(): Observable<DomainModel> {
return dao.getResponse().map { it.mapToDomain() }
}
}
My colleagues argues that according to Uncle Bob this is due to the dependency rule.
This rule says that source code dependencies can only point inwards.
Nothing in an inner circle can know anything at all about something in
an outer circle. In particular, the name of something declared in an
outer circle must not be mentioned by the code in the an inner circle.
That includes, functions, classes. variables, or any other named
software entity.
I very much disagree with the approach of mapping to domain directly inside the source because then the repositories become anaemic and we are consequently adopting the anti-pattern of anaemic repositories being useless and all they do is to blindly propagating everything that comes from the source. (Now you may say that sources are also anaemic and we could simply remove them and include the dao object directly into the repo but this is out of the question in our case).
Instead I propose that sources would return the raw database entity (or remote entity if we are into rest calls) as it makes sense for a source to return the raw data for later processing. It's the job of the repo to get the result from the source then map it to domain and lastly propagate this domain object to use cases something like so.
class SomeRepoImpl(private val someSource: SomeSource) : SomeRepo {
override fun get(haId: String): Observable<DomainModel> {
return otherAssetSource.get().map { it.mapToDomain() }
}
I also came across some samples on github where they map to domain inside their repos rather than the sources
Here
Here
Here
Here is one for iOS too
What would be the strict rule in clean architecture principles regarding the place one can map an entity into a domain object?
Quoting the rule
source code dependencies can only point inwards
That would depend on the architecture I guess. Let me explain this with an example:
Architecture:
DOMAIN <- DATA <- PRESENTATION
Where:
DATA -> LOCAL
|
v
REMOTE
NOTE: DOMAIN represents the innermost circle and PRESENTATION represents the outmost circle.
Now DOMAIN is a pure Kotlin module and does not have any Android dependencies. Let's define a repository:
interface ProfileRepository {
fun getProfile(): Profile?
fun updateProfile(profile: Profile): Profile
}
We implement this in the DATA layer(which is an Android library):
class ProfileRepositoryImpl(
private val networkManager: NetworkManager,
private val remoteDataSource: ProfileRemoteDataSource,
private val localDataSource: ProfileLocalDataSource
): ProfileRepository {
override fun getProfile(): Profile? {
return if(networkManager.isNetworkAvailable) {
localDataSource.insert(remoteDataSource.get())
} else {
localDataSource.get()
}
}
override fun updateProfile(profile: Profile): Profile {
val updatedProfile = remoteDataSource.update(profile)
return localDataSource.insert(updatedProfile)
}
}
class ProfileRemoteDataSource(
private val api: ProfileApi,
private val mapper: Mapper<ProfileDto, Profile>
) {
fun get(): Profile {
return mapper.toModel(api.getProfile())
}
fun update(profile: Profile): Profile {
val dto = api.updateProfile(
mapper.fromModel(profile)
)
return mapper.toModel(dto)
}
}
class ProfileLocalDataSource(
private val dao: ProfileDao,
private val mapper: Mapper<ProfileEntity, Profile>
) {
fun insert(profile: Profile): Profile {
dao.insert(mapper.fromModel(profile))
return requireNotNull(get())
}
fun get(): Profile? {
return dao.get()?.let(mapper::toModel)
}
}
interface Mapper<T : Any, Model : Any> {
fun toModel(value: T): Model
fun fromModel(value: Model): T
}
The LOCAL module is an Android library independent of any dependencies and exposes the DAO and Entity objects:
interface ProfileDao {
fun insert(profile: ProfileEntity)
fun get(): ProfileEntity?
}
Similarly, for the REMOTE module:
interface ProfileApi {
fun get(): ProfileDto
fun update(profile: ProfileDto): ProfileDto
}
So, it doesn't make sense for me to have the Source classes return DTO and Entity objects. The repo class would look something like this:
class ProfileRepositoryImpl(
private val networkManager: NetworkManager,
private val remoteDataSource: ProfileRemoteDataSource,
private val remoteDataMapper: Mapper<ProfileDto, Profile>,
private val localDataSource: ProfileLocalDataSource,
private val localDataMapper: Mapper<ProfileEntity, Profile>
) : ProfileRepository {
override fun getProfile(): Profile? {
if (networkManager.isNetworkAvailable) {
val dto = remoteDataSource.get()
val profile = remoteDataMapper.toModel(dto)
val entity = localDataMapper.fromModel(profile)
localDataSource.insert(entity)
}
return localDataSource.get()?.let(localDataMapper::toModel)
}
override fun updateProfile(profile: Profile): Profile {
val request = remoteDataMapper.fromModel(profile)
val dto = remoteDataSource.update(request)
val updatedProfile = remoteDataMapper.toModel(dto)
val entity = localDataMapper.fromModel(updatedProfile)
localDataSource.insert(entity)
return localDataMapper.toModel(
requireNotNull(localDataSource.get())
)
}
}
In your example, you have taken only the GET operation into consideration. Here, for the UPDATE operation we need to map the DOMAIN object as well. So as we add more functionalities the Repo class would become very messy if the mapping of objects is done in the Repo class.
I believe it would depend on the overall architecture of the system.
I am learning the subject, but my approach is on Clean Architecture which I use in most projects.
The Domain Layer is the innermost layer, therefore, it does not depend on other layers. Thus, the first decision is to make mappers stay in the data and presentation layers, or whichever layers I use for the two purposes.
Then, I have an interface that defines how I define my mappers
interface EntityMapper<M : Model, ME : ModelEntity> {
fun mapToDomain(entity: ME): M
fun mapToEntity(model: M): ME
}
Then, I have classes, in the data layer which map from data models to domain model. An example is
class ItemEntityMapper #Inject constructor(
private val ownerEntityMapper: OwnerEntityMapper
) : EntityMapper<Item, ItemEntity> {
override fun mapToDomain(entity: ItemEntity) = Item(
id = entity.id,
name = entity.name,
fullName = entity.fullName,
description = entity.description,
url = entity.url,
stars = entity.stars,
owner = entity.ownerEntity?.let { ownerEntityMapper.mapToDomain(it) }
)
override fun mapToEntity(model: Item) = ItemEntity(
id = model.id,
name = model.name,
fullName = model.fullName,
description = model.description,
url = model.url,
stars = model.stars,
ownerEntity = model.owner?.let { ownerEntityMapper.mapToEntity(it) }
)
}
I prefer OOP classes over functional Kotlin to ease with DI
Related
Am learning android kotlin follow this:
https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers(it)
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Dont know how to write the fun loadUsers()
Here is my User:
class User {
constructor(name: String?) {
this.name = name
}
var name:String? = null
}
If dont use the keyword 'also' , i know how to do it.
But if use 'also' , it seems not work.
Here is how i try to write the fun loadUsers:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it = MutableLiveData<List<User>>(users)
}
Error tips near it : Val cant be ressigned
Part 1: According to the Kotlin documentation, also provides the object in question to the function block as a this parameter. So, every function call and property object you access is implied to refer to your MutableLiveData<List<User>>() object. also returns this from the function block when you are done.
Thus, another way of writing your MutableLiveData<> would be like this:
val users = MutableLiveData<List<User>>()
users.loadUsers()
Part 2: As far as how to implement loadUsers(), that is a separate issue (your question is not clear). You can use Retrofit + RxJava to load the data asynchronously, and that operation is totally outside of the realm of ViewModel or also.
Part 3: With your approach, you have conflicting things going on. Instead of doing a loadUsers() from your lazy {} operation, I would remove your lazy {} operation and create a MutableLiveData<> directly. Then, you can load users later on and update the users property any time new data is loaded. Here is a similar example I worked on a while ago. It uses state flows, but the idea is similar. Also use a data class to model the User instead of a regular class. Another example.
It is solved change to code:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it.value = users
}
it can't be reassigned , but it.value could .
I am learning android development and I decided to build a weather app using api that comes from service named open water map. Unfortunately I’ve got the following problem:
In order to get the weather data for wanted city, I first need to perform request to get the geographical coordinates. So what I need to do is to create one request, wait until it is finished, and after that do another request with data that has been received from the first one.
This is how my view model for location looks like:
class LocationViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
fun refresh() {
CoroutineScope(Dispatchers.IO).launch {
// call fetch location here in coroutine
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
}
And this is how my view model for weather looks like”
class WeatherSharedViewModel constructor(private val repository: WeatherRepository): ViewModel() {
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<List<SharedWeatherModel>>()
val weatherLoadError = MutableLiveData<Boolean>()
val loading = MutableLiveData<Boolean>()
fun refresh(lat: String, lon: String) {
loading.value = true
CoroutineScope(Dispatchers.IO).launch {
// call fetchWeather here in coroutine
}
loading.value = false
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
I am using both view models in a fragment in such way:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val weatherService = WeatherApi.getInstance()
val repository = WeatherRepository(weatherService)
locationViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(LocationViewModel::class.java)
weatherViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(WeatherSharedViewModel::class.java)
locationViewModel.refresh()
Log.d(TAG, "lat: ${locationViewModel.location.value?.get(0)?.get(0)?.lat.toString()}, lon: ${locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()}")
weatherViewModel.refresh(
locationViewModel.location.value?.get(0)?.get(0)?.lat.toString(),
locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()
)
val weatherList = view?.findViewById<RecyclerView>(R.id.currentWeatherList)
weatherList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentWeatherAdapter
}
val cityList = view?.findViewById<RecyclerView>(R.id.currentCityList)
cityList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentLocationAdapter
}
observerLocationViewModel()
observeWeatherViewModel()
}
So on a startup both models are refreshed, which means that requests are made. I was trying to somehow synchronize those calls but my last attempt ended that data passed to the refresh method of weather view model was null. So problem is that both coroutine are launched one after another, first one is not waiting for second.
The main question: is there any synchronisation mechanism in coroutines? That I can launch one coroutine and wait with launching second one as long as first is not finished?
You are violating the "Single Responsibilty Principle" you need to learn how to write CLEAN code. that is why you are running into such problems. A member of stackoverflow has explained it in depth: single responsibility
A few tips:
Your general design is somewhat convoluted because you are trying to update LiveData with coroutines, but one LiveData's exposed data is something determined by the other LiveData. This is theoretically OK if you need to be able to access the city even after you already have the weather for that city, but since you've split this behavior between two ViewModels, you end up having to manage that interaction externally with your Fragment, which is very messy. You cannot control it from a single coroutine unless you use the fragment's lifecycle scope, but then the fetch tasks restart if the screen rotates before they're done. So I would use a single ViewModel for this.
In a ViewModel, you should use viewModelScope for your coroutines instead of creating an ad hoc CoroutineScope that you never cancel. viewModelScope will automatically cancel your coroutines when the ViewModel goes out of scope.
Coroutines make it extremely easy to sequentially do background work. You just need to call suspend functions in sequence within a single coroutine. But to do that, once again, you really need a single ViewModel.
It's convoluted to have separate LiveDatas for the loading and error states. If you use a sealed class wrapper, it will be much simpler for the Fragment to treat the three possible states (loading, error, have data).
Putting this together gives the following. I don't really know what your repo is doing and how you convert Response<GeocodingModel> to List<GeocodingModel> (or why), so I am just using a placeholder function for that. Same for the weather.
sealed class WeatherState {
object Loading: WeatherState()
object Error: WetaherState()
data class LoadedData(val data: List<SharedWeatherModel>)
}
class WeatherViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<LoadedData>().apply {
value = WeatherState.Loading
}
fun refreshLocation() = viewModelScope.launch {
weather.value = WeatherState.Loading
val locationResponse = fetchLocation() //Response<GeocodingModel>
val locationList = unwrapLocation(location) //List<GeocodingModel>
location.value = locationList
val latitude = locationList.get(0).get(0).lat.toString()
val longitude = locationList.get(0).get(0).lon.toString()
try {
val weatherResponse = fetchWeather(latitude, longitude) //Response<SharedWeatherModel>
val weatherList = unwrapWeather(weatherResponse) //List<SharedWeatherModel>
weather.value = WeatherState.LoadedData(weatherList)
} catch (e: Exception) {
weather.value = WeatherState.Error
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
And in your Fragment you can observe either LiveData. The weather live data will always have one of the three states, so you have only one place where you can use a when statement to handle the three possible ways your UI should look.
Without referring to your actual code only to the question itself:
By default code inside coroutines is sequential.
scope.launch(Dispatcher.IO) {
val coordinates = repository.getCoordinates(place)
val forecast = repository.getForecast(coordinates)
}
Both getCoordinates(place) and getForecast(coordinates) are suspend functions since they're making network requests and waiting for the result.
getForecast(coordinates) won't execute until getCoordinates(place) is done and returned the coordinates.
I have recently completed this (links below) codelabs tutorial which walks through how to implement Room with LiveData and Databinding in Kotlin.
https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin
Following on from this, I want to write some tests around the ViewModel, however, the GitHub repository where the code is stored does not contain any (it has a few tests around the DAO, not what I am interested in for now).
The ViewModel I am trying to test looks like this:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
My test class for the ViewModel looks like this:
#RunWith(JUnit4::class)
class WordViewModelTest {
private val mockedApplication = mock<Application>()
#Test
fun checkAllWordsIsEmpty() {
val vm = WordViewModel(mockedApplication)
assertEquals(vm.allWords, listOf<String>())
}
}
I get an error saying java.lang.IllegalArgumentException: Cannot provide null context for the database. This error then points to this line in the WordViewModel: val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao(). To get this to not crash, I believe I need to mock a lot of what is in the ViewModel, which I am fine with.
I would like to be able to run the test above and in the future, I would also like to mock return a list of data when repository.allWords is called. However, I am not sure how to do this. So my question is, how can I mock the following lines from WordViewModel to allow me to do this?
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
I'm trying to change my app from having no design pattern to using MVP.
Originally I had the following code:
override fun onCreateInputView(): View {
//favoritesData is an instance variable, same with "Gson", "parser", "favorites", and "stringArrayListType"
favoritesData = File(filesDir, "favorites_data.json")
if (favoritesData.exists()) {
favorites = Gson.fromJson(parser.parse(FileReader(favoritesData)), stringArrayListType)
}
}
and
fun updateFavoritesFile() {
favoritesData.writeText(Gson.toJson(favorites))
}
After trying to use MVP I changed the code to:
class AnimeFaceKeyboardPresenter(val view : AnimeFaceKeyboardView, private val model : KeyboardModel = KeyboardModel()) : Presenter {
override fun onCreateInputView() {
model.favorites = view.loadFavoritesFile()
//At some point, call view.updateFavoritesFile(arrayListOf("test","test2"))
}
override fun onStartInputView() {
}
}
and the code in the activity itself to:
override fun loadFavoritesFile() : ArrayList<String> {
val favoritesData = File(filesDir, favoritesFileName)
var favorites = ArrayList<String>()
//"favorites" is no longer an instance variable
if (favoritesData.exists()) {
favorites = Gson.fromJson(parser.parse(FileReader(favoritesData)), stringArrayListType)
}
return favorites
}
override fun updateFavoritesFile(favorites: ArrayList<String>) {
File(filesDir, favoritesFileName).writeText(Gson.toJson(favorites))
}
override fun onCreateInputView(): View {
super.onCreateInputView()
presenter = AnimeFaceKeyboardPresenter(this)
presenter.onCreateInputView()
}
I'm not sure if I'm using MVP correctly, but if I am, how would I go about testing this code. For example - writing a test that calls updateFavoritesFile(arrayListOf("test1","test2")) and uses loadFavoritesFile() to check if the contents is as expected.
Well, you might want to relocate your file read and write to your model (they are associated with data which doesn't really belong in your view).
Then your test consists of instantiating your model object, and testing the methods which can be done without the view and presenter (simplifying the tests).
I would be very tempted to abstract your file as some form of "Repository" object that knows how to read and write strings (but you don't care where or how). You would pass the repository object to your model as a construction property. The advantage of this is that you can create a mock or fake Repository object which you can use to "feed" test data and examine written data, making testing that part of your model a little easier.
Don't forget, your view shouldn't have direct access to your model under MVP .. that would me more like MVC (one of the very few differences between MVP and MVC).
I'm starting to apply the Uncle Bob Clear Architecture in commercially Android to the projects I'm working with.
There are some blind spots that I don't know how to tidy my code, and in this case there's no UI.
My example would be a dependency that stays as a Singleton containing miscellaneous content:
DATA LAYER
ContentService.kt has the interface for Retrofit
ContentRepository.kt has the repository that connects with the service
DOMAIN LAYER
ContentUseCase.kt contains the use case I'll use to transform into business logic and test it
Now, I have a file called ContentManager.kt that holds my information in a Singleton:
class ContentManager {
object Singleton {
var instance : ContentManager? = null
}
var todaysRastafariDay : String? = null
var imageRastafariDayUrl: String? = null
fun setRastafaryDay(day: String, imageUrl: String) {
todaysRastafariDay = day
imageRastafariDayUrl = imageUrl
}
companion object {
fun getInstance() : ContentManager {
if (Singleton.instance == null) {
Singleton.instance = ContentManager()
}
return Singleton.instance!!
}
}
}