首页 > 解决方案 > 为什么数据不显示在片段中

问题描述

我正在开发一个新闻应用程序,我已经使用 ViewModel 和 Koin 对 Kotlin 协程进行了改造,但在我的 TopHeadlinesFragment 数据中没有显示。我不明白我在哪里犯错

在我实现网络的 appModules.kt 下方。

const val BASE_URL = "https://newsapi.org/"

val appModules = module {
    // The Retrofit service using our custom HTTP client instance as a singleton
    single {
        createWebService<SportNewsInterface>(
            okHttpClient = createHttpClient(),
            factory = RxJava2CallAdapterFactory.create(),
            baseUrl = BASE_URL
        )
    }
    // Tells Koin how to create an instance of CatRepository
    factory<NewsRepository> { (NewsRepositoryImpl(sportNewsInterface = get())) }
    // Specific viewModel pattern to tell Koin how to build MainViewModel
    viewModel { MainViewModel (newsRepository = get ())  }
}

/* Returns a custom OkHttpClient instance with interceptor. Used for building Retrofit service */
fun createHttpClient(): OkHttpClient {
    val client = OkHttpClient.Builder()
    client.readTimeout(5 * 60, TimeUnit.SECONDS)
    return client.addInterceptor {
        val original = it.request()
        val requestBuilder = original.newBuilder()
        requestBuilder.header("Content-Type", "application/json")
        val request = requestBuilder.method(original.method, original.body).build()
        return@addInterceptor it.proceed(request)
    }.build()
}

/* function to build our Retrofit service */
inline fun <reified T> createWebService(
    okHttpClient: OkHttpClient,
    factory: CallAdapter.Factory, baseUrl: String
): T {
    val retrofit = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addCallAdapterFactory(factory)
        .client(okHttpClient)
        .build()
    return retrofit.create(T::class.java)
}

在 NewsRepository.kt 下面

interface NewsRepository {
    // Suspend is used to await the result from Deferred
    suspend fun getNewsList(): UseCaseResult<List<Article>>
}

class NewsRepositoryImpl(private val sportNewsInterface: SportNewsInterface) : NewsRepository {
    override suspend fun getNewsList(): UseCaseResult<List<Article>> {

        return try {
            val result = sportNewsInterface.getNews()
            UseCaseResult.Success(result) as UseCaseResult<List<Article>>
        } catch (ex: Exception) {
            UseCaseResult.Error(ex)
        }
    }
}

在我的新闻界面下方

接口 SportNewsInterface {

@GET("v2/top-headlines?country=us&apiKey=da331087e3f3462bb534b3b0917cbee9")
suspend fun getNews(): Deferred<List<SportNewsResponse>>

@GET("/v2/top-headlines?sources=espn&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getEspn(): Deferred<List<SportNewsResponse>>

@GET("/v2/top-headlines?sources=football-italia&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getFootballItalia(): Deferred<List<SportNewsResponse>>

@GET("/v2/top-headlines?sources=bbc-sport&apiKey=da331087e3f3462bb534b3b0917cbee9")
fun getBBCSport(): Deferred<List<SportNewsResponse>>

}

在我的 SportNewsResponse.kt 下方

data class SportNewsResponse(
    val articles: List<Article>,
    val status: String,
    val totalResults: Int
)

在 Article.kt 模型类下面

data class Source(
    val id: Any?,
    val name: String
)

在未显示数据的 TopHeadlinesFragment 下方

class TopHeadlinesFragment : Fragment() {

    private var viewModel: MainViewModel? = null
    private lateinit var topHeadlinesAdapter: TopHeadlinesAdapter
    private   val newsRepository: NewsRepository by inject()



    //3
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(
            R.layout.fragment_top_headlines
            , container, false
        )


        val recyclerView = view.findViewById(R.id.recyclerView) as RecyclerView
        val pb = view.findViewById(R.id.pb) as ProgressBar
        topHeadlinesAdapter = TopHeadlinesAdapter(recyclerView.context)
        recyclerView.layoutManager = LinearLayoutManager(context)
        recyclerView.adapter = topHeadlinesAdapter
        val param = newsRepository
        val factory = MainViewModelFactory(param)
        viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)
        initViewModel()

        return view
    }

    private fun initViewModel() {
        viewModel?.sportList?.observe(this, Observer { newList ->
            topHeadlinesAdapter.updateData(newList)
        })

        viewModel?.showLoading?.observe(this, Observer { showLoading ->
            pb.visibility = if (showLoading) View.VISIBLE else View.GONE
        })

        viewModel?.showError?.observe(this, Observer { showError ->
            (showError)
        })

        viewModel?.loadNews()
    }
}

在 TopHeadlinesAdapter.kt 下方

class TopHeadlinesAdapter(val context: Context) :
RecyclerView.Adapter<TopHeadlinesAdapter.MyViewHolder>() {



private var articleList: List<Article> by Delegates.observable(emptyList()) { _, _, _ ->

    notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

    val view = LayoutInflater.from(parent.context).inflate(R.layout.news_list, parent, false)
    return MyViewHolder(view)
}

override fun getItemCount(): Int {
    return articleList.size
}

@SuppressLint("NewApi")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {

    holder.articleTitle.text = articleList.get(position).title
    holder.articleSourceName.text = articleList.get(position).source.name
    Picasso.get().load(articleList.get(position).urlToImage).into(holder.image)

    val input = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX")
    val output = SimpleDateFormat("dd/MM/yyyy")
    var d = Date()
    try {
        d = input.parse(articleList[5].publishedAt)
    } catch (e: ParseException) {
        try {
            val fallback = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
            fallback.timeZone = TimeZone.getTimeZone("UTC")
            d = fallback.parse(articleList[5].publishedAt)
        } catch (e2: ParseException) {
            // TODO handle error
            val formatted = output.format(d)
            val timelinePoint = LocalDateTime.parse(formatted)
            val now = LocalDateTime.now()

            var elapsedTime = Duration.between(timelinePoint, now)

            println(timelinePoint)
            println(now)
            elapsedTime.toMinutes()

            holder.articleTime.text = "${elapsedTime.toMinutes()}"

        }
    }

}

fun updateData(newList: List<Article>) {
     articleList = newList
    Log.e("articleListSize",articleList?.size.toString())

}


class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView!!) {

    val image: ImageView = itemView!!.findViewById(R.id.imageView)
    val articleTitle: TextView = itemView!!.findViewById(R.id.articleTitle)
    val articleSourceName: TextView = itemView!!.findViewById(R.id.articleSourceName)
    val imageCategory: ImageView = itemView!!.findViewById(R.id.imageCategory)
    val articleTime: TextView = itemView!!.findViewById(R.id.articleTime)

}

}

在我来自服务器的 json 响应下方

{
    "status": "ok",
    "totalResults": 38,
    "articles": [
        {
            "source": {
                "id": "cnbc",
                "name": "CNBC"
            },
            "author": "Holly Ellyatt",
            "title": "Russia is now not the only pressing issue that NATO has to deal with - CNBC",
            "description": "Heads of state and government are meeting in the U.K. this week for the 70th anniversary of the military alliance NATO.",
            "url": "https://www.cnbc.com/2019/12/02/nato-summit-alliance-has-more-pressing-issues-than-russia-now.html",
            "urlToImage": "https://image.cnbcfm.com/api/v1/image/106272467-1575218599700gettyimages-997112494.jpeg?v=1575218712",
            "publishedAt": "2019-12-02T07:39:00Z",
            "content": "US president Donald Trump is seen during his press conference at the 2018 NATO Summit in Brussels, Belgium on July 12, 2018.\r\nAs heads of state and government meet in the U.K. this week for the 70th anniversary of the military alliance NATO, discussions are l… [+8623 chars]"
        },
        {
            "source": {
                "id": null,
                "name": "Chron.com"
            },
            "author": "Aaron Wilson",
            "title": "Bill O'Brien gets game ball from Deshaun Watson after Texans' win over Patriots - Chron",
            "description": "In an emotional moment, Texans coach Bill O'Brien was presented with the game ball by quarterback Deshaun Watson following a pivotal win over the New England Patriots.",
            "url": "https://www.chron.com/sports/texans/article/Bill-O-Brien-Deshaun-Watson-Texans-Patriots-14874678.php",
            "urlToImage": "https://s.hdnux.com/photos/01/07/23/50/18692664/3/rawImage.jpg",
            "publishedAt": "2019-12-02T06:16:00Z",
            "content": "<ul><li>Houston Texans head coach Bill O'Brien on the sidelines during the fourth quarter of an NFL game against the New England Patriots at NRG Stadium Sunday, Dec. 1, 2019, in Houston.\r\nHouston Texans head coach Bill O'Brien on the sidelines during the four… [+1583 chars]"


        }
    ]
}

在我的 MainViewModel.kt 下面

class MainViewModel(newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
    // Coroutine's background job
     val job = Job()
     val sportNewsInterface: SportNewsInterface? = null
    // Define default thread for Coroutine as Main and add job
    override val coroutineContext: CoroutineContext = Dispatchers.Main + job

     val showLoading = MutableLiveData<Boolean>()
     val sportList = MutableLiveData <List<Article>>()
    val showError = SingleLiveEvent<String>()



    fun loadNews(

    ) {
        // Show progressBar during the operation on the MAIN (default) thread
        showLoading.value = true
        // launch the Coroutine
        launch {
            // Switching from MAIN to IO thread for API operation
            // Update our data list with the new one from API
            val result = withContext(Dispatchers.IO) {
                sportNewsInterface?.getNews()
            }
            // Hide progressBar once the operation is done on the MAIN (default) thread
            showLoading.value = false
            when (result) {

                is UseCaseResult.Success<*> -> {
                    sportList.value = result.data as List<Article>
                }
                is Error -> showError.value = result.message
            }
        }


    }

    override fun onCleared() {
        super.onCleared()
        // Clear our job when the linked activity is destroyed to avoid memory leaks
        job.cancel()
    }
}

标签: androidkotlinfragmentkotlin-coroutineskoin

解决方案


根据我在您的代码中看到的内容MainViewModel,您正在尝试从变量中获取结果sportNewsInterface

val result = withContext(Dispatchers.IO) {
   sportNewsInterface?.getNews()
}

这个变量被初始化null并且永远不会被初始化。我想这是一个拼写错误,你应该newsRepository.getNewsList()改用它。

在的构造函数中添加valbefore ,因此变量将成为该类的成员并使用它而不是在方法中。newsRepositoryMainViewModelsportNewsInterfaceloadNews()

更新

作者更新了他的代码并使用 gist 文件发送更新版本:https ://gist.github.com/kyodgorbek/9f0e1eb8696e4b88a354e8a372a8e690

恢复您的代码后,您必须添加一些更改:

  • 班级TopHeadlinesFragment

首先,那里的 viewmodel 变量没有初始化。由于您使用 Koin 提供它,因此使用以下代码更改第 3 行: private var viewModel by viewModel<MainViewModel>()

  • 文件NewsRepository.kt,类NewsRepositoryImpl

这就是循环依赖的地方。如果您查看 的构造函数NewsRepositoryImpl,您会注意到该类接受类型为 的参数NewsRepository。这实际上产生了循环依赖,因为这种类型已经存在并且在appModulesKoin 模块中提供。话虽如此,您应该将该参数替换为 的实例SportNewsInterface,因此该类NewsRepositoryImpl将如下所示:

@Suppress("UNCHECKED_CAST")
class NewsRepositoryImpl(private val sportsNewsApi: SportNewsInterface) : NewsRepository {
    override suspend fun getNewsList(): UseCaseResult<List<Article>> {

        return try {
            val result = sportsNewsApi.getNews()
            UseCaseResult.Success(result) as UseCaseResult<List<Article>>
        } catch (ex: Exception) {
            UseCaseResult.Error(ex)
        }
    }
}
  • 班级MainViewModel

在这里您应该删除第 5 行val newsRepository: NewsRepository? = null,因为您已经在类的构造函数中将该依赖项作为参数。除此之外,为了将参数放入字段中,请在val此之前添加。结果,您的代码将如下所示:

@Suppress("UNCHECKED_CAST")
class MainViewModel(val newsRepository: NewsRepository) : ViewModel(), CoroutineScope {
    // Coroutine's background job
    val job = Job()
    // Define default thread for Coroutine as Main and add job
    override val coroutineContext: CoroutineContext = Dispatchers.Main + job

    val showLoading = MutableLiveData<Boolean>()
    val sportList = MutableLiveData <List<Article>>()
    val showError = SingleLiveEvent<String>()

    fun loadNews() {
        // Show progressBar during the operation on the MAIN (default) thread
        showLoading.value = true
        // launch the Coroutine
        launch {
            // Switching from MAIN to IO thread for API operation
            // Update our data list with the new one from API
            val result = withContext(Dispatchers.IO) {
                newsRepository?.getNewsList()
            }
            // Hide progressBar once the operation is done on the MAIN (default) thread
            showLoading.value = false
            when (result) {

                is UseCaseResult.Success<*> -> {
                    sportList.value = result.data as List<Article>
                }
                is Error -> showError.value = result.message
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        // Clear our job when the linked activity is destroyed to avoid memory leaks
        job.cancel()
    }
}

推荐阅读