Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android w/o DI

Android w/o DI

Android architecture has come a long way in standards and best practices. Especially near the UI. I don’t see many apps that don’t use ViewModels and LiveData to simplify the code. But one step further from the UI, where the business logic resides there are almost as many implementations as there are applications. Still there seem to be a general idea that dependency injection should be the golden way, but I don’t see it, not yet at least. The popular frameworks have been around long, and haven’t convinced so far. As I am a big fan of reactive and loosely coupled systems I have an alternative to share; Actors. Let me show you my approach to Android Architecture with Actors and why I believe this is the way to move forward.

0297b9b4bfd45c0f9c6c52bf696b7735?s=128

Bob Dahlberg

October 16, 2020
Tweet

Transcript

  1. Bob Dahlberg Author 2020-10-16 Date Android w/o DI Actor based

    architecture with no external dependency injection framework
  2. Android Architecture BEST PRACTICES </>

  3. MVVM

  4. View ViewModel LiveData MVVM

  5. View ViewModel LiveData Navigation Pagination Lifecycle aware ... MVVM

  6. The frameworks HILT & KOIN </>

  7. The frameworks • Steep learning curve • Poor overview •

    Needs building • Needs extending to inject • Easier to write than to read • Requires your context Hilt Koin
  8. The frameworks • Steep learning curve • Poor overview •

    Needs building • Needs extending to inject • Easier to write than to read • Requires your context Hilt Koin
  9. The frameworks • Steep learning curve • Poor overview •

    Needs building • Needs extending to inject • Easier to write than to read • Requires your context Hilt Koin
  10. The X Business Logic </>

  11. The X Business Logic </> Models Modules Beans Repositories Components

    Stores Repo Data Business Domain UseCase Services Workers Tasks Pipes Manager Container
  12. Actor the model </>

  13. "An actor is the primitive unit of computation. It’s the

    thing that receives a message and do some kind of computation based on it." https://www.brianstorti.com/the-actor-model/ </>
  14. Actor - one minute lecture actor<Message> { private var state:

    UserState for (msg in channel) { when(msg) { } } } 1. An actor has a private mutable state. 2. An actor has a mailbox where others can send messages to it. 3. An actor can perform one of three things when receiving a message • Create more actors • Send messages to other actors • Mutate its state (designate what to do with the next message) Rules of an actor
  15. The setup a reactive approach </>

  16. Rx or Channels? Is it different </>

  17. Fragment User clicks login in the UI and the Fragment

    calls its ViewModel to act. ViewModel Gets called to login. Transforms the input into a Message and sends it on the App Stream AppStream Broadcasts the Message to anyone who wants to receive it UserActor Listens to LoginMessages and performs an action to fullfil its intent. basic flow Fragment Observes LiveData on the ViewModel for changes and updates the UI accordingly. ViewModel Listens to the new state and emits it on a LiveData object. AppStream Broadcasts the Message to anyone who wants to receive it UserActor When the private state changes, or fails to change, send a Message with the new state to the AppStream
  18. Small example Authenticate </>

  19. The stream/broker/bus object AppStream { private val streamScope = CoroutineScope(EmptyCoroutineContext

    + SupervisorJob()) private val stream: BroadcastChannel<Message> = BroadcastChannel(100) fun send(event: Message) = streamScope.launch { stream.send(event) } val messages: ReceiveChannel<Message> get() = stream.openSubscription() }
  20. The stream/broker/bus object AppStream { private val streamScope = CoroutineScope(EmptyCoroutineContext

    + SupervisorJob()) private val stream: BroadcastChannel<Message> = BroadcastChannel(100) fun send(event: Message) = streamScope.launch { stream.send(event) } val messages: ReceiveChannel<Message> get() = stream.openSubscription() }
  21. The stream/broker/bus object AppStream { private val streamScope = CoroutineScope(EmptyCoroutineContext

    + SupervisorJob()) private val stream: BroadcastChannel<Message> = BroadcastChannel(100) fun send(event: Message) = streamScope.launch { stream.send(event) } val messages: ReceiveChannel<Message> get() = stream.openSubscription() }
  22. The stream/broker/bus object AppStream { private val streamScope = CoroutineScope(EmptyCoroutineContext

    + SupervisorJob()) private val stream: BroadcastChannel<Message> = BroadcastChannel(100) fun send(event: Message) = streamScope.launch { stream.send(event) } val messages: ReceiveChannel<Message> get() = stream.openSubscription() val states: Flow<State> get() = flow { emitAll(stream.openSubscription()) }.filterIsInstance() }
  23. The abstract Actor abstract class Actor { private val scope

    = CoroutineScope(Dispatchers.Default + Job()) fun start() = scope.launch { val actor = actor<Message> { for (msg in channel) { act(msg) } } AppStream.messages.consumeEach { actor.send(it) } } fun stop() = scope.cancel() protected open suspend fun act(message: Message) {} protected fun send(message: Message) = AppStream.send(message) }
  24. The abstract Actor abstract class Actor { private val scope

    = CoroutineScope(Dispatchers.Default + Job()) fun start() = scope.launch { val actor = actor<Message> { for (msg in channel) { act(msg) } } AppStream.messages.consumeEach { actor.send(it) } } fun stop() = scope.cancel() protected open suspend fun act(message: Message) {} protected fun send(message: Message) = AppStream.send(message) }
  25. The abstract Actor abstract class Actor { private val scope

    = CoroutineScope(Dispatchers.Default + Job()) fun start() = scope.launch { val actor = actor<Message> { for (msg in channel) { act(msg) } } AppStream.messages.consumeEach { actor.send(it) } } fun stop() = scope.cancel() protected open suspend fun act(message: Message) {} protected fun send(message: Message) = AppStream.send(message) }
  26. The UserActor class UserActor(private val api: Api) : Actor() {

    private var user: User = User.EMPTY override suspend fun act(message: Message) { when(message) { is LoginMessage login(message.username, message.password) } } private suspend fun login(username: String, password: String) { val result = api.login(username, password) .onSuccess { user = it } send(UserState(user, result.getError())) } }
  27. The UserActor class UserActor(private val api: Api) : Actor() {

    private var user: User = User.EMPTY override suspend fun act(message: Message) { when(message) { is LoginMessage login(message.username, message.password) } } private suspend fun login(username: String, password: String) { val result = api.login(username, password) .onSuccess { user = it } send(UserState(user, result.getError())) } }
  28. The UserActor class UserActor(private val api: Api) : Actor() {

    private var user: User = User.EMPTY override suspend fun act(message: Message) { when(message) { is LoginMessage login(message.username, message.password) } } private suspend fun login(username: String, password: String) { val result = api.login(username, password) .onSuccess { user = it } send(UserState(user, result.getError())) } }
  29. The LogActor class LogActor : Actor() { override suspend fun

    act(message: Message) { val tag = if(message is State) " " else " " Timber.tag(tag).i(message.toString()) } }
  30. The App (DI ahead) class App : Application() { override

    fun onCreate() { super.onCreate() val api = createApi() UserActor(api).start() LogActor().start() } }
  31. The ViewModel class MainViewModel : ViewModel() { private val userFlow:

    Flow<UserState> = flow { emitAll(AppStream.states.filterIsInstance()) } val loggedIn = userFlow .map { it.user User.EMPTY it.error null } .asLiveData() val errorMessage = userFlow .map { it.error message "" } .asLiveData() fun login(username:String, password:String) { AppStream.send(LoginMessage(username, password)) } }
  32. The ViewModel class MainViewModel : ViewModel() { private val userFlow:

    Flow<UserState> = flow { emitAll(AppStream.states.filterIsInstance()) } val loggedIn = userFlow .map { it.user User.EMPTY it.error null } .asLiveData() val errorMessage = userFlow .map { it.error message "" } .asLiveData() fun login(username:String, password:String) { AppStream.send(LoginMessage(username, password)) } }
  33. The ViewModel class MainViewModel : ViewModel() { private val userFlow:

    Flow<UserState> = flow { emitAll(AppStream.states.filterIsInstance()) } val loggedIn = userFlow .map { it.user User.EMPTY it.error null } .asLiveData() val errorMessage = userFlow .map { it.error message "" } .asLiveData() fun login(username:String, password:String) { AppStream.send(LoginMessage(username, password)) } }
  34. The ViewModel class MainViewModel : ViewModel() { private val userFlow:

    Flow<UserState> = flow { emitAll(AppStream.states.filterIsInstance()) } val loggedIn = userFlow .map { it.user User.EMPTY it.error null } .asLiveData() val errorMessage = userFlow .map { it.error message "" } .asLiveData() fun login(username:String, password:String) { AppStream.send(LoginMessage(username, password)) } }
  35. The View class MainFragment : Fragment() { private val viewModel:

    MainViewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.loggedIn.observe(viewLifecycleOwner) { loggedIn message.text = "Is the user logged in: $loggedIn" } viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage error.text = errorMessage } login_button.setOnClickListener { viewModel.login(login_user.text, login_pass.text) } } }
  36. The View class MainFragment : Fragment() { private val viewModel:

    MainViewModel by viewModels() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.loggedIn.observe(viewLifecycleOwner) { loggedIn message.text = "Is the user logged in: $loggedIn" } viewModel.errorMessage.observe(viewLifecycleOwner) { errorMessage error.text = errorMessage } login_button.setOnClickListener { viewModel.login(login_user.text, login_pass.text) } } }
  37. The Log I/ : LoginMessage(username=bob, password= ) I/ : UserState(user=User(username=NONE,

    token=), error=java.lang.Error: We got error) I/ : LoginMessage(username=bob, password= ) I/ : UserState(user=User(username=Bob, token=secret), error=null)
  38. The additions open class AwaitMessage<T>( private val deferred: CompletableDeferred<T> =

    CompletableDeferred() ) : Message() { suspend fun await() T = deferred.await() fun complete(value:T) { deferred.complete(value) } }
  39. The additions open class AwaitMessage<T>( private val deferred: CompletableDeferred<T> =

    CompletableDeferred() ) : Message() { suspend fun await() T = deferred.await() fun complete(value:T) { deferred.complete(value) } } data class ReadPrefsIntMessage( val key: String, val default: Int ) : AwaitMessage<Int>()
  40. The additions class PrefsActor(private val prefs: SharedPreferences) : Actor() {

    override suspend fun act(msg: Message) { when(msg) { is ReadPrefsStringMessage msg.complete(readString(msg.key, msg.default)) is ReadPrefsBoolMessage msg.complete(readBool(msg.key, msg.default)) is ReadPrefsIntMessage msg.complete(readInt(msg.key, msg.default)) } } }
  41. The additions class PrefsActor(private val prefs: SharedPreferences) : Actor() {

    override suspend fun act(msg: Message) { when(msg) { is ReadPrefsStringMessage msg.complete(readString(msg.key, msg.default)) is ReadPrefsBoolMessage msg.complete(readBool(msg.key, msg.default)) is ReadPrefsIntMessage msg.complete(readInt(msg.key, msg.default)) } } } val message = ReadPrefsBoolMessage(ALL_NOTIFICATIONS_ON, true) AppStream.send(message) val pushOn = message.await()
  42. The additions class PrefsActor(private val prefs: SharedPreferences) : Actor() {

    override suspend fun act(msg: Message) { when(msg) { is ReadPrefsStringMessage msg.complete(readString(msg.key, msg.default)) is ReadPrefsBoolMessage msg.complete(readBool(msg.key, msg.default)) is ReadPrefsIntMessage msg.complete(readInt(msg.key, msg.default)) } } } val pushOn = ReadPrefsBoolMessage(ALL_NOTIFICATIONS_ON, true).send().await()
  43. Injecting the Api • Would be quite an overhead •

    The mailbox (channel) is fair and handles messages one at the time Why not a NetworkActor?
  44. Bob Dahlberg Tech Lead Mobile See more GDG Stockholm Android

    twitter.com/mr_bob speakerdeck.com/bobdahlberg medium.com/@dahlberg.bob Thank you bob@qvik.com