so in MVVM architecture even in google samples we can see things like this:
class CharacterListActivity :BaseActivity() {
val ViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getData() // Bad!!!
...
viewModel.state.observe(this) { state ->
when(state) { // handling state is not views job
Success -> { navigatetoNextPage() } // navigating is not views job
Progress -> { showProgress() }
NetworkError -> { ShowSnackbar(viewModel.error) } // I,m not sure about this one either
Error -> { showErrorDialog(viewModel.error)
}
}
We know that any architecture has its own rules that makes the code testable, maintainable, and scalable over time.
in MVVM pattern according to both Wikipedia and Microsoft docs this is the View:
the view is the structure, layout, and appearance of what a user sees on the screen.[6] It displays a representation of the model and receives the user's interaction with the view (clicks, keyboard, gestures, etc.), and it forwards the handling of these to the view model via the data binding (properties, event callbacks, etc.) that is defined to link the view and view model.
each view is defined in XAML, with a limited code-behind that does not contain business logic. However, in some cases, the code-behind might contain UI logic that implements visual behavior such as animations.
XAML is a Xamarin thing, so now let's get back to our code:
here, since activity decides what to do with the state, the activity works as Controller like in MVC but, activity supposed to be the View ,view is just supposed to do the UI logic.
the activity even tells the ViewModel to get data. this is again not the View's job.
please note that telling what to do to the other modules in the code is not the View's job. this is making the view act as controller. view is supposed to handle its state via callbacks from the ViewModel.
the View is supposed to just tell the ViewModel about events like onClick().
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
so what is an alternative approach to do this without violation of architecture rules? should I have a function for any lif cycle event in ViewModel, like viewModel.onCreate? or viewModel.onStart? what about navigation or showing dialogs?
For The Record I'm not mixing Up mvc and mvvm, I'm saying that this pattern does which is recommended buy google.
This is not opinion-based, surely anyone can have their own implementation of any architecture but the rules must always be followed to achieve overtime maintainability.
I can name the violations in this code one by one for you:
1) UI is not responsible for getting data, UI just needs to tell ViewModel about events.
2) UI is not responsible for handling state which is exactly what it does here. more general, UI shouldn't contain any non-UI logic.
3) UI is not responsible for navigating between screens
the activity even tells the ViewModel to get data. this is again not the View's job.
Correct. The data fetch should be triggered either by ViewModel.init, or more accurately the activation of a reactive data source (modeled by LiveData, wrapping said reactive source with onActive/onInactive).
If the fetch MUST happen as a result of create, which is unlikely, then it could be done using the DefaultLifecycleObserver using the Jetpack Lifecycle API to create a custom lifecycle-aware component.
Refer to https://stackoverflow.com/a/59109512/2413303
since ViewModel doesn't have access to View, it can't show a dialog or navigate through the app directly!
You can use a custom lifecycle aware component such as EventEmitter (or here) to send one-off events from the ViewModel to the View.
You can also refer to a slightly more advanced technique where rather than just an event, an actual command is sent down in the form of a lambda expression sent as an event, which will be handled by the Activity when it becomes available.
Refer to https://medium.com/#Zhuinden/simplifying-jetpack-navigation-between-top-level-destinations-using-dagger-hilt-3d918721d91e
typealias NavigationCommand = NavController.() -> Unit
#ActivityRetainedScoped
class NavigationDispatcher #Inject constructor() {
private val navigationEmitter: EventEmitter<NavigationCommand> = EventEmitter()
val navigationCommands: EventSource<NavigationCommand> = navigationEmitter
fun emit(navigationCommand: NavigationCommand) {
navigationEmitter.emit(navigationCommand)
}
}
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
#Inject
lateinit var navigationDispatcher: NavigationDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigationDispatcher.navigationCommands.observe(this) { command ->
command.invoke(Navigation.findNavController(this, R.id.nav_host))
}
}
}
class LoginViewModel #ViewModelInject constructor(
private val navigationDispatcher: NavigationDispatcher,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
fun onRegisterClicked() {
navigationDispatcher.emit {
navigate(R.id.logged_out_to_registration)
}
}
}
If Hilt is not used, the equivalent can be done using Activity-scoped ViewModel and custom AbstractSavedStateViewModelFactory subclasses.
Related
I have a problem when using compose, then i found the answer
If you use Compose with Fragments, then you may not have the Fragments dependency where viewModels() is defined.
Adding:
implementation "androidx.fragment:fragment-ktx:1.5.2"
use Compose with Fragments, but I use Pure Compose, Also had this problem.
What am I missing? Or is there some connection between fragment and compose?
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val userViewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Content(userViewModel)
}
}
}
#Composable
fun Content(userViewModel: UserViewModel) {
val lazyArticleItem = userViewModel.list().collectAsLazyPagingItems()
thread {
repeat(200) {
userViewModel.insert(User())
}
}
LazyColumn(verticalArrangement = Arrangement.spacedBy(16.dp)) {
items(lazyArticleItem) { user ->
Text("user ${user?.id}")
}
}
}
The above is my ui interface code, based on this, I don't think I'm using fragment.
I want to declare my logic. I use Pure Compose instead of Fragment, but actually want to run the code must depend on androidx.fragment:fragment-ktx:1.5.2
It happens because you are using
val userViewModel: UserViewModel by viewModels()
You can access a ViewModel from any composable by calling the viewModel() function.
Use:
val userViewMode : UserViewModel = viewModel()
To use the viewModel() functions, add the androidx.lifecycle:lifecycle-viewmodel-compose:x.x.x
In programming, "fragment" and "compose" can refer to two related concepts:
Fragment: In UI design, a fragment is a portion of an activity's UI, which can be reused in multiple activities or combined to form a single activity. It provides a way to modularize the UI and make it more manageable.
Compose: Compose is a modern UI toolkit for Android app development introduced by Google, which allows developers to build and style UI elements using composable functions. It provides a way to create and reuse UI components that can be combined to form a complete app UI.
Both concepts are aimed at making UI design and development more modular, reusable and maintainable.
Hope this helps!
I have a working Activity (TwalksRouteActivity) that accepts a record id (routeID) from a bundle (passed from a Fragment), pulls the associated record from my repository (routesRepository), and passes an associated value/column (routeName) to my UI. This works fine. However, as I understand best practice (I am learning Android development), the call to my Repository should be in a ViewModel, not an Activity. Is this correct? I have tried but failed to do this myself and would really appreciate some help in how to do this please.
TwalksRouteActivity:
class TwalksRouteActivity() : AppCompatActivity() {
private lateinit var viewModel: RouteViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Log.i("CWM","Called ViewModelProvider")
//viewModel = ViewModelProvider(this).get(RouteViewModel::class.java)
var bundle: Bundle? = intent.extras
var routeID = bundle?.getInt("routeID")
lifecycleScope.launch (Dispatchers.Main) {
val database = getDatabase(application)
val routesRepository = RoutesRepository(database)
val selectedRoute = routesRepository.getRoute(routeID)
val routeName = selectedRoute.routeName
Log.d("CWM", routeName.toString())
setContentView(R.layout.route_detail)
val routeName_Text: TextView = findViewById(R.id.routeName_text)
routeName_Text.text = routeName.toString()
val routeID_Text: TextView = findViewById(R.id.routeID)
routeID_Text.text = routeID.toString()
}
}
}
You are correct. Best practices include the idea of a ViewModel that handles communications between bussiness logic (your repository) and the activity or fragment which uses or/and dislpays the data. You should check Android Developers ViewModel's official documentation at: ViewModel Overview. Also the guide to app architecture. Check the following image:
As you can see, it describes the data-driven communication flow, and as you said, the ViewModel will call the repository functions that get the data. The ViewModel will then provide the activity with variables and / or functions that can be observed (such as: LiveData), and fire events that the activity will take to make its state changes / data presentation in the UI (this is call reactive pattern).
You should check these Codelabs (free lessons from Google): Incorporate Lifecycle-Aware Components and Android Room with a View - Kotlin (although it mainly covers Room Library, the codelab makes use of ViewModel and Android's best practices recommended by Google). Also, you could check this article: ViewModels and LiveData: Patterns + AntiPatterns.
I could write a lot of code but I think it is beyond the scope of this answer. I'm also learning, and my way was to first understand how these things work and why these things are called "best practices".
The company I just started working at uses a so called Navigator, which I for now interpreted as a stateless ViewModel. My Navigator receives some usecases, with each contains 1 suspend function. The result of any of those usecases could end up in a single LiveData. The Navigator has no coroutine scope, so I pass the responsibility of scoping suspending to the Fragment using fetchValue().
Most current code in project has LiveData in the data layer, which I tried not to. Because of that, their livedata is linked from view to dao.
My simplified classes:
class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
val url = MediatorLiveData<String>()
fun goToUrl1() {
url.fetchValue { getUrl1() }
}
fun goToUrl2() {
url.fetchValue { getUrl2() }
}
fun <T> MediatorLiveData<T>.fetchValue(provideValue: suspend () -> T) {
val liveData = liveData { emit(provideValue()) }
addSource(liveData) {
removeSource(liveData)
value = it
}
}
}
class MyFeatureFragment : Fragment {
val viewModel: MyFeatureViewModel by viewModel()
val navigator: MyFeatureNavigator by inject()
fun onViewCreated() {
button.setOnClickListener { navigator.goToUrl1() }
navigator.url.observe(viewLifecycleOwner, Observer { url ->
openUrl(url)
})
}
}
My two questions:
Is fetchValue() a good way to link a suspend function to LiveData? Could it leak? Any other concerns?
My main reason to only use coroutines (and flow) in the data layer, is 'because Google said so'. What's a better reason for this? And: what's the best trade off in being consistent with the project and current good coding practices?
Is fetchValue() a good way to link a suspend function to LiveData?
Could it leak? Any other concerns?
Generally it should work. You probably should remove the previous source of the MediatorLiveData before adding new one, otherwise if you get two calls to fetchValue in a row, the first url can be slower to fetch, so it will come later and win.
I don't see any other correctness concerns, but this code is pretty complicated, creates a couple of intermediate objects and generally difficult to read.
My main reason to only use coroutines (and flow) in the data layer,
is 'because Google said so'. What's a better reason for this?
Google has provided a lot of useful extensions to use coroutines in the UI layer, e.g. take a look at this page. So obviously they encourage people to use it.
Probably you mean the recommendation to use LiveData instead of the Flow in the UI layer. That's not a strict rule and it has one reason: LiveData is a value holder, it keeps its value and provides it immediately to new subscribers without doing any work. That's particularly useful in the UI/ViewModel layer - when a configuration change happens and activity/fragment is recreated, the newly created activity/fragment uses the same view model, subscribes to the same LiveData and receives the value at no cost.
At the same time Flow is 'cold' and if you expose a flow from your view model, each reconfiguration will trigger a new flow collection and the flow will be to execute from scratch.
So e.g. if you fetch data from db or network, LiveData will just provide the last value to new subscriber and Flow will execute the costly db/network operation again.
So as I said there is no strict rule, it depends on the particular use-case. Also I find it very useful to use Flow in view models - it provides a lot of operators and makes the code clean and concise. But than I convert it to a LiveData with help of extensions like asLiveData() and expose this LiveData to the UI. This way I get best from both words - LiveData catches value between reconfigurations and Flow makes the code of view models nice and clean.
Also you can use latest StateFlow and SharedFlow often they also can help to overcome the mentioned Flow issue in the UI layer.
Back to your code, I would implement it like this:
class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
private val currentUseCase = MutableStateFlow<UseCase?>(null)
val url = currentUseCase.filterNotNull().mapLatest { source -> source.getData()}.asLiveData()
fun goToUrl1() {
currentUseCase.value = getUrl1
}
fun goToUrl2() {
currentUseCase.value = getUrl2
}
}
This way there are no race conditions to care about and code is clean.
And: what's the best trade off in being consistent with the project
and current good coding practices?
That's an arguable question and it should be primarily team decision. In most projects I participated we adopted this rule: when fixing bugs, doing maintenance of existing code, one should follow the same style. When doing big refactoring/implementing new features one should use latest practices adopted by the team.
I want implement router like in the code below, but in this case I should pass fragment or activity through ViewModel, but guide to app architecture says
The ViewModel doesn't know about UI components
How I can resolve this problem?
class RouterImpl : Router {
override fun openItem(fragment: Fragment, item: Item) {
val action = ItemListFragmentDirections.actionNavigationItemToInfoFragment(item)
findNavController(fragment).navigate(action)
}
}
class ItemListViewModel #Inject constructor(
private val router: Router
) : ViewModel() {
fun openItem(fragment: Fragment, item: Item) = router.openItem(fragment, item)
}
If you take a closer look at the VIPER architecture, you'll notice that the Router is tied to the Presenter, which, effectively is Activity/Fragment.
If you still try to combine the MVVM with the VIPER pattern, I'd suggest you take a different approach and try with the DI - Dependency inversion. This way the MVVM will know only about the abstraction of an entity (interface) that is able to perform the routing procedure (traverse fragments).
However, this is a slippery slope, since you're still able to create dependency between the ViewModel and Activity/Fragment and, thus, lead you to the memory leak.
Much safer way would be to implement some sort of observer in the Router (eg. live data observer), which will do exactly what you want in a safe (async) manner.
App Descripion
I'm trying to implement for my first time an Android app with MVP, where a message (taken from a pool of messages) is displayed and it's changed when the user click on the screen. Once all message have been displayed, the process will start over (following the same order of messages). A requirement is to display the same message if the app is closed/reopened. So, we have to implement some implement some store/restore state mechanism in the MVP model.
Here's a basic demo of the app:
MVP Design
I've implemented this MVP for this app as follow:
The Model takes care of what it will be the next message (it implements the application state).
The Presenter decides when to ask for the next message (to update the state), depending on the received events from the user (through the View).
The View decides how to show the actual message and communicates events from the user (clicks on the screen) to the presenter. In addition, since the View is also the MainActivity, it takes care to instantiate the Presenter and Model implementations. Finally, it saves the Model state (as a Parcelable) with onSaveInstanceState (and also restores it).
Some Code
(Partial) View implementation:
class MainActivity : AppCompatActivity(), ViewMVC {
private lateinit var presenter: Presenter
private var model: Model? = CircularModel(LinkedList<State>(Arrays.asList(
State("First"),
State("Second"),
State("Third")
)))
override fun onCreate(savedInstanceState: Bundle?) {
if (savedInstanceState != null) {
model = savedInstanceState.getParcelable("model")
}
presenter = PresenterImpl(this, model!!)
}
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putParcelable("model", model!!)
super.onSaveInstanceState(outState)
}
(Partial) Model implementation:
#Parcelize
class CircularModel constructor(var states: #RawValue Deque<State>?) : Model, Parcelable {
override fun getModelState(): State {
return states!!.peekFirst()
}
override fun getModelNextState(): State {
// Black magic happening here!
return getModelState()
}
}
The Problem / My question
Since Presenter and Model should be "Android agnostic", saving the app state (i.e., the Model object) is taken care by the View. However, this breaks the principle where the View doesn't know the Model. My question is: how to save the Model object, without the View knowing the actual implementation of it? What is the best way to deal with the Model state in this scenario?
An actual solution could be to write the code to serialize the Model in the Model itself and save it for each getNextState(), but this would mean use Android calls in the Model (and reduce its testability).
You should use a different persistence mechanism. The onSaveInstanceState() is really used for situations where the OS needs to restore UI state because of things like configuration / orientation changes. It's not a general purpose storage mechanism.
The model is the correct place to persist data and it is correct that you should try to keep the model as Android agnostic as possible. What you can do is define an interface that represents your persistence requirements:
interface SampleRepo{
fun saveData(...)
fun getData(...)
}
Then your preferred persistence mechanism (e.g. SharedPreferences, SQlite etc.) in a class implements that interface. This is where your Android specific stuff will be hidden.
class SharedPrefRepo : SampleRepo{
override fun saveData(...)
override fun getData(...)
}
Ideally you'll want some injection mechanism so you can inject an instance of the above to your model class (e.g. Dagger). It requires some more plumbing code, but that is the price of loose coupling. For a simpler app, like what you're doing, all of this is overkill. But if you're trying to study proper Android app architecture and loose coupling then it's worth exploring how to do it properly.