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!
Related
How can I use acquireToken - which requires a Fragment to be passed in - with Jetpack Compose, where I don't have a Fragment?
val parameters = AcquireTokenParameters.Builder()
.withScopes(scopes.toList())
.withCallback(authenticationCallback)
.withFragment(<what can I pass in here?>) // <--------- relevant line
.build()
_msalPublicClient.acquireToken(parameters)
I am on the latest MSAL for Android, version 4.1.0
Disclaimer: This answer might work/not work based on the architecture of your app.
To begin with ,quite strange why microsoft requires Fragment and not sure about what is the usecase behind it.
It seems issue raised here.Have to wait for update regarding that.
Lets assume for now Fragment is compulsory. Actually we can use Fragment in compose app. Because legacy apps using xml must have support to gradual migration from fragment architecture to compose ui.By the way, Android provides that flexibility to integrate composable to legacy activity/fragment architecture.
2 Cases .
If you have single activity app architecture and all other are compose UI. You might need to do little more work to adapt fragment in compose app.
If the signin/signup screen where you are using msal is separate activity and for core features you have other activity , you can integrate fragment only to activity where msal is being used.
Lets jump in to implementation.
First thing ,you need fragment dependancy.
implementation 'androidx.fragment:fragment:1.5.5'
Assuming you are having activity.
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
Here your activity should extend FragmentActivity or any activity classes extending FragmentActivity. ComponentActivity may not work.
You need to have your xml file for activity and use setcontent view. You might need to create layout resource folder and layout file if not present.
Your activity xml.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment"
android:layout_width="match_parent"
android:name="com.example.myapplication.MyFragment"
android:layout_height="match_parent"/>
Here were are having FragmentContainerView and refer to the fragment class we are going to create. No need to inflate.
My Fragment class.
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
CompositionLocalProvider(MyFragmentProvider provides this#MyFragment) {
SigninScreen("My App Name")
}
}
}
}
}
}
}
Here Fragment does not need xml file. Fragment will now be the root of all your compose content. Here we are using CompositionLocalProvider to hold the fragment instance and later it can be used in multiple composables.
You can also use LocalCurrent.context and typecast to fragment. But that you might need to do everytime. Better way is to use CompositionLocalProvider.
You can add staticCompositionLocalOf outside of your fragment class in.
val MyFragmentProvider = staticCompositionLocalOf<MyFragment> {
error("No Fragment Instance Provided")
}
So you can access fragment instance in any composables which are in same activity and fragment layout tree.
Here this is the composable function ui. Here I am getting the fragment instance and passing to MSAL.
#Composable
fun SigninScreen(name: String) {
//Here I am getting instance of the fragment.
val myFragment = MyFragmentProvider.current
Text(text = "Signin $name!")
Button(onClick = {
val parameters = AcquireTokenParameters.Builder()
.withScopes(scopes.toList())
.withCallback(authenticationCallback)
.withFragment(myFragment)
.build()
_msalPublicClient.acquireToken(parameters)
}) {
Text(text = "Signin")
}
}
The most important thing is your composable's direct parent should be fragment instead of activity. And its instance will be provided to all child composables via CompositionLocalProvider. And this migration is wholly based on existing architecture and size of the project.
Maybe I'm blind but I can't find anything about injecting a dependency that needs parameters in side a composable using dagger hilt.
Lets say my ViewModel looks something like this:
class MyViewModel #AssistedInject constructor(#Assisted myValue: Int) : ViewModel() {
...
}
and I've got a factory interface like this:
#AssistedFactory
interface MyViewModelAssistedFactory {
fun create(myValue: Int): MyViewModel
}
how can I inject that dependency with a certain value as parameter?
All answers I found where like:
#Inject
var myViewModelFactory: MyViewModelAssistedFactory;
and
val initValue = 4
fun onCreate(){
val viewModel = myViewModelFactory.create(initValue)
}
but that doesn't work inside a composable fun.
Not sure if still relevant but if you use the navigation component you can just call hiltViewModel() in the navgraph builder.
Example:
https://github.com/pablichjenkov/ComposeStudy/blob/04298ca8393d3eea0f5b7883fb223161ef79a962/app/src/main/java/com/pablichj/study/compose/home/HomeNavigation.kt#L24
If not using Jetpack Navigation then the solution is a bit more complex. You will need to create a State tree in your App where they implement LifecycleOwner and ViewModelStoreOwner, in order to be able to install the ViewModel appropriately. The good news is that there is work out there already doing so, check this:
https://github.com/Syer10/voyager
I have a ViewModel that takes a string as an argument
class ComplimentIdeasViewModel(ideaCategory : String) : ViewModel() {
//some code here
}
What is the best way to initiate this ViewModel inside a composable fun without using a ViewModel factory and Hilt? A simple statement seems to achieve this inside a composable fun
#Composable
fun SampleComposableFun() {
val compIdeasViewModel = remember { ComplimentIdeasViewModel("someCategory") }
}
There is no warning in Android studio when I try to do this, but this seems too easy to be true, I am able to do this without Dependency Injection and with a ViewModelFactory class. Am I missing something here?
I've tried how you have written yours out and I had issues with screen rotation resetting the view model. I suspect you may too.
I was able to fix it by utilizing the the factory parameter on viewModel() for this, which worked well for me. See this answer on a similar question with example on how to use it: jetpack compose pass parameter to viewModel
This will not provide you the correct instance if viewmodel. See if you store some state in the viewmodel, then using the factory to initialise it is necessary to ensure that you get the same and latest copy of the viewmodel currently present. There is no error since the syntactic implementation is correct. I do not know of any way to do this because most of the times, you don't need to. Why don't you initialise it in the top-level container, like the activity? Then pass it down wherever necessary.
Create a CompositionLocal for your ViewModel.
val YourViewModel = compositionLocalOf { YourViewModel(...) }
Then initialise it (You'd likely use the ViewModelProvider.Factory here). And then provide that to your app.
CompositionLocalProvider(
YourViewModel provides yourInitialisedViewModel,
) {
YourApp()
}
Then reference it in the composable.
#Composable
fun SampleComposableFun(
compIdeasViewModel = YourViewModel.current
) {
...
}
Note, the docs say that ViewModels are not a good fit for CompositionLocals because they will make your composable harder to test, make your composables tied to this app and make it harder to use #Preview.
Some get pretty angry about this. However, if you manage to mock out the ViewModel, so you can test the app and use #Preview and your composables are tied to the app and not generic, then I see no problem.
You can mock a ViewModel fairly simply, providing its dependencies are included as parameters (which is good practice anyway).
open class MockedViewModel : MyViewModel(
app = Application(),
someOtherDependeny = MockedDependecy(),
)
The more dependencies your ViewModel has the more mocking you'll need to do. But I've not found it prohibitive and including the ViewModel as a default parameter has massively sped up development.
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".
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.