Skip to content

5. Framework Layer

Khoshnaw edited this page Mar 10, 2022 · 1 revision

UI

The UI module contains any UI related code Activity, Fragment, Adapter, XML resources etc..., Notice that our architecture make sure that our UI model is as dumb as possible. Since our entities and use-cases are responsible for the business logic. And then our ViewModel is translating between our UI and our use-cases. This will make sure that our UI doesn't have any important code. It is just showing what the view model telling it to show. And Then tells the ViewModel what the user is doing.

Base Implementation

Activity

At the top level, we have BaseActivity which is not doing much. it is just a good practice to have this class.

abstract class BaseActivity : AppCompatActivity()

And then we have the MVIActivity which implements MVIView and has two generics for data binding and ViewModel.

abstract class MVIActivity<B : ViewDataBinding, V : StandardViewModel<*, *>> :
    BaseActivity(),
    MVIView<B, V>

MVIView on the other hand has a binding and viewModel variable with addition to viewModelVariableId this is the id of the ViewModel in the xml layouts.

interface MVIView<B : ViewDataBinding, V : MVIViewModel<*, *>> {
    val binding: B
    val viewModel: V
    val viewModelVariableId: Int

    fun onViewReady()
}

Then We have StandardActivity which has some common behaviours for our activity. for example we choose that the default value for viewModelVariableId in our activities are BR.viewModel. But you still can change that by overriding the variable. then in the onCreate method, we use binding to set content view so you don't need to do that in every activity.

We also inject the view model into the data binding object. and then we call observeState() method which is observing the view model states. and we observe the errors from the view model. and we also have the onViewReady() method which will be called when the view is ready for extra change that the child activity might need.

abstract class StandardActivity<B : ViewDataBinding, V : StandardViewModel<*, *>> :
    MVIActivity<B, V>(), StandardView<B, V> {

    override val viewModelVariableId = BR.viewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        binding.setVariable(viewModelVariableId, viewModel)
        viewModel.observeState()
        viewModel.observeError()
        onViewReady()
    }

    fun <I : MVIIntent> MVIViewModel<*, I>.runIntent(intent: I) {
        runIntentInScope(lifecycleScope, intent)
    }

    private fun V.observeState() =
        state.observe(this@StandardActivity) { it?.let { handleState(it) } }

    private fun V.observeError() {
        lifecycleScope.launch {
            error.receiveAsFlow().collect { showError(it) }
        }
    }

    override fun showError(message: ErrorMessage) {
        val messageStr = if (message == ErrorMessage.DEFAULT) getString(R.string.default_error)
        else message.message

        showError(messageStr)
    }

    private fun showError(messageStr: String) = Snackbar
        .make(binding.root, messageStr, Snackbar.LENGTH_SHORT)
        .show()

    override fun onViewReady() = Unit
    override fun handleState(state: MVIState) = Unit

}

Fragment

For the Fragments we have a similar structure as activity the BaseFragment is an important empty class. Notice that the BaseFragment is getting its layout from the constructor. this is making implementing data binding easier.

abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId)

And then we have MVIFragment which is very similar to MVIActivity. It is implementing MVIView in addition to data binding and view model generics.

abstract class MVIFragment<B : ViewDataBinding, V : StandardViewModel<*, *>>(
    @LayoutRes contentLayoutId: Int
) : BaseFragment(contentLayoutId),
    MVIView<B, V>

And then StandardFragment is also similar to StandardActivity. but notice that the standard fragment is using the main activity viewModel to send the error. this will give us a more stable error message implementation.

abstract class StandardFragment<B : ViewDataBinding, V : StandardViewModel<*, *>>(
    @LayoutRes contentLayoutId: Int
) : MVIFragment<B, V>(contentLayoutId), StandardView<B, V> {

    override val viewModelVariableId = BR.viewModel

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.setVariable(viewModelVariableId, viewModel)
        viewModel.observeState()
        viewModel.observeError()
        onViewReady()
    }

    fun <I : MVIIntent> MVIViewModel<*, I>.runIntent(intent: I) {
        runIntentInScope(viewLifecycleOwner.lifecycleScope, intent)
    }

    private fun V.observeState() = state.observe(viewLifecycleOwner) { it?.let { handleState(it) } }

    private fun V.observeError() {
        viewLifecycleOwner.lifecycleScope.launch {
            error.receiveAsFlow().collect { showError(it) }
        }
    }

    override fun showError(message: ErrorMessage) {
        if (activity is StandardView<*, *>) {
            val standardActivity = (activity as StandardView<*, *>)
            standardActivity.viewModel.updateError(message)
        }
    }

    override fun onViewReady() = Unit
    override fun handleState(state: MVIState) = Unit
}

Movie Fragment

In the MoviesFragment we are giving the implementation for binding and ViewModel using property delegation. And when the user swipes the list to refresh the movies we are running MoviesIntent.RefreshMovies intent this will inform the view model to refresh the list.

In handleState function, we are handling the MovieList state by showing the list and the loading if the UI is in the loading state.

@AndroidEntryPoint
class MoviesFragment : StandardFragment<FragmentMoviesBinding, MoviesViewModel>(
    R.layout.fragment_movies
) {
    override val binding by dataBindings(FragmentMoviesBinding::bind)
    override val viewModel: MoviesViewModel by viewModels()
    private val moviesAdapter by lazy { MovieAdapter(viewModel) }

    override fun onViewReady() {
        binding.movieRV.setHasFixedSize(true)
        binding.movieRV.adapter = moviesAdapter
        binding.swipeRefresh.setOnRefreshListener {
            viewModel.runIntent(MoviesIntent.RefreshMovies)
        }
    }

    override fun handleState(state: MVIState) {
        when (state) {
            is MoviesState.MovieList -> handleMovieList(state)
        }
    }

    private fun handleMovieList(moviesState: MoviesState.MovieList) {
        binding.swipeRefresh.isRefreshing = moviesState.isLoading
        moviesAdapter.items = moviesState.movies
    }

}

Remote

The Remote module is an Android module that is heavenly depending on the android framework to do the system remote operation. This module is using tools like OkHttp, Retrofit and Moshi. To perform a network HTTP requests to the Movie DB API then parse and map the result to an entity represented object. Again there isn't any important code in this module it is rather just performing what the repository is asking in the third layer.

APIDataSource

The API data source is the actual implementation of the remote data sources. Those classes are using movie DB API to access the remote data in the system.

Base Implementation

The base implementation is an empty Class called APIDataSource.

abstract class APIDataSource

Movie API DataSource

The MovieAPIDataSource is using movieApi that is provided by retrofit to perform movie-related remote operating. Like loading movie list using LoadMovieList function.

class MovieAPIDataSource @Inject constructor(
    private val movieApi: MovieApi
) : APIDataSource(), MovieRemoteDataSource {

    override suspend fun loadMovieList(
    ): List<MovieRemoteDTO> = movieApi.loadMovieList().bodyOrException().movieList

}

DB

The DB module is also an Android module that is heavenly depending on the android framework to cash data locally. This module is using the Room library to perform its actions. and also we need to make this module as dumb as possible. It just needs to do what is the repository is asking it to do.

Base Implementation

The base implementation is an empty Class called DBDataSource.

abstract class DBDataSource

Movie DB DataSource

DB data sources is the actual implementation of the Local data source introduced in the repository module. Those data sources are using room DB to cash data locally and perform operations on it.

Our MovieDBDataSource Implements MovieLocalDataSource interface and gives concrete implementation for it. and notice that it takes a MovieDao object in its constructor and uses it to perform its operations. for example, updateMovieList function is using insertAll function in MovieDao to insert the movies to the movie table.

class MovieDBDataSource @Inject constructor(
    private val movieDao: MovieDao
) : DBDataSource(), MovieLocalDataSource {

    override suspend fun updateMovieList(movieList: List<MovieLocalDTO>): Unit =
        movieDao.insertAll(movieList)

    override suspend fun observeMovies(): Flow<List<MovieLocalDTO>> =
        movieDao.observeMovies()

    override suspend fun loadMovieSize(): Int = movieDao.loadMovieSize()
}

APP

Our App module is the actual application. it contains our Hilt/Dagger modules. So it has the DI configuration. In addition to App class. notice that in our app/build that the app is depending on all our other modules. which is actually a limitation in the hilt library. HiltAndroidApp needs to have access to all our modules in order to work.

dependencies {
    //region setup
    api project(path: ':ui')
    api project(path: ':db')
    api project(path: ':remote')
    //endregion setup
    
    ...
}