当前位置: 首页 > 知识库问答 >
问题:

web服务手册同步后RecyclerView未刷新

吴飞语
2023-03-14

在手动同步web服务后,我遇到了RecyclerView无法刷新的问题。手动同步是通过在列表上向下滑动或点击ActionBar项目触发的。手动同步使用截取请求以JSON格式检索数据,数据被解析并保存到SQLite数据库表中。同步日期时间也会保存到SQLite数据库表中,然后显示在片段的ActionBar字幕中。截击请求通过WorkManager OneTimeWorkRequest启动。

问题是没有刷新RecyclerView列表。但是,如果我随后触发另一次手动同步,ActionBar字幕和RecyclerView内容中的同步数据时间将更新,但会使用上一次手动同步的数据。如果我从应用程序导航到设备的主屏幕,然后导航回我的应用程序,它现在会显示最新手动同步的刷新数据,这一点就会变得很清楚。

我已经查看了许多关于这个问题的帖子(见下文),虽然我认为我已经改进了我的代码,但没有推荐的解决方案解决了这个问题。

Recyclerview未调用onCreateViewHolder Recyclerview未调用onCreateViewHolder或onBindView Recyclerview未调用onCreateViewHolder Recyclerview未刷新

从web获取JSON数据,并使用RecyclerView Recycler视图显示为空,不显示SQLite数据

RecyclerView onClick工作不正常?为什么RecyclerView没有McClickListener()?

ListView在Web服务同步后不更新

其他资源包括:https://www.mytrendin.com/display-data-recyclerview-using-sqlitecursor-in-android/ https://medium.com/@studymongolian/Updated-data-in-an-android-recyclerview-842e56adbfd8https://www.youtube.com/watch?v=ObU-wCqoo2Ihttps://www.youtube.com/watch?v=_0C18cbv6UE

因此,在尝试解决这个问题几个月后,我现在转向StackOverflow社区寻求帮助。

碎片


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_warning_list, container, false);

        mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.warning_swipe_refresh_layout);

        /* Set the Refresh Listener for the Swipe gesture */
        mSwipeRefreshLayout.setOnRefreshListener(
                new SwipeRefreshLayout.OnRefreshListener() {
                    @Override
                    public void onRefresh() {
                        SyncWarningsScheduler.oneTime();
                        updateUI();
                    }
                }
        );

        mWarningRecyclerView = (RecyclerView) view.findViewById(R.id.warning_recycler_view);

        /* Set the Toolbar to replace the default ActionBar, which has been hidden */
        if (mActivity != null) {
            Toolbar toolbar = (Toolbar) mActivity.findViewById(R.id.toolbar_abstract_single_fragment);
            mActivity.setSupportActionBar(toolbar);

            /* Set the toolbar title */
            ActionBar actionbar = mActivity.getSupportActionBar();
            if (actionbar != null) {
                actionbar.setDisplayHomeAsUpEnabled(true);
                actionbar.setTitle(getString(R.string.warning_list_fragment_toolbar_title));
            }
        }

        updateUI();
        return view;
    }


    @Override
    public void onResume() {
        super.onResume();
        updateUI();
    }


    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.fragment_warning_list, menu);
    }


    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.sync:
                mSwipeRefreshLayout.setRefreshing(true);
                SyncWarningsScheduler.oneTime();
                updateUI();

                return true;
            case R.id.information:
                /* Handle the Information Menu Item */
                FragmentManager fm = getFragmentManager();
                if (fm != null) {
                    WarningListFragmentTFBInformationDialogFragment dialog = new WarningListFragmentTFBInformationDialogFragment();
                    dialog.show(fm, TFB_INFO_DIALOG_TAG);
                }
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }


    private void updateUI() {
        WarningList warningList = WarningList.get(mActivity);
        List<Warning> warnings = warningList.getWarnings();

        if (mWarningAdaptor == null) {
            mWarningAdaptor = new WarningAdaptor(warnings);
            mWarningRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
            mWarningRecyclerView.setAdapter(mWarningAdaptor);
        } else {
            mWarningAdaptor.setWarnings(warnings);
            mWarningAdaptor.notifyDataSetChanged();
        }

        /* Update the ToolBar sub title to show the latest sync datetime */
        updateToolBarSubTitle();

        /* If visible, turn off the Swipe Refresh Progress Circle */
        if (mSwipeRefreshLayout != null && mSwipeRefreshLayout.isRefreshing()) {
            mSwipeRefreshLayout.setRefreshing(false);
        }
    }


    private void updateToolBarSubTitle() {
        SyncInformationList syncInformationList = SyncInformationList.get(mContext);
        Date syncDate = syncInformationList.getSyncDatetime(ORMSync.getWarningSyncTypeKey());

        ActionBar actionBar = mActivity.getSupportActionBar();
        if (actionBar != null) {
            actionBar.setSubtitle(DatabaseUtilities.formatDateSpecial(syncDate, true));
        }
    }


    private class WarningHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        private Warning mWarning;
        private TextView mIssueForTextView;
        private TextView mDeclarationTextView;
        private View mStatusWarningViewLeft;
        private View mStatusWarningViewRight;

        public WarningHolder(LayoutInflater inflater, ViewGroup parent) {
            super(inflater.inflate(R.layout.list_item_warning, parent, false));
            /* Handlers a user press on a Warning */
            itemView.setOnClickListener(this);

            mIssueForTextView = (TextView) itemView.findViewById(R.id.issueFor_textView);
            mDeclarationTextView = (TextView) itemView.findViewById(R.id.declaration_textView);
            mStatusWarningViewLeft = (View) itemView.findViewById(R.id.status_warning_left);
            mStatusWarningViewRight = (View) itemView.findViewById(R.id.status_warning_right);
        }

        public void bind(Warning warning) {
            mWarning = warning;
            String issueForDate;

            issueForDate = DatabaseUtilities.formatDateSpecial(mWarning.getIssuedFor(), "d MMM yyyy");
            mIssueForTextView.setText(issueForDate);

            mDeclarationTextView.setText(mWarning.getTfbDeclaration());
            /* Set Declaration text colour */
            if (mWarning.isTfbStatus()) {
                /* If the day is a TFB set text color to Red */
                mIssueForTextView.setTextColor(getResources().getColor(R.color.red));
                mDeclarationTextView.setTextColor(getResources().getColor(R.color.red));
            }

            /* Set left and right status warning colour based on TFB status */
            mStatusWarningViewLeft.setBackgroundResource(mWarning.setStatusWarningColor());
            mStatusWarningViewRight.setBackgroundResource(mWarning.setStatusWarningColor());
        }

        @Override
        public void onClick(View v) {
            /* Process onClick */
            Intent intent = WarningPagerActivity.newIntent(mActivity, mWarning.getUID());
            startActivity(intent);
        }
    }


    private class WarningAdaptor extends RecyclerView.Adapter<WarningHolder> {
        private List<Warning> mWarnings;

        public WarningAdaptor(List<Warning> warnings) {
            mWarnings = warnings;
        }

        @NonNull
        @Override
        public WarningHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            LayoutInflater layoutInflater = LayoutInflater.from(mActivity);
            return new WarningHolder(layoutInflater, parent);
        }

        @Override
        public void onBindViewHolder(@NonNull WarningHolder holder, int position) {
            /* Bind data */
            Warning warning = mWarnings.get(position);
            holder.bind(warning);
        }

        @Override
        public int getItemCount() {
            return mWarnings.size();
        }

        public void setWarnings(List<Warning> warnings) {
            mWarnings.clear();
            mWarnings = warnings;
        }

        public List<Warning> getWarnings() {
            return mWarnings;
        }
    }
}

WorkManager oneTimeWorkRequest调度程序

public class SyncWarningsScheduler {
    private static final String TAG = "SyncWarningsScheduler";
    private static final String ONE_TIME_WORK_REQUEST = "OneTime";
    private static final String ONE_TIME_WORK_REQUEST_TAG = TAG + ONE_TIME_WORK_REQUEST;
    private static final String ONE_TIME_WORK_REQUEST_TAG_UNIQUE = ONE_TIME_WORK_REQUEST_TAG + "Unique";


    /* Getters and Setters */
    public static String getOneTimeWorkRequestTagUnique() {
        return ONE_TIME_WORK_REQUEST_TAG_UNIQUE;
    }

    public static void oneTime() {
        WorkManager workManager = WorkManager.getInstance();

        /* Create a Constraints object that defines when and how the task should run */
        Constraints constraints = new Constraints.Builder()
                .setRequiresCharging(false)
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build();

        /* Build the Input Data to pass to the Worker */
        Data inputData = new Data.Builder()
                .putString(SyncWarningsWorker.getWorkRequestTypeKey(), ONE_TIME_WORK_REQUEST)
                .build();

        /* Build the One Time Work Request */
        OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(SyncWarningsWorker.class)
                .setConstraints(constraints)
                /* Sets the input data for the ListenableWorker */
                .setInputData(inputData)
                .addTag(ONE_TIME_WORK_REQUEST_TAG)
                .build();

        workManager.enqueueUniqueWork(ONE_TIME_WORK_REQUEST_TAG_UNIQUE, ExistingWorkPolicy.REPLACE, oneTimeWorkRequest);
    }
}

WorkManager工人

public class SyncWarningsWorker extends Worker {
    private static final String TAG = "SyncWarningsWorker";
    private Context mContext;
    private SQLiteDatabase mDatabase;
    private WarningList mWarningList;

    private static final String WORK_REQUEST_TYPE_KEY = "warningworkrequesttype";
    private static final String SYNC_DATE_TIME_KEY = "warningsyncdatetime";

    public SyncWarningsWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);

        /* Set the Context which must be the Application Context */
        mContext = context;

        mDatabase = IncidentsDatabaseHelper.get(context).getWritableDatabase();
        /* Get a refer to the WarningList Singleton */
        mWarningList = WarningList.get(context);
    }

    /* Getters and Setters */
    public static String getWorkRequestTypeKey() {
        return WORK_REQUEST_TYPE_KEY;
    }

    public static String getSyncDateTimeKey() {
        return SYNC_DATE_TIME_KEY;
    }


    @NonNull
    @Override
    public Result doWork() {
        /* Read passed-in argument(s) */
        String workRequestType = getInputData().getString(WORK_REQUEST_TYPE_KEY);
        LogUtilities.info(TAG, "doWork() - Processing EMV Warnings Work Request Type: " + workRequestType);

        try {
            downloadWarnings();

            Date now = new Date();
            long nowMilliSeconds = now.getTime();
            now.setTime(nowMilliSeconds);

            /* Update the WarningSyncType in SyncInformationList with the Warnings Sync Datetime */
            SyncInformationList syncInformationList = SyncInformationList.get(mContext);
            syncInformationList.updateSyncDatetime(ORMSync.getWarningSyncTypeKey(), now);

            Data syncDateTime = new Data.Builder()
                    .putLong(SYNC_DATE_TIME_KEY, nowMilliSeconds)
                    .build();

            return Result.success(syncDateTime);
        } catch (Exception e){
            LogUtilities.error(TAG, "doWork() - Can't download EMV Warnings data.\n\n" + e.toString());
            return Result.failure();
        }
    }

    private void downloadWarnings() {
        VolleyRequestQueue volleyRequestQueue;

        StringRequest request = new StringRequest(Request.Method.GET, JSONWarningsSchema.getTfbFdrJsonEndPoint(), onPostsLoaded, onPostsError);

        volleyRequestQueue = VolleyRequestQueue.get(mContext);

        volleyRequestQueue.addToVolleyRequestQueue(request);
    }

    private final Response.Listener<String> onPostsLoaded = new Response.Listener<String>() {
        ContentValues contentvalues;
        String noData = "NO DATA";

        @Override
        public void onResponse(String response) {
            /* Delete all the Warning records from the SQLite table */
            mWarningList.deleteAllWarnings();

            try {
                JSONObject jsonBody = new JSONObject(response);
                /* Within jsonBody is one nested JSON Array */
                JSONArray jsonArrayResults = jsonBody.getJSONArray(JSONWarningsSchema.getJsonRootArrayName());

                for (int i = 0; i < jsonArrayResults.length(); i++) {
                    /*
                     * Within jsonArrayResults are 10 sometimes 9 JSON Objects, the first 5 Objects are for TFB declarations and
                     * the last 5 (4) Objects are for FDR declarations.
                     */
                    JSONObject warningMetadata = jsonArrayResults.getJSONObject(i);

                    if (i < 5) {
                        /*
                         * The first 5 Objects are for Today and the next 4 days worth of TFB declarations. The TFB declaration in these Objects are
                         * used to INSERT new records into the warnings table using the issueFor date as the Alternate Primary Key.
                         * The FDR declarations for each day are defaulted to "NO DATA" to cater for the sometimes missing FDR data on the 5th day, this is
                         * to avoid null pointer errors when displaying the data in fragment_warning.
                         */
                        Warning warning = new Warning();

                        String issueForDate = warningMetadata.getString(JSONWarningsSchema.Keys.getIssueFor());
                        warning.setIssuedFor(JSONUtilities.stringToDate(issueForDate, JSONWarningsSchema.getJsonIssueForDateFormat()));

                        String status = warningMetadata.getString(JSONWarningsSchema.Keys.getStatus());
                        warning.setTfbStatus(JSONUtilities.stringToBoolean(status));

                        warning.setTfbDeclaration(warningMetadata.getString(JSONWarningsSchema.Keys.getDeclaration()));

                        /*
                         * Within the warningMetadata JSONObject is a JSONArray called declareList. Need to get the Array and
                         * iterate through the Array to extract the TFB warning for each District for this day.
                         * We know the exact number of JSONObjects in the declareList Array (ie an Object for each District).
                         */
                        JSONArray jsonArrayTFBDeclareList = warningMetadata.getJSONArray(JSONWarningsSchema.getJsonDeclareListArrayName());

                        /* Iterate through the TFB declareList Array */
                        for (int j = 0; j < jsonArrayTFBDeclareList.length(); j++) {
                            /* Get the JSON Object within the jsonArrayDeclareList Array */
                            JSONObject declareListMetadata = jsonArrayTFBDeclareList.getJSONObject(j);

                            /* Get the name and status pairs from the declareListMetadata Object */
                            String name = declareListMetadata.getString(JSONWarningsSchema.Keys.getDeclareListName());
                            String declareListStatus = declareListMetadata.getString(JSONWarningsSchema.Keys.getDeclareListStatus());

                            switch (name) {
                                case "Mallee":
                                    warning.setTfbMallee(declareListStatus);
                                    warning.setFdrMallee(noData);
                                    break;
                                case "Wimmera":
                                    warning.setTfbWimmera(declareListStatus);
                                    warning.setFdrWimmera(noData);
                                    break;
                                case "South West":
                                    warning.setTfbSouthWest(declareListStatus);
                                    warning.setFdrSouthWest(noData);
                                    break;
                                case "Northern Country":
                                    warning.setTfbNorthernCountry(declareListStatus);
                                    warning.setFdrNorthernCountry(noData);
                                    break;
                                case "North Central":
                                    warning.setTfbNorthCentral(declareListStatus);
                                    warning.setFdrNorthCentral(noData);
                                    break;
                                case "Central":
                                    warning.setTfbCentral(declareListStatus);
                                    warning.setFdrCentral(noData);
                                    break;
                                case "North East":
                                    warning.setTfbNorthEast(declareListStatus);
                                    warning.setFdrNorthEast(noData);
                                    break;
                                case "West and South Gippsland":
                                    warning.setTfbWestAndSouthGippsland(declareListStatus);
                                    warning.setFdrWestAndSouthGippsland(noData);
                                    break;
                                case "East Gippsland":
                                    warning.setTfbEastGippsland(declareListStatus);
                                    warning.setFdrEastGippsland(noData);
                                    break;
                                default:
                                    break;
                            } 
                        } 

                        contentvalues = ContentValueUtilities.getWarningListContentValues(warning, true);

                        mDatabase.beginTransaction();
                        try {
                            mDatabase.insert(ORMWarnings.getTableName(), null, contentvalues);
                            mDatabase.setTransactionSuccessful();
                        } catch (SQLiteException e) {
                            LogUtilities.error(TAG, "onPostsLoaded > onResponse - ERROR Inserting record into the '" + ORMWarnings.getTableName() + "' Table.\n\n" + e.toString());
                        } finally {
                            mDatabase.endTransaction();
                        } 
                    } else {
                        /*
                         * The last 5 or sometimes 4 Objects are for Today and the next 4 days worth of FDR declarations. The FDR declarations
                         * in these Objects are used to UPDATE FDR attributes in the warnings table using the issueFor date to find the existing warnings
                         * record.
                         */
                        String issueForFDR = warningMetadata.getString(JSONWarningsSchema.Keys.getIssueFor());
                        /* Ensure the retrieved issueFor date string is converted consistently */
                        Date issueForFDRDate = JSONUtilities.stringToDate(issueForFDR, JSONWarningsSchema.getJsonIssueForDateFormat());

                        /* Find the record in the warnings table by using the issueForFDRDate date. */
                        Warning warningExists = mWarningList.getWarning(issueForFDRDate);

                        /* Make sure a warning record has been returned */
                        if (warningExists != null) {
                            String issueAtDate = warningMetadata.getString(JSONWarningsSchema.Keys.getIssueAt());
                            warningExists.setFdrIssuedAt(JSONUtilities.stringToDate(issueAtDate, JSONWarningsSchema.getJsonIssueAtDateFormat()));

                            /*
                             * Within the warningMetadata JSONObject is a JSONArray called declareList. Need to get the Array and
                             * iterate through the Array to extract the FDR warning for each District for this day.
                             * We know the exact number of JSONObjects in the declareList Array (ie an Object for each District).
                             */
                            JSONArray jsonArrayFDRDeclareList = warningMetadata.getJSONArray(JSONWarningsSchema.getJsonDeclareListArrayName());

                            /* Iterate through the FDR declareList Array */
                            for (int z = 0; z < jsonArrayFDRDeclareList.length(); z++) {
                                /* Get the JSON Object within the jsonArrayFDRDeclareList Array */
                                JSONObject declareListMetadataFDR = jsonArrayFDRDeclareList.getJSONObject(z);

                                /* Get the name and status pairs from the declareListMetadataFDR Object */
                                String nameFDR = declareListMetadataFDR.getString(JSONWarningsSchema.Keys.getDeclareListName());
                                String declareListStatusFDR = declareListMetadataFDR.getString(JSONWarningsSchema.Keys.getDeclareListStatus());

                                switch (nameFDR) {
                                    case "Mallee":
                                        warningExists.setFdrMallee(declareListStatusFDR);
                                        break;
                                    case "Wimmera":
                                        warningExists.setFdrWimmera(declareListStatusFDR);
                                        break;
                                    case "South West":
                                        warningExists.setFdrSouthWest(declareListStatusFDR);
                                        break;
                                    case "Northern Country":
                                        warningExists.setFdrNorthernCountry(declareListStatusFDR);
                                        break;
                                    case "North Central":
                                        warningExists.setFdrNorthCentral(declareListStatusFDR);
                                        break;
                                    case "Central":
                                        warningExists.setFdrCentral(declareListStatusFDR);
                                        break;
                                    case "North East":
                                        warningExists.setFdrNorthEast(declareListStatusFDR);
                                        break;
                                    case "West and South Gippsland":
                                        warningExists.setFdrWestAndSouthGippsland(declareListStatusFDR);
                                        break;
                                    case "East Gippsland":
                                        warningExists.setFdrEastGippsland(declareListStatusFDR);
                                        break;
                                    default:
                                        break;
                                } 
                            }

                            contentvalues = ContentValueUtilities.getWarningListContentValues(warningExists, false);

                            mDatabase.beginTransaction();
                            try {
                                mDatabase.update(ORMWarnings.getTableName(), contentvalues, ORMWarnings.getUUIDColumn() + " = ?", new String[] {warningExists.getUID().toString()});
                                mDatabase.setTransactionSuccessful();
                            } catch (SQLiteException e) {
                                LogUtilities.error(TAG, "onPostsLoaded > onResponse - ERROR Updating record in the '" + ORMWarnings.getTableName() + "' Table.\n\n" + e.toString());
                            } finally {
                                mDatabase.endTransaction();
                            }
                        } else {
                            /* Something went wrong can't find warning record using the issueForFDRDate date */
                            LogUtilities.wtf(TAG, "onPostsLoaded > onResponse - " + issueForFDRDate.toString() + " warning record not found.\n\n");
                        } 
                    }
                }
            } catch (JSONException e) {
                LogUtilities.error(TAG, "onPostsLoaded > onResponse - Failed to Parse JSON body.\n\n" + e.toString());
            }
        }
    };


    private final Response.ErrorListener onPostsError = new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            LogUtilities.error(TAG, "onPostsError > onErrorResponse - Failed to download JSON body.\n\n" + error.toString());
        }
    }; 
} 

共有2个答案

澹台玉石
2023-03-14

我想你应该更新一下;方法调用,因为当api调用时,它在后台工作。或者你可以设定

sleep(5000)
updateUI();
须彭亮
2023-03-14

在您的适配器中使用此功能

 public void setWarnings(List<Warning> warnings) {
        mWarnings.clear();
        mWarnings = warnings;
        notifyDataSetChanged();

    }
 类似资料:
  • 删除项目后,“我的回收器”视图未更新。此recyclerView位于片段内部。我试过各种方法,但都不管用。 片段类中的适配器声明 回收服务适配器: } 调试项目时,我可以看到该项实际上正在从ArrayList中删除。但不在循环视图中更新。 删除后,如果滚动回收站视图,则从回收站视图中删除已删除的项。但不是没有滚动。

  • 你能告诉我在Android中做同步工作的正确方法是什么吗(例如,如果我有大约5个工作)<注意 我所说的同步作业是指在后台运行并通过Web服务发送一些数据(如分析)的线程。。。 有关更多详细信息,请阅读更详细的描述: 我的任务是实现一些后台作业,这些作业将使一些数据与restful web服务同步。一些作业应定期安排,并有特定的延迟。如果没有internet连接,我只需缓存数据,然后当连接再次出现时

  • 我是web服务开发的初学者。我们正在使用Spring3用java构建RESTWeb应用程序。 我们正在使用的Web服务具有异步登录方法。我们为他们提供了一个回调监听器URL,他们的服务在其中发回响应。 因此,当我们发送登录请求时,我们会收到一个空白响应作为确认。和服务发送一个响应,其中包含侦听器URL上的实际数据。 请帮助,我应该如何设计/实现调用登录服务作为同步调用?谢谢 编辑:下面是回发消息的

  • 问题内容: 我有一个作为常规SOAP Web服务公开的接口。该接口的一种方法包括让客户端将文件发送到服务器,然后服务器处理该文件并返回结果文件。处理文件可能需要一些时间,因此我认为使用异步调用此方法是一个更好的主意。我考虑了以下流程: 客户端调用异步方法,并使用附件(MTOM)发送文件。服务器接收到文件后,会将响应发送回客户端,表明已接收到该文件,并将在不久后对其进行处理。处理完文件后,会将响应发

  • 当我执行时,我的服务按预期列出,但当我从web或通过功能测试访问我的包时,我得到: 未捕获的PHP异常Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException:“您请求了一个不存在的服务”xx。处理程序”在xx/app/bootstrap。php。缓存行2031{“异常”:“[对象](Symfony\Compo

  • Seafile 是一个开源的文件云存储平台,解决文件集中存储、同步、多平台访问的问题,注重安全和性能。 Seafile 通过“资料库”来分类管理文件,每个资料库可单独同步,用户可加密资料库, 且密码不会保存在服务器端,所以即使是服务器管理员也无权访问你的文件。 Seafile 允许用户创建“群组”,在群组内共享和同步文件,方便了团队协同工作。