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.
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'm creating custom dialog in some Fragments but I came up with a base fragment to use there and then just call from CHILD Fragment.
For instance, I've 10 fragments out of which just 3 are using that.
Does it good to move dialogs with base fragment and I just no need to recreate it.
Anything that affects here will be good to know for me
like cohesion, coupling, performance, architecture, etc
This is a bit of an opinion-based subject. But I think "composition over inheritance" is pretty universally accepted advice in the OOP world.
What you're doing doesn't matter one way or the other with respect to performance, but it's kind of a bad solution for code maintainability.
Some relevant things to read:
Composition over inheritance
Android Nightmares 😱 | Base classes
Say no to BaseActivity and BaseFragment
BaseActivity and BaseFragment are monsters
To do this without inheritance in this case, you can make a top-level extension function with the relevant arguments that returns the dialog. Example:
fun Fragment.showANumberDialog(theNumber: Int): AlertDialog =
AlertDialog.Builder(requireContext())
.setTitle(theNumber.toString())
.setPositiveButton(android.R.string.ok) {}
.show()
Note: You should really be using DialogFragment instead of a bare Dialog so it is maintained after screen rotations and when returning to the app after it has been in the background for a while. So the above example more appropriately would look like this:
fun Fragment.showANumberDialog(theNumber: Int) =
ANumberDialogFragment().apply {
arguments = bundleOf(NUM_KEY to theNumber.toString())
show(childFragmentManager, ANumberDialogFragment.TAG)
}
private const val NUM_KEY = "the number"
class ANumberDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
AlertDialog.Builder(requireContext())
.setTitle(arguments.getString(NUM_KEY))
.setPositiveButton(android.R.string.ok) {}
.show()
companion object {
const val TAG = "ANumberDialogFragment"
}
}
I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation
#Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel())
Now I noticed that the data that was loaded or set in the first screen is reset in the second.
I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?
According to the method's documentation it should return either an existing ViewModel or create a new one.
I also see that the view models are different objects. So viewModel() always creates a new one. But why?
Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?
Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
I am trying to get a view model in two places, one in the MainActivity using:
val viewModel:MyViewModel by viewModels()
The Other place is inside a compose function using:
val viewModel:MyViewModel = hiltViewModel()
When I debug, it seems that those are two different objects. Is there anyway where I can get the same object in two places ?
Even though you solved your issue without needing the view model, the question remained unanswered so I am posting this in case someone else finds it helpful.
This answer explains how view model scopes are shared and how you can override it.
In case you are using Navigation component, this should help. However, if you don't want to pass down view models or override the provided ViewModelStoreOwners, you can access the parent activity's view model in any child composable like below.
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
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.