I am trying to create a list of items in Jetpack Compose that automatically updates whenever the underlying data changes, but it is not updating and I cannot add an item to the LazyColumnFor after it has initially composed.
private var latestMessages = mutableListOf<ChatMessage>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(context = requireContext()).apply {
setContent {
LatestMessages(latestMessages)
}
}
}
#Composable
fun LatestMessages(messages: MutableList<ChatMessage>) {
Column {
LazyColumnFor(items = messages) { chatMessage ->
LatestMessageItem(chatMessage) {
// onclick
}
}
}
Box(alignment = Alignment.Center) {
Button(onClick = {
latestMessages.add(
ChatMessage(
"testId",
"testText2",
"testFromId2",
"testToId",
"timestamp",
0
)
)
}) {
}
}
}
The Button in the Box is just to test adding an item. From what I understand LazyColumnFor should update whenever the underlying data is changed, similar to ForEach in SwiftUI. What am I doing wrong?
Edit March 2022
Jetpack compose no longer uses LazyColumnFor so I've updated the answer to reflect the current state of compose.
Jetpack compose always needs a state object to be modified before recomposition can occur. You need to use a MutableStateList<T>. You can pass a reference to it around and update in any method which accepts a SnapShotStateList<T>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(context = requireContext()).apply {
setContent {
val latestMessages = mutableStateListOf<ChatMessage>()
LatestMessages(latestMessages)
}
}
}
#Composable
fun LatestMessages(messages: SnapshotStateList<ChatMessage>) {
Column {
LazyColumn {
items(messages){ chatMessage ->
LatestMessageItem(chatMessage) {
// onclick
}
}
}
Box(alignment = Alignment.Center) {
Button(onClick = {
latestMessages.add(
ChatMessage(
"testId",
"testText2",
"testFromId2",
"testToId",
"timestamp",
0
)
)
}) {
}
}
}
Related
I am training with a simple app to show movies, I use an MVVM pattern and Flow.
Problem
This is my home, filterable through chips
I click on a movie , the details screen comes up then I go back to the home and this is the result:
Using logcat the home screen gets the list of movies to show but is not shown in the recyclerview (which uses diffUtil).
Below is the code for my fragment:
#AndroidEntryPoint
class Home2Fragment : Fragment() {
private val TAG = Home2Fragment::class.simpleName
private var _binding: FragmentHome2Binding? = null
private val binding: FragmentHome2Binding
get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHome2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
initChipGroupSpecificMovieList()
val adapter = MovieAdapter()
sectionRv.setHasFixedSize(true)
sectionRv.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.movieListBySpecification.collectLatest {
Log.d(TAG, "onViewCreated: received list")
adapter.addItems(it)
}
}
}
}
}
private fun FragmentHome2Binding.initChipGroupSpecificMovieList() {
val sortByMap = HomeViewModel.Companion.MovieListSpecification.values()
chipGroup.removeAllViews()
for (specification in sortByMap) {
val chip = Chip(context)
chip.isCheckable = true
chip.id = specification.ordinal
chip.text = getString(specification.nameResource)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked)
viewModel.setMovieListSpecification(specification)
}
chipGroup.addView(chip)
}
chipGroup.check(sortByMap.lastIndex - sortByMap.size + 1)//check first element
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
it seems at the line of code where I try to insert the list of movies in the adapter this doesn't add them because maybe via diffUtil it finds that it is the previous list and so it doesn't load it. However it doesn't show the previous one either, possible solutions?
as java code you can use this
#Override
public void onResume() {
super.onResume();
if(it.size() > 0) {
adapter.addItems(it)
}
}
I have a compose screen in a fragment, and in that screen there is a button.
I want to dismiss the fragment/go back to previous screen when this button is pressed.
But I can't access any activity/fragment methods inside onClick.
How can I do that?
#AndroidEntryPoint
class MyFragment #Inject constructor() : Fragment(){
#ExperimentalComposeUiApi
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
this.setContent {
Button(
onClick = {
//Dismiss fragment.
},
) {
Text(
"Click me"
)
}
}
}
}
}
You can get the LocalOnBackPressedDispatcherOwner inside any composable, and use onBackPressed() or navigate in an other way:
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
Button(
onClick = {
onBackPressedDispatcher?.onBackPressed()
},
) {
Text(
"Click me"
)
}
Solved like this:
#AndroidEntryPoint
class MyFragment #Inject constructor(
) : Fragment(){
#ExperimentalComposeUiApi
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
this.setContent {
val shouldDismiss = remember { mutableStateOf(false) }
if (shouldDismiss) {
dismissFragment()
}else{
Button(
onClick = {
shouldDismiss.value = true
},
) {
Text(
"Click me"
)
}
}
}
}
}
private fun dismissFragment(){
activity?.onBackPressed()
}
}
So, what I'm trying to do is to get information from api and add it to model: MutableList and then set model information to recycler view. But the problem is, recycler view is being called before api gets all responses and so, my view is empty. This is what i concluded, if I'm wrong please correct me. But if not, How can I fix this problem?
class FavouritesFragment(myContext : HomeActivity, fvts: ArrayList<String>): Fragment() {
var activity = myContext
var favourites = fvts
var model : MutableList<CurrentWeatherModel> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_favourites, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
init()
}
private fun init() {
model.clear()
favourites.forEach {
getForecastModel(it)}
Log.d("other", model.size.toString())
favouritesRecyclerView.layoutManager = LinearLayoutManager(activity)
val adapter = CurrentRecyclerViewAdapter(model, activity)
favouritesRecyclerView.adapter = adapter
}
private fun getForecastModel(value: String?) {
if (!value.isNullOrEmpty()) {
DataLoader.getRequestForQuery("weather", value, "a77840cefd5a443793bd5e6b54776905", object: CustomCallback{
override fun onSuccess(result: String) {
if (result != "null") {
var converted = Gson().fromJson(result, CurrentWeatherModel::class.java)
model.add(converted)
}
}
})
}
}
}
You need to notify the adapter that data has changed, you can achieve this by calling adapter.notifyDataSetChanged() whenever the data has changed.
So you need to add this line after adding data to the list
model.add(converted)
adapter.notifyDataSetChanged()
HomeFragment.kt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return setupBindings(inflater, container, savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
//filter the data in step 2
return false
}
override fun onQueryTextChange(query: String): Boolean {
orderHistoryViewModel.search(query)
return false
}
})
}
I have written setOnQueryTextListener in the Fragment. When I run the app, it is working only once. But how do I erase the filtered list and populate the original list everytime i clear the data from search bar and filter again when the new data is typed.
I wrote the search function in viewmodel. The below code filters data from the orinal list and populates the filtered list.
ViewModel.kt
fun search(query: String) {
var filteredList =
orderHistoryDetails?.orderList?.value?.filter { x -> x.order.contains(query) }
orderHistoryDetails?.orderList?.value = filteredList
}
You need to store original list also,and check whether query string is empty or not.
fun search(text: String) {
val filterList: MutableList<OrderItem> = arrayListOf()
if (text.trim().length == 0) {
filterList.clear()
filterList.addAll(listOriginal)
notifyDataSetChanged()
} else {
listOriginal.filter {x->
x.order.toUpperCase.contains(text)
}.apply {
filterList.clear()
filterList.addAll(this)
}
notifyDataSetChanged()
}
}
The documentation describes how to create UI Jetpack Compose inside Activity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
But how can I use it inside fragment?
setContent on ViewGroup is now deprecated.
The below is accurate as of Compose v1.0.0-alpha01.
For pure compose UI Fragment:
class ComposeUIFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
Text(text = "Hello world.")
}
}
}
}
For hybrid compose UI Fragment - add ComposeView to xml layout, then:
class ComposeUIFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_compose_ui, container, false).apply {
findViewById<ComposeView>(R.id.composeView).setContent {
Text(text = "Hello world.")
}
}
}
}
You don't need Fragments with Compose. You can navigate to another screen without needing a Fragment or an Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = "welcome") {
composable("welcome") { WelcomeScreen(navController) }
composable("secondScreen") { SecondScreen() }
}
}
}
}
#Composable
fun WelcomeScreen(navController: NavController) {
Column {
Text(text = "Welcome!")
Button(onClick = { navController.navigate("secondScreen") }) {
Text(text = "Continue")
}
}
}
#Composable
fun SecondScreen() {
Text(text = "Second screen!")
}
With 1.0.x you can :
- Define a ComposeView in the xml-layout.
add a androidx.compose.ui.platform.ComposeView in your layout-xml files:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
...>
<TextView ../>
<androidx.compose.ui.platform.ComposeView
android:id="#+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Then get the ComposeView using the XML ID, set a Composition strategy and call setContent():
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentExampleBinding.inflate(inflater, container, false)
val view = binding.root
view.composeView.apply {
// Dispose the Composition when viewLifecycleOwner is destroyed
setViewCompositionStrategy(
DisposeOnLifecycleDestroyed(viewLifecycleOwner)
)
setContent {
// In Compose world
MaterialTheme {
Text("Hello Compose!")
}
}
}
return view
}
/** ... */
}
- Include a ComposeView directly in a fragment.
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
// Dispose the Composition when viewLifecycleOwner is destroyed
setViewCompositionStrategy(
DisposeOnLifecycleDestroyed(viewLifecycleOwner)
)
setContent {
MaterialTheme {
// In Compose world
Text("Hello Compose!")
}
}
}
}
}
Found it:
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val fragmentView = inflater.inflate(R.layout.fragment_login, container, false)
(fragmentView as ViewGroup).setContent {
Hello("Jetpack Compose")
}
return fragmentView
}
#Composable
fun Hello(name: String) = MaterialTheme {
FlexColumn {
inflexible {
// Item height will be equal content height
TopAppBar( // App Bar with title
title = { Text("Jetpack Compose Sample") }
)
}
expanded(1F) {
// occupy whole empty space in the Column
Center {
// Center content
Text("Hello $name!") // Text label
}
}
}
}
}
On my mind if you want to use Jetpack Compose with fragments in a pretty way like this
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = contentView {
Text("Hello world")
}
you can create you own extension functions for Fragments
fun Fragment.requireContentView(
compositionStrategy: ViewCompositionStrategy = DisposeOnDetachedFromWindow,
context: Context = requireContext(),
content: #Composable () -> Unit
): ComposeView {
val view = ComposeView(context)
view.setViewCompositionStrategy(compositionStrategy)
view.setContent(content)
return view
}
fun Fragment.contentView(
compositionStrategy: ViewCompositionStrategy = DisposeOnDetachedFromWindow,
context: Context? = getContext(),
content: #Composable () -> Unit
): ComposeView? {
context ?: return null
val view = ComposeView(context)
view.setViewCompositionStrategy(compositionStrategy)
view.setContent(content)
return view
}
I like this approach because it looks similar to Activity's setContent { } extension
Also you can define another CompositionStrategy
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = contentView(DisposeOnLifecycleDestroyed(viewLifecycleOwner)) {
Text("Hello world")
}