https://kotlinlang.org logo
#android-architecture
Title
# android-architecture
u

ursus

02/18/2022, 1:27 PM
I have a design question which comes up often for me I have 2 apps which share a viewmodel which emits a list of 3 types which is a sealed class Now business requirement came where app B wants to add 4th type What to do? Do you make the hierqrchy not sealed? Do you remap into new hierarchy? Do you copy paste the original list generation logic to avoid mapping?
l

Lukasz Kalnik

02/18/2022, 2:02 PM
If they are two different apps with two different business requirements then I wouldn't force them to use a shared ViewModel.
If there are some shared aspects I would rather extract them e.g. in use cases and provide as a library.
Can you provide more details? What is common about the ViewModel in both apps and what is different?
In general it's less painful to have some duplicated code than to try to "squeeze in" conflicting business requirements to maintain "one common logic". Code should follow business requirements, not the other way around.
u

ursus

02/18/2022, 2:08 PM
Lets say you have Spotify where dashboard is a list of songs, authors and now they make new app Spotify+ where its songs, authors and podcasts
and you already have a provider of List of Item = Song | Author which already does some sorting logic etc
So, how would you extract a use case? what type should it work with?
l

Lukasz Kalnik

02/18/2022, 2:17 PM
Can
Song
,
Author
and
Podcast
be mixed together? I.e. are they displayed in the UI within one list in a mixed fashion?
In this case I would just extend the type with
Podcast
for both apps. So what that the other app doesn't use it?
Do you get
Song
,
Author
and
Podcast
all from one data source (API call, database query)?
u

ursus

02/18/2022, 2:21 PM
yea well.. but why, now app A knows about some unrelated complexity, need to throw errors in
when
etc
l

Lukasz Kalnik

02/18/2022, 2:21 PM
Yes
Then I would create separated ViewModels (even if parts of them are duplicated code)
And I would extract common logic / data access (API calls, database queries) into a shared library
But only the domain/data layer logic.
Maybe also if you have some shared UI components (which are identical and work with identical data types) you could extract this part of the ViewModel as well into the shared library.
So you have
ViewModel1
and
ViewModel2
where you inject
CommonViewModel
(which is the part where UI logic is really identical, and not only similar)
u

ursus

02/18/2022, 2:26 PM
well if you bake the type into upstream then sure you can do all that but should you? arent we just biased because its a "nice" type hierarchy? songs podcasts etc they go together in reality
l

Lukasz Kalnik

02/18/2022, 2:26 PM
That's the example you have given
u

ursus

02/18/2022, 2:27 PM
what if its some random corporate bs, PromoItem
then when I look at the enum im not so pleased
l

Lukasz Kalnik

02/18/2022, 2:27 PM
Yes, you would need separate type hierarchies for this
You need composition and not inheritance
u

ursus

02/18/2022, 2:28 PM
but then I cannot share anything really when its a different type... bummer
l

Lukasz Kalnik

02/18/2022, 2:28 PM
No
But your types should reflect reality
If in reality the types don't go together, why should they belong to the same hierarchy?
u

ursus

02/18/2022, 2:29 PM
not sure what you mean
l

Lukasz Kalnik

02/18/2022, 2:29 PM
But you can share the call to get
Song
and
Author
types
App1: API call to get
Song
and
Author
App2: Makes the same API call to get
Song
and
Author
, and then a second call to get
Podcast
. Data is combined inside your domain layer.
Unless App2 has also only one API call to get
Song
,
Author
and
Podcast
. Then you cannot share anything.
u

ursus

02/18/2022, 2:31 PM
im aware the consquences, im just not sure which way to go
l

Lukasz Kalnik

02/18/2022, 2:31 PM
If in reality the types don't go together, why should they belong to the same hierarchy?
Reality = description of your business case
What do the apps really share?
u

ursus

02/18/2022, 2:32 PM
I dont wanna copy paste a lot of code, and I also dont want to muddy the hierarchy
l

Lukasz Kalnik

02/18/2022, 2:32 PM
Do they call the same API endpoint?
u

ursus

02/18/2022, 2:33 PM
ok Ill come up with smoething more real
l

Lukasz Kalnik

02/18/2022, 2:33 PM
You can take a look at Domain Driven Design. If your business cases differ, you should not force the data into one shared code hierarchy.
I would need some more specific example, because we are talking about general principles and it's hard to give concrete advice
This talk is quite helpful to understand how to model your business domain as types:

https://www.youtube.com/watch?v=2JB1_e5wZmU

u

ursus

02/18/2022, 2:41 PM
here
Copy code
sealed class Item
data class DateItem(val timestamp: Long) : Item()
data class ActivatePushItem(val processing: Boolean) : Item()
object NonDefaultSubscriberItem : Item()
data class MessageItem(val message: BusinessMessage) : Item()

list looks like

ActivatePushItem
DateItem
MessageItem
MessageItem
MessageItem
DateItem
MessageItem
MessageItem

api usage

ViewModel.items: Flow<List<Item>>

fun ViewModel.messageClick(message: BusinessMessage) {
    navigator.goToDetail(message.id)
}

--------

appB wants to add
data class PromotionItem(...)

list looks like

ActivatePushItem
PromotionItem
PromotionItem
DateItem
MessageItem
MessageItem
MessageItem
DateItem
MessageItem
MessageItem
so, if it were a pure composition, how to insert into 1 and 2 indicies? remap the list, look for ActivatePushItem, then insert PromotionItems .. then the rest .. idk, feels dirty
and also what to do about the click function, cant use it unless I keep the businessMessage type (here im open to refactor the upstream, but just giving concrete example)
l

Lukasz Kalnik

02/18/2022, 2:44 PM
You are mixing the presentation and domain layers
That's what causes the problem
Item
is presentation (view) layer hierarchy
You need
Item1
and
Item2
hierarchy for both apps
And for API calls you need different types than for the UI layer
u

ursus

02/18/2022, 2:51 PM
yea Item is view hierarchy
there is no api calls, but yes those Items are generated from database items
okay ill elaborate more
l

Lukasz Kalnik

02/18/2022, 2:52 PM
Exactly
You need a domain layer between the database and the view
Which will convert the data types you read from the DB to the ones you display in your view
u

ursus

02/18/2022, 2:53 PM
I do have it but I wanted to share the logic of generating items as its not trivial
l

Lukasz Kalnik

02/18/2022, 2:53 PM
Understand
Then extract this logic into use cases (in the domain layer)
Also if the database can in both cases return a
PromotionItem
how do you deal with this in AppA?
You need a query for AppA which will filter out all the `PromotionItem`s
u

ursus

02/18/2022, 2:55 PM
this is how the list of Item is generated
Copy code
fun ItemMapper(
    subscriber: LoadedSubscriber?,
    messages: List<BusinessMessage>,
    notificationMethod: BusinessNotificationMethod?,
    pushActivating: Boolean
): List<Item> {
    if (subscriber == null) return emptyList()
    val showSettings = subscriber.default && notificationMethod == BusinessNotificationMethod.PUSH

    val items = mutableListOf<Item>()
    if (!subscriber.default) {
        items += NonDefaultSubscriberItem

    } else {
        if (notificationMethod == BusinessNotificationMethod.SMS) {
            items += ActivatePushItem(pushActivating)
        }
        var previousDate: LocalDate? = null
        for (message in messages) {
            val date = message.timestamp.toLocalDateTime().toLocalDate()
            if (previousDate == null || !date.isEqual(previousDate)) {
                items += DateItem(date.atStartOfDay(TIMEZONE_SLOVAKIA).toInstant().toEpochMilli())
            }
            items += MessageItem(message)
            previousDate = date
        }
    }
    return items
}
I mean its not rocket science but there is a bit logic to it so im not super keen on copypasting it, and then remembering both versions when something changes
and now I need to make it like this
Copy code
fun ItemMapper(
    subscriber: LoadedSubscriber?,
    messages: List<BusinessMessage>,
    notificationMethod: BusinessNotificationMethod?,
    pushActivating: Boolean,
    promos: List<Promo> <------------------------------------------------
): Triple<List<Item>, Set<BusinessMessage.Category>, Boolean> {
    if (subscriber == null) return tupleOf(emptyList(), emptySet(), false)
    val showSettings = subscriber.default && notificationMethod == BusinessNotificationMethod.PUSH

    val items = mutableListOf<Item>()
    if (!subscriber.default) {
        items += NonDefaultSubscriberItem

    } else {
        if (notificationMethod == BusinessNotificationMethod.SMS) {
            items += ActivatePushItem(pushActivating)
        }
        for (promo in promos) { <------------------------------------------------
        	items += PromoItem(promo.id, promo.title etc) <------------------------------------------------
        }
        var previousDate: LocalDate? = null
        for (message in messages) {
            val date = message.timestamp.toLocalDateTime().toLocalDate()
            if (previousDate == null || !date.isEqual(previousDate)) {
                items += DateItem(date.atStartOfDay(TIMEZONE_SLOVAKIA).toInstant().toEpochMilli())
            }
            items += MessageItem(message)
            previousDate = date
        }
    }
    return items
}
so how would you piece meal this
l

Lukasz Kalnik

02/18/2022, 3:28 PM
I would create 3 different sealed hierarchies: AppA -> without
PromoItem
AppB -> with
PromoItem
shared library for data access and conversion -> with
PromoItemDto
.
The shared library does the DB call and gets a list of items, it does the mapping and returns a list of
ItemDto
Then AppB calls the shared library and converts
ItemDto
to its
UiItem
hierarchy
AppA does the same but filters out
PromoItemDto
and converts
ItemDto
to its own
UiItem
hierarchy (without
PromoItem
)
You have to make a cut somewhere and filter out what you don't need
this is best done at the architecture layer boundary
u

ursus

02/18/2022, 3:32 PM
what a ItemDto?
l

Lukasz Kalnik

02/18/2022, 3:35 PM
Dto = data transfer object, it's just a name for a class used e.g. to read data from data base
But this is just a name
The point is that you have a separate sealed class for reading the data from DB
and then you convert the objects for the types for your UI
Do you know clean architecture? Presentation, domain, data layers?
u

ursus

02/18/2022, 3:37 PM
I dont agree with everything but in general sure
l

Lukasz Kalnik

02/18/2022, 3:38 PM
So you need separate sealed class hierarchy in your data layer
to just read the data, whatever is in the DB, doesn't matter
u

ursus

02/18/2022, 3:38 PM
well, not really, there is no hierarchy, its just one table of Messages, and then another table of Promos
l

Lukasz Kalnik

02/18/2022, 3:38 PM
Ah ok
that's even easier
Then you just have two methods
But anyway you can create a hierarchy in the domain layer
If it's easier for you to convert
BTW in your code you convert a
List<BusinessMessage>
to
List<Item>
appending to mutable List
It's better to convert lists using
map
u

ursus

02/18/2022, 3:40 PM
so you want to join Messages + Promo into some MessageOrPromo type and then map that to UiItem ?
l

Lukasz Kalnik

02/18/2022, 3:41 PM
If it helps you with simplifying the logic then yes
I personally would just call
getMessages()
in AppA and
getMessages()
and
getPromos()
in AppB
And then I would combine them in the respective app
u

ursus

02/18/2022, 3:42 PM
I dont really see why I need the intermediate MessageOrPromo here but okay, my issue mostly was how to share the ItemMapper logic, if it generates AppAUiItem and then AppBUiItem in respective apps
factories? copy paste?
l

Lukasz Kalnik

02/18/2022, 3:45 PM
No, you don't need
MessageOrPromo
u

ursus

02/18/2022, 3:45 PM
thats what I thought your ItemDto was
l

Lukasz Kalnik

02/18/2022, 3:45 PM
Why can't you convert `Message`s and `Promo`s separately and then join them in the App
Sorry, I didn't understand your use case completely.
ItemDto
is not needed here
u

ursus

02/18/2022, 3:46 PM
its okay nevermind, the question now is would you/how to share the ItemMapper logic if you would NOT share the UiItem hierarchy. Yolo copy paste? Factories?
l

Lukasz Kalnik

02/18/2022, 3:48 PM
Yes, that's a good question, I'm just analyzing your code
I think I know
u

ursus

02/18/2022, 3:52 PM
what about remaping the shared list ? i.e. looking for the 1st item and then adding promoitems and then rest?
l

Lukasz Kalnik

02/18/2022, 3:53 PM
I would separate mapping of Items based on the data which you get from DB (in your example code
DateItem
,
MessageItem
and
PromoItem
) into separate converters
They don't have to be part of a hierarchy (for now)
u

ursus

02/18/2022, 3:53 PM
how? I mean if copy of UiItem type lives in each app, what would those converters return?
l

Lukasz Kalnik

02/18/2022, 3:54 PM
These apps would have different
UiItemA
and
UiItemB
hierarchies
after converting the items from DB you would then just remap the types
Extract the converting logic into separate functions
I will try to write an example, give me a moment
u

ursus

02/18/2022, 3:55 PM
I know what you mean but how would the signature of Message to MessageItem converter look like if its shared code
l

Lukasz Kalnik

02/18/2022, 3:55 PM
No
ah, sorry
Shared code
MessageItem
doesn't have to be part of any sealed hierarchy
u

ursus

02/18/2022, 3:56 PM
so youd go Message -> MessageItem -> AppAMessageItem ?
l

Lukasz Kalnik

02/18/2022, 3:57 PM
You need the sealed hierarchy only in the UI, right? To pass the `Item`s to a RecyclerView or a
Column
Exactly
This means a little bit of boilerplate in the app
u

ursus

02/18/2022, 3:57 PM
isnt that a bit of overkill?
l

Lukasz Kalnik

02/18/2022, 3:57 PM
No
That's good separation of concerns
In the app you would do
u

ursus

02/18/2022, 3:58 PM
well yea but its 2 basically ununsed instances
l

Lukasz Kalnik

02/18/2022, 3:58 PM
They are used, for conversion
u

ursus

02/18/2022, 3:58 PM
well yea, to make compiler happy, not much else
l

Lukasz Kalnik

02/18/2022, 3:58 PM
No
To make your app type safe
u

ursus

02/18/2022, 3:59 PM
its java defensive copying again, i.e. why swift is fast when it doesnt do this (optimizes copying away)
l

Lukasz Kalnik

02/18/2022, 3:59 PM
in clean architecture you can have different types at different layers (data, domain, presentation) even if they are almost the same
I don't know what you mean exactly
Speed is usually not a concern in current apps
Unless you have millions of items
u

ursus

02/18/2022, 4:00 PM
one of things that swift does is it sees every callsite and rather mutates the collections rather than creating new ones, where possibly
l

Lukasz Kalnik

02/18/2022, 4:00 PM
Then anyway the work should be offloaded to the backend
Yes, but it has nothing to do with my example
My example is: data layer: Message domain layer: MessageItem presentation layer: MessageUiItem <- belongs to sealed hierarchy
u

ursus

02/18/2022, 4:02 PM
well, its about needless instantiation, I do agree with Clean in general, but one shoud not create UseCases which are 1 or 2 lines, etc like many people seem to think
l

Lukasz Kalnik

02/18/2022, 4:02 PM
But that's your case exactly!
You want to reuse some logic between data and presentation
u

ursus

02/18/2022, 4:02 PM
true
l

Lukasz Kalnik

02/18/2022, 4:02 PM
You said it yourself "I don't want to copy paste"
Which is correct
u

ursus

02/18/2022, 4:02 PM
I know, im schyzo on this all the time
l

Lukasz Kalnik

02/18/2022, 4:03 PM
I have maybe 20 use cases in my 40K lines production app
And I think it's 10 too much
I do most of the logic inside ViewModels, because in my app every ViewModel does different thing
However when 2 ViewModels have to perform the same logic on some data, I abstract it away into a usecase
In one case I also have a parent ViewModel and 2 screens which inherit from it, because 80% of the logic is the same
So in the subclasses I just add some specific buttons for each screen etc.
u

ursus

02/18/2022, 4:05 PM
yea Im all for composition, but it somehow doesnt fit in my head with data .. for exmaple what if the difference was enum? i.e. Message(type: Type)
l

Lukasz Kalnik

02/18/2022, 4:05 PM
Difference between what?
u

ursus

02/18/2022, 4:05 PM
and now app B wanst to extend the enum .. no I need AppBMessage(type: AppBType) and all polymorphism is gone
l

Lukasz Kalnik

02/18/2022, 4:06 PM
You create two enums
Don't force the business case to fit your code, just because you want to have one type less
u

ursus

02/18/2022, 4:06 PM
okay but then you need the parent type extended as well
l

Lukasz Kalnik

02/18/2022, 4:07 PM
I must say I don't fully understand what you mean here
But anyway, do what works for you
I would extract common conversion logic to domain layer and create intermediate types
u

ursus

02/18/2022, 4:08 PM
different example alrogether, forget the ui items thing lets say shared code is this data class Message(val type: MessageType) where MessageType = Text | Image now app B wants to add video to the enum
l

Lukasz Kalnik

02/18/2022, 4:08 PM
Again, two hierarchies in UI layer
It's the same example
u

ursus

02/18/2022, 4:08 PM
you create AppBMessageType enum of Text | Image | Video, this enum is unrelated to the original
l

Lukasz Kalnik

02/18/2022, 4:08 PM
Yes
u

ursus

02/18/2022, 4:09 PM
which means you need AppBMessage as well
l

Lukasz Kalnik

02/18/2022, 4:09 PM
Yes, and?
What's wrong with having two enums in two different apps?
u

ursus

02/18/2022, 4:09 PM
now, woulld you make AppBMessage and Message related or no?
l

Lukasz Kalnik

02/18/2022, 4:09 PM
No
Why would I make AppB dependent on code in AppA? They are separate apps
u

ursus

02/18/2022, 4:10 PM
then how would you pass the AppBMessage into some other interface which takes Message?
l

Lukasz Kalnik

02/18/2022, 4:10 PM
You need an intermediary type
u

ursus

02/18/2022, 4:10 PM
Message lives in shared code, not app A
l

Lukasz Kalnik

02/18/2022, 4:10 PM
And you convert manually
u

ursus

02/18/2022, 4:10 PM
what intermediary type?
l

Lukasz Kalnik

02/18/2022, 4:10 PM
Ah ok, you should have said it. But then it's exactly the same example as your previous one
Domain layer type, which is not a sealed hierarchy
u

ursus

02/18/2022, 4:10 PM
lets say the api is
AnalyticsTracker.track(mesasge: Message)
l

Lukasz Kalnik

02/18/2022, 4:11 PM
You don't need a sealed hierarchy at every level
You can have it in your data layer and in your UI
u

ursus

02/18/2022, 4:11 PM
you created AppBMessage via mapping from Message .. so do you have a converter which goes backwards as well? so you can plug it into the AnalyticsTracker?
l

Lukasz Kalnik

02/18/2022, 4:12 PM
But in the domain layer you just have some other
MessageConverted
type, which is just independent type
Yes
It's like with JSON: you need a deserializer to read it and a serializer if you want to write it
This is clean separation of concerns
You can test every layer separately
Compiler guarantees you type safety
This is good architecture
I know it's more boilerplate, but you make your trade-offs
You have to analyze if the code reuse is worth the boilerplate
Or it's better to copy-paste
u

ursus

02/18/2022, 4:14 PM
so your decision rule is “how much would I hate copy pasting this”?
l

Lukasz Kalnik

02/18/2022, 4:14 PM
Usually in real life AppA and AppB will diverge more and more with time
My rule is "If I would hate copy pasting this even a little bit, I will change my architecture to be more composable"
And it usually means that the feature will be delivered a few days (or sprints) later
But in the long run we are saving money and time because of optimized architecture
Like I said, project get life of their own. You have to modularize them all the time to not introduce tight coupling
Have a nice weekend and good luck with your project!
u

ursus

02/18/2022, 4:18 PM
yea, I have to massage the backwards mapping in to my head so im fine with it 😄 thank you for your help!
l

Lukasz Kalnik

02/18/2022, 4:19 PM
You're welcome!
BTW depending on the complexity of the AnalyticsTracker, if it doesn't have any logic, I would just create
AnalyticsTrackerAppA
and
AnalyticsTrackerAppB
and skip the backwards mapping
backwards mapping you really only need for saving back to the database
And stuff like this
Or even for this not, if saving is a trivial operation -> then you can also just copy paste
In the end the question is where do you introduce complexity (by copy-pasting) and where you reduce it (by abstracting away)
If abstracting away introduces more complexity than copy-pasting, then just copy-paste
If you know you will have some complex common logic which is prone to change in the future, abstract it away into common domain layer
The whole point is this is always a little bit guessing, as we don't know what will happen in the future
That's why we are always refactoring 🙂
And decoupling where we see things became too dependent on each other
u

ursus

02/18/2022, 4:24 PM
yea and the guessing gives me paralysis by analysis
l

Lukasz Kalnik

02/18/2022, 4:26 PM
Yes, it's better to try 10 different architectures (even bad ones) instead of analyzing everything
I really recommend you to look into domain driven design if you haven't yet
And Scott Wlaschin's designing with types
We are lucky enough to program in Kotlin, where the compiler guarantees type-correctness of our code (as opposed to JavaScript)
u

ursus

02/18/2022, 4:27 PM
btw this is all sort of straightforward, but what if the Message was serialized to database? I flatten columns, i.e. the sql table has columns of all fields in the hierarchy, and then its mapped into ImageMessage, TextMessage etc at runtime all is fine .. but obviously you cannot just extend the table in App B, what do you do then? some extra table for the AppB specific columns?
l

Lukasz Kalnik

02/18/2022, 4:27 PM
If you make good use of types, compiler will basically check most of your program logic for free
This is a big question
Is there a business case for the apps to use the same database?
I mean if it's a local SQLite database on the smartphone you could reuse the common part of the schema in both apps and add the necessary extra table to AppB
Database design should not dictate how you structure your code
This should be the least of your concerns
To be honest, I would just create AppA schema and then have a completely separate schema for AppB
On Android anyway you cannot share a database between apps, so this question is kind of pointless
u

ursus

02/18/2022, 4:31 PM
thats what I do now but the bigger the type is the less I want to do it
why, yes I have a single database, think sharing tables
I was thinking to have the type specific columns in a separate tables, but unsure about performance of that
l

Lukasz Kalnik

02/18/2022, 4:32 PM
Hmm ok, then I don't know. I have experience mostly with data on REST servers
🤷
u

ursus

02/18/2022, 4:33 PM
basically you could view it as Message and MessageDetails, stitching it together into MessageWithDetails
l

Lukasz Kalnik

02/18/2022, 4:34 PM
Like I said, I have no idea about DB sharing and optimization
u

ursus

02/18/2022, 4:35 PM
but the issue is you want to do the stiching at database level, so you wont get glitch emits with reactive code, etc
c

Colton Idle

02/18/2022, 5:10 PM
What to do?
I probably wouldn't let the two apps share a VM.
👆 1
"Duplication is far cheaper than the wrong abstraction" https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
u

ursus

02/18/2022, 7:00 PM
cheaper until you have to patch multiple places and fotget, but yea, the guestion is where is the threshold
3 Views